Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ jobs:
./roundtable db migrate
./roundtable db migrate
./roundtable db doctor
./roundtable db import tests/fixtures/ci-roster.json --replace
./roundtable lanes list
./roundtable db export | diff - tests/fixtures/ci-roster.json
./roundtable import-yaml FlatlineRoundtable.yaml.example --dry-run

- name: Shipped example config parses
run: |
Expand Down
4 changes: 2 additions & 2 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ and vice versa.
- [x] Split config validation from YAML parsing; route scripts through
`rt.fetch_keys` ([#106](https://github.com/CryptoJones/FlatlineRoundtable/issues/106)) — shipped in PR #113
- [x] Encrypted store: `db init|migrate|doctor` and
`secrets set|check|list|rotate|rm|rekey` ([#107](https://github.com/CryptoJones/FlatlineRoundtable/issues/107)) — on feat/sqlite-store-encrypted-store
`secrets set|check|list|rotate|rm|rekey` ([#107](https://github.com/CryptoJones/FlatlineRoundtable/issues/107)) — shipped in PR #114
- [ ] Lane config in the store: `lanes`, `defaults`, `globals`, `lane_versions`,
`db export|import`, `import-yaml` ([#108](https://github.com/CryptoJones/FlatlineRoundtable/issues/108))
`db export|import`, `import-yaml` ([#108](https://github.com/CryptoJones/FlatlineRoundtable/issues/108)) — on feat/sqlite-store-lane-config
- [ ] Run from the store: `load_config` reads the DB, `fetch_keys` reads secrets,
YAML retired ([#109](https://github.com/CryptoJones/FlatlineRoundtable/issues/109))
- [ ] `db backup|restore` and the telesto restic job ([#110](https://github.com/CryptoJones/FlatlineRoundtable/issues/110))
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,40 @@ value AES-256-GCM-encrypted under that key. A value typed on the command line is
refused. `secrets check` prints only presence, length and key id. Runs do not
read the store yet; until #109 lands they still use YAML and `pass`.

The store also holds the lane roster (#108). Move a YAML config in once:

```sh
roundtable import-yaml ~/.config/flatline-roundtable/FlatlineRoundtable.yaml --dry-run
roundtable import-yaml ~/.config/flatline-roundtable/FlatlineRoundtable.yaml --pull-secrets
```

`--dry-run` writes nothing and prints each lane with the notes taken from its
YAML comments, because the store has no comments of its own. `--pull-secrets`
copies each `key_entry` value from `pass` into the store, encrypted. Re-running
the import changes nothing that has not changed.

After that, lanes are edited with commands, and every write is checked by the
same rules `load_config()` applies:

```sh
roundtable lanes list [--all]
roundtable lanes add Skeptic --harness http --set model=vendor/model \
--set base_url=https://openrouter.ai/api/v1 --key-entry openrouter/skeptic
roundtable lanes edit Skeptic --set timeout=120 --reason "slow on long briefs"
roundtable lanes history Skeptic
roundtable globals set budget_usd=0.50
roundtable db export --out roster.json # no secret values, ever
roundtable db import roster.json --replace # on another host
```

A change to a lane's config or `key_entry` is a new version in `lane_versions`.
Renaming, enabling, disabling and editing notes are not, so a lane's record
follows the lane rather than its name. `rename` keeps the old name as an alias,
`retire` frees the name and keeps the history, and `clone` makes an inactive
copy. `--set` values are JSON when they parse and strings otherwise.
[`examples/roster.example.json`](examples/roster.example.json) is the example
YAML config after `import-yaml` and `db export`.

## Cost

**`harness` decides cost, not `model`.**
Expand Down
127 changes: 127 additions & 0 deletions examples/roster.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{
"defaults": {
"max_tokens": 2000,
"temperature": 0.7,
"timeout": 300
},
"format": "flatline-roundtable/roster",
"format_version": 1,
"globals": {
"concurrency": 8,
"deadline_seconds": 600,
"retries": 1
},
"lanes": [
{
"active": true,
"config": {
"base_url": "https://openrouter.ai/api/v1",
"harness": "http",
"model": "some-vendor/some-model",
"personality": "You are the Skeptic. Assume every claim is unproven until it is shown.\nAttack the weakest link in the argument, not the conclusion.\n",
"price_per_mtok": 1.5,
"vendor": "openrouter"
},
"key_entry": "openrouter/agent/skeptic",
"lane_id": "4df90436e446425aa1904b1b4ccd195e",
"name": "Skeptic",
"notes": "http | Any OpenAI-compatible endpoint. `vendor` is a human label used for grouping, per-vendor concurrency and per-vendor rate limiting; routing depends on harness + base_url, not on it. | a pass ENTRY NAME, never a key"
},
{
"active": true,
"config": {
"base_url": "https://openrouter.ai/api/v1",
"harness": "http",
"model": "some-vendor/some-model:free",
"personality": "You are the Contrarian. Argue the position the others are least likely\nto take, as long as it is defensible.\n",
"vendor": "openrouter"
},
"key_entry": "openrouter/agent/contrarian",
"lane_id": "e2b70e9eefcf460a97e85d5d58e9e8b7",
"name": "Contrarian",
"notes": "A free-tier model: same shape, no price."
},
{
"active": false,
"config": {
"base_url": "http://localhost:8080/v1",
"concurrency": 1,
"harness": "http",
"model": "your-local-model",
"personality": "You are the house model. Be concise and concrete.\n",
"timeout": 180,
"vendor": "local"
},
"key_entry": null,
"lane_id": "8710760359fc4f4cb3a6efa6d6b3b1a4",
"name": "Local",
"notes": "local | A llama.cpp / vLLM server on your own hardware. No key needed."
},
{
"active": false,
"config": {
"args": [
"-p"
],
"command": "claude",
"harness": "cli",
"model": "opus",
"personality": "You chair the roundtable. Weigh the strongest case on each side and say\nwhich one actually survives.\n",
"stdin": true,
"vendor": "anthropic"
},
"key_entry": null,
"lane_id": "094cda59430d405e8ce8c77d36be5ce0",
"name": "Chair",
"notes": "cli | Subscription-backed assistants, driven in one-shot print mode. THIS IS A COST BOUNDARY, NOT A STYLE CHOICE. These are $0 only through their own OAuth session. Rewriting one as an `http` lane pointed at the vendor API silently moves it onto metered credits, which bill separately from the subscription. The tool scrubs ANTHROPIC_API_KEY / OPENAI_API_KEY and friends from the child environment for exactly this reason."
},
{
"active": false,
"config": {
"args": [
"--model",
"some-model",
"-p",
"{prompt}"
],
"command": "agy",
"harness": "cli",
"model": "some-model",
"personality": "You are the outsider. You have not seen this codebase before. Say what is\nconfusing on first read.\n",
"vendor": "google"
},
"key_entry": null,
"lane_id": "a5c645ac69084069a6a46e4fc4c5189b",
"name": "Outsider",
"notes": "Arg templates rather than hardcoded per-CLI logic: some CLIs take the prompt as an argument and consume the NEXT token as its value, so flags must come first. Encoding that here means a CLI change is a config edit, not a patch."
},
{
"active": false,
"config": {
"command": "/path/to/some-acp-binary",
"harness": "acp",
"model": "some-model",
"personality": "You are the protocol lane.\n",
"timeout": 180,
"vendor": "example"
},
"key_entry": null,
"lane_id": "c310510a988240d485a87080ec1fd023",
"name": "Protocol",
"notes": "acp | For agents that speak JSON-RPC over stdio and have no one-shot print mode. The tool does initialize -> session/new -> session/prompt -> kill, for one turn only. An ACP agent is a SERVER: it will sit there forever if allowed, so the process is created for the turn and killed with its process group at the end of it. Do not keep one warm between lanes."
},
{
"active": false,
"config": {
"base_url": "https://example.invalid/v1",
"harness": "http",
"model": "some-model",
"vendor": "example"
},
"key_entry": null,
"lane_id": "1b2436e0c4254cf48b991c9c882f48ec",
"name": "Parked",
"notes": "parked 2026-08-25 — answers were incoherent; see lane quality notes | disabled | Keep a lane with active:false rather than deleting it, so the reason it is off does not get lost."
}
]
}
Loading
Loading