@r1z4x/tezgah 1.0.0 → 1.2.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 (60) hide show
  1. package/CHANGELOG.md +92 -1
  2. package/MANIFEST +10 -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-taste +504 -5
  12. package/bin/tezgah-triage +5 -3
  13. package/docs/README.md +1 -0
  14. package/docs/architecture.md +5 -2
  15. package/docs/citations-baseline.json +1 -1
  16. package/docs/clarity-baseline.json +9 -9
  17. package/docs/contract.md +68 -26
  18. package/docs/environment.md +6 -3
  19. package/docs/evidence.md +7 -0
  20. package/docs/gate.md +13 -7
  21. package/docs/glossary.md +1 -1
  22. package/docs/hosts.md +48 -1
  23. package/docs/index.json +34 -3
  24. package/docs/judge.md +75 -60
  25. package/docs/models.md +14 -8
  26. package/docs/operations.md +80 -25
  27. package/docs/orca.md +126 -0
  28. package/docs/research.md +38 -3
  29. package/docs/skills.md +74 -0
  30. package/docs/status-line.md +3 -1
  31. package/docs/testing.md +33 -21
  32. package/docs/vision.md +12 -8
  33. package/hooks/projects-posttooluse.py +7 -0
  34. package/hooks/projects-pretooluse.py +17 -5
  35. package/hooks/tezgah_agents.py +135 -13
  36. package/hooks/tezgah_components.py +3 -0
  37. package/hooks/tezgah_context.py +84 -22
  38. package/hooks/tezgah_gate.py +133 -17
  39. package/hooks/tezgah_integrity.py +60 -13
  40. package/hooks/tezgah_judge.py +288 -66
  41. package/hooks/tezgah_models.py +41 -5
  42. package/hooks/tezgah_orca.py +144 -0
  43. package/hooks/tezgah_paths.py +40 -6
  44. package/hooks/tezgah_policy.py +109 -35
  45. package/hooks/tezgah_research.py +104 -8
  46. package/hooks/tezgah_skill_pick.py +459 -8
  47. package/hooks/tezgah_stack.py +382 -0
  48. package/hooks/tezgah_stopspec.py +2 -2
  49. package/hooks/tezgah_taste.py +35 -9
  50. package/hooks/tezgah_taste_ledger.py +628 -0
  51. package/hosts/codex/hook.py +12 -1
  52. package/hosts/cursor/hook.py +12 -4
  53. package/hosts/omp/hook.py +18 -6
  54. package/hosts/omp/tezgah-hook.ts.in +20 -7
  55. package/hosts/opencode/plugins/tezgah.js +28 -15
  56. package/output-styles/tezgah.md +34 -19
  57. package/package.json +1 -1
  58. package/skills/i-have-adhd/SKILL.md +2 -2
  59. package/skills/research/SKILL.md +60 -17
  60. package/skills/tezgah-contract/SKILL.md +55 -13
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,31 +21,34 @@ 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.
30
-
31
- Every caller reads an answer through the same two accessors rather than reaching
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
+
31
+ Every caller reads an answer through the same accessors rather than reaching
32
32
  into the raw reply, so a Choice and a Noul are read one way for all five
33
33
  (`choice`, `hooks/tezgah_judge.py::choice`; `noul`,
34
- `hooks/tezgah_judge.py::noul`). Both are total: a missing or wrongly-typed
35
- answer is a `None`, never an exception.
34
+ `hooks/tezgah_judge.py::noul`). A `text` question is the one shape that asks
35
+ for prose. `bin/tezgah-taste` asks it for a learning's readable line, and only
36
+ a generative provider answers one. TypeSafe is never sent one (`text`,
37
+ `hooks/tezgah_judge.py::text`). All three are total: a missing or
38
+ wrongly-typed answer is a `None`, never an exception.
36
39
 
37
40
  A judgement is an aid, never the claim. It ranks units or names a page; the agent
38
41
  still reads the selected refs and owns the finding, and the ledger's step kinds
39
42
  are untouched, so a model answer can never license a "done" (`STEP_KINDS`,
40
43
  `hooks/tezgah_integrity.py::STEP_KINDS`).
41
44
 
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
45
+ The state leaves the machine. By default it goes to the session's own vendor,
46
+ through the session's own CLI and credential ([below](#the-providers-and-their-order)).
47
+ A third party - `api.typesafe.ai` or `openrouter.ai` (`OPENROUTER_URL`,
48
+ `hooks/tezgah_judge.py::OPENROUTER_URL`) - reads it only as the `fallback`
49
+ setting allows. For the triage the state is the screen's own text, so whoever
50
+ answers reads any personal data on it. For the docs fallback it is the query.
51
+ Nothing else goes: no session id, no
49
52
  workspace path, no environment. The seam does not rewrite the state, because
50
53
  sending it is the point. Two callers rewrite what they send. `bin/tezgah-route`
51
54
  sends its brief through the ledger's own redactor (`redact`,
@@ -69,7 +72,7 @@ why its redaction is not optional. The switches below are the off buttons.
69
72
  | `bin/tezgah-docs` | the docs page fallback: only when the keyword index placed nothing, one Choice over the pages with `none` offered (`judge_pick`, `bin/tezgah-docs::judge_pick`; the question wording is `ASK`, `bin/tezgah-docs::ASK`) | reads as no judgement; with none (no credential, `judge-off`, `docs-judge-off`, or a call that failed or came back without one of its options) the pages are ranked by shared words instead (`ranked`, BM25 in `hooks/tezgah_rank.py` over each page's title and answers and their Turkish phrasings) and the top three printed; a query sharing no word with any page, or a judged `none`, exits 1. With the opt-in embedding feature on, the ranking is fused with a static embedding (`hooks/tezgah_embed.py`), which places every page, so only a judged `none` exits 1 |
70
73
  | `hooks/tezgah_skill_pick.py` | the prompt-path skill hint: a Choice over the roster skills plus one Noul (`judge`, `hooks/tezgah_skill_pick.py::judge`, with the criteria cut from each skill's own clauses, `clause`, `hooks/tezgah_skill_pick.py::clause`), behind a threshold (`GATE`, `hooks/tezgah_skill_pick.py::GATE`), one attempt and a 4 s wall-clock deadline (`ASK_DEADLINE`, `hooks/tezgah_skill_pick.py::ASK_DEADLINE`), the prompt redacted and cut at 2,000 characters | returns `""`; the turn loses the hint |
71
74
  | `bin/tezgah-route` | the tier router: after the deterministic overrides, one Choice over the three tiers for a delegation brief (`route`, `hooks/tezgah_models.py::route`; `TIER_QUESTION`, `hooks/tezgah_models.py::TIER_QUESTION`) - see [models](models.md) | the static phase table, else the middle tier |
72
- | `bin/tezgah-taste` | the coding-taste measurement: `measure` and `rate` label the prompts a user sent after a writing turn, one Choice per prompt batched into one request, each prompt redacted first (`classify`, `bin/tezgah-taste::classify`) | with no credential or `judge-off` it exits 2 before any request; a failed call leaves its prompts unlabelled |
75
+ | `bin/tezgah-taste` | the coding-taste measurement and learning: `measure` and `rate` label the prompts a user sent after a writing turn, one Choice per prompt batched into one request, each prompt redacted first (`classify`, `bin/tezgah-taste::classify`). `learn` asks one typed decision per signal and requires TypeSafe (`decide`, `bin/tezgah-taste::decide`). For a learning that turns active it asks one `text` question of a generative provider (`write_line`, `bin/tezgah-taste::write_line`) | with no credential or `judge-off` it exits 2 before any request. A failed call leaves its prompts unlabelled. A decision another provider gave is recorded `unverified` and never applied. A missing line keeps the user's own words |
73
76
 
74
77
  The third caller is the one no shell row can see: it runs on the prompt path,
75
78
  caches one answer per `(session, prompt)` (`_remember`,
@@ -77,37 +80,48 @@ caches one answer per `(session, prompt)` (`_remember`,
77
80
  `hooks/tezgah_skill_pick.py::suggest`), and asks at all only when its own marker is
78
81
  armed. The other four are bin tools a session runs by name.
79
82
 
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.
83
+ ## The providers and their order
84
+
85
+ `providers()` (`hooks/tezgah_judge.py::providers`) is the order `ask()` tries.
86
+ The next one is asked only when the previous failed. First comes the session's own
87
+ CLI (`tp.session_cli`, `hooks/tezgah_paths.py::session_cli`): `omp` when
88
+ `OMPCODE` is set, else `claude` when `CLAUDECODE` is. It runs headless with no
89
+ tools, rules, skills, extensions, MCP servers or settings (`SESSION_ARGV`,
90
+ `hooks/tezgah_judge.py::SESSION_ARGV`). Measured 2026-10-07, a call cost $0.007
91
+ on claude and $0.015 on omp. The session's own credential pays, OAuth subscription
92
+ included. The model and usage are read from the CLI's JSON output
93
+ (`_session_request`, `hooks/tezgah_judge.py::_session_request`). Then come the
94
+ third parties. TypeSafe reads `TYPESAFE_API_KEY`, else `~/.config/typesafe/key`
95
+ (`key`, `hooks/tezgah_judge.py::key`). OpenRouter reads `OPENROUTER_API_KEY`, else
96
+ `~/.config/openrouter/key` (`openrouter_key`, `hooks/tezgah_judge.py::openrouter_key`).
97
+ It asks the table's cheap row unless `TEZGAH_JUDGE_MODEL` names another
98
+ (`fallback_model`, `hooks/tezgah_judge.py::fallback_model`). The key file is
99
+ read because a hook runs where `~/.zshenv` never did. A chat answer is asked for in prose
100
+ (`CHAT_SYSTEM`, `hooks/tezgah_judge.py::CHAT_SYSTEM`) and filtered to the ids
101
+ asked, a wrongly typed answer dropped (`_chat_answers`,
102
+ `hooks/tezgah_judge.py::_chat_answers`).
103
+
104
+ `fallback` in `~/.config/tezgah/config.json` (`fallback_policy`,
105
+ `hooks/tezgah_paths.py::fallback_policy`) decides who may stand in:
106
+
107
+ | `fallback` | Session CLI present | No session CLI |
108
+ |---|---|---|
109
+ | `vendor` (default) | the session CLI only; when it fails, no judgement | TypeSafe, then OpenRouter |
110
+ | `any` | the session CLI, then TypeSafe, then OpenRouter | TypeSafe, then OpenRouter |
111
+ | `none` | the session CLI only | nobody: `available()` is false |
112
+
113
+ Nothing that stands in is silent. A result carries `provider`, the `model`
114
+ the reply named and `fallback` - None when the session answered, else why it did
115
+ not. That answer, and a refusal with its failures, is said on stderr as one
116
+ `tezgah-judge:` line. It is also kept as the last-use record (`_record`,
117
+ `hooks/tezgah_judge.py::_record`), which `tezgah-status --judge` prints. The skill
118
+ hint's cost row and the router's `route` row also carry `judge=<provider>/<model>`.
119
+
120
+ A caller that needs one provider in particular passes `only` (`named`,
121
+ `hooks/tezgah_judge.py::named`). `learn`'s decision has to come from a typed
122
+ model, so it passes `only=("typesafe",)`. It gets `None` rather than another
123
+ provider's answer. The caller named the provider, so the `vendor` order does not
124
+ apply. `none` still keeps every third party out.
111
125
 
112
126
  ## The switches
113
127
 
@@ -152,11 +166,11 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
152
166
  3. **No seam-level answer cache, and no caller cache beyond the prompt-keyed one
153
167
  that exists** (`_remember`, `hooks/tezgah_skill_pick.py::_remember`). A judgement
154
168
  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
169
+ one would make two runs of the same command disagree. Beside the last-use
170
+ record, the seam keeps a failure marker (`DOWN_FOR`, 300 s, in `hooks/tezgah_judge.py`). A
171
+ call that ends on a 401, 402, 5xx or a session CLI's non-zero exit marks that
172
+ provider, endpoint and credential down for five minutes. Until the marker
173
+ expires, that provider is skipped and sent no request. A dead key or an empty account then costs
160
174
  one refusal per five minutes, not one per prompt. A rotated key gets a new
161
175
  marker, so the seam asks it at once.
162
176
  4. **No SDK, no `requests`, no async, no local model.** Each would trade one of
@@ -185,8 +199,9 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
185
199
  - `hooks/tezgah_skill_pick.py` — the prompt-path skill hint, its threshold, its
186
200
  per-prompt cache and its `skill-suggest-on` marker.
187
201
  - `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.
202
+ receives; `hooks/tezgah_paths.py` — where a switch file is read, the session CLI
203
+ and the `fallback` setting, and the redirect guard (`guarded_opener`) the seam,
204
+ consult and codegen share; `bin/tezgah-status --judge` — the last-use record.
190
205
  - `hooks/tezgah_integrity.py` — the ledger and the `judge` counter;
191
206
  `hooks/tezgah_context.py` — the status mark and the class carve-out.
192
207
  - `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
@@ -384,15 +385,18 @@ extension the engine has no parser for and an extensionless script it cannot key
384
385
  on at all. The extensionless case is why `bin/*` carries `.py` twins, and the
385
386
  report counts a tracked `bin/x` as covered when the index holds `bin/x.py`.
386
387
 
387
- ## Opt-in taste capture
388
+ ## Opt-in taste: capture, learn, apply
388
389
 
389
- Phase 1 of a coding-taste learner stores the signals tezgah already receives.
390
- It learns nothing and injects nothing yet. It stays off until the user arms it
391
- with a `taste-on` file in `~/.config/tezgah/` (`enabled`,
392
- `hooks/tezgah_taste.py::enabled`). The off path costs two stats. Armed, it writes
393
- only into a repository that already has a `.tezgah/` directory. A `.no-taste`
394
- mark turns it off for one repository (`hooks/tezgah_context.py::repo_marks`).
395
- A workspace of repo-provided data gets nothing.
390
+ Taste learns the user's coding preferences from their own corrections. It is
391
+ taste-1's architecture as Command Code documents it, ported to tezgah (research
392
+ line taste-architecture, design T1P). An LLM-free structured signal goes to a
393
+ typed decision step, then a symbolic ledger, then a meta loop. It stays off
394
+ until the user arms it with a `taste-on` file in `~/.config/tezgah/` (`enabled`,
395
+ `hooks/tezgah_taste.py::enabled`). The off path costs two stats. Armed, it
396
+ writes only into a repository that already has a `.tezgah/` directory. A
397
+ `.no-taste` mark turns it off for one repository
398
+ (`hooks/tezgah_context.py::repo_marks`). A workspace of repo-provided data gets
399
+ nothing.
396
400
 
397
401
  Rows go to `<repo>/.tezgah/taste/signals.jsonl` through the ledger's own writer
398
402
  (`_write`, `hooks/tezgah_taste.py::_write`). A user turn adds a `prompt` row,
@@ -408,22 +412,69 @@ ledger row. A rollback therefore never reads it as a pre-state (`capture_after`,
408
412
  `hooks/tezgah_snapshot.py::capture_after`). A file outside the repository gets no
409
413
  after blob.
410
414
  opencode reaches the same call through a process (`noteTaste`,
411
- `hosts/opencode/plugins/tezgah.js:2310-2319`).
415
+ `hosts/opencode/plugins/tezgah.js:2380-2390`).
412
416
 
413
417
  ```sh
414
418
  touch ~/.config/tezgah/taste-on # arm; rm it to disarm
419
+ bin/tezgah-taste learn # decide the new capture signals, update the ledger
420
+ bin/tezgah-taste learn --from-transcripts # bootstrap from this repo's omp/Claude history
421
+ bin/tezgah-taste list # learnings: state, rule/hint, confidence, scope
422
+ bin/tezgah-taste accept|reject|show ID # the user's call on one learning
423
+ bin/tezgah-taste edit ID --text "..." # reword it; an edited line is never rewritten
424
+ bin/tezgah-taste export # accepted learnings into AGENTS.md
425
+ bin/tezgah-taste label; bin/tezgah-taste label ID preference # the calibration sample
426
+ bin/tezgah-taste calibrate # the bound a rule needs
427
+ bin/tezgah-taste gate # the pooled before/after test
415
428
  bin/tezgah-taste mine --host omp --stats # sessions, writes, prompts after a writing turn
416
- bin/tezgah-taste mine --out mined.jsonl # one row per such prompt, redacted
417
- bin/tezgah-taste measure --samples S --labels L # judge precision/recall against labels
418
- bin/tezgah-taste rate --in mined.jsonl # preference corrections per writing turn
429
+ bin/tezgah-taste measure --samples S --labels L [--typed] # judge precision/recall
419
430
  ```
420
431
 
421
- `mine` reads the host transcripts already on disk, so it needs no capture. It
422
- reads omp's top-level session files and Claude's top-level transcripts whose cwd
423
- is under `--root` (`cmd_mine`, `bin/tezgah-taste::cmd_mine`). `measure` and `rate`
424
- send the sample text to the judge seam's provider ([judge](judge.md)), redacted
425
- first (`classify`, `bin/tezgah-taste::classify`). They use a fixed labelling rule
426
- (`RULE`, `bin/tezgah-taste::RULE`). Without a credential they send nothing.
432
+ `learn` turns each prompt that followed a writing turn into one signal, and code
433
+ builds it (`capture_signals`, `bin/tezgah-taste::capture_signals`). A signal
434
+ holds the prompt, the files written, the agent's edits, and the user's own edit
435
+ between two agent writes. That last one compares the after blob with the next
436
+ write's pre-state, or with the file on disk (`user_diff`,
437
+ `bin/tezgah-taste::user_diff`). `learn` asks TypeSafe for one typed decision per
438
+ signal (`decide`, `bin/tezgah-taste::decide`, [judge](judge.md)). The decision
439
+ names preference, defect or none. It picks one of eleven fixed categories and a
440
+ scope. It also says how the signal relates to each learning held: supports,
441
+ contradicts, narrows or unrelated. When another provider answers, `learn` writes
442
+ the decision to `decisions.jsonl` as `unverified` and never applies it. A defect
443
+ goes to `defects.jsonl` and never into the ledger.
444
+
445
+ The ledger is `<repo>/.tezgah/taste/ledger.json`, plus
446
+ `~/.config/tezgah/taste/ledger.json` for user-scope learnings
447
+ (`hooks/tezgah_taste_ledger.py`). Confidence is (r+1)/(r+s+2) over supporting
448
+ and contradicting evidence. Each piece counts at its decision probability, and
449
+ the ledger forgets it per day at its scope's rate (`confidence`,
450
+ `hooks/tezgah_taste_ledger.py::confidence`). A learning turns active in its
451
+ second session. A contradiction flags an active learning `conflicted` and stores
452
+ the contrary claim `quarantined`. Neither overwrites the other. `narrows` adds a
453
+ narrower learning beside the broader one. When the user corrects an injected
454
+ learning's category in the same session, that learning loses confidence
455
+ (`apply`, `hooks/tezgah_taste_ledger.py::apply`). The hooks only read the
456
+ ledgers. `learn`, `accept`, `reject` and `edit` rewrite them one at a time on
457
+ this machine, under a lock on `~/.config/tezgah/taste/ledger.lock` (`locked`,
458
+ `hooks/tezgah_taste_ledger.py::locked`). The lock spans every repository,
459
+ because each command also rewrites the user ledger. A second one exits 2 and
460
+ names the holder. Every taste row goes through the evidence ledger's locked
461
+ writer (`append`, `hooks/tezgah_taste_ledger.py::append`).
462
+
463
+ Session start injects the active learnings (`block`,
464
+ `hooks/tezgah_taste_ledger.py::block`). A rule needs confidence 0.8 and a
465
+ Clopper-Pearson lower bound of 0.8 on preference precision, measured on the
466
+ labelled sample (`calibration`, `hooks/tezgah_taste_ledger.py::calibration`). A
467
+ hint needs 0.6. A learning whose evidence paths are all gone is withheld. A
468
+ write also carries the in-scope learnings it has not yet shown in the session,
469
+ on every host's post-tool channel (`write_note`, `hooks/tezgah_taste.py::write_note`).
470
+ `gate` compares preference corrections per writing turn before and after the
471
+ first injection, by when each turn happened. Once both arms hold 980 turns and
472
+ the after-rate is not lower
473
+ at one-sided p < 0.1, it writes `gate.json` and injection stops (`gate`,
474
+ `hooks/tezgah_taste_ledger.py::gate`). `mine` reads omp's top-level session
475
+ files and Claude's top-level transcripts whose cwd is under `--root`
476
+ (`cmd_mine`, `bin/tezgah-taste::cmd_mine`). `measure` and `rate` use a fixed
477
+ labelling rule (`RULE`, `bin/tezgah-taste::RULE`).
427
478
 
428
479
  ## Tidying the lessons ledger
429
480
 
@@ -541,7 +592,11 @@ with `embed-m2v`. The fusion found it for 0.75 and 0.74, BM25 for 0.68.
541
592
 
542
593
  Reversible and safe to repeat: every link and settings key it writes is
543
594
  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
595
+ copies it to `<file>.<timestamp>.tezgah-bak`. A host file's copy goes under
596
+ `~/.config/tezgah/backups/` (`bin/tezgah-setup::backup_base`). A host lists the
597
+ files in its own dirs. tezgah's own state dir
598
+ keeps its copies beside the file. `--install` moves the copies an older release
599
+ left in a host dir (`bin/tezgah-setup::sweep_backups`). The oldest of those copies is
545
600
  always kept, beside the newest four (`backup()`, `BACKUP_CAP`). The oldest is
546
601
  the file before tezgah's first timestamped write, which is tezgah's own content
547
602
  for a file tezgah created. On an upgraded install the pre-tezgah copy is the
@@ -575,8 +630,8 @@ question instead of paying an agent to read a page: the analyze-app loop's snaps
575
630
  triage (`bin/tezgah-triage`), `bin/tezgah-docs` for a query its keyword index
576
631
  cannot place, and the prompt-path skill hint (`hooks/tezgah_skill_pick.py`), the
577
632
  one caller no shell row sees, `bin/tezgah-route`, the tier router
578
- ([models](models.md)), and `bin/tezgah-taste`'s `measure` and `rate`
579
- ([above](#opt-in-taste-capture)). All five go through the same stdlib-only seam,
633
+ ([models](models.md)), and `bin/tezgah-taste`'s `measure`, `rate` and `learn`
634
+ ([above](#opt-in-taste-capture-learn-apply)). All five go through the same stdlib-only seam,
580
635
  which returns `None` rather than raising because a hook may import it
581
636
  (`available()`, `hooks/tezgah_judge.py::available`); the request is
582
637
  one batched call, and the credential resolves per call (`ask()`,
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`