@r1z4x/tezgah 1.0.0 → 1.1.0

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 (56) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/MANIFEST +8 -0
  3. package/bin/tezgah-agents +10 -1
  4. package/bin/tezgah-docs +1 -1
  5. package/bin/tezgah-doctor +11 -0
  6. package/bin/tezgah-research +31 -14
  7. package/bin/tezgah-setup +154 -29
  8. package/bin/tezgah-skill +59 -0
  9. package/bin/tezgah-skill.py +59 -0
  10. package/bin/tezgah-status +62 -1
  11. package/bin/tezgah-triage +5 -3
  12. package/docs/README.md +1 -0
  13. package/docs/architecture.md +5 -2
  14. package/docs/citations-baseline.json +1 -1
  15. package/docs/clarity-baseline.json +9 -9
  16. package/docs/contract.md +68 -26
  17. package/docs/environment.md +6 -3
  18. package/docs/evidence.md +7 -0
  19. package/docs/gate.md +13 -7
  20. package/docs/glossary.md +1 -1
  21. package/docs/hosts.md +48 -1
  22. package/docs/index.json +29 -1
  23. package/docs/judge.md +61 -55
  24. package/docs/models.md +14 -8
  25. package/docs/operations.md +10 -5
  26. package/docs/orca.md +126 -0
  27. package/docs/research.md +38 -3
  28. package/docs/skills.md +74 -0
  29. package/docs/status-line.md +3 -1
  30. package/docs/testing.md +33 -21
  31. package/docs/vision.md +12 -8
  32. package/hooks/projects-pretooluse.py +17 -5
  33. package/hooks/tezgah_agents.py +135 -13
  34. package/hooks/tezgah_components.py +3 -0
  35. package/hooks/tezgah_context.py +65 -22
  36. package/hooks/tezgah_gate.py +133 -17
  37. package/hooks/tezgah_integrity.py +60 -13
  38. package/hooks/tezgah_judge.py +239 -56
  39. package/hooks/tezgah_models.py +41 -5
  40. package/hooks/tezgah_orca.py +144 -0
  41. package/hooks/tezgah_paths.py +40 -6
  42. package/hooks/tezgah_policy.py +109 -35
  43. package/hooks/tezgah_research.py +104 -8
  44. package/hooks/tezgah_skill_pick.py +459 -8
  45. package/hooks/tezgah_stack.py +382 -0
  46. package/hooks/tezgah_stopspec.py +2 -2
  47. package/hosts/codex/hook.py +7 -1
  48. package/hosts/cursor/hook.py +7 -4
  49. package/hosts/omp/hook.py +12 -6
  50. package/hosts/omp/tezgah-hook.ts.in +20 -7
  51. package/hosts/opencode/plugins/tezgah.js +18 -9
  52. package/output-styles/tezgah.md +34 -19
  53. package/package.json +1 -1
  54. package/skills/i-have-adhd/SKILL.md +2 -2
  55. package/skills/research/SKILL.md +60 -17
  56. package/skills/tezgah-contract/SKILL.md +55 -13
package/docs/hosts.md CHANGED
@@ -141,6 +141,52 @@ An installed host whose core line is missing is a health check's business
141
141
  (`host_checks_<host>`), not a second copy: two delivery paths for one host is the
142
142
  same rules paid twice per session.
143
143
 
144
+ ## Precedence: what each host loads, and in which order
145
+
146
+ Several host dirs are read by more than one host. The table gives each host's
147
+ documented order. "First wins" means the host keeps the first copy of a name
148
+ and drops the later ones.
149
+
150
+ | concern | host | order | tezgah's part |
151
+ |---|---|---|---|
152
+ | context | claude | `~/.claude/CLAUDE.md`, then the repo's `CLAUDE.md`, `.claude/CLAUDE.md`, `CLAUDE.local.md`; all load | block in the first |
153
+ | context | codex | `$CODEX_HOME/AGENTS.override.md`, else `AGENTS.md`, then the repo's `AGENTS.md` | block in the first |
154
+ | context | cursor | the `sessionStart` hook context, the repo's `AGENTS.md`, `.cursor/rules/*.mdc`, `.cursorrules` | the hook |
155
+ | context | opencode | `~/.config/opencode/AGENTS.md`, `opencode.json` `instructions`, the repo's `AGENTS.md` | the contract and router files in `instructions` |
156
+ | context | omp | `SYSTEM.md`, `APPEND_SYSTEM.md`, `RULES.md` (sticky), the repo's `.omp/AGENTS.md`, `AGENTS.md`, `CLAUDE.md`; another host's user files only with `enabledProviders` | block in `RULES.md`, the extension at session start |
157
+ | skills | omp | `skills.customDirectories`, then `.omp/skills` and `~/.omp/agent/skills`, Claude's project dir (user dir opt-in), `.agents/skills` and `~/.agents/skills`; first wins, identical copies collapse, a differing copy is renamed `<namespace>/<name>` | links in `~/.omp/agent/skills` |
158
+ | skills | claude | `.claude/skills`, `~/.claude/skills`; plugin skills are namespaced `tezgah:<name>` | the plugin copy |
159
+ | skills | codex | `.agents/skills` from the cwd up to the repo root, `~/.agents/skills`, `$CODEX_HOME/skills`; one name in two dirs is listed twice, not merged | links in `$CODEX_HOME/skills` |
160
+ | skills | cursor | `.agents/skills`, `.cursor/skills`, `~/.agents/skills`, `~/.cursor/skills`, then Claude's and Codex's project and user dirs; no documented winner | links in `~/.cursor/skills` |
161
+ | skills | opencode | its own skill tool is denied; the router lists `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills`, first wins (`bin/tezgah-setup::skill_groups`) | links and the router |
162
+ | hooks | claude | user `settings.json` rows (Orca's), project rows, the plugin's `hooks/hooks.json` (tezgah's); every matching row runs, none is ordered before another | plugin rows |
163
+ | hooks | codex, cursor | `hooks.json` array order, then the repo's `.codex/` or `.cursor/hooks.json`; Codex runs a row only once its hash is trusted | own rows after Orca's |
164
+ | hooks | opencode | every `*.js`/`*.ts` in `plugins/` and `plugin/`; `tezgah.js` is linked into both, and its `globalThis.__tezgahPluginLoaded` guard makes the second load return `{}` | the plugin |
165
+ | hooks | omp | `~/.omp/agent/extensions/` first, then `config.yml` `extensions:`, one load per absolute path; per event in that order, and the first `block` ends a tool call | the bridge in `hooks/pre/` |
166
+ | mcp | each | omp: project `.omp/mcp.json`, then `~/.omp/agent/mcp.json`, first name wins; claude: `~/.claude.json`, the repo's `.mcp.json`, the plugin's `.mcp.json`; codex: `$CODEX_HOME/config.toml`; cursor: `~/.cursor/mcp.json`; opencode: `opencode.json` `mcp` | one `tezgah` and one `codegraph` row per host |
167
+
168
+ Orca is a launcher, not a host. It starts omp with its own `--extension`. It
169
+ gives each Codex account its own `CODEX_HOME` under
170
+ `~/Library/Application Support/orca/codex-accounts/<id>/home`. It adds its
171
+ status rows to Claude's `settings.json`, to Cursor's `hooks.json`, to that
172
+ Codex home's `hooks.json` and to opencode's `plugins/`. Its rows sit beside
173
+ tezgah's. The installer arms the `CODEX_HOME` it runs under
174
+ (`hooks/tezgah_paths.py::HOST_DIRS`). An install from a plain terminal and one
175
+ from an Orca pane therefore arm two different Codex homes.
176
+
177
+ `tezgah-doctor --stack [--repo PATH] [--json]` prints this stack as it stands
178
+ on the machine (`hooks/tezgah_stack.py::host_stack`). It exits 1 on a conflict
179
+ (`hooks/tezgah_stack.py::conflicts`). The kinds are `skill-differs`, `skill-dup`,
180
+ `skill-stale`, `rule-twice`, `hook-twice`, `mcp-twice`, `backup-in-scan` and
181
+ `orphan-copy`. `skill-differs` names the copy that wins. `skill-stale` is a
182
+ tezgah copy that is not this tree's bytes. `orphan-copy` is a Claude plugin
183
+ copy that `installed_plugins.json` no longer names.
184
+
185
+ A backup tezgah writes never lands in a host dir. It goes under
186
+ `~/.config/tezgah/backups/` (`bin/tezgah-setup::backup_base`), because omp
187
+ lists every file in `hooks/pre/` as a hook. `--install` moves the backups older
188
+ releases left in host dirs (`bin/tezgah-setup::sweep_backups`).
189
+
144
190
  ## The model wire
145
191
 
146
192
  The matrix above is about events. This is about the other side of a host: which
@@ -196,7 +242,7 @@ In order, each step verified by the one below it:
196
242
  (`hosts/omp/tezgah-hook.ts.in` rendered at `hooks/tezgah_attest.py::omp_bridge`).
197
243
  3. `install_<host>()` writing that host's files, registered in `INSTALLERS`
198
244
  (`bin/tezgah-setup::INSTALLERS`), and `uninstall_<host>()` removing only
199
- tezgah-managed links and blocks (`bin/tezgah-setup:3286-3418`).
245
+ tezgah-managed links and blocks (`bin/tezgah-setup:3287-3419`).
200
246
  4. `host_checks_<host>()` returning `(label, bool)` rows over what was actually
201
247
  written, registered in `HOST_CHECKS` (`bin/tezgah-setup::HOST_CHECKS`); the
202
248
  `--report` output is that list (`bin/tezgah-setup::report`). Give the row a home-qualified
@@ -398,3 +444,4 @@ In order, each step verified by the one below it:
398
444
  - `hosts/opencode/plugins/tezgah.js`, `hosts/opencode/tui/tezgah-tui.tsx`
399
445
  - `statusline.py`, `bin/tezgah-status`, `hooks/tezgah_context.py`
400
446
  - `hooks/tezgah_models.py` - the per-host model surface in `## The model wire`
447
+ - `hooks/tezgah_stack.py`, `bin/tezgah-doctor` `--stack` - the order in `## Precedence`
package/docs/index.json CHANGED
@@ -466,6 +466,34 @@
466
466
  "docs/testing.md",
467
467
  "AGENTS.md"
468
468
  ]
469
+ },
470
+ {
471
+ "path": "docs/orca.md",
472
+ "title": "Orca: what it owns, what tezgah owns, where they meet",
473
+ "title_tr": "Orca: neyin sahibi, tezgah neyin sahibi, nerede kesişirler",
474
+ "audience": "maintainer, agent working inside Orca",
475
+ "answers": [
476
+ "how does a session know it runs inside Orca",
477
+ "should a slice use orca worktree create or git worktree add",
478
+ "which checkouts of this repo does Orca track",
479
+ "does tezgah overwrite Orca's codex home or hooks"
480
+ ],
481
+ "answers_tr": [
482
+ "bir oturum Orca içinde çalıştığını nasıl anlar",
483
+ "bir dilim orca worktree create mi yoksa git worktree add mi kullanmalı",
484
+ "Orca bu deponun hangi çalışma kopyalarını izliyor",
485
+ "tezgah Orca'nın codex dizinini veya hook'larını ezer mi"
486
+ ],
487
+ "sources": [
488
+ "hooks/tezgah_orca.py",
489
+ "hooks/tezgah_context.py",
490
+ "hooks/tezgah_policy.py",
491
+ "hooks/tezgah_agents.py",
492
+ "bin/tezgah-status",
493
+ "bin/tezgah-setup",
494
+ "skills/tezgah-contract/SKILL.md",
495
+ "statusline.py"
496
+ ]
469
497
  }
470
498
  ]
471
- }
499
+ }
package/docs/judge.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Judge: the judgement seam, its five callers and its switches
2
2
 
3
- The judgement seam is `hooks/tezgah_judge.py`: one module that asks TypeSafe
4
- (Jev) for a batched structured judgement over a state tezgah would otherwise pay
5
- an agent to read. Read this page when you are about to call one of its four
3
+ The judgement seam is `hooks/tezgah_judge.py`. It asks the session's own model,
4
+ through the host's CLI, for a batched structured judgement over a state tezgah
5
+ would otherwise pay an agent to read. Read this page when you are about to call one of its four
6
6
  callers, when you need to say what leaves the machine, or when you have to tell a
7
7
  user how to switch it off. It is an on-demand capability, not a rule: no paragraph
8
8
  of it is injected into a session, and a session that never asks pays one clause in
@@ -11,7 +11,7 @@ the kill-switch paragraph and nothing else (`CORE`,
11
11
 
12
12
  ## What it is
13
13
 
14
- One seam, five callers, one credential, one egress boundary, one price, one
14
+ One seam, five callers, one provider order, one egress boundary, one price, one
15
15
  redirect guard. The module is stdlib only and total - a failure is a `None`, never
16
16
  an exception, because a hook imports it and a hook that raises takes a session
17
17
  down (`_request`, `hooks/tezgah_judge.py::_request`). One endpoint and one key path
@@ -21,12 +21,12 @@ are module constants (`URL`, `hooks/tezgah_judge.py::URL`; `KEY_FILE`,
21
21
  endpoint's host, so a `Location` cannot carry the bearer (`guarded_opener`,
22
22
  `hooks/tezgah_paths.py`, the one copy `bin/consult` and `bin/codegen` use too). A
23
23
  transient failure - a timeout, a connection error, a 5xx - gets exactly one more
24
- attempt; a 4xx and a malformed reply never do (`_transient`,
25
- `hooks/tezgah_judge.py::_transient`). A hook caller asks for one attempt and a
26
- wall-clock `deadline` instead (`ask`, `hooks/tezgah_judge.py::ask`).
27
- urllib's timeout bounds one socket operation, not the whole call.
28
- `TEZGAH_TYPESAFE_URL` repoints the endpoint and `TEZGAH_OPENROUTER_URL` the
29
- fallback's; both are test seams, not fallbacks.
24
+ attempt. A 4xx, a session CLI's non-zero exit and a malformed reply never do
25
+ (`_transient`, `hooks/tezgah_judge.py::_transient`). A hook caller asks for one
26
+ attempt and a wall-clock `deadline` instead (`ask`, `hooks/tezgah_judge.py::ask`).
27
+ A session CLI answers in 4-10 s (measured 2026-10-07), so the skill hint's
28
+ 4 s deadline usually ends first. `TEZGAH_TYPESAFE_URL`, `TEZGAH_OPENROUTER_URL`
29
+ and `TEZGAH_OMP_BIN`/`TEZGAH_CLAUDE_BIN` are the test seams.
30
30
 
31
31
  Every caller reads an answer through the same two accessors rather than reaching
32
32
  into the raw reply, so a Choice and a Noul are read one way for all five
@@ -39,13 +39,13 @@ still reads the selected refs and owns the finding, and the ledger's step kinds
39
39
  are untouched, so a model answer can never license a "done" (`STEP_KINDS`,
40
40
  `hooks/tezgah_integrity.py::STEP_KINDS`).
41
41
 
42
- The state leaves the machine. A judgement sends the state and the questions to
43
- `api.typesafe.ai` - for the triage that is the screen's own text, so a screen
44
- carrying personal data is read by a third party, and for the docs fallback it is
45
- the reader's query. When no TypeSafe key resolves, the same state goes to
46
- `openrouter.ai` instead (`OPENROUTER_URL`, `hooks/tezgah_judge.py::OPENROUTER_URL`), which is
47
- the price of a machine that has no Jev credential still having a judge at all:
48
- the destination changes, not what is sent. Nothing else goes: no session id, no
42
+ The state leaves the machine. By default it goes to the session's own vendor,
43
+ through the session's own CLI and credential ([below](#the-providers-and-their-order)).
44
+ A third party - `api.typesafe.ai` or `openrouter.ai` (`OPENROUTER_URL`,
45
+ `hooks/tezgah_judge.py::OPENROUTER_URL`) - reads it only as the `fallback`
46
+ setting allows. For the triage the state is the screen's own text, so whoever
47
+ answers reads any personal data on it. For the docs fallback it is the query.
48
+ Nothing else goes: no session id, no
49
49
  workspace path, no environment. The seam does not rewrite the state, because
50
50
  sending it is the point. Two callers rewrite what they send. `bin/tezgah-route`
51
51
  sends its brief through the ledger's own redactor (`redact`,
@@ -77,37 +77,42 @@ caches one answer per `(session, prompt)` (`_remember`,
77
77
  `hooks/tezgah_skill_pick.py::suggest`), and asks at all only when its own marker is
78
78
  armed. The other four are bin tools a session runs by name.
79
79
 
80
- ## The credential's channels
81
-
82
- The credential resolves from `TYPESAFE_API_KEY`, else from the key file the module
83
- names (`KEY_FILE`, `hooks/tezgah_judge.py::KEY_FILE`). The environment variable is the
84
- channel an interactive shell has; the file is the channel that matters, because a
85
- hook or a bin tool a host starts runs in a non-interactive shell where a
86
- `~/.zshenv` export never ran, so the variable is absent exactly where a judgement
87
- runs. That is why the file is read rather than the environment trusted. The
88
- install report's health row reads its own pair of channels ([hosts](hosts.md)),
89
- which is a different question from the one the seam asks.
90
-
91
- When neither channel resolves, the same two channels are read again for the
92
- fallback provider - `OPENROUTER_API_KEY`, then `~/.config/openrouter/key`
93
- (`openrouter_key()`, `hooks/tezgah_judge.py::openrouter_key`) - and the request goes to an
94
- OpenAI-compatible chat endpoint instead (`OPENROUTER_URL`,
95
- `hooks/tezgah_judge.py::OPENROUTER_URL`). TypeSafe wins whenever it resolves, so the fallback
96
- cannot move a machine that already judges with Jev (`credential()`,
97
- `hooks/tezgah_judge.py::credential`). The chat model is asked for the same shapes in prose
98
- (`CHAT_SYSTEM`, `hooks/tezgah_judge.py::CHAT_SYSTEM`), at temperature 0
99
- (`_chat_body()`, `hooks/tezgah_judge.py::_chat_body`), and its reply is filtered to the ids
100
- that were asked for, dropping any answer whose type does not match its question
101
- (`_chat_answers()`, `hooks/tezgah_judge.py::_chat_answers`; `_clean_answer()`,
102
- `hooks/tezgah_judge.py::_clean_answer`) - so a dropped answer reads the same as no answer at
103
- all, which is what the callers already handled. The model is the table's cheap row
104
- (`cheap_model`, `hooks/tezgah_models.py`) unless `TEZGAH_JUDGE_MODEL` names
105
- another, and a table that cannot be read falls back to its own literal
106
- (`fallback_model()`, `hooks/tezgah_judge.py::fallback_model`). The result carries the provider
107
- that answered and the model the reply names. The requested model stands only
108
- when the reply names none, so a silent upgrade behind `jev-latest` shows. The
109
- skill hint's cost row and the router's `route` row print both as one
110
- `judge=<provider>/<model>` word.
80
+ ## The providers and their order
81
+
82
+ `providers()` (`hooks/tezgah_judge.py::providers`) is the order `ask()` tries.
83
+ The next one is asked only when the previous failed. First comes the session's own
84
+ CLI (`tp.session_cli`, `hooks/tezgah_paths.py::session_cli`): `omp` when
85
+ `OMPCODE` is set, else `claude` when `CLAUDECODE` is. It runs headless with no
86
+ tools, rules, skills, extensions, MCP servers or settings (`SESSION_ARGV`,
87
+ `hooks/tezgah_judge.py::SESSION_ARGV`). Measured 2026-10-07, a call cost $0.007
88
+ on claude and $0.015 on omp. The session's own credential pays, OAuth subscription
89
+ included. The model and usage are read from the CLI's JSON output
90
+ (`_session_request`, `hooks/tezgah_judge.py::_session_request`). Then come the
91
+ third parties. TypeSafe reads `TYPESAFE_API_KEY`, else `~/.config/typesafe/key`
92
+ (`key`, `hooks/tezgah_judge.py::key`). OpenRouter reads `OPENROUTER_API_KEY`, else
93
+ `~/.config/openrouter/key` (`openrouter_key`, `hooks/tezgah_judge.py::openrouter_key`).
94
+ It asks the table's cheap row unless `TEZGAH_JUDGE_MODEL` names another
95
+ (`fallback_model`, `hooks/tezgah_judge.py::fallback_model`). The key file is
96
+ read because a hook runs where `~/.zshenv` never did. A chat answer is asked for in prose
97
+ (`CHAT_SYSTEM`, `hooks/tezgah_judge.py::CHAT_SYSTEM`) and filtered to the ids
98
+ asked, a wrongly typed answer dropped (`_chat_answers`,
99
+ `hooks/tezgah_judge.py::_chat_answers`).
100
+
101
+ `fallback` in `~/.config/tezgah/config.json` (`fallback_policy`,
102
+ `hooks/tezgah_paths.py::fallback_policy`) decides who may stand in:
103
+
104
+ | `fallback` | Session CLI present | No session CLI |
105
+ |---|---|---|
106
+ | `vendor` (default) | the session CLI only; when it fails, no judgement | TypeSafe, then OpenRouter |
107
+ | `any` | the session CLI, then TypeSafe, then OpenRouter | TypeSafe, then OpenRouter |
108
+ | `none` | the session CLI only | nobody: `available()` is false |
109
+
110
+ Nothing that stands in is silent. A result carries `provider`, the `model`
111
+ the reply named and `fallback` - None when the session answered, else why it did
112
+ not. That answer, and a refusal with its failures, is said on stderr as one
113
+ `tezgah-judge:` line. It is also kept as the last-use record (`_record`,
114
+ `hooks/tezgah_judge.py::_record`), which `tezgah-status --judge` prints. The skill
115
+ hint's cost row and the router's `route` row also carry `judge=<provider>/<model>`.
111
116
 
112
117
  ## The switches
113
118
 
@@ -152,11 +157,11 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
152
157
  3. **No seam-level answer cache, and no caller cache beyond the prompt-keyed one
153
158
  that exists** (`_remember`, `hooks/tezgah_skill_pick.py::_remember`). A judgement
154
159
  costs a fraction of a cent, so a cache is not worth its state file, and a naive
155
- one would make two runs of the same command disagree. The one state the seam
156
- keeps is a failure marker (`DOWN_FOR`, 300 s, in `hooks/tezgah_judge.py`). A
157
- call that ends on a 401, 402 or 5xx marks that provider, endpoint and
158
- credential down for five minutes. Until the marker expires, every call returns
159
- no judgement and sends no request. A dead key or an empty account then costs
160
+ one would make two runs of the same command disagree. Beside the last-use
161
+ record, the seam keeps a failure marker (`DOWN_FOR`, 300 s, in `hooks/tezgah_judge.py`). A
162
+ call that ends on a 401, 402, 5xx or a session CLI's non-zero exit marks that
163
+ provider, endpoint and credential down for five minutes. Until the marker
164
+ expires, that provider is skipped and sent no request. A dead key or an empty account then costs
160
165
  one refusal per five minutes, not one per prompt. A rotated key gets a new
161
166
  marker, so the seam asks it at once.
162
167
  4. **No SDK, no `requests`, no async, no local model.** Each would trade one of
@@ -185,8 +190,9 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
185
190
  - `hooks/tezgah_skill_pick.py` — the prompt-path skill hint, its threshold, its
186
191
  per-prompt cache and its `skill-suggest-on` marker.
187
192
  - `hooks/tezgah_policy.py` — the `**Kill switches:**` paragraph the session
188
- receives; `hooks/tezgah_paths.py` — where a switch file is read, and the
189
- redirect guard (`guarded_opener`) the seam, consult and codegen share.
193
+ receives; `hooks/tezgah_paths.py` — where a switch file is read, the session CLI
194
+ and the `fallback` setting, and the redirect guard (`guarded_opener`) the seam,
195
+ consult and codegen share; `bin/tezgah-status --judge` — the last-use record.
190
196
  - `hooks/tezgah_integrity.py` — the ledger and the `judge` counter;
191
197
  `hooks/tezgah_context.py` — the status mark and the class carve-out.
192
198
  - `tests/test_judge.py`, `tests/test_triage.py`, `tests/test_docs_router.py` —
package/docs/models.md CHANGED
@@ -20,7 +20,12 @@ and every slot a model per family (`SLOTS`, `hooks/tezgah_models.py::SLOTS`). Th
20
20
  three tier workers - `tezgah-cheap`, `tezgah-standard`, `tezgah-frontier` - are
21
21
  generated in every tezgah root because routing needs no capability (`ROLES`,
22
22
  `hooks/tezgah_agents.py::ROLES`; their brief, `_worker_body`,
23
- `hooks/tezgah_agents.py::_worker_body`). A cheaper worker that meets work above its tier
23
+ `hooks/tezgah_agents.py::_worker_body`). So are the five specialists:
24
+ `tezgah-tester`, `tezgah-docs`, `tezgah-security`, `tezgah-ui` and
25
+ `tezgah-researcher` (`hooks/tezgah_agents.py::SPECIALISTS`). A specialist's
26
+ description names its slot from `AGENT_SLOT`. The tier a host shows and the
27
+ model line it runs therefore cannot disagree, and `tezgah-agents --list` prints
28
+ both. A cheaper worker that meets work above its tier
24
29
  answers `ESCALATE: <why>`, and the router restarts the task on `tezgah-frontier`
25
30
  with the original brief rather than handing the failed trajectory up: continuing a
26
31
  cheap trajectory on a frontier model was the most expensive option measured
@@ -30,10 +35,10 @@ cheap trajectory on a frontier model was the most expensive option measured
30
35
 
31
36
  | slot | agents | anthropic | zai (omp's own provider) | openai | any (OpenRouter id) |
32
37
  |---|---|---|---|---|
33
- | cheap | tezgah-cheap | Opus 5.5 @low | `zai/glm-5.3-flash` | GPT-6.1 Sol @low | GLM-5.3 Flash |
38
+ | cheap | tezgah-cheap, tezgah-tester | Opus 5.5 @low | `zai/glm-5.3-flash` | GPT-6.1 Sol @low | GLM-5.3 Flash |
34
39
  | explore | omp's bundled `scout` | Opus 5.5 @medium | `zai/glm-5.3-flash` | GPT-6.1 Sol @low | DeepSeek V4.1 Flash |
35
- | standard | tezgah-standard | Opus 5.5 @medium | `zai/glm-5.3` | GPT-6.1 Sol @high | GPT-6.1 Sol @high |
36
- | frontier | tezgah-frontier, tezgah-reviewer | Opus 5.5 @high | `zai/glm-5.3` | GPT-6 Astra @high | Opus 5.5 @high |
40
+ | standard | tezgah-standard, tezgah-docs, tezgah-ui | Opus 5.5 @medium | `zai/glm-5.3` | GPT-6.1 Sol @high | GPT-6.1 Sol @high |
41
+ | frontier | tezgah-frontier, tezgah-reviewer, tezgah-security, tezgah-researcher | Opus 5.5 @high | `zai/glm-5.3` | GPT-6 Astra @high | Opus 5.5 @high |
37
42
 
38
43
  Why these, briefly. Inside the Anthropic family the lever is effort, not model:
39
44
  Opus 5.5 and Sonnet 5.5 read cache at the same $0.20 per 1M tokens, and on
@@ -56,7 +61,7 @@ invoices).
56
61
  | Claude Code | anthropic | the agent file's alias and `effort:` (`_model_lines`): an alias follows the provider and keeps the main session's variant, where a full id breaks on Bedrock, Vertex and a gateway (code.claude.com/docs/en/sub-agents) |
57
62
  | Codex | openai | `model` and `model_reasoning_effort` in `.codex/agents/*.toml` |
58
63
  | opencode | any | a selector `--refresh` found in `opencode models` for the slot's model under any configured provider (`resolve_opencode`); none found, no model line, so the agent inherits. The slot's effort rides the JSON agent config as `reasoningEffort` (opencode.ai/docs/agents) |
59
- | omp | anthropic when the session default is an Anthropic model, any otherwise (`omp_mode`); a non-Anthropic default is `any`, never a silent fall back to the Anthropic column | `task.agentModelOverrides`, which omp resolves before the agent or bundled model (omp's own docs: `omp://task-agent-discovery.md`, *Model and structured-output precedence*); every slot is written, the frontier row included, so no agent in the row runs on the session default; each override is a **cross-family fallback chain** (`omp_overrides`): the mode's family first, then every other family this machine holds a credential for (`funded_families` reads omp's auth store and the provider env keys; a family it cannot see is left out, never guessed in) - omp splits the chain into per-spawn patterns (omp's own docs: `omp://settings.md (its `retry.fallbackChains` section)`), so one provider's 429 falls through instead of killing the spawn, measured before this: 24 of 26 post-routing 429s hit a subagent's first request, all on one Opus account every slot shared; `apply_omp` also writes `modelRoles.plan` and `.slow` on that frontier row (same record, same ownership) so plan mode designs on it, and their fallbacks go to `retry.fallbackChains.<role>`, the only chain keys tezgah owns (`default` and provider-keyed chains stay the user's); `apply_omp` writes through the installer's own binary and agent dir, from HOME so a repository's `.omp/config.yml` never leaks machine-wide, and touches only the entries tezgah wrote (recorded in `~/.config/tezgah/models.json` as `omp_written`; a machine written before that record is adopted by its value, `_ours_by_shape`) |
64
+ | omp | anthropic when the session default is an Anthropic model, any otherwise (`omp_mode`); a non-Anthropic default is `any`, never a silent fall back to the Anthropic column | `task.agentModelOverrides`, which omp resolves before the agent or bundled model (omp's own docs: `omp://task-agent-discovery.md`, *Model and structured-output precedence*); every slot is written, the frontier row included, so no agent in the row runs on the session default; each override is a **fallback chain** (`omp_overrides`, `_chain`): the mode's family first, then what `fallback` in `~/.config/tezgah/config.json` allows (`tp.fallback_policy`) over the families this machine holds a credential for (`funded_families` reads omp's auth store and the provider env keys; a family it cannot see is left out, never guessed in) - `vendor`, the default, keeps only the session vendor's model (`openrouter/anthropic/claude-opus-5.5:<effort>` behind an Anthropic session, `OPENROUTER_ID`), so a 429 never moves an Opus session's subagent onto GPT or GLM; `any` adds every other funded family, rotated per agent; `none` writes the first entry alone - omp splits the chain into per-spawn patterns (omp's own docs: `omp://settings.md (its `retry.fallbackChains` section)`), so one provider's 429 falls through instead of killing the spawn, measured before this: 24 of 26 post-routing 429s hit a subagent's first request, all on one Opus account every slot shared; `apply_omp` also writes `modelRoles.plan` and `.slow` on that frontier row (same record, same ownership) so plan mode designs on it, and their fallbacks go to `retry.fallbackChains.<role>` under the same `fallback` rule, the only chain keys tezgah owns (`default` and provider-keyed chains stay the user's); `apply_omp` writes through the installer's own binary and agent dir, from HOME so a repository's `.omp/config.yml` never leaks machine-wide, and touches only the entries tezgah wrote (recorded in `~/.config/tezgah/models.json` as `omp_written`; a machine written before that record is adopted by its value, `_ours_by_shape`); a changed `fallback` reaches omp on the next `tezgah-route --refresh` or `tezgah-setup --install` |
60
65
  | dsh | - | none: no model setting was found to write |
61
66
 
62
67
  The orchestrator agent keeps `model: inherit`: it is the main thread's own agent.
@@ -88,11 +93,12 @@ fixed:
88
93
  undone on. A 12-brief fixture pins it: 8 high-stakes briefs route to
89
94
  frontier, 4 near-miss controls do not (`tests/test_models.py`).
90
95
  2. Otherwise the brief - redacted with the ledger's own reader
91
- (`redact`, `hooks/tezgah_integrity.py::redact`) - goes to Jev as one Choice over three tiers (`TIER_QUESTION`,
92
- `hooks/tezgah_models.py::TIER_QUESTION`). Measured on 40 English briefs labelled by
96
+ (`redact`, `hooks/tezgah_integrity.py::redact`) - goes to the judge as one Choice over three tiers (`TIER_QUESTION`,
97
+ `hooks/tezgah_models.py::TIER_QUESTION`). The judge asks the session's own model first ([judge](judge.md)).
98
+ Measured with Jev on 40 English briefs labelled by
93
99
  the same session that wrote the rubric (2026-09-30, twice): under-route 0.025,
94
100
  accuracy 0.925 and 0.900, 392 ms median, about 656 input tokens per call; a
95
- keyword rule on the same set under-routed 0.100.
101
+ keyword rule on the same set under-routed 0.100. The session model was not measured on that set.
96
102
  3. With no judgement (`judge-off`, no key, a failed call) `--phase` picks the tier
97
103
  from the static table (`PHASE_TIER`, `hooks/tezgah_models.py::PHASE_TIER`), and
98
104
  with no phase the middle tier is used.
@@ -23,7 +23,7 @@ before an install is considered.
23
23
  | `--refresh` | Re-render every always-on contract artifact of the armed hosts - the managed blocks of `CLAUDE.md`, Codex's `AGENTS.md` and omp's `RULES.md`, opencode's contract and skill routers - after a policy edit or a kill switch; prints the paths it refreshed, else `contract is current`. Before the first install there is no hash record, so it changes nothing (`refresh_contract`, `bin/tezgah-setup::refresh_contract`). |
24
24
  | `--uninstall` | Remove everything tezgah installed — per host the wiring it wrote, plus (a full run) the Claude plugin copy with its registry rows, the generated config state, the kill switches, the caches and the versioned install tree — then verify the removal; nonzero exit while anything tezgah wrote survives (`bin/tezgah-setup::verify_uninstall`, `bin/tezgah-setup::uninstall`). |
25
25
  | `--adopt` | Move pre-tezgah wiring aside instead of deleting it; alone it stops there, with `--install` it runs first (`bin/tezgah-setup::main`, `bin/tezgah-setup::adopt`). |
26
- | `--sync` | Copy this checkout over every installed Claude plugin [copy](glossary.md#plugin-copy) (`bin/tezgah-setup::sync`). Each copy is built in a staging tree beside the plugin cache and swapped in with two renames, its `.git` carried over, so a failed or killed sync leaves the previous copy whole (`bin/tezgah-setup::_swap`). Every copied file is 0644, or 0755 when the tree marks it executable, whatever mode the source has (`bin/tezgah-setup::_copy_normalised`). |
26
+ | `--sync` | Copy this checkout over the Claude plugin [copy](glossary.md#plugin-copy) `installed_plugins.json` names, or over every copy when it names none (`bin/tezgah-setup::sync`, `bin/tezgah-setup::live_plugin_copies`). An older version dir is Claude's cache to collect, so it is left alone. Each copy is built in a staging tree beside the plugin cache and swapped in with two renames, its `.git` carried over, so a failed or killed sync leaves the previous copy whole (`bin/tezgah-setup::_swap`). Every copied file is 0644, or 0755 when the tree marks it executable, whatever mode the source has (`bin/tezgah-setup::_copy_normalised`). |
27
27
  | `--hook-entries` | Print, as JSON, the tezgah-owned hook entries this tree arms per armed host - what `tezgah update` asks the new tree for (`bin/tezgah-setup::hook_entries`). |
28
28
  | `--status [PATH]` | Print the armed/used checklist for PATH (default cwd) and stop — the same line as `bin/tezgah-status` (`bin/tezgah-setup::main`, `hooks/tezgah_context.py::health_lines`). It also prints the report's `config.json hosts (...) match the hosts wired (...)` row when the recorded list and the wiring on disk disagree (`hosts_row`), and nothing when they agree: install health is checked here, not only in a full report. |
29
29
  | `--agents [PATH]` | Regenerate PATH's per-repo subagent set (default cwd); outside a [root](glossary.md#root) it prints `no agents generated` (`bin/tezgah-setup::main`). |
@@ -158,7 +158,7 @@ or `default`), a common block (config, the code-graph binary, the consult option
158
158
  `orx`, `npx`, the artifacts dir, `git`, the manifest, a stale-contract line), one
159
159
  block per host, then the budget (`bin/tezgah-setup::report`). Each block is
160
160
  `host_checks_<host>` (`bin/tezgah-setup::HOST_CHECKS`), and those rows are the source of truth for
161
- "is this host armed" (`AGENTS.md:136-137`) — not the presence of a directory, and
161
+ "is this host armed" (`AGENTS.md:147-148`) — not the presence of a directory, and
162
162
  not the status line. A row can read ` MISS ` too: claude's `plugin copy current`
163
163
  row fails on a machine with no copy at all, because an absent copy is not a
164
164
  current one (`bin/tezgah-setup::host_checks_claude`).
@@ -284,8 +284,8 @@ A **full** run also takes everything the wiring was serving from:
284
284
  `bin/tezgah-setup` plus the `current` flip (`remove_install_tree()`,
285
285
  `bin/tezgah-setup::remove_install_tree`).
286
286
 
287
- Kept in every run: the user's own files, every `.tezgah-bak` backup
288
- outside the config dir (the timestamped ones too), `~/.config/tezgah/adopted/` (the only copy of the
287
+ Kept in every run: the user's own files, every host-file backup under
288
+ `~/.config/tezgah/backups/` (the timestamped ones too), `~/.config/tezgah/adopted/` (the only copy of the
289
289
  predecessor wiring adopt moved aside), and every repository's `.tezgah/`
290
290
  research state. A **partial** run also keeps config.json and the install tree,
291
291
  because the hosts still armed read both at runtime, and says so. It also leaves
@@ -372,6 +372,7 @@ does not hold.
372
372
  | `tezgah-doctor` | nothing: sizes, session and event counts, whether opencode is running, the two context-hygiene settings, the `.codegraph` bytes and index presence per repository, and the three host state dirs (`bin/tezgah-doctor::collect`, `bin/tezgah-doctor::report`) |
373
373
  | `tezgah-doctor --clean` | vacuums opencode's database, and only when opencode is not running (`bin/tezgah-doctor::vacuum_db`, `bin/tezgah-doctor::main`); and deletes tezgah's own hook state not modified for `--retention-days` (default 30) - the ledgers under `evidence/`, `turns/`, `sessions/`, `classify.log`, `context-drops.log` and `debug.log`, in the cache and its sandbox fallback - keeping the current session's (`$TEZGAH_SESSION`) files whatever their age (`sweep_state`) |
374
374
  | `tezgah-doctor --coverage` | every tracked file the codegraph index does not hold, in two classes — a supported extension the index is missing, and a shebang-only script with no `.py` twin — with the file counts it read, so an empty result cannot read as "everything is covered" |
375
+ | `tezgah-doctor --stack` | per host: the context files in load order, the skill dirs and which copy wins, the hook rows per event with their owner, the MCP servers, and every conflict; exits 1 on one ([hosts](hosts.md#precedence-what-each-host-loads-and-in-which-order), `hooks/tezgah_stack.py::report`) |
375
376
  | `tezgah-doctor --prune-sessions DAYS` | deletes sessions idle longer than DAYS through `opencode session delete`, then vacuums; skipped when opencode is running or its CLI is missing (`bin/tezgah-doctor::prune_sessions`, `bin/tezgah-doctor::main`) |
376
377
 
377
378
  `VACUUM` alone cannot shrink that database — its pages are all live — so
@@ -541,7 +542,11 @@ with `embed-m2v`. The fusion found it for 0.75 and 0.74, BM25 for 0.68.
541
542
 
542
543
  Reversible and safe to repeat: every link and settings key it writes is
543
544
  idempotent and re-created by `--install`. A write that changes a file first
544
- copies it to `<file>.<timestamp>.tezgah-bak`. The oldest of those copies is
545
+ copies it to `<file>.<timestamp>.tezgah-bak`. A host file's copy goes under
546
+ `~/.config/tezgah/backups/` (`bin/tezgah-setup::backup_base`). A host lists the
547
+ files in its own dirs. tezgah's own state dir
548
+ keeps its copies beside the file. `--install` moves the copies an older release
549
+ left in a host dir (`bin/tezgah-setup::sweep_backups`). The oldest of those copies is
545
550
  always kept, beside the newest four (`backup()`, `BACKUP_CAP`). The oldest is
546
551
  the file before tezgah's first timestamped write, which is tezgah's own content
547
552
  for a file tezgah created. On an upgraded install the pre-tezgah copy is the
package/docs/orca.md ADDED
@@ -0,0 +1,126 @@
1
+ # Orca: what it owns, what tezgah owns, where they meet
2
+
3
+ Orca is an IDE that runs agent sessions in its own terminals and worktrees.
4
+ This page is for a maintainer or an agent working inside Orca. Read it when a
5
+ session asks how tezgah and Orca share a machine. It describes Orca 1.4.221 and
6
+ 1.4.222 as observed on 2026-10-07. Each Orca claim names the command or file
7
+ behind it, so a reader can check it again after a later release.
8
+
9
+ ## How a process knows it runs in Orca
10
+
11
+ Every terminal Orca starts exports its own markers. The table lists the ones
12
+ tezgah reads, as `env` printed them inside an Orca terminal.
13
+
14
+ | Variable | Value shape | Read by |
15
+ |---|---|---|
16
+ | `ORCA_WORKTREE_ID` | `<repoId>::<absolute path>` of the Orca worktree | `hooks/tezgah_orca.py::session` |
17
+ | `ORCA_TERMINAL_HANDLE` | `term_<uuid>`, the handle `orca terminal` commands take | `hooks/tezgah_orca.py::session` |
18
+ | `ORCA_PANE_KEY` | `<tab>:<pane>` | `hooks/tezgah_orca.py::session` |
19
+ | `TERM_PROGRAM` | `Orca` | `hooks/tezgah_orca.py::session` |
20
+ | `ORCA_APP_VERSION` | `1.4.221` | `hooks/tezgah_orca.py::session` |
21
+ | `ORCA_CLI_COMMAND` | the wrapper Orca exports for WSL sessions (orca-cli skill) | `hooks/tezgah_orca.py::cli` |
22
+ | `ORCA_CLI_BIN_DIR` | `/Applications/Orca.app/Contents/Resources/bin` | `hooks/tezgah_orca.py::cli` |
23
+ | `CODEX_HOME`, `ORCA_CODEX_HOME` | `~/Library/Application Support/orca/codex-accounts/<id>/home` | `hooks/tezgah_paths.py::HOST_DIRS` (CODEX_HOME only) |
24
+
25
+ Orca also exports `ORCA_AGENT_HOOK_*` for its hook endpoint, port and token.
26
+ It exports `ORCA_OMP_*` for its omp status extension. tezgah reads none of
27
+ them and writes no `ORCA_*` variable. The marker set in `MARKERS` is the whole
28
+ contract.
29
+
30
+ A shell started from an Orca terminal inherits the variables. A subagent whose
31
+ working directory is a raw worktree still reports the parent's
32
+ `ORCA_WORKTREE_ID`. So the marker says "inside Orca", not "inside this
33
+ checkout".
34
+
35
+ ## What Orca owns
36
+
37
+ | Surface | What Orca keeps | Evidence |
38
+ |---|---|---|
39
+ | Worktrees | a git worktree plus a card (status, comment, lineage, links), by default under `~/orca/workspaces/<repo>/<name>` | `orca worktree create --help`; `orca repo show` lists paths under `~/orca/workspaces/tezgah/` |
40
+ | Terminals | live PTYs with handles, read/send/wait | `orca terminal list --json` |
41
+ | Agent sessions | agent type and state per pane, shown in `orca worktree ps` | `agents[].agentType`, `agents[].state` in `orca worktree ps --json` |
42
+ | Orchestration | runs, tasks, dispatch, supervised workers, decision gates | `orca --help`, section Orchestration |
43
+ | Embedded browser | tabs per worktree, snapshot and element refs | `orca --help`, section Browser Automation |
44
+ | Artifacts | public HTML or Markdown links, off until a human enables them in Settings | `orca skills get orca-cli`, section Artifacts |
45
+ | Codex accounts | one codex home per account, marked by `.orca-managed-home` | the account home's directory listing |
46
+ | Its hooks | `~/.orca/agent-hooks/`, plus entries tagged `orca-agent-hook-form=1` in each managed codex `hooks.json` | the account home's `hooks.json` |
47
+ | omp extensions | `orca-agent-status.ts`, `orca-prefill.ts` and `orca-titlebar-spinner.ts` in `~/.omp/agent/extensions/` | that directory's listing |
48
+
49
+ The `orca-cli` skill is a stub. The full guide comes from the binary itself:
50
+ `orca skills get orca-cli`. Run it before any Orca command a session has not
51
+ run before.
52
+
53
+ ## What tezgah owns
54
+
55
+ tezgah owns the contract text, the gate, the evidence ledger and the status
56
+ line. It also owns its own hook entries in each host config
57
+ ([architecture](architecture.md), [hosts](hosts.md)). It does not create
58
+ worktrees or terminals in code. A parallel slice is an instruction to the
59
+ session: the `Tier 1` paragraph of `hooks/tezgah_policy.py::ORCHESTRATE`,
60
+ mirrored in `skills/tezgah-contract/SKILL.md`, and the orchestrator agent body
61
+ `hooks/tezgah_agents.py::_orch_body`.
62
+
63
+ ## Where they meet
64
+
65
+ | Point | Behaviour | Code |
66
+ |---|---|---|
67
+ | Session start inside Orca | one `Orca:` line names the worktree and tells the session to use `orca worktree create`, `orca terminal create` and `orca worktree ps`. It reads env only and starts no process. | `hooks/tezgah_orca.py::context_line`, called from `hooks/tezgah_context.py::context_for` |
68
+ | Slice checkouts | inside Orca, `orca worktree create --name <slug> --parent-worktree active --json`, else `git worktree add` | `hooks/tezgah_policy.py::ORCHESTRATE`, `hooks/tezgah_agents.py::_orch_body` |
69
+ | Tracking | `tezgah-status --orca` lists each checkout Orca tracks (card, state, live terminals, agent states) and each git worktree Orca does not list | `bin/tezgah-status::print_orca`, `hooks/tezgah_orca.py::checkouts` |
70
+ | Install report | `tezgah-setup --status` prints one `orca:` line where Orca is present. The line names the runtime, the terminal and an Orca-managed CODEX_HOME. | `hooks/tezgah_orca.py::summary`, `bin/tezgah-setup::main` |
71
+ | Codex hooks | tezgah drops only entries holding `tezgah` and appends its own groups, so Orca's entries stay | `bin/tezgah-setup::install_codex` |
72
+ | Cursor hooks | the same merge | `bin/tezgah-setup::install_cursor` |
73
+ | Claude status line | tezgah's wrapper runs Orca's `~/.orca/agent-hooks/claude-statusline.sh` first and appends its own segment | `statusline.py::ORCA`, `bin/tezgah-setup::wire_claude_statusline` |
74
+ | Codex rules file | Orca links the account home's `AGENTS.md` to `~/.codex/AGENTS.md`. tezgah writes the managed block through that link with a plain `open`, so the link stays. | `bin/tezgah-setup::set_managed_md` |
75
+
76
+ ### The collision this fixes
77
+
78
+ On 2026-10-07 the tezgah repository had eleven git checkouts and Orca listed
79
+ one. `git worktree list` printed the main checkout and ten under
80
+ `~/Projects/tezgah-wt/`, and `orca worktree list --repo path:<main> --json`
81
+ returned `totalCount: 1`. The ten came from raw `git worktree add`, so Orca
82
+ showed no card, terminal or agent state for them. `tezgah-status --orca` now
83
+ prints that gap.
84
+
85
+ A smoke run on the live runtime closed the loop. It ran `orca worktree create
86
+ --name tezgah-orca-smoke --parent-worktree active --setup skip --json` from the
87
+ main checkout. `tezgah-status --orca --json` then listed the new checkout as
88
+ tracked, with one terminal. `orca worktree rm --force` removed it, its branch
89
+ included.
90
+
91
+ ### CODEX_HOME inside Orca
92
+
93
+ Inside Orca, CODEX_HOME names an Orca-managed account home. A `tezgah-setup
94
+ --install` run from an Orca terminal arms that home, and a run from another
95
+ terminal arms `~/.codex` (`hooks/tezgah_paths.py::HOST_DIRS`). On this machine
96
+ both homes carry tezgah's seven hook events. The Orca home keeps
97
+ Orca's own entries beside them. Orca also keeps
98
+ `.orca-hook-trust-provenance.json` there, keyed by event and entry index.
99
+ tezgah appends after Orca's entries, so Orca's index does not move. Whether
100
+ Orca re-trusts a changed tezgah entry on its own is not verified.
101
+
102
+ ## What the Orca CLI does not offer
103
+
104
+ - No command adopts an existing raw git worktree as an Orca worktree. The
105
+ `Worktrees` section of `orca --help` lists `list`, `show`, `current`,
106
+ `create`, `set`, `rm` and `ps` only. `orca repo set --repo <sel>
107
+ --external-worktree-visibility show` makes non-Orca worktrees visible in the
108
+ sidebar. It does not give them cards.
109
+ - An agent cannot turn on artifact publishing. A human enables it in Settings
110
+ (the `Artifacts` section of `orca skills get orca-cli`).
111
+
112
+ ## Tests
113
+
114
+ `tests/test_orca.py` runs with no real Orca. A fake `orca` on disk answers
115
+ `worktree ps` and `status`, and every marker is a fixture. `tests/support.py`
116
+ strips the developer's own `ORCA_*` markers and `TERM_PROGRAM=Orca`, and pins
117
+ `TEZGAH_ORCA_BIN` to a missing path. Without that, a suite run inside Orca
118
+ would reach the live runtime.
119
+
120
+ ## Source of truth
121
+
122
+ - `hooks/tezgah_orca.py`: detection, CLI resolution, the listing and both report lines
123
+ - `hooks/tezgah_context.py`: where the session line joins the session-start block
124
+ - `hooks/tezgah_policy.py`, `skills/tezgah-contract/SKILL.md`, `hooks/tezgah_agents.py`: the slice rule
125
+ - `bin/tezgah-status`, `bin/tezgah-setup`: the two report surfaces
126
+ - `tests/test_orca.py`, `tests/support.py`
package/docs/research.md CHANGED
@@ -65,7 +65,7 @@ nothing recorded the ask and `close --limit` accepted any string.
65
65
  `state.json` `rules` is the line's format version. `init` writes `RULES`, the
66
66
  newest set (`hooks/tezgah_research.py::RULES`). Each set keeps its own gate. Rules 2
67
67
  are the standards rules. Rules 3 add the ask contract. Rules 4 add the protocol
68
- sections below. A bump never weakens an older set.
68
+ sections below. Rules 5 add the scope lock. A bump never weakens an older set.
69
69
 
70
70
  A `rules` above `RULES` means a later tezgah wrote the line. `check` then reports
71
71
  one error that names both numbers and reads nothing else. The writers refuse the
@@ -110,14 +110,49 @@ it. The rules come from the gortex/SoL-Pi adoption's ideas 3 and 4 and from the
110
110
  layer audit's P2 probe (`_check_protocol_sections`, `_token_claim_problem`).
111
111
  Older lines keep the word rule.
112
112
 
113
+ ## Scope lock (rules 5)
114
+
115
+ The owner reported on 2026-10-07 that research turned every question into
116
+ metrics. It also grew past the ask, stayed shallow, and drifted off topic. The
117
+ rule text pushed each of these. The injected rule sent every research task to
118
+ orx's experiment tree. The skill locked a metric at bootstrap. `check` refused a
119
+ line past `bootstrap` without one. An analysis owed three compared variants. The
120
+ ideation moves had no fence. Rules 5 lock a line to its question:
121
+
122
+ - `init --in "<covered>" --out "<left alone>"` (repeatable) write `state.json`
123
+ `scope`. The in-scope items are `S1`, `S2`, ... in order. `--method
124
+ qualitative|measured` writes `method`. `scope_problems`
125
+ (`hooks/tezgah_research.py::scope_problems`) names an empty `scope.in`, an
126
+ empty `scope.out` and a missing method. `check` refuses them past `bootstrap`
127
+ and warns at it.
128
+ - A claim's `trace` names the in-scope item it answers (`S<n>` or the item's
129
+ text). `claim_problems` and `_check_claims` both call `trace_problem`
130
+ (`hooks/tezgah_research.py::trace_problem`). The writer and the checker refuse
131
+ the same off-topic claim.
132
+ - A `qualitative` line owes no metric. `_check_evaluation`
133
+ (`hooks/tezgah_research.py::_check_evaluation`) skips it. `_check_scope`
134
+ refuses one that records `results.jsonl`. A measured question says `measured`
135
+ and locks its evaluation first.
136
+ - An `analysis` deliverable owes no variants (`deliverable_problems` takes the
137
+ line's rules). A design, plan or code still does. Older lines keep three.
138
+
139
+ The session reads the same lock in the research rule
140
+ (`hooks/tezgah_policy.py::RESEARCH` and the paragraph `**Research: route it to
141
+ OpenResearch.**`). Restate the question verbatim. Name what is in and out of
142
+ scope. Drop a finding that answers nothing asked. Pick the method from the
143
+ question. Answer a plain factual question in the reply from primary sources, with
144
+ no line. Cite each finding's source with a confidence. The host-subagent fallback
145
+ gets the question, its scope and its method in its brief. The research skill's
146
+ "Scope and method" section is the long form.
147
+
113
148
  ## One line, one directory
114
149
 
115
150
  | Path | Holds |
116
151
  |---|---|
117
- | `state.json` | the question, the phase (`bootstrap`/`inner`/`outer`/`concluded`), the direction, the locked evaluation and its optional second gate (`capability_tolerance`, `counter_metric`), the session events (`PHASES`, `hooks/tezgah_research.py::PHASES`), `rules` (the rule set `init` opened it under, `RULES`), `deliverable` (`kind`, `path`, `ask`, `min_variants`), `supersedes` for a new version of an older line, and `closed` once `close` records a deliberate limit. The phase is the author's declaration; `derived_phase` (`hooks/tezgah_research.py::derived_phase`) reads the *other* authority beside it from the line's own artifacts, and the checker names the two disagreeing |
152
+ | `state.json` | the question, the phase (`bootstrap`/`inner`/`outer`/`concluded`), the direction, `method` and `scope` (rules 5, see "Scope lock"), the locked evaluation and its optional second gate (`capability_tolerance`, `counter_metric`), the session events (`PHASES`, `hooks/tezgah_research.py::PHASES`), `rules` (the rule set `init` opened it under, `RULES`), `deliverable` (`kind`, `path`, `ask`, `min_variants`), `supersedes` for a new version of an older line, and `closed` once `close` records a deliberate limit. The phase is the author's declaration; `derived_phase` (`hooks/tezgah_research.py::derived_phase`) reads the *other* authority beside it from the line's own artifacts, and the checker names the two disagreeing |
118
153
  | `log.md` | the decision log, newest last: one line per decision, experiment, dead end or pivot, with the evidence that drove it |
119
154
  | `findings.md` | the four sections every line answers, named by `FINDINGS_SECTIONS` (`hooks/tezgah_research.py::FINDINGS_SECTIONS`) |
120
- | `claims.jsonl` | one JSON object per row: `statement`, `falsification`, `proof`, `provenance`, `status`, `kind`, `scope` (what the claim's numbers were measured on, which may not be wider than the rows it rests on), and `supersedes` when the row replaces an earlier claim |
155
+ | `claims.jsonl` | one JSON object per row: `statement`, `falsification`, `proof`, `provenance`, `status`, `kind`, `trace` (the in-scope item, rules 5), `scope` (what the claim's numbers were measured on, which may not be wider than the rows it rests on), and `supersedes` when the row replaces an earlier claim |
121
156
  | `predictions.jsonl` | one JSON object per row: `commit` (a 40-character sha the row is bound to), `claim` (an id the line holds, or empty when the prediction stands alone), `metric`, `value_before`, `value_after`, `falsifier`, `components` (the keys of the components the change touches, read from `hooks/tezgah_components.py`), and `granted_by` when a human granted what the frozen paths below forbid |
122
157
  | `experiments/<hypothesis>/protocol.md` | what the change is, what it predicts, what result would falsify it, why - committed before the run |
123
158
  | `experiments/<hypothesis>/results.jsonl` | the rows the run produced, one JSON object per non-blank line, each carrying a non-empty `source` and, once declared, a `scope` of `real`, `fixture` or `derived`, and - when that scope is `fixture` - a `fixture` description of what was generated |