teamrunner 0.0.3__tar.gz → 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. {teamrunner-0.0.3 → teamrunner-0.0.4}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
  2. {teamrunner-0.0.3 → teamrunner-0.0.4}/.github/ISSUE_TEMPLATE/question.yml +1 -1
  3. {teamrunner-0.0.3 → teamrunner-0.0.4}/CHANGELOG.md +40 -0
  4. {teamrunner-0.0.3 → teamrunner-0.0.4}/NOTICE +11 -1
  5. {teamrunner-0.0.3 → teamrunner-0.0.4}/PKG-INFO +47 -8
  6. {teamrunner-0.0.3 → teamrunner-0.0.4}/README.md +46 -7
  7. {teamrunner-0.0.3 → teamrunner-0.0.4}/pyproject.toml +1 -1
  8. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/__init__.py +1 -1
  9. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/authoring/AGENTS.md +2 -0
  10. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/builtin_tools.py +103 -0
  11. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/checks.py +14 -1
  12. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/cli.py +11 -6
  13. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/console.py +1 -1
  14. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/engine.py +12 -2
  15. teamrunner-0.0.4/src/teamrunner/flowgraph.py +136 -0
  16. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/run.py +79 -19
  17. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/runfolder.py +9 -6
  18. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/runs.py +82 -14
  19. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/tool_registry.py +2 -0
  20. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/turns.py +1 -0
  21. teamrunner-0.0.4/src/teamrunner/ui/__init__.py +1 -0
  22. teamrunner-0.0.4/src/teamrunner/ui/api.py +297 -0
  23. teamrunner-0.0.4/src/teamrunner/ui/diagrams.py +131 -0
  24. teamrunner-0.0.4/src/teamrunner/ui/jobs.py +272 -0
  25. teamrunner-0.0.4/src/teamrunner/ui/runviews.py +231 -0
  26. teamrunner-0.0.4/src/teamrunner/ui/server.py +265 -0
  27. teamrunner-0.0.4/src/teamrunner/ui/static/app.css +89 -0
  28. teamrunner-0.0.4/src/teamrunner/ui/static/app.js +346 -0
  29. teamrunner-0.0.4/src/teamrunner/ui/static/diagrams.js +53 -0
  30. teamrunner-0.0.4/src/teamrunner/ui/static/index.html +134 -0
  31. teamrunner-0.0.4/src/teamrunner/ui/static/run.js +144 -0
  32. teamrunner-0.0.4/src/teamrunner/ui/static/runs.js +249 -0
  33. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/LICENSE +21 -0
  34. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/SOURCE.txt +5 -0
  35. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/dompurify-LICENSE.txt +202 -0
  36. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/dompurify-SOURCE.txt +5 -0
  37. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/dompurify.js +7 -0
  38. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/editor.css +344 -0
  39. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/editor.js +11383 -0
  40. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/marked-LICENSE.txt +44 -0
  41. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/marked-SOURCE.txt +5 -0
  42. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/marked.js +86 -0
  43. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/mermaid-LICENSE.txt +21 -0
  44. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/mermaid-SOURCE.txt +5 -0
  45. teamrunner-0.0.4/src/teamrunner/ui/static/vendor/mermaid.js +4329 -0
  46. teamrunner-0.0.4/src/teamrunner/ui/stream.py +78 -0
  47. teamrunner-0.0.4/src/teamrunner/ui/trash.py +79 -0
  48. teamrunner-0.0.4/tests/fixtures/ui_history/README.txt +1 -0
  49. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-05_090000_debate/events.jsonl +9 -0
  50. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-05_090000_debate/output.md +14 -0
  51. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-05_090000_debate/run.json +26 -0
  52. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-06_100000_debate/events.jsonl +9 -0
  53. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-06_100000_debate/inputs/brief.txt +1 -0
  54. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-06_100000_debate/output.md +14 -0
  55. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-06_100000_debate/run.json +26 -0
  56. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-06_100000_debate/work/scores.csv +2 -0
  57. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-07_101500_review/events.jsonl +9 -0
  58. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-07_101500_review/output.md +14 -0
  59. teamrunner-0.0.4/tests/fixtures/ui_history/runs/2026-11-07_101500_review/run.json +26 -0
  60. teamrunner-0.0.4/tests/fixtures/ui_history/team.yaml +2 -0
  61. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_run.py +10 -0
  62. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_tools.py +51 -0
  63. teamrunner-0.0.4/tests/test_ui.py +793 -0
  64. teamrunner-0.0.4/tests/test_ui_diagrams.py +128 -0
  65. teamrunner-0.0.4/tests/test_ui_review.py +145 -0
  66. teamrunner-0.0.4/tests/ui_transcript_check.js +44 -0
  67. {teamrunner-0.0.3 → teamrunner-0.0.4}/uv.lock +1 -1
  68. {teamrunner-0.0.3 → teamrunner-0.0.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  69. {teamrunner-0.0.3 → teamrunner-0.0.4}/.github/workflows/publish.yml +0 -0
  70. {teamrunner-0.0.3 → teamrunner-0.0.4}/.github/workflows/tests.yml +0 -0
  71. {teamrunner-0.0.3 → teamrunner-0.0.4}/.gitignore +0 -0
  72. {teamrunner-0.0.3 → teamrunner-0.0.4}/CLA.md +0 -0
  73. {teamrunner-0.0.3 → teamrunner-0.0.4}/CONTRIBUTING.md +0 -0
  74. {teamrunner-0.0.3 → teamrunner-0.0.4}/LICENSE +0 -0
  75. {teamrunner-0.0.3 → teamrunner-0.0.4}/PREVIEW.md +0 -0
  76. {teamrunner-0.0.3 → teamrunner-0.0.4}/SECURITY.md +0 -0
  77. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/LICENSE +0 -0
  78. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/.gitignore +0 -0
  79. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/against.md +0 -0
  80. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/chair.md +0 -0
  81. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/fact_checker.md +0 -0
  82. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/for.md +0 -0
  83. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/judge.md +0 -0
  84. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/sandbox.md +0 -0
  85. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/team.yaml +0 -0
  86. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/fact_checked/web.md +0 -0
  87. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/hiring_committee/chair.md +0 -0
  88. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/hiring_committee/chen.md +0 -0
  89. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/hiring_committee/clerk.md +0 -0
  90. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/hiring_committee/diaz.md +0 -0
  91. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/hiring_committee/models/member.yaml +0 -0
  92. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/hiring_committee/okafor.md +0 -0
  93. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/hiring_committee/team.yaml +0 -0
  94. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/multi_agent_debate/.gitignore +0 -0
  95. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/multi_agent_debate/against.md +0 -0
  96. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/multi_agent_debate/chair.md +0 -0
  97. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/multi_agent_debate/for.md +0 -0
  98. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/multi_agent_debate/judge.md +0 -0
  99. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/multi_agent_debate/team.yaml +0 -0
  100. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/.gitignore +0 -0
  101. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/chair.md +0 -0
  102. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/derek.md +0 -0
  103. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/emma.md +0 -0
  104. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/lily.md +0 -0
  105. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/models/engineer.yaml +0 -0
  106. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/noah.md +0 -0
  107. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/team.yaml +0 -0
  108. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/orchestrators_lite/zoe.md +0 -0
  109. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/catalogue.md +0 -0
  110. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/chair.md +0 -0
  111. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/context_checker.md +0 -0
  112. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/editor.md +0 -0
  113. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/intake.md +0 -0
  114. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/library.md +0 -0
  115. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/metadata_checker.md +0 -0
  116. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/reader.md +0 -0
  117. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/reference_check/team.yaml +0 -0
  118. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/round_robin/.gitignore +0 -0
  119. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/round_robin/01_for.md +0 -0
  120. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/round_robin/02_against.md +0 -0
  121. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/round_robin/03_judge.md +0 -0
  122. {teamrunner-0.0.3 → teamrunner-0.0.4}/examples/round_robin/team.yaml +0 -0
  123. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/__main__.py +0 -0
  124. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/authoring/LICENSE +0 -0
  125. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/authoring/__init__.py +0 -0
  126. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/build.py +0 -0
  127. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/describe.py +0 -0
  128. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/desk.py +0 -0
  129. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/environment.py +0 -0
  130. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/events.py +0 -0
  131. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/explain.py +0 -0
  132. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/flow.py +0 -0
  133. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/gather.py +0 -0
  134. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/ids.py +0 -0
  135. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/inputs.py +0 -0
  136. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/ir.py +0 -0
  137. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/keys.py +0 -0
  138. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/litellm_model.py +0 -0
  139. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/loader.py +0 -0
  140. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/minutes.py +0 -0
  141. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/model.py +0 -0
  142. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/new.py +0 -0
  143. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/prices.py +0 -0
  144. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/problems.py +0 -0
  145. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/rehearsal.py +0 -0
  146. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/sandbox/__init__.py +0 -0
  147. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/sandbox/run.js +0 -0
  148. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/schema_check.py +0 -0
  149. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/schemas/member.schema.json +0 -0
  150. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/schemas/model_profile.schema.json +0 -0
  151. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/schemas/team.schema.json +0 -0
  152. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/show.py +0 -0
  153. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/validate.py +0 -0
  154. {teamrunner-0.0.3 → teamrunner-0.0.4}/src/teamrunner/vocabulary.py +0 -0
  155. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_authoring.py +0 -0
  156. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_engine.py +0 -0
  157. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_explain.py +0 -0
  158. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_flow.py +0 -0
  159. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_minutes.py +0 -0
  160. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_new.py +0 -0
  161. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_prices.py +0 -0
  162. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_runs.py +0 -0
  163. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_sandbox.py +0 -0
  164. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_show.py +0 -0
  165. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_turns.py +0 -0
  166. {teamrunner-0.0.3 → teamrunner-0.0.4}/tests/test_validate.py +0 -0
@@ -20,7 +20,7 @@ body:
20
20
  attributes:
21
21
  label: TeamRunner version
22
22
  description: The output of `teamrunner --version`
23
- placeholder: teamrunner 0.0.3
23
+ placeholder: teamrunner 0.0.4
24
24
  validations:
25
25
  required: true
26
26
  - type: input
@@ -15,4 +15,4 @@ body:
15
15
  id: version
16
16
  attributes:
17
17
  label: TeamRunner version, if it matters
18
- placeholder: teamrunner 0.0.3
18
+ placeholder: teamrunner 0.0.4
@@ -4,6 +4,46 @@ TeamRunner is in preview: 0.0.x versions are a soft launch, and until 0.1.0
4
4
  is announced a version may change in ways that break teams and run folders
5
5
  made with an earlier one. Each such change is listed here.
6
6
 
7
+ ## 0.0.4
8
+
9
+ - **Local browser interface:** `teamrunner ui [folder]` (`gui` is an alias)
10
+ opens the team's files in a browser. Edit with offline syntax highlighting,
11
+ save, validate with clickable problems, inspect resolved instructions and
12
+ flow diagrams, create teams from examples, manage members and set up keys.
13
+ - Start one run at a time with defaults, a message, a file or a task file;
14
+ rehearse for free, follow events live, stop cleanly and re-run saved inputs
15
+ with the current team. Browse and search run history, read conversations,
16
+ outputs and records, inspect what members could read, and download raw files.
17
+ - Compare two runs' numbered member versions, outputs, cost and paths. New
18
+ event logs keep exact member source text and fingerprints, the declared
19
+ flow graph and the recorded engine path for source diffs and arrow counts.
20
+ Older logs remain readable and label missing historical data explicitly.
21
+ A saved `resolved/team.yaml`, when present, can be copied to a new team for
22
+ editing; ordinary runs do not yet save a complete resolved team.
23
+ - The interface uses the CLI's Python functions, adds no Python runtime
24
+ dependencies or database, and bundles its editor, Markdown renderer and
25
+ Mermaid diagrams for offline use. It binds only to `127.0.0.1`, requires a
26
+ random access token, confines editing to the opened folder, keeps run
27
+ records read-only and never displays stored key values.
28
+ - The README's tutorial gives a browser alternative beside each CLI step.
29
+ - UI review fixes: teams under an unrelated folder named `runs` remain editable;
30
+ actual run records stay read-only. Case-only renames work on macOS, rename
31
+ selects the old filename, and new-member names are checked before creation.
32
+ Non-UTF-8 files and validation in an empty workspace show friendly messages.
33
+ - Files changed on disk reload automatically when there are no unsaved edits.
34
+ The file tree refreshes automatically and after runs finish. macOS uses its
35
+ native Trash operation, preserving Finder's Put Back support. The runs list
36
+ keeps comparison controls aligned and offers column navigation on narrow screens.
37
+ - Live transcripts append new cards and populate read context on demand;
38
+ member-version history is cached in memory and invalidated when run files change.
39
+ The historical diagram helpers have been reformatted for readability.
40
+ - **Two email tools**: `read_email` hands a member one email from a folder of
41
+ `.eml` files in the package (`mailbox`, `max_chars`); `save_draft` saves a
42
+ reply in the run's `work/outbox/` and never sends it. By default a draft can
43
+ answer only the email's sender (`reply_only: false` lifts that).
44
+ - Runs started in the same second get their own folders instead of failing
45
+ with `FileExistsError`.
46
+
7
47
  ## 0.0.3
8
48
 
9
49
  - **Backup models**: `model:` and `default_model:` may name several models,
@@ -5,7 +5,17 @@ TeamRunner is licensed under GPL-3.0-or-later (see LICENSE). The shipped
5
5
  examples, the files `teamrunner new` writes and the authoring guide are
6
6
  MIT-0 (see examples/LICENSE).
7
7
 
8
- TeamRunner bundles no third-party code. It depends on these packages, which
8
+ TeamRunner bundles CodeMirror 5.65.21 (MIT), by Marijn Haverbeke and others,
9
+ for offline browser editing. Its licence and pinned source information are
10
+ in src/teamrunner/ui/static/vendor/LICENSE and SOURCE.txt.
11
+ The offline run viewer also bundles Marked 18.1.3 (MIT) and DOMPurify
12
+ 3.4.17 (Apache-2.0 OR MPL-2.0). Their licences and pinned source information
13
+ are in the same vendor folder, under marked-* and dompurify-*.
14
+
15
+ Offline flow diagrams bundle Mermaid 12.3.0 (MIT); its licence, source
16
+ information and bundled dependency notices are in vendor/mermaid-*.
17
+
18
+ It depends on these packages, which
9
19
  pip installs separately, each under its own licence:
10
20
 
11
21
  LiteLLM MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: teamrunner
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: The experimentation platform for agent organizations — run multi-agent orchestrations and factorial experiments entirely from text files.
5
5
  Project-URL: Homepage, https://github.com/teamrunner/teamrunner
6
6
  Project-URL: Repository, https://github.com/teamrunner/teamrunner
@@ -68,6 +68,7 @@ No Python. No graph-building API. No framework vocabulary. If you can edit a tex
68
68
  - [Why we built it](#why-we-built-it)
69
69
  - [The five ideas](#the-five-ideas)
70
70
  - [Tutorial: your first debate in 10 minutes](#tutorial-your-first-debate-in-10-minutes)
71
+ - [Use TeamRunner in your browser](#use-teamrunner-in-your-browser)
71
72
  - [What lands in a run folder](#what-lands-in-a-run-folder)
72
73
  - [What your runs keep, and where your words go](#what-your-runs-keep-and-where-your-words-go)
73
74
  - [Going further: the worked examples](#going-further-the-worked-examples)
@@ -164,7 +165,7 @@ Two more ideas are designed but not built yet — a whiteboard for who sees what
164
165
 
165
166
  We'll build a three-agent debate, run it, give it rounds, a fact checker and the web. Zero code throughout. Turning it into a 40-run experiment comes with 0.2: see [PREVIEW.md](PREVIEW.md#experiments-v02).
166
167
 
167
- > Everything in this tutorial works in the preview. See [Status](#status-and-roadmap).
168
+ > Every step works in a terminal, and from 0.0.4 also in your browser: see [Use TeamRunner in your browser](#use-teamrunner-in-your-browser).
168
169
 
169
170
  ### Step 0 — install
170
171
 
@@ -185,6 +186,8 @@ Keys stay with you, never with your teams: team folders get copied, zipped and s
185
186
 
186
187
  Models on your own machine work too, free: install [Ollama](https://ollama.com), pull a model (`ollama pull gpt-oss:20b`), and write `default_model: ollama/gpt-oss:20b` in `team.yaml`. If Ollama runs somewhere other than this computer, put its address in the key file as `OLLAMA_API_BASE`. Choose one of about 20 billion parameters or more: the smallest models often fail at exactly what teams need most — answering in a fixed form and asking toolkeepers properly — so they test the wiring better than they stand in for a real team. Before the first turn of every run, TeamRunner checks that each model your team names is available and lists every problem it finds in one table, so you never pay for half a run. `teamrunner validate --online` runs the same check without a run.
187
188
 
189
+ Or open `teamrunner ui` and choose **Set up keys** after selecting a team. It saves keys to the same key file and shows whether each key is present, never its saved value. **Check keys and models online** is a separate check.
190
+
188
191
  ### Step 1 — make a team
189
192
 
190
193
  ```bash
@@ -199,6 +202,8 @@ Two other ways to start, when you'd rather not begin from nothing:
199
202
  - `teamrunner new my_debate --from fact_checked` copies one of the examples that ship with TeamRunner (or any team folder you name), with fresh ids; each file remembers what it was copied from.
200
203
  - `teamrunner new my_debate --describe "two debaters argue a topic, a fact checker checks each speech, and a judge decides"` asks a model to write the team for you. It checks its own work with `validate`, never overwrites files, tells you what it cost, and ends by printing `teamrunner explain`, so you can read what it made before you run it. (If you work with an AI coding assistant, `teamrunner skill` gives it the same instructions: it installs a skill for Claude Code and Codex, and `teamrunner skill --agents-md <folder>` writes an `AGENTS.md` for other assistants that read one.)
201
204
 
205
+ Or run `teamrunner ui` in the folder where you want to keep your teams. Choose **New team**, enter `debate`, and select an empty team or a shipped example. Select `team.yaml` on the left to edit it.
206
+
202
207
  This tutorial starts from nothing, so you see every file.
203
208
 
204
209
  `team.yaml` is the only required file. It marks the folder as a team, says what the team does, and names the model its agents run on:
@@ -220,6 +225,8 @@ Two things worth knowing now:
220
225
 
221
226
  An agent file is Markdown. Frontmatter configures, body prompts.
222
227
 
228
+ In the browser, select the team, choose **New member** for each Markdown file below, and paste its contents into the editor. **Save** (Ctrl/Cmd+S), then use **Show as it will run** to read the instructions with inputs filled in.
229
+
223
230
  ```markdown
224
231
  <!-- advocate.md -->
225
232
  ---
@@ -314,6 +321,8 @@ teamrunner run . --message "Four-day weeks make teams more productive"
314
321
 
315
322
  The first runs the default topic; the second replaces it. Either way, the run log records the topic and whether it was given or the default.
316
323
 
324
+ Or do the editing, validation and running in `teamrunner ui .`: **Validate** checks the same files, and clicking a problem opens its file at the line to fix. Choose **Run…**, keep **Rehearse (free, no model)** checked for a free test, and start. To use a model, clear that checkbox; choose team defaults, a message, a file or a task file. The page shows the conversation as it happens and a **Stop run** button.
325
+
317
326
  What you see while it runs (the words are invented; the layout is the real one):
318
327
 
319
328
  ```
@@ -341,6 +350,8 @@ Each turn shows who spoke, what they decided, how long it took, what it cost and
341
350
 
342
351
  Later, `teamrunner runs` lists every run of this package — when, which team, the input, the result (`winner = advocate`), status, time and cost — and `teamrunner runs latest` (or `teamrunner runs 14:32`) shows one again exactly as it looked while it ran.
343
352
 
353
+ In the browser, choose **Runs** to reopen the conversation, output, path or record. Select two runs with the checkboxes and choose **Compare selected runs** to see member versions, outputs, cost and paths side by side.
354
+
344
355
  That's a working multi-agent system: four text files, one command.
345
356
 
346
357
  ### Step 5 — take control of the flow
@@ -364,7 +375,7 @@ The flow grammar is deliberately tiny — it is a whiteboard sketch that happens
364
375
  - **[v0.3]** `[a, b]` — **at the same time**: `editor -> [reviewer_1, reviewer_2] -> editor` sends the paper to both reviewers at once, and the editor waits for both reports. Neither sees the other's work in progress.
365
376
  - `done` ends the flow — or, inside a named block, just that block; `stop` ends the whole run immediately.
366
377
 
367
- Now the debate runs `advocate → critic → advocate → critic → advocate → critic → judge`. To see it as a picture, `teamrunner explain --diagram` opens the flow as a flowchart in your browser.
378
+ Now the debate runs `advocate → critic → advocate → critic → advocate → critic → judge`. To see it as a picture, `teamrunner explain --diagram` opens the flow as a flowchart in your browser. Or, in the local interface, refresh the file tree after adding `chair.md`, select it, and choose **Flow diagram**. Save an edit to redraw the diagram and plain-words summary.
368
379
 
369
380
  ### Step 6 — add a fact checker and a decision
370
381
 
@@ -459,10 +470,37 @@ A toolkeeper step means the member just before it must end its turn with a reque
459
470
 
460
471
  `teamrunner explain .` prints, in plain English, who may use which toolkeeper and on what terms.
461
472
 
462
- TeamRunner 0.1 has four built-in tools: `web_search_brave` (Brave Search), `read_page` (a link in, the page's text out), `read_file` (a file the run was given with `--file`, its text out) and `run_code` (Python in a sandbox: no internet unless the toolkeeper allows it, and files written only in the run's own work folder). The sandbox is Python running in WebAssembly, and it installs with TeamRunner — nothing else to set up; it can use packages built for it (numpy, pandas, scipy, sympy and many more), but not every package. `examples/fact_checked` gives the fact checker a second toolkeeper, `sandbox.md`, so it can check the sums in a claim by running them. A toolkeeper holds one tool in 0.1, so its limits always mean one thing; a member who needs two tools asks two toolkeepers, as the fact checker does here. `examples/reference_check` uses `read_page` and `read_file`: it checks every reference in a paper.
473
+ TeamRunner 0.1 has six built-in tools: `web_search_brave` (Brave Search), `read_page` (a link in, the page's text out), `read_file` (a file the run was given with `--file`, its text out), `run_code` (Python in a sandbox: no internet unless the toolkeeper allows it, and files written only in the run's own work folder), `read_email` (one email from a folder of `.eml` files in the package) and `save_draft` (a reply saved in the run's `work/outbox/`, never sent, addressed only to the email's sender unless `reply_only: false`). The sandbox is Python running in WebAssembly, and it installs with TeamRunner — nothing else to set up; it can use packages built for it (numpy, pandas, scipy, sympy and many more), but not every package. `examples/fact_checked` gives the fact checker a second toolkeeper, `sandbox.md`, so it can check the sums in a claim by running them. A toolkeeper holds one tool in 0.1, so its limits always mean one thing; a member who needs two tools asks two toolkeepers, as the fact checker does here. `examples/reference_check` uses `read_page` and `read_file`: it checks every reference in a paper.
463
474
 
464
475
  ---
465
476
 
477
+ ## Use TeamRunner in your browser
478
+
479
+ From 0.0.4, open a team folder with:
480
+
481
+ ```bash
482
+ teamrunner ui debate
483
+ ```
484
+
485
+ `teamrunner ui` opens the current folder when you leave out `debate`; `teamrunner gui` is an alias. To create teams, open their parent folder instead. The command opens a browser tab and prints its local address. Keep that terminal running while you work; press Ctrl+C there to stop the server. If the tab does not open, paste the full printed address, including its token, into your browser.
486
+
487
+ The page works on the same files as the CLI. Choose a team or file on the left. You can create an empty team or copy a shipped example, add members, edit and save, rename members, or move files to the system trash. The editor warns about unsaved edits and files changed by another editor. Validation points to the file, line and suggested fix. **Show as it will run** fills in a member's instructions; **Flow diagram** shows the team's flow and summary. For a ready-made flow to edit, start from the `fact_checked` example.
488
+
489
+ **Run…** accepts team defaults, a message, a file or a YAML task file. Rehearsal is selected by default for a team with no prior run; it uses placeholder words and random answers, needs no key and costs nothing. A model run uses your configured provider and its normal charges. The interface validates before starting, allows one active run at a time, streams the recorded events, and can stop the run cleanly. Key setup uses `~/.teamrunner/.env`; stored key values are never displayed.
490
+
491
+ **Runs** is the archive. Filter by team or status, search the message and output, then open a run:
492
+
493
+ - **Conversation:** member messages, decisions, tool calls, retries, tokens, models and cost, with a toggle for what each member could read.
494
+ - **Path:** the saved flow with taken arrows highlighted and transition counts. Older logs without a saved engine path show only observed completed-turn transitions, labelled accordingly.
495
+ - **Output:** the rendered `output.md`.
496
+ - **Record:** inputs, numbered member versions, recorded settings and files. Version numbers come from the available run history.
497
+
498
+ Select two runs to compare their outputs, cost, time, paths and member versions. Exact member source diffs are available for new logs that record the source and its fingerprint; older logs show their recorded prompts, which may include substituted inputs.
499
+
500
+ **Re-run with these inputs…** uses the current team with the old run's input values and copied input files. It does not restore old member versions. **Edit a copy of the saved team…** appears only when a run contains `resolved/team.yaml`; it creates an independent team with fresh IDs and lineage. Open the teams' parent folder to give that copy a place outside the original team. Ordinary runs do not yet save a complete resolved team, so their original files cannot be reconstructed from prompts alone.
501
+
502
+ Every run has **Open folder** and raw-file download links. The editor stays inside the folder you opened, and run records remain read-only. The server listens only on this computer at `127.0.0.1`, with a random access token. Editor, Markdown and diagram assets are bundled for offline use; model calls and online checks still need their usual services. The page adds no database or separate project format, and shows equivalent CLI commands alongside actions.
503
+
466
504
  ## What lands in a run folder
467
505
 
468
506
  Every run is logged in a `runs/` folder at the top of the package you ran — even when you run just one subteam (**[v0.3]**). The folder is git-ignored, so sharing the package never shares its history.
@@ -483,12 +521,12 @@ runs/01JZXG7T3M9QKWP5RVB2C8HD6N/
483
521
 
484
522
  | Event | What it records |
485
523
  |---|---|
486
- | `run_started` | the inputs, and every member exactly as it will run — model, settings, prompt |
524
+ | `run_started` | the inputs, and every member exactly as it will run — model, settings, prompt; from 0.0.4, also each member's source text, content fingerprints and the declared flow graph |
487
525
  | `agent_message` | from, **to** (the *realized* topology), stage, attempt, **what it could read**, everything it said, typed outputs, the model actually used, tokens, cost, time |
488
526
  | `route_decision` | who decided, which option, which arrow — the audit trail for "no hidden if-statements" |
489
527
  | `tool_call` | who asked, which toolkeeper, the toolkeeper's yes or no, tool, arguments, the full result (for a search, how many results came back, unless `keep_results: true`) |
490
528
  | `state_write` **[v0.3]** | item, old → new, writer |
491
- | `check`, `retry`, `warning`, `error`, `run_finished` | the pre-run check, retries, warnings, failures, and the totals |
529
+ | `check`, `retry`, `warning`, `error`, `run_finished` | the pre-run check, retries, warnings, failures, and the totals; from 0.0.4, `run_finished` also saves the path taken, for arrow counts |
492
530
 
493
531
  A field never changes its meaning between versions — later versions only add — so an analysis script you write today keeps working.
494
532
 
@@ -567,9 +605,10 @@ The recasting campaign is the point, not decoration: each one is run *before* th
567
605
  |---|---|
568
606
  | **Design** | Spec drafted; paper recasts written as pure text ✅ |
569
607
  | **0.1 · The org package** ← now: in preview as 0.0.x | A folder that gets a job done: validation with friendly errors, its own runner, the event log, toolkeepers and four built-in tools, rehearsals, `run`, `runs`, `explain`, `new`, the authoring skill ✅ |
608
+ | **Before 0.2 · Local browser interface** | In 0.0.4 ✅ `teamrunner ui` / `gui`: edit, validate, rehearse or run, browse recorded conversations and paths, compare two runs |
570
609
  | **0.2 · Experiments** | Experiments outside the package (`base:`), factorial designs, seeded replications, manifests, response cache, the answer-key metric, `teamrunner experiment`, export; `promote` / `export`. First real result: leader vs. flat |
571
610
  | **0.3 · Richer organizations** | Managers and rules-table chairs, `whiteboard.yaml` (private information, sealed and conditional visibility, addressed messages), subteams, parallel work, announcers; the recasts run for real, and the Invisible Orchestrators replication |
572
- | **0.4+ · Later** | LLM-as-judge metrics, `teamrunner ui` (run browser, cross-run comparison, communication-graph visualization), template library of canonical org structures, human-in-the-loop, automated improvement, running packages in production |
611
+ | **0.4+ · Later** | LLM-as-judge metrics, communication-network statistics and visualization, template library of canonical org structures, human-in-the-loop, automated improvement, running packages in production |
573
612
 
574
613
  Human-in-the-loop nodes (`kind: human` — the runner pauses and asks a person) are for mixed human–agent studies.
575
614
 
@@ -597,7 +636,7 @@ Inside a package, these names are reserved. **Every one of them is optional exce
597
636
  | `chair.md` | How a protocol team is run: the script, as the file's body, or a `kind: rules` table **[v0.3]** | Everyone may talk to everyone; members act in alphabetical filename order, one turn each |
598
637
  | `manager.md` **[v0.3]** | How a managed team is run: an LLM that decides who acts next; only it may use input placeholders. A team has at most one of `chair.md` and `manager.md` | — |
599
638
  | `whiteboard.yaml` **[v0.3]** | Who sees what: declared items with `read_by` / `written_by`, and who can read what each member says (`said`) | Everyone reads everything said; built-in items only (e.g. `stage`) |
600
- | `tools/` | Reserved for a package's own tools, which come after 0.1. In 0.1 toolkeepers use the built-ins: `web_search_brave`, `read_page`, `read_file`, `run_code` | Built-ins only |
639
+ | `tools/` | Reserved for a package's own tools, which come after 0.1. In 0.1 toolkeepers use the built-ins: `web_search_brave`, `read_page`, `read_file`, `run_code`, `read_email`, `save_draft` | Built-ins only |
601
640
  | `models/` | Model profiles: a model and its settings in one `.yaml`, named by agents as `model: careful.yaml` or by `team.yaml` as `default_model:` | Agents name provider models directly |
602
641
  | `runs/` | Where every run of this package is logged, whichever team you started; git-ignored | Created on the first run |
603
642
  | *subfolder with `team.yaml`* **[v0.3]** | A subteam — a node in the parent flow, with its own whiteboard; the parent hears only its report (`report_from:`) | — |
@@ -33,6 +33,7 @@ No Python. No graph-building API. No framework vocabulary. If you can edit a tex
33
33
  - [Why we built it](#why-we-built-it)
34
34
  - [The five ideas](#the-five-ideas)
35
35
  - [Tutorial: your first debate in 10 minutes](#tutorial-your-first-debate-in-10-minutes)
36
+ - [Use TeamRunner in your browser](#use-teamrunner-in-your-browser)
36
37
  - [What lands in a run folder](#what-lands-in-a-run-folder)
37
38
  - [What your runs keep, and where your words go](#what-your-runs-keep-and-where-your-words-go)
38
39
  - [Going further: the worked examples](#going-further-the-worked-examples)
@@ -129,7 +130,7 @@ Two more ideas are designed but not built yet — a whiteboard for who sees what
129
130
 
130
131
  We'll build a three-agent debate, run it, give it rounds, a fact checker and the web. Zero code throughout. Turning it into a 40-run experiment comes with 0.2: see [PREVIEW.md](PREVIEW.md#experiments-v02).
131
132
 
132
- > Everything in this tutorial works in the preview. See [Status](#status-and-roadmap).
133
+ > Every step works in a terminal, and from 0.0.4 also in your browser: see [Use TeamRunner in your browser](#use-teamrunner-in-your-browser).
133
134
 
134
135
  ### Step 0 — install
135
136
 
@@ -150,6 +151,8 @@ Keys stay with you, never with your teams: team folders get copied, zipped and s
150
151
 
151
152
  Models on your own machine work too, free: install [Ollama](https://ollama.com), pull a model (`ollama pull gpt-oss:20b`), and write `default_model: ollama/gpt-oss:20b` in `team.yaml`. If Ollama runs somewhere other than this computer, put its address in the key file as `OLLAMA_API_BASE`. Choose one of about 20 billion parameters or more: the smallest models often fail at exactly what teams need most — answering in a fixed form and asking toolkeepers properly — so they test the wiring better than they stand in for a real team. Before the first turn of every run, TeamRunner checks that each model your team names is available and lists every problem it finds in one table, so you never pay for half a run. `teamrunner validate --online` runs the same check without a run.
152
153
 
154
+ Or open `teamrunner ui` and choose **Set up keys** after selecting a team. It saves keys to the same key file and shows whether each key is present, never its saved value. **Check keys and models online** is a separate check.
155
+
153
156
  ### Step 1 — make a team
154
157
 
155
158
  ```bash
@@ -164,6 +167,8 @@ Two other ways to start, when you'd rather not begin from nothing:
164
167
  - `teamrunner new my_debate --from fact_checked` copies one of the examples that ship with TeamRunner (or any team folder you name), with fresh ids; each file remembers what it was copied from.
165
168
  - `teamrunner new my_debate --describe "two debaters argue a topic, a fact checker checks each speech, and a judge decides"` asks a model to write the team for you. It checks its own work with `validate`, never overwrites files, tells you what it cost, and ends by printing `teamrunner explain`, so you can read what it made before you run it. (If you work with an AI coding assistant, `teamrunner skill` gives it the same instructions: it installs a skill for Claude Code and Codex, and `teamrunner skill --agents-md <folder>` writes an `AGENTS.md` for other assistants that read one.)
166
169
 
170
+ Or run `teamrunner ui` in the folder where you want to keep your teams. Choose **New team**, enter `debate`, and select an empty team or a shipped example. Select `team.yaml` on the left to edit it.
171
+
167
172
  This tutorial starts from nothing, so you see every file.
168
173
 
169
174
  `team.yaml` is the only required file. It marks the folder as a team, says what the team does, and names the model its agents run on:
@@ -185,6 +190,8 @@ Two things worth knowing now:
185
190
 
186
191
  An agent file is Markdown. Frontmatter configures, body prompts.
187
192
 
193
+ In the browser, select the team, choose **New member** for each Markdown file below, and paste its contents into the editor. **Save** (Ctrl/Cmd+S), then use **Show as it will run** to read the instructions with inputs filled in.
194
+
188
195
  ```markdown
189
196
  <!-- advocate.md -->
190
197
  ---
@@ -279,6 +286,8 @@ teamrunner run . --message "Four-day weeks make teams more productive"
279
286
 
280
287
  The first runs the default topic; the second replaces it. Either way, the run log records the topic and whether it was given or the default.
281
288
 
289
+ Or do the editing, validation and running in `teamrunner ui .`: **Validate** checks the same files, and clicking a problem opens its file at the line to fix. Choose **Run…**, keep **Rehearse (free, no model)** checked for a free test, and start. To use a model, clear that checkbox; choose team defaults, a message, a file or a task file. The page shows the conversation as it happens and a **Stop run** button.
290
+
282
291
  What you see while it runs (the words are invented; the layout is the real one):
283
292
 
284
293
  ```
@@ -306,6 +315,8 @@ Each turn shows who spoke, what they decided, how long it took, what it cost and
306
315
 
307
316
  Later, `teamrunner runs` lists every run of this package — when, which team, the input, the result (`winner = advocate`), status, time and cost — and `teamrunner runs latest` (or `teamrunner runs 14:32`) shows one again exactly as it looked while it ran.
308
317
 
318
+ In the browser, choose **Runs** to reopen the conversation, output, path or record. Select two runs with the checkboxes and choose **Compare selected runs** to see member versions, outputs, cost and paths side by side.
319
+
309
320
  That's a working multi-agent system: four text files, one command.
310
321
 
311
322
  ### Step 5 — take control of the flow
@@ -329,7 +340,7 @@ The flow grammar is deliberately tiny — it is a whiteboard sketch that happens
329
340
  - **[v0.3]** `[a, b]` — **at the same time**: `editor -> [reviewer_1, reviewer_2] -> editor` sends the paper to both reviewers at once, and the editor waits for both reports. Neither sees the other's work in progress.
330
341
  - `done` ends the flow — or, inside a named block, just that block; `stop` ends the whole run immediately.
331
342
 
332
- Now the debate runs `advocate → critic → advocate → critic → advocate → critic → judge`. To see it as a picture, `teamrunner explain --diagram` opens the flow as a flowchart in your browser.
343
+ Now the debate runs `advocate → critic → advocate → critic → advocate → critic → judge`. To see it as a picture, `teamrunner explain --diagram` opens the flow as a flowchart in your browser. Or, in the local interface, refresh the file tree after adding `chair.md`, select it, and choose **Flow diagram**. Save an edit to redraw the diagram and plain-words summary.
333
344
 
334
345
  ### Step 6 — add a fact checker and a decision
335
346
 
@@ -424,10 +435,37 @@ A toolkeeper step means the member just before it must end its turn with a reque
424
435
 
425
436
  `teamrunner explain .` prints, in plain English, who may use which toolkeeper and on what terms.
426
437
 
427
- TeamRunner 0.1 has four built-in tools: `web_search_brave` (Brave Search), `read_page` (a link in, the page's text out), `read_file` (a file the run was given with `--file`, its text out) and `run_code` (Python in a sandbox: no internet unless the toolkeeper allows it, and files written only in the run's own work folder). The sandbox is Python running in WebAssembly, and it installs with TeamRunner — nothing else to set up; it can use packages built for it (numpy, pandas, scipy, sympy and many more), but not every package. `examples/fact_checked` gives the fact checker a second toolkeeper, `sandbox.md`, so it can check the sums in a claim by running them. A toolkeeper holds one tool in 0.1, so its limits always mean one thing; a member who needs two tools asks two toolkeepers, as the fact checker does here. `examples/reference_check` uses `read_page` and `read_file`: it checks every reference in a paper.
438
+ TeamRunner 0.1 has six built-in tools: `web_search_brave` (Brave Search), `read_page` (a link in, the page's text out), `read_file` (a file the run was given with `--file`, its text out), `run_code` (Python in a sandbox: no internet unless the toolkeeper allows it, and files written only in the run's own work folder), `read_email` (one email from a folder of `.eml` files in the package) and `save_draft` (a reply saved in the run's `work/outbox/`, never sent, addressed only to the email's sender unless `reply_only: false`). The sandbox is Python running in WebAssembly, and it installs with TeamRunner — nothing else to set up; it can use packages built for it (numpy, pandas, scipy, sympy and many more), but not every package. `examples/fact_checked` gives the fact checker a second toolkeeper, `sandbox.md`, so it can check the sums in a claim by running them. A toolkeeper holds one tool in 0.1, so its limits always mean one thing; a member who needs two tools asks two toolkeepers, as the fact checker does here. `examples/reference_check` uses `read_page` and `read_file`: it checks every reference in a paper.
428
439
 
429
440
  ---
430
441
 
442
+ ## Use TeamRunner in your browser
443
+
444
+ From 0.0.4, open a team folder with:
445
+
446
+ ```bash
447
+ teamrunner ui debate
448
+ ```
449
+
450
+ `teamrunner ui` opens the current folder when you leave out `debate`; `teamrunner gui` is an alias. To create teams, open their parent folder instead. The command opens a browser tab and prints its local address. Keep that terminal running while you work; press Ctrl+C there to stop the server. If the tab does not open, paste the full printed address, including its token, into your browser.
451
+
452
+ The page works on the same files as the CLI. Choose a team or file on the left. You can create an empty team or copy a shipped example, add members, edit and save, rename members, or move files to the system trash. The editor warns about unsaved edits and files changed by another editor. Validation points to the file, line and suggested fix. **Show as it will run** fills in a member's instructions; **Flow diagram** shows the team's flow and summary. For a ready-made flow to edit, start from the `fact_checked` example.
453
+
454
+ **Run…** accepts team defaults, a message, a file or a YAML task file. Rehearsal is selected by default for a team with no prior run; it uses placeholder words and random answers, needs no key and costs nothing. A model run uses your configured provider and its normal charges. The interface validates before starting, allows one active run at a time, streams the recorded events, and can stop the run cleanly. Key setup uses `~/.teamrunner/.env`; stored key values are never displayed.
455
+
456
+ **Runs** is the archive. Filter by team or status, search the message and output, then open a run:
457
+
458
+ - **Conversation:** member messages, decisions, tool calls, retries, tokens, models and cost, with a toggle for what each member could read.
459
+ - **Path:** the saved flow with taken arrows highlighted and transition counts. Older logs without a saved engine path show only observed completed-turn transitions, labelled accordingly.
460
+ - **Output:** the rendered `output.md`.
461
+ - **Record:** inputs, numbered member versions, recorded settings and files. Version numbers come from the available run history.
462
+
463
+ Select two runs to compare their outputs, cost, time, paths and member versions. Exact member source diffs are available for new logs that record the source and its fingerprint; older logs show their recorded prompts, which may include substituted inputs.
464
+
465
+ **Re-run with these inputs…** uses the current team with the old run's input values and copied input files. It does not restore old member versions. **Edit a copy of the saved team…** appears only when a run contains `resolved/team.yaml`; it creates an independent team with fresh IDs and lineage. Open the teams' parent folder to give that copy a place outside the original team. Ordinary runs do not yet save a complete resolved team, so their original files cannot be reconstructed from prompts alone.
466
+
467
+ Every run has **Open folder** and raw-file download links. The editor stays inside the folder you opened, and run records remain read-only. The server listens only on this computer at `127.0.0.1`, with a random access token. Editor, Markdown and diagram assets are bundled for offline use; model calls and online checks still need their usual services. The page adds no database or separate project format, and shows equivalent CLI commands alongside actions.
468
+
431
469
  ## What lands in a run folder
432
470
 
433
471
  Every run is logged in a `runs/` folder at the top of the package you ran — even when you run just one subteam (**[v0.3]**). The folder is git-ignored, so sharing the package never shares its history.
@@ -448,12 +486,12 @@ runs/01JZXG7T3M9QKWP5RVB2C8HD6N/
448
486
 
449
487
  | Event | What it records |
450
488
  |---|---|
451
- | `run_started` | the inputs, and every member exactly as it will run — model, settings, prompt |
489
+ | `run_started` | the inputs, and every member exactly as it will run — model, settings, prompt; from 0.0.4, also each member's source text, content fingerprints and the declared flow graph |
452
490
  | `agent_message` | from, **to** (the *realized* topology), stage, attempt, **what it could read**, everything it said, typed outputs, the model actually used, tokens, cost, time |
453
491
  | `route_decision` | who decided, which option, which arrow — the audit trail for "no hidden if-statements" |
454
492
  | `tool_call` | who asked, which toolkeeper, the toolkeeper's yes or no, tool, arguments, the full result (for a search, how many results came back, unless `keep_results: true`) |
455
493
  | `state_write` **[v0.3]** | item, old → new, writer |
456
- | `check`, `retry`, `warning`, `error`, `run_finished` | the pre-run check, retries, warnings, failures, and the totals |
494
+ | `check`, `retry`, `warning`, `error`, `run_finished` | the pre-run check, retries, warnings, failures, and the totals; from 0.0.4, `run_finished` also saves the path taken, for arrow counts |
457
495
 
458
496
  A field never changes its meaning between versions — later versions only add — so an analysis script you write today keeps working.
459
497
 
@@ -532,9 +570,10 @@ The recasting campaign is the point, not decoration: each one is run *before* th
532
570
  |---|---|
533
571
  | **Design** | Spec drafted; paper recasts written as pure text ✅ |
534
572
  | **0.1 · The org package** ← now: in preview as 0.0.x | A folder that gets a job done: validation with friendly errors, its own runner, the event log, toolkeepers and four built-in tools, rehearsals, `run`, `runs`, `explain`, `new`, the authoring skill ✅ |
573
+ | **Before 0.2 · Local browser interface** | In 0.0.4 ✅ `teamrunner ui` / `gui`: edit, validate, rehearse or run, browse recorded conversations and paths, compare two runs |
535
574
  | **0.2 · Experiments** | Experiments outside the package (`base:`), factorial designs, seeded replications, manifests, response cache, the answer-key metric, `teamrunner experiment`, export; `promote` / `export`. First real result: leader vs. flat |
536
575
  | **0.3 · Richer organizations** | Managers and rules-table chairs, `whiteboard.yaml` (private information, sealed and conditional visibility, addressed messages), subteams, parallel work, announcers; the recasts run for real, and the Invisible Orchestrators replication |
537
- | **0.4+ · Later** | LLM-as-judge metrics, `teamrunner ui` (run browser, cross-run comparison, communication-graph visualization), template library of canonical org structures, human-in-the-loop, automated improvement, running packages in production |
576
+ | **0.4+ · Later** | LLM-as-judge metrics, communication-network statistics and visualization, template library of canonical org structures, human-in-the-loop, automated improvement, running packages in production |
538
577
 
539
578
  Human-in-the-loop nodes (`kind: human` — the runner pauses and asks a person) are for mixed human–agent studies.
540
579
 
@@ -562,7 +601,7 @@ Inside a package, these names are reserved. **Every one of them is optional exce
562
601
  | `chair.md` | How a protocol team is run: the script, as the file's body, or a `kind: rules` table **[v0.3]** | Everyone may talk to everyone; members act in alphabetical filename order, one turn each |
563
602
  | `manager.md` **[v0.3]** | How a managed team is run: an LLM that decides who acts next; only it may use input placeholders. A team has at most one of `chair.md` and `manager.md` | — |
564
603
  | `whiteboard.yaml` **[v0.3]** | Who sees what: declared items with `read_by` / `written_by`, and who can read what each member says (`said`) | Everyone reads everything said; built-in items only (e.g. `stage`) |
565
- | `tools/` | Reserved for a package's own tools, which come after 0.1. In 0.1 toolkeepers use the built-ins: `web_search_brave`, `read_page`, `read_file`, `run_code` | Built-ins only |
604
+ | `tools/` | Reserved for a package's own tools, which come after 0.1. In 0.1 toolkeepers use the built-ins: `web_search_brave`, `read_page`, `read_file`, `run_code`, `read_email`, `save_draft` | Built-ins only |
566
605
  | `models/` | Model profiles: a model and its settings in one `.yaml`, named by agents as `model: careful.yaml` or by `team.yaml` as `default_model:` | Agents name provider models directly |
567
606
  | `runs/` | Where every run of this package is logged, whichever team you started; git-ignored | Created on the first run |
568
607
  | *subfolder with `team.yaml`* **[v0.3]** | A subteam — a node in the parent flow, with its own whiteboard; the parent hears only its report (`report_from:`) | — |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "teamrunner"
7
- version = "0.0.3"
7
+ version = "0.0.4"
8
8
  description = "The experimentation platform for agent organizations — run multi-agent orchestrations and factorial experiments entirely from text files."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later AND MIT-0"
@@ -11,7 +11,7 @@ https://github.com/teamrunner/teamrunner.
11
11
 
12
12
  import os as _os
13
13
 
14
- __version__ = "0.0.3"
14
+ __version__ = "0.0.4"
15
15
 
16
16
  # LiteLLM fetches its price table from the internet on import unless told not to. TeamRunner
17
17
  # uses the table that came with the installed LiteLLM, so a run's prices depend only on what
@@ -153,6 +153,8 @@ I search the web. Send me a query and I return the top five results.
153
153
  | `read_page` | a link → the page's text, PDFs too | `domains`, `max_chars`, `types: [pdf]` |
154
154
  | `read_file` | a file the run was given with `--file` → its text | `max_chars`, `types: [pdf]` |
155
155
  | `run_code` | Python → its output and errors, in a sandbox | `network: true` (off by default), `paths: [folder]` to read — the code finds it at `/package/<folder>` |
156
+ | `read_email` | an email's id → its sender, recipients, date, subject and text, from a folder of `.eml` files in the package | `mailbox: <folder>` (`mailbox` if not written), `max_chars` |
157
+ | `save_draft` | an email's id and a reply → the reply saved in the run's `work/outbox/`, never sent; it answers only the email's sender | `mailbox: <folder>`, `max_chars`, `reply_only: false` (lets a draft name any address) |
156
158
 
157
159
  Every tool also understands `calls_per_turn` (5 if not written),
158
160
  `calls_per_run` and `timeout` (seconds). Any other limit is an error.
@@ -298,6 +298,97 @@ async def run_code(args: dict, guardrails: dict, ctx: Context) -> Answer:
298
298
  service=f"pyodide {sandbox.PYODIDE} (Python {ran.python}), deno {version('deno')}")
299
299
 
300
300
 
301
+ # --- read_email and save_draft: a mailbox folder of .eml files --------------------------
302
+
303
+ EMAIL_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,99}")
304
+ DEFAULT_MAILBOX = "mailbox"
305
+
306
+
307
+ def mailbox_folder(guardrails: dict, ctx: Context) -> Path:
308
+ """The mailbox this toolkeeper reads: a folder of .eml files, written relative to the package."""
309
+ written = str(guardrails.get("mailbox", DEFAULT_MAILBOX))
310
+ folder = Path(written).expanduser()
311
+ if not folder.is_absolute() and ctx.package_top is not None:
312
+ folder = ctx.package_top / folder
313
+ if not folder.is_dir():
314
+ raise Refused(f"the mailbox {written} that this toolkeeper reads does not exist")
315
+ return folder
316
+
317
+
318
+ def find_email(asked: str, guardrails: dict, ctx: Context):
319
+ """One email in the mailbox, by its id (its file name without .eml)."""
320
+ from email import policy
321
+ from email.parser import BytesParser
322
+ asked = asked.strip().removesuffix(".eml")
323
+ if not EMAIL_ID.fullmatch(asked):
324
+ raise Refused("an email is asked for by its id, such as m001")
325
+ path = mailbox_folder(guardrails, ctx) / f"{asked}.eml"
326
+ if not path.is_file():
327
+ raise Refused(f"there is no email {asked} in the mailbox")
328
+ return asked, BytesParser(policy=policy.default).parsebytes(path.read_bytes())
329
+
330
+
331
+ def email_text(message) -> str:
332
+ """The headers a reader needs, the body as plain text, and the names of any attachments."""
333
+ lines = [f"{h}: {message[h]}" for h in ("From", "To", "Cc", "Date", "Subject") if message[h]]
334
+ part = message.get_body(preferencelist=("plain", "html"))
335
+ body = "" if part is None else part.get_content()
336
+ if part is not None and part.get_content_type() == "text/html":
337
+ body = html_text(body)
338
+ attached = [p.get_filename() or p.get_content_type() for p in message.iter_attachments()]
339
+ if attached:
340
+ lines.append("Attachments: " + ", ".join(attached))
341
+ return "\n".join(lines) + "\n\n" + body.strip()
342
+
343
+
344
+ async def read_email(args: dict, guardrails: dict, ctx: Context) -> Answer:
345
+ _, message = find_email(str(args.get("id", "")), guardrails, ctx)
346
+ return Answer(cut(email_text(message), guardrails), service="mailbox folder")
347
+
348
+
349
+ async def save_draft(args: dict, guardrails: dict, ctx: Context) -> Answer:
350
+ """A reply saved to the run's work/outbox/, never sent. By default it goes to the
351
+ email's sender and no one else (`reply_only`)."""
352
+ from email.message import EmailMessage
353
+ from email.utils import getaddresses
354
+ asked, original = find_email(str(args.get("reply_to", "")), guardrails, ctx)
355
+ body = str(args.get("body", "")).strip()
356
+ if not body:
357
+ raise Refused("the draft was empty")
358
+ limit = guardrails.get("max_chars", DEFAULT_MAX_CHARS)
359
+ if len(body) > limit:
360
+ raise Refused(f"the draft is {len(body)} characters; this outbox takes at most {limit}")
361
+ sender = original.get("Reply-To") or original.get("From") or ""
362
+ sender_address = next((a for _, a in getaddresses([sender]) if a), "")
363
+ wanted = str(args.get("to") or "").strip()
364
+ wanted_addresses = [a for _, a in getaddresses([wanted]) if a]
365
+ if wanted and guardrails.get("reply_only", True):
366
+ if [a.lower() for a in wanted_addresses] != [sender_address.lower()]:
367
+ raise Refused("this outbox only answers the sender of the email "
368
+ f"({sender_address}); it does not write to other addresses")
369
+ to = wanted if wanted and not guardrails.get("reply_only", True) else sender
370
+ if not to:
371
+ raise Refused(f"email {asked} has no sender to answer")
372
+ outbox = ctx.work_dir / "outbox"
373
+ path = outbox / f"{asked}.eml"
374
+ if path.exists():
375
+ raise Refused(f"a draft answering {asked} is already in the outbox")
376
+ draft = EmailMessage()
377
+ draft["To"] = to
378
+ if original["To"]:
379
+ draft["From"] = original["To"]
380
+ subject = str(original.get("Subject") or "")
381
+ draft["Subject"] = subject if subject.lower().startswith("re:") else f"Re: {subject}".strip()
382
+ if original["Message-ID"]:
383
+ draft["In-Reply-To"] = original["Message-ID"]
384
+ draft["X-TeamRunner"] = "draft, not sent"
385
+ draft.set_content(body + "\n", cte="8bit") # readable as it is, in any text editor
386
+ outbox.mkdir(parents=True, exist_ok=True)
387
+ path.write_bytes(bytes(draft))
388
+ return Answer(f"Saved a draft to {to} in the outbox as outbox/{asked}.eml. It has not been sent.",
389
+ service="outbox folder")
390
+
391
+
301
392
  TOOLS: dict[str, Tool] = {
302
393
  "web_search_brave": Tool("web_search_brave", "Searches the web: a query in, titles, links and snippets out.",
303
394
  {"type": "object", "properties": {"query": {"type": "string"}},
@@ -312,6 +403,18 @@ TOOLS: dict[str, Tool] = {
312
403
  "errors out. Files it writes in the current folder are kept for the rest of the run.",
313
404
  {"type": "object", "properties": {"code": {"type": "string"}},
314
405
  "required": ["code"], "additionalProperties": False}, run_code),
406
+ "read_email": Tool("read_email", "Reads one email from the mailbox: its id in, its sender, "
407
+ "recipients, date, subject and text out.",
408
+ {"type": "object", "properties": {"id": {"type": "string"}},
409
+ "required": ["id"], "additionalProperties": False}, read_email),
410
+ "save_draft": Tool("save_draft", "Saves a reply to an email in the outbox, where a person reads it "
411
+ "before anything is sent: the id of the email it answers and the reply's text in. "
412
+ "Nothing is sent.",
413
+ {"type": "object", "properties": {
414
+ "reply_to": {"type": "string", "description": "the id of the email this answers"},
415
+ "body": {"type": "string", "description": "the reply's text"},
416
+ "to": {"type": "string", "description": "who it goes to; leave out to answer the sender"}},
417
+ "required": ["reply_to", "body"], "additionalProperties": False}, save_draft),
315
418
  }
316
419
  NOT_BUILT: dict[str, str] = {} # tool -> why this version cannot run it yet
317
420
  TOOL_KEYS = {"web_search_brave": BRAVE_KEY}
@@ -23,6 +23,15 @@ from .keys import NO_KEY, PROVIDER_KEYS, SET_UP_ELSEWHERE, Keys
23
23
  Lister = Callable[[str | None, dict], "set[str] | None"]
24
24
 
25
25
 
26
+ def online_check(report):
27
+ """The explicit CLI/browser online check, without printing or prompting."""
28
+ from .build import load_team
29
+ from .keys import extra_settings, find_keys
30
+ team, _ = load_team(report.package.team.folder)
31
+ keys = find_keys(needed_keys(team), team.package_top, None)
32
+ return run_check(team, keys, LISTERS, extra_settings(), litellm_data, litellm_params), keys
33
+
34
+
26
35
  @dataclass
27
36
  class Problem:
28
37
  what: str # the model or key
@@ -213,8 +222,12 @@ def _probe(model: str, keys: Keys, listers: dict[str, Lister], settings: dict, l
213
222
  available = lists[provider]
214
223
  if isinstance(available, Exception):
215
224
  e = available
225
+ reason = str(e)
226
+ for secret in keys.values.values():
227
+ if secret:
228
+ reason = reason.replace(secret, "[key hidden]")
216
229
  return Probe(row, lambda who: Problem(
217
- model, who, f"could not reach {provider} to check it: {e}",
230
+ model, who, f"could not reach {provider} to check it: {reason}",
218
231
  "check your connection" + (", and that Ollama is running" if provider in NO_KEY else
219
232
  f", and that {key_name} is right")),
220
233
  f"{provider} could not be reached")
@@ -52,13 +52,9 @@ def print_problems(report: Report, out: Style) -> None:
52
52
  def _online_check(report, out, quiet: bool):
53
53
  """validate --online (D100): the same key and model check as `run`, and no run.
54
54
  It never asks: a key found only in the shell is used for the check, not saved."""
55
- from .build import load_team
56
- from .checks import LISTERS, litellm_data, litellm_params, needed_keys, run_check
55
+ from .checks import online_check
57
56
  from .console import Console
58
- from .keys import extra_settings, find_keys
59
- team, _ = load_team(report.package.team.folder)
60
- keys = find_keys(needed_keys(team), team.package_top, None)
61
- check = run_check(team, keys, LISTERS, extra_settings(), litellm_data, litellm_params)
57
+ check, keys = online_check(report)
62
58
  if not quiet:
63
59
  Console(False, sys.stdout).check(check, keys)
64
60
  return check
@@ -356,6 +352,11 @@ def cmd_skill(args) -> int:
356
352
  return 1 if failed else 0
357
353
 
358
354
 
355
+ def cmd_ui(args) -> int:
356
+ from .ui.server import serve
357
+ return serve(Path(args.folder))
358
+
359
+
359
360
  def main(argv=None) -> int:
360
361
  for stream in (sys.stdout, sys.stderr): # Windows sends piped output in its old code page, without ✓ or ≈
361
362
  if hasattr(stream, "reconfigure") and (stream.encoding or "").lower().replace("-", "") != "utf8":
@@ -365,6 +366,10 @@ def main(argv=None) -> int:
365
366
  parser.add_argument("--version", action="version", version=f"teamrunner {__version__}")
366
367
  commands = parser.add_subparsers(dest="command", required=True)
367
368
 
369
+ ui = commands.add_parser("ui", aliases=["gui"], help="open this folder in the local browser interface")
370
+ ui.add_argument("folder", nargs="?", default=".", help="the folder to open (default: this folder)")
371
+ ui.set_defaults(func=cmd_ui)
372
+
368
373
  v = commands.add_parser("validate", help="check a team's files before running it")
369
374
  v.add_argument("team", nargs="?", default=".", help="the team folder (default: this folder)")
370
375
  v.add_argument("-v", "--verbose", action="store_true",
@@ -164,7 +164,7 @@ class Console:
164
164
  request = " ".join(str(first).split())
165
165
  request = request if len(request) <= 50 else request[:49] + "…"
166
166
  verb = {"web_search_brave": "search", "read_page": "read", "read_file": "read",
167
- "run_code": "run"}.get(e["tool"], e["tool"])
167
+ "run_code": "run", "read_email": "read", "save_draft": "draft"}.get(e["tool"], e["tool"])
168
168
  if e["access"] == "allowed" and e.get("result") is not None:
169
169
  allowance = e.get("allowance") or {}
170
170
  mark = p.green(f"✓ {allowance.get('used')} of {allowance.get('of')}")