Repository navigation
Expand file tree
/
Copy pathconfig.example.toml
More file actions
1057 lines (1032 loc) · 56.7 KB
/
Copy pathconfig.example.toml
File metadata and controls
1057 lines (1032 loc) · 56.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# io-harness's configuration file, as `io setup` writes it.
#
# This is the harness's schema, not io-cli's — io-cli has no configuration parser
# and never will. Everything below except `[app.io-cli]` is read and validated by
# io-harness itself; `[app.io-cli]` is the section it deliberately leaves alone
# for whoever is building on top of it.
#
# Where this file goes. The order is io-harness's:
#
# $IO_CONFIG a file, named outright
# $IO_CONFIG_HOME/io.toml a directory, named outright
# $XDG_CONFIG_HOME/io/io.toml unix
# ~/.config/io/io.toml unix, when XDG_CONFIG_HOME is unset
# %APPDATA%\io\io.toml Windows
#
# From 0.15.0 io-cli sets $IO_CONFIG_HOME to its own home — `~/.io-cli`, or
# `%USERPROFILE%\.io-cli` on Windows — before io-harness resolves anything, so
# the second row is what answers unless you have named a location yourself. Set
# $IO_CONFIG or $IO_CONFIG_HOME and io-cli sets nothing and moves nothing. The
# run store `runs.db` sits beside this file wherever this file ends up, and on
# the first 0.15.0 run an existing pair is moved into the home together, each
# file named on screen, nothing overwritten and nothing deleted.
#
# SET $IO_CONFIG_HOME YOURSELF AND THE HOME IS TWO DIRECTORIES, NOT ONE. `io.toml`,
# `runs.db` and `IO.md` follow the variable. The skills directory, its
# `.skills-manifest`, `marketplaces/`, the staging directory and the session lock
# do NOT: io-cli builds all five from `~/.io-cli` (`%USERPROFILE%\.io-cli` on
# Windows) unconditionally, because pointing at a different *configuration* is a
# different act from moving a cache of other people's repositories. So when you
# move machines, copy BOTH — the directory the variable names and `~/.io-cli` —
# or you arrive with your settings and none of your skills.
#
# A project's own `io.toml` and a gitignored `io.local.toml` layer on top.
# --- The provider -----------------------------------------------------------
#
# One of: openrouter, anthropic, openai, compatible.
#
# `api_key` absent means the provider's own environment variable —
# OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY. That is the better
# arrangement, not a fallback: a key that is never on disk cannot leak from disk.
# An environment variable always wins over a key written here.
[[provider]]
kind = "openrouter"
model = "anthropic/claude-sonnet-4"
# api_key = "sk-or-v1-..." # written mode 0600 when the wizard writes it
# Any OpenAI-shaped endpoint — a proxy, a gateway, a local runtime. Exactly one
# of `preset` and `base_url`.
#
# [[provider]]
# kind = "compatible"
# base_url = "http://localhost:11434/v1"
# model = "llama3.2"
# auth = "none"
# --- The permission boundary ------------------------------------------------
#
# What the agent may do before anything asks. The three postures the wizard
# offers are exactly these three shapes:
#
# sandboxed workspace read/write/exec allow, net deny
# ask before writes read allow, write/exec ask, net deny
# read only read allow, write/exec/net deny
#
# `ask` stops the turn and asks in an overlay. It was answered by declining
# through 0.1.1, and the release that changed it said so at the time: a posture
# whose behaviour is not what its name suggests has to say so rather than imply
# otherwise. In a headless `io exec` run `ask` is still a decline, because nothing
# there can answer it — see the README.
#
# NOTE: a file INSIDE THE WORKSPACE may narrow this and may never widen it — a
# repository you cloned must not be able to grant itself permission. Widening
# belongs in the user-scope file, which is where `io setup` writes.
#
# THAT MEANS io.local.toml TOO, SINCE io-harness 0.74.0. The rule used to be
# about the committed file alone; it is now about any file inside the workspace
# root, because io.local.toml is a path the run's own agent can write to — so one
# write_file of it would have declared an argv the next discovery runs, outside
# the policy and outside the sandbox. The user scope is the one file no workspace
# can reach.
#
# THERE ARE THIRTEEN (KEY, VALUE) PAIRS ACROSS TWELVE KEYS THAT A FILE IN THE
# WORKSPACE MAY NOT WRITE, and io-harness refuses the WHOLE FILE for any one of
# them rather than dropping that key — the check runs before the file is
# deserialized, so what you get is a configuration that no longer parses rather
# than a rejected setting:
#
# policy.defaults.read = "allow"
# policy.defaults.write = "allow"
# policy.defaults.exec = "allow"
# policy.defaults.net = "allow"
# sandbox.allow_network = true
# sandbox.force_floor = false
# sandbox.mode = "full-access" (and "workspace-write")
# sandbox.limits.max_cpu_secs = 0
# sandbox.limits.max_wall_secs = 0
# sandbox.limits.max_memory_bytes = 0
# sandbox.limits.max_processes = 0
# sandbox.limits.max_open_files = 0
#
# The five limits are refused only at ZERO, which is what "no cap" means — a
# merely large finite cap is still a cap. The narrowing value of each key stays
# legal in a workspace file, which is what the scope is for; `read-only` is the
# sandbox mode that narrows in every direction and stays legal everywhere. Write
# any of these in THIS user-scope file. `/config` knows the list and says so on
# the row BEFORE it writes, rather than writing and reporting the rollback.
# Each of the four takes "allow", "ask" or "deny" — io-harness's own three, and
# nothing else parses. `/config` offers them as a list rather than asking you to
# type one, and a horizontal arrow steps through them on the row.
[policy.defaults]
read = "allow"
write = "allow"
exec = "allow"
net = "deny"
# Named layers. A DENY ANYWHERE BEATS AN ALLOW ANYWHERE, AND LAYER ORDER IS NOT
# WHAT DECIDES IT. io-harness walks `Effect::ALL` reversed — strictest effect
# first — and for each effect it scans EVERY layer before it moves on, so the
# first deny found anywhere wins over an allow found anywhere, whether that allow
# was written before it or after it. The defaults above are not a layer at all:
# they are the fallback, returned only when no rule in any layer matched. A layer
# can therefore add capability the defaults withheld, and no layer can ever
# re-allow what another one denied.
#
# [[policy.layers]]
# name = "no-secrets"
# rules = [{ act = "read", effect = "deny", pattern = ".env" }]
# --- What bounds a turn, in a session and in `io exec` alike (0.14.0) --------
#
# Every section below is io-harness's own — read and validated by it — and every
# one of them reaches BOTH arms. A session turn and an `io exec` run build their
# contract from the same call, so what you write here has the same effect in your
# terminal as it has in CI.
#
# [sandbox] resource limits and network allowances for a
# contained command
# [run] the step, token and duration budgets, the retry,
# stall and context policies, the exec timeout, the
# read ceiling and the skills directory
# [run.commit_identity] who a commit the agent makes is authored as. It is
# INSIDE [run]: a file with no [run] table has none.
# [instructions] AGENTS.md and whatever else you point it at
# [[mcp]] MCP servers. Merged with [[app.io-cli.mcp]] below.
# [[lsp]] language servers. Merged with [[app.io-cli.lsp]].
# [[agent]] the roster a fan-out draws its children from. 0.25.0
# adds `worktree`; see the block below
# [web] provider-executed search and fetch
# [memory] how much durable memory a run may keep
# [toolchain.<eco>] how THIS repository is built, tested and linted — one
# table per ecosystem, `[toolchain.cargo]`,
# `[toolchain.npm]`. It is what `/gates` detection reads
# to offer you a command; see the block below
# [[plugin]] capability bundles, declared by path. 0.20.0; see the
# block below
# [[hook]] what to do on an event, or in front of a tool call.
# Parsed and inert before 0.20.0; see the block below
#
# THIS BLOCK USED TO SAY THE OPPOSITE, and the reason it gave went stale a
# release before the block did. Through 0.13.1 these sections reached `io exec`
# and nothing else, because the harness's steerable session turn built its own
# contract and took none from the caller. That stopped being how a session runs
# in 0.11.0, when the ordinary turn moved to an entry point that does take one —
# and Ctrl+C survived the move, because an interrupt travels on the observer
# rather than through a steer inbox. What was left was an omission rather than a
# constraint, and 0.14.0 closes it.
#
# WHAT CHANGES FOR A FILE YOU ALREADY HAVE. A [run] block written for CI now
# bounds your terminal too. `max_steps = 20` is a reasonable thing to have set
# for an unattended run and an unreasonable cap on a conversation — so if that is
# what this file says, it is now what your session says. The status line carries
# each budget in force with what is left of it, and `/status` lists them all, so
# a turn that will stop at a ceiling says which one before it gets there. An
# operator who wants [run] for CI only moves it to a project file or narrows it
# by scope.
#
# [web] IS A CAPABILITY AND NOT A PREFERENCE. It gives the model the provider's
# own search and fetch, and the VENDOR is what dials the URL — so the `net` rule
# in [policy.defaults] above is not what governs it. That rule decides what THIS
# machine may reach. Writing [web] here turns the capability on for every session
# as well as for every CI run, from a file that previously did nothing; the
# session says so, in its own words, when it starts.
#
# [browser] is io-harness's too and is applied the same way, but it is REFUSED IN
# A PROJECT-SCOPED FILE by io-harness itself: it names a program to execute, and
# a project's io.toml arrives with a `git clone`. It works in the user-scope file
# — the one `io setup` writes, and the one this example documents — and there is
# no project-scope route to a browser at all. io-cli's own [app.io-cli.browser]
# is below, and reaches a turn from either scope.
#
# The keys themselves, by section. `/config` lists every one of these with the
# value in force and the file that decided it, so this block is the written form
# of a surface you can now open in a session — and each key is left commented,
# because a key present with its own default is a key a reader has to wonder
# about.
#
# EVERY COMMENT BELOW SAYS WHAT THE KEY TAKES, and that is not decoration: from
# 0.28.0 `/config` chooses a value rather than asking you to type one, and it
# decides what to offer from the same reading. A key whose values are a closed set
# gets that set — from io-harness's own types where the set is the harness's — a
# whole number gets a 1-2-5 ladder built around whatever the value in force is, a
# model name gets the models `[prices.models]` already prices, a path gets the
# workspace. What is left to type is only what no menu can hold: a substring, a
# rubric, a URL, a command line. So a mismatch between one of these comments and
# what io-cli offers is a defect in one of the two, and the comment is the half
# you can check.
#
# [run] # every NUMBER here is a WHOLE one, none negative
# max_steps = 40 # how many steps one turn may take
# max_tokens = 250000 # the token ceiling for one turn
# max_duration_secs = 900 # wall clock for one turn
# max_retries = 3 # how many times a failed step is retried
# exec_timeout_secs = 120 # how long one command may run
# max_read_chars = 200000 # the ceiling ONE read is refused against
# max_wait_secs = 300 # the ceiling on ONE blocking mailbox read
# templates = "/srv/io/prompt-templates" # a DIRECTORY of prompt templates —
# # a path, not a number, and no tilde (see below)
#
# THAT IS EIGHT OF THIRTEEN, AND THE OTHER FIVE ARE NOT MISSING. `[run]` takes
# thirteen keys; the eight above are the eight that are a plain value on a line.
# `skills` is a directory — and note it is a DIFFERENT key from io-cli's own
# `[app.io-cli] skills` further down, which is the one tilde exception in this
# file. `commit_identity` is the sub-table indexed above. `retry`, `stall` and
# `context` are policies of io-harness's own, each its own table. A key `[run]`
# does not know is refused WITH THE WHOLE FILE rather than dropped, so a typo
# here is a configuration that stops parsing rather than a budget that quietly
# is not in force.
#
# [sandbox]
# mode = "workspace-write" # read-only | workspace-write | full-access
# allow_network = false # true or false: may a contained command reach the network
# force_floor = true # true or false: keep the floor even where the caller asked for less
#
# NOTE on the three sandbox keys above: `allow_network = true`, `force_floor =
# false` and `mode = "full-access"` are three of the thirteen pairs a file inside
# the workspace may not write — io.local.toml included — see the list under the
# policy block above, and note that io-harness refuses the WHOLE FILE for one of
# them rather than that key. `/config` says so on the row before it writes, and
# shows you the harness's own sentence rather than a summary of it when a write
# does reach that refusal.
#
# [memory] # bounds the STORE, which outlives every run over it
# # every key here is a WHOLE NUMBER, none negative
# max_entries = 500 # how many durable entries are kept
# max_chars = 200000 # the total size of them
# max_entry_chars = 4000 # the size of any one of them
#
# [toolchain.cargo] # ONE TABLE PER ECOSYSTEM, and the name after the
# # dot is yours: `cargo`, `npm`, `go`, whatever this
# # repository actually uses. `manager` is a STRING;
# # the other six are ARGV ARRAYS, never shell strings,
# # for the reason every other command key in this file
# # is an array — nothing is word-split or expanded.
# manager = "cargo"
# install = ["cargo", "fetch"]
# build = ["cargo", "build", "--all-targets"]
# test = ["cargo", "test", "--all-features"]
# lint = ["cargo", "clippy", "--all-targets"]
# format = ["cargo", "fmt", "--check"]
# run = ["cargo", "run"]
#
# WHAT THIS IS FOR, IN io-cli. It is what `/gates` detection reads: the offer it
# puts in front of you — the row you accept, which then writes
# `[app.io-cli.gates] command` in full further down — comes from io-harness
# reading this repository, and this table is how you tell it the answer rather
# than letting it guess. io-cli holds no list of build tools of its own,
# deliberately, and that is stated again where the gates section says so.
# --- MCP servers ------------------------------------------------------------
#
# The shape `/mcp` writes, and the shape `/import` translates another tool's
# servers into. io-cli defines no schema for one — this is io-harness's own, with
# the TRANSPORT AS A TAG beside the other fields rather than as a nested table.
#
# id what the server is called
# transport "stdio" for a program on this machine, "http" for a URL
# command the program, for a stdio server
# args its arguments, as an ARRAY — never a shell string
# env variables handed to that program
# url the endpoint, for an http server
# headers headers sent with every call to an http server, as a table. This
# is where a bearer token goes, and it takes the same ${env:NAME}
# reference the paragraph below describes rather than a literal
# timeout_secs how long ONE call to this server may take. 60 WHEN ABSENT, which
# is a real ceiling and not "no limit": a server that answers
# slowly fails the call rather than holding the turn open
#
# NEITHER OF THE LAST TWO IS A KEY YOU HAVE TO TYPE. `io mcp add` takes
# `--header 'Authorization=Bearer ${env:TOKEN}'` and `--timeout-secs 30` and
# writes exactly the two rows above; `io mcp edit <id> --timeout-secs 30` changes
# the second afterwards. Both are in the README with the rest of those flags.
#
# A CREDENTIAL IS A REFERENCE AND NOT A LITERAL. io-harness resolves ${env:NAME}
# from the environment at the moment it reads this file, so the secret stays in
# your shell and never lands here. A substitution RESOLVES OR FAILS and never
# empties: a variable that is not set is an error naming the key, rather than a
# server that starts holding an empty token and fails somewhere less obvious.
# `/import` writes exactly this form — it reads the NAMES of another tool's
# environment variables and never their values — and tells you which variables
# your shell has to have.
#
# ${file:PATH} and ${cmd:PROGRAM ARGS} are the other two forms. ${cmd:} is
# REFUSED in a project-scoped io.toml, because that file arrives with a
# `git clone`.
#
# NO TILDE, HERE OR ANYWHERE IN THIS FILE. io-harness expands none: those three
# substitutions are the whole of what it rewrites, so a leading `~` in a value is
# a directory literally named `~`. Write the path out, or use ${env:HOME}. The
# one exception is io-cli's own `skills` key further down, which io-cli expands
# itself before io-harness ever sees it.
#
# [[mcp]]
# id = "github"
# transport = "stdio"
# command = "npx"
# args = ["-y", "@modelcontextprotocol/server-github"]
# env = { GITHUB_TOKEN = "${env:GITHUB_TOKEN}" }
#
# [[mcp]]
# id = "issues"
# transport = "http"
# url = "https://mcp.example.com/v1"
# headers = { Authorization = "Bearer ${env:ISSUES_TOKEN}" }
# timeout_secs = 30
#
# `enabled` (io-harness 0.70.0, absent means true) declares a server WITHOUT
# starting it. io-harness skips it at connect, so no process is spawned and no
# socket is dialled, and every other key of the entry is kept — which is what
# makes this different from removing it.
#
# WRITING THIS KEY COSTS AN OLDER BINARY SOMETHING, AND IT IS THE QUIET KIND.
# An io-cli built against io-harness 0.69.0 does not refuse an `[[mcp]]` entry
# carrying `enabled` — it IGNORES the key and starts the server anyway. The
# `[[plugin]]` version of the same key fails the other way, refusing the whole
# file; that one announces itself and this one does not.
#
# From 0.30.0 `/mcp` has a toggle row and `io mcp enable|disable <id>` writes the
# same bytes, so this is not a key you have to type. `io mcp probe <id>` starts a
# configured server on request and reports what it offers.
#
# [[mcp]]
# id = "staging"
# transport = "stdio"
# command = "staging-mcp"
# enabled = false
# --- The agent roster (0.25.0) ----------------------------------------------
#
# io-harness's own section: the roster a contained turn's fan-out draws its
# children from. `name` is what the parent asks for, and every other key narrows
# that child against its parent — a `role`, a `model`, a `max_steps`, an
# `effort`, and the two refusals `deny_write` and `deny_net`. An entry with
# nothing but a name is exactly the child a bare spawn produced before the
# roster existed.
#
# `worktree = true` GIVES ONE CHILD ITS OWN CHECKOUT. Every agent in a tree
# shares one working directory otherwise, so two children editing the same file
# are one overwriting the other. Set this and io-harness roots that child under
# `.worktrees/` in the repository, on a new branch created before its first
# step. THE BRANCH IS NOT NAMED AFTER THIS ENTRY: it is the entry's name, the
# parent run, the step and a digest of the child's goal — `reviewer` becomes
# something like `reviewer-12-3-a1b2c3d4`. That is what makes the line below
# true, and it is the shape to look for when you go and find the work.
# `/fleet` marks the rows whose definition asked for one.
#
# WHAT IT COSTS. Another checkout of the repository on disk, and a branch per
# CHILD rather than per definition — two children of the same entry spawned in
# the same step get one each, which is the collision this key exists to remove.
# It also gives the spawn a new way to fail: no git, not a repository, or the
# boundary above refusing that path, and then NO CHILD STARTS. Quietly sharing
# the parent's tree would be the exact overwrite the key was set to prevent.
#
# AND NOTHING REMOVES ONE. Not io-harness, not io-cli, not the end of the run:
# the worktree and its branch are still there when the tree finishes, because
# removing either throws away the work the child was spawned to produce. So
# `.worktrees/` accumulates until you clear it out yourself.
#
# [[agent]]
# name = "reviewer"
# role = "You read a diff and say what is wrong with it."
# model = "anthropic/claude-haiku-4.5"
# max_steps = 20
# deny_write = true
#
# [[agent]]
# name = "author"
# worktree = true
# --- Capability bundles (0.20.0) --------------------------------------------
#
# A bundle is a directory with a `plugin.toml` in it, and it is loaded because
# THIS FILE NAMES IT. There is no directory io walks looking for bundles: a
# `[[plugin]]` entry is the whole of installing one, and deleting the entry is
# the whole of removing one.
#
# THERE IS STILL NO REGISTRY, and from 0.29.0 that sentence needs its second
# half. `/plugin marketplace add <owner>/<repo>` clones a git repository into
# `~/.io-cli/marketplaces` and lists the directories in it that carry a
# `plugin.toml`, so a bundle can be found without knowing its path. io operates
# no index, hosts nothing, curates nothing and ranks nothing — a marketplace is
# a repository you named, on your disk, and installing out of one still writes
# exactly the `[[plugin]]` entry below. The clone is a `git` invocation; there
# is no HTTP client here and no network path beside io-harness's.
#
# One directory can contribute seven kinds of thing at once — skills, prompt
# templates, [[agent]] definitions, [[mcp]] servers, [[hook]] tables, [[bin]]
# executables and policy layers — which is why there is a surface for it. A
# [[bin]] is io-harness 0.73.0's, and io places it by appending the declaring
# file's own directory to the run's PATH: appended so nothing a bundle ships can
# shadow a system command, and no file is written, so the program answers to the
# name it already has rather than to the name the manifest gave it.
# `/plugin` lists THREE sets:
# what loaded and what each bundle contributed; what was declared `enabled =
# false`, under its own mark, with what switching it back on would bring; and
# every bundle that was declared and DROPPED with io-harness's own reason beside
# it. That loader has no error path: a dropped bundle is recorded and otherwise
# silently absent while every other one loads.
#
# `enabled` is io-harness 0.70.0's, and it defaults to true, so an entry written
# before it existed means what it always did. WRITING IT COSTS SOMETHING: an
# io-cli built against io-harness 0.69.0 does not know the key and refuses the
# WHOLE FILE rather than ignoring it. Remove the `enabled` keys before
# downgrading. io says this at the moment it writes one.
#
# WHICH FILE DECLARES A BUNDLE DECIDES WHAT IT MAY CONTRIBUTE. A bundle named in
# the project-scoped io.toml — the file a `git clone` delivers — may contribute
# skills, templates, agents and policy, and may NOT contribute hooks or MCP
# servers, because both of those run a program on this machine. One that tries is
# refused WHOLE: it contributes nothing at all, not the half that would have been
# safe. Name it from THIS user-scope file and the same directory loads completely;
# io.local.toml is refused for the same reason since io-harness 0.74.0, being a
# path inside the workspace the run's own agent can write to. The rule is about
# which file, not about the bundle.
#
# A BUNDLE'S POLICY MAY ONLY DENY. A `[policy] defaults` table in a manifest is
# refused by name, and a single rule whose effect is not `deny` drops the bundle.
# So the worst a bundle can do to the boundary above is narrow it.
#
# An id must match [a-z0-9][a-z0-9-]{0,31}, and every agent name, MCP server id
# and policy layer name a bundle contributes is rewritten to `<bundle>__<name>`
# by io-harness at load. That is the name a refusal, a call and a spawn will all
# use, and the name `/plugin` draws.
#
# io-cli CANNOT LIST A BUNDLE'S HOOKS. io-harness's `Hook` type is private and
# there is no accessor, so `/plugin` reports that a bundle contributed hooks and
# nothing about how many there are or what they run.
#
# `path` IS TAKEN LITERALLY, AND A TILDE HERE IS A TRAP THAT MAKES NO NOISE. The
# three `${...}` substitutions are the whole of what io-harness rewrites — the
# rule stated under the MCP block above — and a plugin path never reaches io-cli's
# own tilde expansion either, which is the `[app.io-cli] skills` key and nothing
# else. So `~/bundles/rust-review` names a directory literally called `~`, that
# directory is not there, and the bundle is DROPPED: recorded by the loader,
# listed by `/plugin` with io-harness's reason beside it, and otherwise silent.
# Write the path out absolutely, or relative to the discovery root, or with
# ${env:HOME}.
#
# [[plugin]]
# path = "/Users/you/bundles/rust-review" # or "${env:HOME}/bundles/rust-review"
# --- Hooks (0.20.0) ---------------------------------------------------------
#
# What to do when something happens. PARSED AND INERT BEFORE 0.20.0 — a table
# here was read, accepted and then installed on nothing, so a file asking for
# every event to be written to a log produced an empty file and no error. From
# 0.20.0 hooks run in a session turn and in an `io exec` run alike.
#
# A hook either writes events down or runs a program, and exactly one of `append`
# and `run` says which — a hook that did both would be a hook whose failure meant
# two things. `on` and `at` are mutually exclusive: `on` observes events, `at`
# gates a tool call, and an `at` hook must have a `run`.
#
# on the event names to observe. EMPTY MEANS EVERY EVENT
# at "before_tool", which is the only one there is
# tools for an `at` hook, which tool calls it sees
# append a file to append one JSON line per event to
# run argv, NEVER a shell string: nothing is word-split or expanded
# on_failure continue | cancel | refuse
# timeout_ms 5000 when absent
#
# `on_failure` is where a hook's power actually is. `continue` lets the turn go
# on, which is what an audit hook wants. `cancel` ends the turn at the next step
# boundary and the run STAYS RESUMABLE — it is a stop, not a crash. `refuse`
# turns that one tool call back and leaves the turn running, which makes a
# `before_tool` hook a rule of your own standing beside the policy engine's.
#
# A PROJECT-SCOPED FILE MAY NOT DECLARE [[hook]] AT ALL, and io-harness refuses
# the WHOLE configuration rather than dropping the table — a hook runs a command
# on this machine, and io.toml is the file a `git clone` delivers. There is no
# Config to be had, so io genuinely cannot start against such a file; what 0.20.0
# changed is that it prints io-harness's own sentence, which names the key, the
# reason and the file that may carry it, instead of a bare error string.
# Write the table HERE, in the user-scope file, and nowhere else: io.local.toml is
# refused too since io-harness 0.74.0, because it sits in a root the run's own
# agent can write to.
#
# [[hook]]
# on = [] # every event
# append = "audit.jsonl"
#
# [[hook]]
# at = "before_tool"
# tools = ["shell"]
# run = ["./scripts/gate.sh"]
# on_failure = "refuse"
# timeout_ms = 5000
# --- What a token costs (0.22.0) --------------------------------------------
#
# io-harness's own section, and the only place a price comes from. NOTHING IS
# COMPILED IN: a rate baked into a binary is a promise the binary cannot keep,
# because providers move prices without announcing it and a release cadence is
# not a pricing cadence. So this table ships EMPTY, and `/cost` reports tokens
# and no currency until it is filled.
#
# Filling it is a row on `/config`: io-cli reads the model catalogue your
# connected provider already serves — the same fetch that offers the wizard its
# model list, whose prices it used to throw away — and writes what it found here.
# EVERY RATE THAT WOULD MOVE IS SHOWN BEFORE A BYTE IS WRITTEN, with what it was
# and what it would become, and declining writes nothing. That is not courtesy:
# this file records what a rate IS and never where it came from, so io-cli cannot
# tell a correction you made by hand from a value an older catalogue served. It
# does not guess; it shows you and lets you refuse the lot.
#
# A refetch that comes back EMPTY, or far shorter than the table it would
# replace, is refused and the old table kept — a truncated response that replaced
# a full table with a handful of rows would turn most of your spending into
# "unpriced" and shrink your reported bill with nothing failing anywhere. A first
# fill has nothing to compare against and is never refused.
#
# WHOSE PRICE IT IS. OpenAI and Anthropic publish no prices on any
# endpoint — their model endpoints carry capabilities and limits and no cost
# field. So for those two the rates necessarily come from the reference
# catalogue rather than from the vendor, and every surface that draws money says
# which and on what date. On OpenRouter the two coincide, because the reference
# catalogue is OpenRouter's own.
#
# THIS SECTION IS deny_unknown_fields AND CARRIES EXACTLY `as_of` AND `models`. A
# key of io-cli's own put beside them would not be ignored — it would make this
# whole file unreadable. io-cli's bookkeeping lives in [app.io-cli.prices] below.
#
# as_of the date the rates were read. REQUIRED: a price list with no date is
# a claim with no expiry. WRITTEN BY A FETCH RATHER THAN TYPED: a date
# set by hand is a claim about a fetch that never happened, and
# `io config set prices.as_of …` is refused by name for saying so. It
# is on `/config` to be READ, and the act beside it is the row that
# re-reads the catalogue and writes this itself.
# models one entry per model, in micro-units PER MILLION TOKENS
#
# Rows for models the catalogue no longer serves are left alone rather than
# pruned. They are not stale: io-harness prices a call by the model name on it, so
# an old row is what prices an old run correctly, and `/cost` reports history as
# well as today.
#
# [prices]
# as_of = "2026-08-27"
#
# [prices.models."anthropic/claude-sonnet-4.5"]
# input = 3000000
# output = 15000000
# cache_read = 300000
# cache_write = 3750000
# --- io-cli's own section ---------------------------------------------------
#
# Nine keys and eight tables. io-harness does not validate any of it, so an
# unrecognised VALUE reads as the default rather than stopping a session. A
# table io-harness cannot parse at all is different: the session starts on the
# defaults and says so, carrying the harness's own message.
[app.io-cli]
# Exactly "dark" or "light". Absent means detect from the terminal background.
theme = "dark"
# How much of a change a diff shows. Exactly "unified" or "minimal".
#
# "unified" the hunk as the harness stored it, context and all. The default,
# and what an absent key means — so a file written before 0.3.0
# needs no change.
# "minimal" the changed lines and the @@ header, without the context. For
# reviewing by file rather than by hunk.
diff = "unified"
# Which marks to draw with: exactly "unicode" or "ascii".
#
# Absent — the default — asks the locale: LC_ALL, then LC_CTYPE, then LANG, and
# the first one PRESENT decides, whatever it says. Name a set here when the locale
# is right about the encoding and wrong about the font, which is what a working
# indicator drawn as boxes looks like.
#
# Independent of colour in both directions: the ASCII set arrives at a fully
# coloured theme, and NO_COLOR keeps the Unicode marks.
#
# glyphs = "ascii"
#
# true or false. Run every session in plain mode: no animation, the ASCII set
# forced, and each state the session enters committed to the scrollback as a line
# of text. The same switch as `--plain`, and the flag wins — there is no
# `--no-plain`, so a file that says true here cannot be turned off for one run.
#
# false is the default and what an absent key means; it is written out so the file
# says which way round it is.
plain = false
#
# true or false. Whether a prompt that is only a question may be answered in one
# completion.
#
# io-harness has classified turns since long before io-cli asked it to: a contract
# carrying no verification criterion turns classification on, and a question then
# comes back after ONE completion — no steps, no tools, no checkpoint and no gate.
# Absent leaves that where it is, which is what every release before 0.26.0 did,
# so there is nothing to write here for the ordinary case.
#
# false OPENS A FULL RUN FOR EVERY PROMPT, which is what you want in a session
# meant to work rather than to answer. The key earns its place on the other side
# of a gate: attaching a criterion is what io-harness reads as "this turn is not a
# conversation", so a repository with `[app.io-cli.gates]` would otherwise have
# every idle question become an agent run that then runs the gate after each of
# its steps. io-cli sets classification back on wherever it attached a criterion,
# and this key is how you say you wanted the runs after all.
#
# conversational = false
#
# true or false. Whether io may read the reference catalogue over the network to
# size a model's context window and price its tokens (0.39.0).
#
# true is the default and what an absent key means. false is the switch for an
# operator who wants no request io did not have to make — an air-gapped machine, a
# self-hosted endpoint the reference catalogue has never heard of, or a policy that
# says so. With it off, the surfaces that would have shown a window or a price say
# what they do not know rather than guessing.
#
# THIS KEY WAS UNSETTABLE FROM 0.39.0 UNTIL 0.40.0 and it is worth saying why, in
# the file that promises a worked example for every key. `io config set` quoted it,
# `CliSettings` could not read `"false"` back, and the refusal was not of that key:
# io-harness reads [app.io-cli] as one opaque value, so the whole section failed —
# theme, keybindings, containment, gates and routing all reverting to defaults
# behind a single warning line. The escape hatch for a network call was the one key
# you could not reach.
#
# reference_catalogue = false
#
# How many steps one turn may take.
#
# DEPRECATED IN 0.14.0 AND REMOVED IN 0.16.0. It existed because the flat turn
# once had no way to raise io-harness's own cap of twelve, and `[run] max_steps`
# above does the same job from a table io-harness documents itself. It is no
# longer read at all: a file still carrying it is told so once at session start,
# with the steps it asks for named and the key they move to. A file with neither
# key gets io-cli's own floor of a thousand, which is not a budget you set and
# therefore draws no field on the status line.
#
# max_steps = 40
# The session's own keys, by action name. Absent means the defaults, which are the
# ones in the README's table; every line below states its default and so changes
# nothing.
#
# A binding is a chord, or two chords separated by a space. Modifiers are ctrl,
# alt and shift, joined to the key with +, in any order and any case. A key is a
# single character, a named key (esc, enter, tab, backtab, space, up, down, left,
# right, home, end, pageup, pagedown, backspace, delete, insert) or f1 .. f12.
# Because + is the join, + itself cannot be bound.
#
# `interrupt` — Ctrl+C — is absent because it cannot be moved, and naming it here
# earns a refusal rather than being ignored: it is the key that interrupts a
# running turn and leaves io, and a file able to take it away is one able to lock
# you inside a running agent. Putting any other action onto ctrl+c is refused for
# the same reason.
#
# A line that cannot be read leaves its action on the default and says which key
# it kept; a name that is no action says which names there are. Neither stops the
# session, and `/help` always shows the keys actually in force.
[app.io-cli.keys]
exit = "ctrl+d"
posture = "shift+tab"
clear = "ctrl+l"
transcript = "ctrl+t"
rewind = "esc esc"
fleet = "ctrl+f"
# The caps a fan-out runs under. **This table is what lets the agent decompose a
# task at all**, and it is absent by default: io-harness has one session entry
# point that reaches its spawn loop, and these caps are what selects it.
#
# What it costs is smaller than it was, and it is not a mode. Every session turn
# carries a steer inbox — every one has since 0.17.0 — so a contained turn gives
# up no steering an ordinary one has. `Ctrl+C` still ends it, at the next point
# where no child is in flight, and `/contain off` gives the next turn back.
#
# THROUGH 0.11.0 THIS TABLE DECIDED EVERY KEY BELOW IT, AND IT NO LONGER DOES.
# io-harness's contained turn was then the only session entry point that took a
# caller's task contract, so a responder, a plan gate, MCP servers, language
# servers, a browser and a skills directory all rode this one switch. 0.11.0 gave
# the ordinary turn a contract too, and 0.14.0 gave it the configuration file as
# well. Every one of those reaches every turn now, and this table means exactly
# what its name says: the caps a fan-out runs under.
#
# The first four keys are required, because there is no safe default for somebody
# else's token ceiling. The last two are optional and are written out here from
# 0.40.0, because five of these six were settable from `io config set` and named
# by no `/config` list at all — a key that can be spelled and cannot be found is a
# key an operator meets only by reading io-cli's source.
#
# max_total_agents refuses: the spawn that would cross it comes back to the
# parent, which adapts. max_concurrent_agents throttles: a spawn past it waits
# for a slot, per tier, so a fleet drains rather than failing. max_depth counts
# from the root, which is 0. max_total_tokens is the whole tree's, drawn down
# together — tokens, because a provider reports tokens and never money.
#
# max_total_duration is a WALL-CLOCK ceiling for the whole tree, measured from
# when the root run started — so it counts the tree's whole life, including time
# the process was down. It is written into the file as a table rather than a
# number, which is io-harness's `Duration` and not io-cli's choice:
# `io config set app.io-cli.containment.max_total_duration 3600` takes the seconds
# and writes the table for you, and a bare 3600 written by hand fails the whole
# [app.io-cli] section.
#
# max_total_cost IS RESERVED AND NOT ENFORCED, and io-harness says so itself:
# enforcing a money ceiling needs a price per token that a provider never reports,
# so any figure the harness compared against would be one it invented. It
# deserializes, so a file that sets it parses and the ceiling does nothing. The
# spend that IS enforced is max_total_tokens — convert your budget at your
# provider's rate and set that instead.
#
# [app.io-cli.containment]
# max_total_agents = 12
# max_concurrent_agents = 4
# max_depth = 2
# max_total_tokens = 200000
# max_total_duration = { secs = 3600, nanos = 0 }
# max_total_cost = 5
# The three ceilings io-harness has no configuration key for. They are
# TaskContract fields, so io-cli names them in its own section — and it applies
# them in the half a session turn and an `io exec` run share, so what you write
# here bounds your terminal exactly as it bounds CI.
#
# max_parallel_reads = 10 # a whole number: read-only tool calls at
# # once; 0 clamps to 1
# spawn_background_after_secs = 120 # a whole number of seconds: when a slow
# # child is backgrounded
# detached_spawns = true # true or false: may a spawn detach at all
# --- io-cli's own capabilities ----------------------------------------------
#
# Every setting below reaches EVERY turn, contained or not. These are fields of
# io-harness's `TaskContract`, and both a session turn and an `io exec` run hand
# it one.
#
# They are also the strongest layer of the five: applied after `Config::apply_to`,
# so a server named here beats one of the same id in the top-level [[mcp]] or
# [[lsp]] — the two lists are merged rather than replaced, and the session names
# the duplicate it dropped when a file writes an id in both places.
#
# A directory of skills for the agent. They appear in the `/` palette by name,
# and choosing one puts `use the <name> skill: ` in the prompt — the file itself
# is read by the MODEL, through a tool, under this session's own policy. Both
# layouts are discovered: `skills/migrations.md` and `skills/api-style/SKILL.md`.
#
# Absent, this is `~/.io-cli/skills`, so writing the key is how you point at a
# directory somewhere else rather than how you turn skills on. A leading `~` is
# your home directory: io-harness substitutes `${env:...}`, `${file:...}` and
# `${cmd:...}`, and a tilde is none of the three — so io-cli expands it itself
# before handing the path over. A `~` anywhere but the front is a directory
# literally named `~`.
#
# THE CEILING IS PER DIRECTORY. io-harness accepts at most 64 skills in one and
# rejects the whole directory rather than the excess — but every capability
# bundle a [[plugin]] entry declares brings a skills directory of its own, and
# the totals are not bounded together.
# Six bundles can put far more than 64 names in front of the model with nothing
# failing anywhere. `/skills` lists every one of them, bundle skills included,
# under the `<bundle>__<name>` the model actually addresses.
#
# skills = "~/.io-cli/skills"
# MCP servers, in io-harness's own shape — io-cli defines no schema for one. The
# transport is a tag beside the rest of the fields rather than a nested table.
# The status line names each server that actually answered and how many tools it
# offered, which is a different fact from the one written here.
#
# [[app.io-cli.mcp]]
# id = "docs"
# transport = "stdio"
# command = "mcp-server-docs"
# args = []
#
# [[app.io-cli.mcp]]
# id = "issues"
# transport = "http"
# url = "https://mcp.example.com/v1"
# Language servers for this workspace. `extensions` is what routes a file to one.
#
# [[app.io-cli.lsp]]
# id = "rust-analyzer"
# command = "rust-analyzer"
# extensions = ["rs"]
# A browser the agent may drive. It is one you already have — nothing is ever
# downloaded — and every navigation it attempts, including redirects and clicks
# nobody typed, is decided by this session's policy. The status line shows the
# last host and whether it was allowed or refused.
#
# Every key of this table except `binary` is settable from `io config set` as of
# 0.40.0 — before it, all five reached the fall-through that quoted an
# unrecognised `app.io-cli.*` word, so `headless false` was written as the string
# "false" and the whole [app.io-cli] section stopped deserializing behind one
# warning line.
#
# `binary` is DELIBERATELY not offered at either `/config` door, and the reason is
# not that it is hard to type. It names a program to execute; io-harness refuses a
# whole top-level [browser] section from ANY file inside the workspace —
# `io.local.toml` included since its 0.74.0 — for exactly that reason, and it
# cannot apply that rule here because [app.io-cli] is the section it reads as an
# opaque value. Write it by hand, in the file you chose, as below.
#
# `args` IS settable, and only into the user-scope file. It is the argument vector
# of that same program, which is where a `--proxy-server=https://user:pass@host` or
# a `--load-extension` goes — committing one is the same act as committing the
# binary, one word further along. `io config set --scope project` refuses it and
# `/config` does not offer the scope.
#
# binary the browser executable. ABSENT resolves it from io-harness's own
# ordered list of well-known names, which is documented — so you
# read which binary will be picked rather than running it to find
# out.
# args extra arguments, a list. Written from the shell as the remaining
# words after `--`, because they begin with a dash and io's own
# flags are spelled the same way:
# `io config set app.io-cli.browser.args -- --disable-gpu`.
# headless true or false.
# width/height the viewport, in whole pixels.
# timeout_secs a whole number of seconds for one navigation.
#
# [app.io-cli.browser]
# binary = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
# args = ["--disable-gpu"]
# headless = true
# width = 1280
# height = 800
# timeout_secs = 30
# Where the prices above came from (0.22.0). The rates themselves are io-harness's
# and live in [prices]; this is the bookkeeping io-harness does not model, in the
# one section that will not refuse a key it has never heard of.
#
# source_url a URL, and free text: no menu can hold one, so it is one of the
# few keys `/config` still opens a composer for — and it names the
# shape and shows an example first rather than opening a bare line.
# The catalogue to read. ABSENT MEANS io-harness's own default,
# which is the reference catalogue. Write this when you are on a
# self-hosted or `compatible` endpoint: the reference catalogue
# cannot speak for a server it has never heard of, and this key is
# the only way such an operator gets prices at all.
# source what the last read was, IN WORDS — the vendor speaking for
# itself, or a named third-party catalogue. WRITTEN BY A FETCH
# RATHER THAN BY HAND: it is what the surfaces that draw money
# cite, and a number attributed to a vendor that never published
# one is the thing it exists to prevent.
# models how many models the last read priced. io-harness 0.71.0 gave
# `PriceTable` `models()`, `len()` and `is_empty()`, so a live count
# IS available now — and a live count still cannot do this job. It
# answers what the merged table holds at the moment you ask; a
# refetch has to be measured against what ONE PARTICULAR READ priced
# AT THE TIME IT WAS WRITTEN, and by the moment that comparison is
# made the live number is a count of the very table being replaced.
# So this key stays, and it is what refuses a refetch that comes
# back far shorter than the table it would replace.
#
# [app.io-cli.prices]
# source_url = "https://openrouter.ai/api/v1/models"
# models = 417
# --- what "done" means for this repository (0.24.0) -------------------------
#
# A gate is the answer to a question only you can answer: what has to be true
# before this turn is finished. io-harness has run a verification after the agent
# stops since long before io-cli asked it to — a task contract carries one, the
# run loop executes it inside the sandbox, and the run comes back a success only
# when it passed. Until this release io-cli supplied none, so every clean turn
# reported "finished", which was an honest report of a contract that asked for
# nothing.
#
# ABSENT MEANS NO GATE, AND THAT IS NOT A DEFAULT WORTH WRITING. Nothing is
# verified for an operator who has not said what verifying would mean, and there
# is no criterion this file could pick for you that would not be a guess about
# somebody else's repository.
#
# EXACTLY ONE OF `command`, `file` AND `rubric` MAY BE SET. They are the three
# kinds, and the key you write is what selects one. Naming none, or naming two,
# is refused where you can still see what you typed rather than resolved by a
# precedence rule — a precedence rule here would quietly gate every turn on
# something you did not choose, which is the one failure a gate must not have.
#
# `retries` is a WHOLE NUMBER, none negative: how many further turns a failing
# gate may buy, defaulting to 1. ZERO MEANS REPORT AND DO NOT RE-DRIVE: the
# failure is shown and the turn ends, which is what you want when a person is
# watching. It is a small number on purpose — a retry is a whole turn against a
# real model, not a loop iteration — so an operator who wants several says so.
#
# --- 1. a command that must exit a status ---------------------------------
#
# `command` IS AN ARRAY OF STRINGS AND NEVER A STRING. It is the argv of a
# program, not a shell line: io-harness checks argv[0] against this session's
# `exec` policy and runs it without a shell, so `&&`, a pipe and a glob are not
# available and were never going to be. A scalar written here — `command = "cargo
# test"` — is a value io-harness cannot read back, which is why it is one of the
# two keys in io-cli's catalogue with a list of its own rather than being lumped
# in with free text. `[app.io-cli.browser] args` is the other, since 0.40.0. `io config set app.io-cli.gates.command -- cargo test --all-features`
# takes the words after the `--` and writes them as the array.
#
# THE `--` IS LOAD-BEARING AND THIS LINE DID NOT CARRY IT UNTIL 0.40.0. A command
# line routinely contains a word beginning with a dash, io's own flags are spelled
# the same way, and the parser refused `--all-features` as a flag it does not take
# — so the worked example this file has shipped since 0.24.0 had never run. `--` is
# the same separator `/mcp add <name> -- <command>` uses, and everything after it
# is copied through as text.
#
# `command` IS ALWAYS A CONCRETE ARGV. An empty list is refused, and an absent one
# is not a criterion at all — this key never means "work it out for me".
#
# `expect_exit` IS A SIGNED WHOLE NUMBER and defaults to 0. It is the one key here
# that may go below zero — a process may legitimately be expected to exit on a
# negative status — and it is the only signed key in the whole catalogue, so
# `/config`'s ladder for it runs through zero into the negatives while every other
# number key stops there.
#
# What detection is for is the OFFER: `/gates` asks io-harness what this repository
# builds and tests with — it is the half of this that knows a Cargo workspace from
# a package.json — and offers you that command as a row you can accept, which
# writes it here in full. io-cli holds no list of its own to consult, deliberately:
# a list of build tools shipped inside a TUI is wrong for somebody the day it is
# written and stale for everybody a year later. So the file always says exactly
# what will run, and you can read it and narrow it.
#
# AND IT RUNS AFTER EVERY STEP, NOT ONCE AT THE END. io-harness evaluates the
# criterion at the bottom of its step loop and keeps going until it passes, so a
# nine-step turn runs this command nine times. `cargo test --all-features` after
# every step of a long turn is a lot of compiling; the gate you want is usually
# the narrowest command that would still catch the mistake.
#
# [app.io-cli.gates]
# retries = 1
# command = ["cargo", "test", "--all-features"]
# expect_exit = 0
#
# --- 2. a file that must exist, and optionally say something --------------
#
# `file` is A PATH, relative to the workspace root — `/config` offers the
# workspace's own files rather than asking you to type one. `contains` is FREE
# TEXT: plain text the file must hold, which no menu can offer, so it is typed
# with the shape stated first. Absent, existence alone is the whole criterion.
#
# This is the cheap gate — nothing is run and nothing is billed — and it is the
# right one for work whose product is a file: a migration written, a changelog
# entry added, a generated artefact that has to appear.
#
# [app.io-cli.gates]
# file = "CHANGELOG.md"
# contains = "## 0.24.0"
#
# --- 3. a rubric a second model answers -----------------------------------
#
# For work no exit status can judge. `rubric` is FREE TEXT — the sentence the
# reviewer is asked to decide, typed, because nothing could offer it as a list.
# `reviewer` is A MODEL NAME, and `/config` offers the models `[prices.models]`
# above already prices: never a network call, because a settings screen that
# reached for a catalogue would be spending your money to draw a menu. A file with
# no priced models has no list to offer, and the row that re-reads the catalogue
# is what fills it. `allow_self_review` is TRUE OR FALSE.
#
# A RUBRIC REQUIRES A REVIEWER, AND THE REVIEWER MAY NOT BE THE MODEL DOING THE
# WORK UNLESS `allow_self_review = true`. Both are refused when the section is
# written rather than when the turn runs, and that is the whole reason they are
# stated here: io-harness answers each of them with a hard configuration error at
# RUN START — before the first billed call, on every turn — so a file that says
# either one turns a configuration edit into a session that will not start, with
# the failure on screen nowhere near the key that caused it. Self review is
# allowed when you ask for it and never by default: a judge marking its own paper
# is a decision, not an oversight.
#
# AND A REVIEW CRITERION IS A REAL BILLED COMPLETION AFTER EVERY STEP — NOT ONCE
# PER TURN. The criterion is evaluated at the bottom of io-harness's step loop and
# keeps being evaluated until it passes, so a nine-step turn asks the reviewer nine
# times, and each retry the gate buys starts that again. The other two kinds cost a
# process and a file read; this one costs money per step, which is worth knowing
# before it is in a file that runs in CI. Keep the rubric to something a model can
# decide quickly, and prefer a command or a file where one would do.
#
# [app.io-cli.gates]
# rubric = "every public item changed in this turn has a doc comment saying why"
# reviewer = "anthropic/claude-sonnet-4.5"
# allow_self_review = false
# --- which model a run asks (0.26.0) ----------------------------------------
#