Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .agents/skills/fleet-queue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ $THURBOX_SESSION` so `session list --parent` enumerates your workers — **excep
on a task that names a `--host`**, where thurbox refuses a parent living on
another machine and there is no way to spell one, so a remote worker has no
parent and is enumerated by its task record instead — and the
task's session profile from `./scripts/session-flags.sh`. Each worker is sent
task's session profile from `orchestration/session-profiles.yaml`. Each worker is sent
one line pointing at the absolute path of its own brief — nothing is copied into
its worktree, so nothing can land in its PR.

Expand Down Expand Up @@ -402,10 +402,11 @@ commits.

### The trust dialog, handled here rather than remembered

Every spawn runs `./scripts/session-trust.sh` between `session create` and the
first `session send`. An agent started in a fresh worktree asks whether it may
Every spawn runs `scripts/lib/session_trust.py` in-process (dispatch calls it
directly rather than shelling out) between `session create` and the first
`session send`. An agent started in a fresh worktree asks whether it may
work there, and sending the brief while that dialog is up types the brief INTO
the dialog — which is how every fleet-spawned worker used to break. The script
the dialog — which is how every fleet-spawned worker used to break. It
confirms the dialog is really there before sending a key, answers with the
sequence that agent needs (Claude's default selection is **`No, exit`**, so a
bare Enter dismisses it), and confirms the dialog is gone. A dialog queued
Expand Down Expand Up @@ -853,7 +854,7 @@ get --json` carries no usage field at all — do not look for one.

The restart is `session restart` (kills the window, re-spawns with `--resume`,
so the conversation and the brief survive) followed by dispatch's own handoff:
`session-trust.sh` first, because a re-spawned agent in a worktree can ask the
`session_trust.py` first, because a re-spawned agent in a worktree can ask the
trust question again and sending into that dialog types the prompt INTO it.
Every restart is recorded on the task and **capped at three** — a session that
runs dry, resumes and runs dry again is a task too big for its window, and a
Expand Down
5 changes: 3 additions & 2 deletions .agents/skills/thurbox-session/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ The per-agent differences, one of which is a trap:
**An agent not in this table is refused, not guessed at** — a wrong keystroke
can exit the agent instead of dismissing a dialog. Teach it one with
`TRUST_SIGNATURE` and `TRUST_KEYS` in `orchestration/agent.conf`
(`TRUST_KEYS=none` for an agent with no dialog at all); `session-trust.sh`'s
header owns the mechanics.
(`TRUST_KEYS=none` for an agent with no dialog at all);
`scripts/lib/session_trust.py`, which `session-trust.sh` forwards to, owns the
mechanics.

**Which path the trust is recorded against** (observed 2026-09-07, Claude Code):
answering inside a worktree records it against the **repository's main worktree
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ jobs:
- 'scripts/queue.sh'
- 'scripts/queue-selftest.sh'
- 'scripts/lib/queue.py'
# Dispatch calls both in-process, and the selftest drives both.
- 'scripts/session-trust.sh'
- 'scripts/lib/session_trust.py'
- 'scripts/lib/session_profiles.py'
- 'scripts/check.sh'
- '.github/workflows/ci.yml'
reconcile:
Expand Down
2 changes: 1 addition & 1 deletion .no-mistakes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ document:
rule, and why completion is a stream plus a file rather than a message.
orchestration/queue/README.md owns the on-disk record shape, and
.agents/skills/fleet-queue/SKILL.md is the working reference for driving
it — a reference, not an owner. scripts/session-trust.sh's header owns the
it — a reference, not an owner. scripts/lib/session_trust.py owns the
trust-dialog mechanics and the per-agent table, and
scripts/trust-thurbox-dir.sh's owns the config-seeding fallback. README.md
owns the human-facing version of all of it. Point at one of those rather
Expand Down
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,12 @@ a second copy — read the skill before you run any of it:
guess. A queue that runs one task at a time is slower than no queue at all.
4. Each worker targets a real repo and its own git worktree — the control plane
holds the plan and the log, never the workers' branches. `dispatch` gets each
new session past its agent's trust dialog before it sends the brief
(`./scripts/session-trust.sh`), because sending one into that dialog is how
every fleet-spawned worker used to break. A task may name a `--host` and run
on that machine instead, probed first and carried by ssh, so that completion
stays one model.
new session past its agent's trust dialog before it sends the brief, calling
`scripts/lib/session_trust.py` in-process (`scripts/session-trust.sh` is a
thin forwarder to the same module, kept for skills and hooks that name it),
because sending one into that dialog is how every fleet-spawned worker used
to break. A task may name a `--host` and run on that machine instead, probed
first and carried by ssh, so that completion stays one model.
5. **Completion is two things you read, never something that interrupts you.**
`queue.sh watch` folds thurbox's event stream into each task's record and
closes nothing; `queue.sh collect` reads the `result.md` the worker wrote,
Expand Down
2 changes: 1 addition & 1 deletion orchestration/agent.example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ FUEL_PROVIDER=
# gone stale AND the agent itself saying it hit a limit. The second half is
# agent-specific by nature — a banner on the pane, or a rate-limit record in a
# transcript — so `scripts/lib/queue.py` keeps one entry per agent fleet has
# actually WATCHED do it, the same way `scripts/session-trust.sh` keeps one
# actually WATCHED do it, the same way `scripts/lib/session_trust.py` keeps one
# entry per agent's trust dialog.
#
# AN AGENT WITH NO ENTRY IS NOT GUESSED AT: refuel reports it `undetermined`,
Expand Down
63 changes: 38 additions & 25 deletions scripts/lib/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,17 @@ def _load_forge():
shares ONE registry, and therefore one answer about which forges are
configured.
"""
if "fleet_forge" in sys.modules:
return sys.modules["fleet_forge"]
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forge.py")
spec = importlib.util.spec_from_file_location("fleet_forge", path)
return _load_lib("forge.py", "fleet_forge")


def _load_lib(filename: str, name: str):
"""A module beside this file, loaded once under `name` — see `_load_forge`."""
if name in sys.modules:
return sys.modules[name]
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules["fleet_forge"] = mod
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod

Expand Down Expand Up @@ -2399,7 +2404,7 @@ def host_entry(name: str) -> tuple[dict | None, str]:

# THE TRUST DIALOG, decided here. `session capture`, `key` and `send` all
# work against a remote session — thurbox delegates each verb to the
# thurbox-cli on the host — so `session-trust.sh` answers a remote dialog
# thurbox-cli on the host — so `session_trust.py` answers a remote dialog
# exactly as it answers a local one. That delegation is switched off
# wholesale by `share_sessions = false`, and then nothing can see the pane:
# the worker would sit on its dialog with the brief unread, which is the
Expand Down Expand Up @@ -2999,16 +3004,20 @@ def read_text(path: str) -> str:


def profile_flags(profile: str) -> list:
"""The agent settings for this task, from orchestration/session-profiles.yaml."""
try:
out = subprocess.run(
["./scripts/session-flags.sh", profile],
capture_output=True,
check=True,
).stdout
except (OSError, subprocess.CalledProcessError):
"""The agent settings for this task, from orchestration/session-profiles.yaml.

In-process, and not through `scripts/session-flags.sh`: on a machine with
no bash that call failed, the failure was swallowed here, and the worker
started without its profile with nothing said (queue-selftest §21b). A
profile that is missing or breaks a rule still renders no flags.
"""
profiles_mod = _load_lib("session_profiles.py", "fleet_session_profiles")
errors: list[str] = []
path = os.path.join(checkout_root(), "orchestration", "session-profiles.yaml")
profiles = profiles_mod.load_profiles(path, errors)
if profiles is None or errors or profile not in profiles:
return []
return [f for f in out.decode().split("\0") if f]
return profiles_mod.render(profiles[profile])


def brief_target(task: Task) -> str:
Expand Down Expand Up @@ -3400,7 +3409,9 @@ def watch_command(extra: list) -> list:
"""The stream command, real or the selftest's recorded-stream override."""
override = os.environ.get("FLEET_QUEUE_WATCH_CMD")
if override:
return ["sh", "-c", override]
# Split into argv with shell quoting and nothing else of a shell: there
# is no `sh` to hand a line to on native Windows.
return shlex.split(override)
return ["thurbox-cli", "watch", "--json"] + extra


Expand Down Expand Up @@ -4568,7 +4579,7 @@ def cmd_reap(args) -> int:
REFUEL_CAP = 3

# HOW EACH AGENT SAYS IT RAN OUT, one entry per agent fleet has actually
# WATCHED do it — the same shape as `scripts/session-trust.sh`'s per-agent
# WATCHED do it — the same shape as `scripts/lib/session_trust.py`'s per-agent
# table, and for the same reason: fleet drives several agents, so knowing one
# of them is a fact about that agent and not a assumption about all of them.
#
Expand Down Expand Up @@ -5829,14 +5840,16 @@ def branch_checkout(repo: str, branch: str, slug: str) -> tuple[str, str]:


def trust_and_send(session: str, text: str, timeout: int = 20) -> tuple[bool, str]:
"""Answer the trust dialog, then type. The order is the whole point (§1b)."""
trust = subprocess.run(
["./scripts/session-trust.sh", session, "--timeout", str(timeout)],
capture_output=True,
check=False,
)
report = (trust.stdout + trust.stderr).decode().strip()
if trust.returncode != 0:
"""Answer the trust dialog, then type. The order is the whole point (§1b).

The dialog is answered in-process by `session_trust.py`, the module
`scripts/session-trust.sh` forwards to: a machine with no bash could not
run the script, and dispatch then failed after `session create`, leaving a
session that was never sent its brief.
"""
trust = _load_lib("session_trust.py", "fleet_session_trust")
code, report = trust.answer_dialogs(session, timeout)
if code != 0:
return False, report
# The returncode is READ. It used to be thrown away, so a send into a
# session that had gone away returned `True` and every caller reported a
Expand Down
6 changes: 4 additions & 2 deletions scripts/lib/session_profiles.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env python3
"""Render one session profile into `thurbox-cli session create` flags.

Called by scripts/session-flags.sh, never on its own — that wrapper owns the
argument handling and the usage message. Two modes:
Called by scripts/session-flags.sh, which owns the argument handling and the
usage message, and in-process by `queue.py dispatch`, which calls
`load_profiles` and `render` directly so that no shell stands between a task
and its profile. Two modes on the command line:

session_profiles.py <path> --check validate every profile, print a
one-line summary
Expand Down
Loading