@r1z4x/tezgah 1.5.1 → 1.7.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 (81) hide show
  1. package/CHANGELOG.md +126 -1
  2. package/MANIFEST +11 -0
  3. package/README.md +6 -3
  4. package/bin/consult +13 -10
  5. package/bin/tezgah-docs +18 -16
  6. package/bin/tezgah-doctor +152 -0
  7. package/bin/tezgah-research +286 -12
  8. package/bin/tezgah-route +48 -1
  9. package/bin/tezgah-setup +27 -14
  10. package/bin/tezgah-task +771 -46
  11. package/bin/tezgah-taste +48 -10
  12. package/docs/architecture.md +7 -4
  13. package/docs/citations-baseline.json +1 -1
  14. package/docs/clarity-baseline.json +5 -5
  15. package/docs/contract.md +112 -25
  16. package/docs/evidence.md +201 -28
  17. package/docs/feature-audit.md +25 -4
  18. package/docs/gate.md +70 -33
  19. package/docs/glossary.md +4 -4
  20. package/docs/hosts.md +8 -8
  21. package/docs/index.json +30 -10
  22. package/docs/judge.md +9 -3
  23. package/docs/models.md +35 -2
  24. package/docs/operations.md +103 -3
  25. package/docs/research.md +392 -13
  26. package/docs/skills.md +24 -6
  27. package/docs/status-line.md +5 -5
  28. package/docs/testing.md +2 -2
  29. package/docs/vision.md +12 -4
  30. package/hooks/hooks.json +2 -2
  31. package/hooks/lang/en.json +323 -0
  32. package/hooks/lang/tr.json +344 -0
  33. package/hooks/projects-posttooluse.py +24 -5
  34. package/hooks/projects-pretooluse.py +9 -4
  35. package/hooks/projects-stop.py +28 -16
  36. package/hooks/tezgah_agents.py +29 -9
  37. package/hooks/tezgah_clarity.py +7 -3
  38. package/hooks/tezgah_components.py +2 -1
  39. package/hooks/tezgah_context.py +460 -390
  40. package/hooks/tezgah_gate.py +640 -104
  41. package/hooks/tezgah_integrity.py +1480 -197
  42. package/hooks/tezgah_models.py +337 -15
  43. package/hooks/tezgah_paths.py +103 -10
  44. package/hooks/tezgah_policy.py +286 -130
  45. package/hooks/tezgah_product.py +107 -0
  46. package/hooks/tezgah_rank.py +29 -27
  47. package/hooks/tezgah_replay.py +200 -30
  48. package/hooks/tezgah_research.py +1297 -12
  49. package/hooks/tezgah_skill_pick.py +58 -17
  50. package/hooks/tezgah_stopspec.py +7 -0
  51. package/hooks/tezgah_store.py +74 -27
  52. package/hooks/tezgah_task.py +917 -8
  53. package/hosts/codex/hook.py +9 -10
  54. package/hosts/cursor/hook.py +21 -14
  55. package/hosts/dsh/hooks.json +2 -2
  56. package/hosts/omp/hook.py +44 -23
  57. package/hosts/omp/tezgah-hook.ts.in +93 -7
  58. package/hosts/opencode/plugins/tezgah.js +119 -39
  59. package/output-styles/tezgah.md +76 -42
  60. package/package.json +1 -1
  61. package/skills/analyze-app/SKILL.md +57 -0
  62. package/skills/design-contract/EVALS.md +24 -1
  63. package/skills/design-contract/SKILL.md +17 -1
  64. package/skills/design-contract/data-views.md +86 -0
  65. package/skills/feature-audit/SKILL.md +32 -3
  66. package/skills/i-have-adhd/EVALS.md +22 -2
  67. package/skills/i-have-adhd/SKILL.md +12 -5
  68. package/skills/plan-add/EVALS.md +8 -8
  69. package/skills/plan-add/SKILL.md +22 -21
  70. package/skills/plan-sync/EVALS.md +2 -2
  71. package/skills/plan-sync/SKILL.md +14 -8
  72. package/skills/pm-frameworks/SKILL.md +6 -0
  73. package/skills/ponytail/EVALS.md +21 -2
  74. package/skills/ponytail/SKILL.md +4 -4
  75. package/skills/product-analysis/EVALS.md +23 -1
  76. package/skills/product-analysis/SKILL.md +50 -11
  77. package/skills/product-flow/EVALS.md +81 -0
  78. package/skills/product-flow/SKILL.md +91 -0
  79. package/skills/research/SKILL.md +99 -2
  80. package/skills/tezgah-contract/EVALS.md +52 -7
  81. package/skills/tezgah-contract/SKILL.md +204 -85
package/bin/tezgah-taste CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """tezgah-taste - mine, measure, learn and apply the coding-taste signals.
3
3
 
4
- tezgah-taste mine [--host omp|claude|all] [--root DIR] [--stats] [--out FILE]
4
+ tezgah-taste mine [--host omp|claude|all] [--root DIR | --all-projects] [--stats]
5
+ [--out FILE]
5
6
  tezgah-taste measure --samples FILE --labels FILE [--model M] [--json]
6
7
  tezgah-taste rate --in MINED [--limit N] [--json]
7
8
  tezgah-taste learn [--repo DIR] [--from-transcripts] [--host omp|claude|all]
@@ -16,7 +17,12 @@
16
17
  `mine` reads the session transcripts on this machine - omp's top-level session
17
18
  files under `~/.omp/agent/sessions/*/` (a nested file is a subagent session) and
18
19
  Claude's top-level transcripts under `~/.claude/projects/*/` - whose cwd is under
19
- `--root` (default `~/Projects`, a `.runs` directory excluded). A writing turn is
20
+ `--root` (default the current repository, a `.runs` directory excluded).
21
+ `--all-projects` reads every session under `~/Projects` instead. Each repository
22
+ a mined session ran in, but the current one (every one under `--all-projects`),
23
+ gets a pseudonym (`project-<sha1[:8]>`). It stands for that repository's `repo`
24
+ and `cwd`, and for its path and its name, in any case, in every prompt. A name
25
+ the miner never saw as a session's repository stays as written. A writing turn is
20
26
  one in which the agent wrote a file without an error (omp `edit`/`write`, Claude
21
27
  `Edit`/`Write`/`MultiEdit`/`NotebookEdit`); the prompt the user sent directly
22
28
  after one, unless it is a harness injection, is a row. The definitions are the
@@ -64,6 +70,7 @@ import hashlib
64
70
  import json
65
71
  import os
66
72
  import random
73
+ import re
67
74
  import subprocess
68
75
  import sys
69
76
 
@@ -236,7 +243,7 @@ def sessions(host, root, nested=False):
236
243
  pattern = ("**", "*.jsonl") if nested else ("*", "*.jsonl")
237
244
  for path in sorted(glob.glob(os.path.join(base(), *pattern), recursive=nested)):
238
245
  cwd = cwd_of(path)
239
- if cwd.startswith(root + os.sep) and ".runs" not in cwd.split(os.sep):
246
+ if (cwd == root or cwd.startswith(root + os.sep)) and ".runs" not in cwd.split(os.sep):
240
247
  yield path, cwd
241
248
 
242
249
 
@@ -258,11 +265,29 @@ def repo_of(cwd, root):
258
265
  """The nearest directory holding `.git` below the root, else the first path
259
266
  component under it - without git, since a session's cwd may be gone."""
260
267
  here = cwd
261
- while here.startswith(root + os.sep):
268
+ while here == root or here.startswith(root + os.sep):
262
269
  if os.path.exists(os.path.join(here, ".git")):
263
270
  return here
264
271
  here = os.path.dirname(here)
265
- return os.path.join(root, os.path.relpath(cwd, root).split(os.sep)[0])
272
+ return os.path.normpath(os.path.join(root, os.path.relpath(cwd, root).split(os.sep)[0]))
273
+
274
+
275
+ def pseudonym(repo):
276
+ """`project-<sha1[:8]>` for a repository path: the same name for every row of
277
+ one repository, and no trace of its own name."""
278
+ return "project-" + hashlib.sha1(repo.encode("utf-8", "replace")).hexdigest()[:8]
279
+
280
+
281
+ def unname(text, repos):
282
+ """`text` with each repository of `repos` - its path, then its name as a
283
+ word, in any case - replaced by its pseudonym, the longest first so a name
284
+ inside another is not cut."""
285
+ for repo in sorted(repos, key=len, reverse=True):
286
+ text = re.sub(re.escape(repo), pseudonym(repo), text, flags=re.I)
287
+ for repo in sorted(repos, key=lambda r: len(os.path.basename(r)), reverse=True):
288
+ text = re.sub(r"(?<![\w.-])%s(?![\w-]|\.\w)" % re.escape(os.path.basename(repo)),
289
+ pseudonym(repo), text, flags=re.I)
290
+ return text
266
291
 
267
292
 
268
293
  def mine_session(events):
@@ -280,24 +305,35 @@ def mine_session(events):
280
305
 
281
306
 
282
307
  def cmd_mine(args):
283
- root = os.path.abspath(os.path.expanduser(args.root or "~/Projects"))
308
+ here = (tezgah_paths.linked_main(os.getcwd()) or repo_root(os.getcwd())
309
+ or os.getcwd())
310
+ root = os.path.abspath(os.path.expanduser(
311
+ args.root or ("~/Projects" if args.all_projects else here)))
284
312
  out = open(args.out, "w", encoding="utf-8") if args.out and not args.stats else sys.stdout
285
313
  try:
286
- for host in (("omp", "claude") if args.host == "all" else (args.host,)):
314
+ hosts = ("omp", "claude") if args.host == "all" else (args.host,)
315
+ found = {host: list(sessions(host, root)) for host in hosts}
316
+ hidden = {r for r in (repo_of(cwd, root) for rows in found.values()
317
+ for _path, cwd in rows) if args.all_projects or r != here}
318
+ for host in hosts:
287
319
  count = writes = prompts = 0
288
- for path, cwd in sessions(host, root):
320
+ for path, cwd in found[host]:
289
321
  count += 1
290
322
  wrote, rows = mine_session(HOSTS[host][2](path))
291
323
  writes += wrote
292
324
  prompts += len(rows)
293
325
  if args.stats:
294
326
  continue
327
+ repo = repo_of(cwd, root)
328
+ repo_out = pseudonym(repo) if repo in hidden else repo
329
+ cwd_out = repo_out + cwd[len(repo):] if cwd.startswith(repo) else repo_out
295
330
  for text, turn in rows:
296
331
  out.write(json.dumps({
297
332
  "host": host,
298
333
  "session": os.path.splitext(os.path.basename(path))[0],
299
- "cwd": cwd, "repo": repo_of(cwd, root),
300
- "prompt": ti.redact(text)[:CUT], "writes_in_turn": turn},
334
+ "cwd": cwd_out, "repo": repo_out,
335
+ "prompt": unname(ti.redact(text), hidden)[:CUT],
336
+ "writes_in_turn": turn},
301
337
  ensure_ascii=False) + "\n")
302
338
  if args.stats:
303
339
  print("%s: sessions=%d writes=%d prompts_after_write=%d"
@@ -883,6 +919,8 @@ def main(argv):
883
919
  mine = sub.add_parser("mine")
884
920
  mine.add_argument("--host", choices=("omp", "claude", "all"), default="all")
885
921
  mine.add_argument("--root")
922
+ mine.add_argument("--all-projects", action="store_true",
923
+ help="every session under ~/Projects, each repo and cwd pseudonymised")
886
924
  mine.add_argument("--stats", action="store_true")
887
925
  mine.add_argument("--out")
888
926
  measure = sub.add_parser("measure")
@@ -114,9 +114,11 @@ One session, in order. Each step names the file that handles the event on Claude
114
114
  and the stale-index notice, which also starts the incremental re-index
115
115
  (`hooks/tezgah_context.py::index_notice`).
116
116
  3. **PreToolUse** — `hooks/projects-pretooluse.py::main` calls `decision` and emits
117
- the deny envelope, or, for a code-symbol search it lets through, the graph line
118
- as `additionalContext` (`hooks/tezgah_gate.py::graph_advice`). The gate is the same
119
- object on every host: `hosts/omp/hook.py::handle`, `hosts/codex/hook.py::gate_reason`.
117
+ the deny envelope, or one `additionalContext` line beside a call it lets through.
118
+ That line is the graph line for a symbol search (`hooks/tezgah_gate.py::graph_advice`)
119
+ or, for a spawn, the shared-worktree warning (`hooks/tezgah_gate.py::spawn_advice`).
120
+ The gate is the same object on every host: `hosts/omp/hook.py::handle`,
121
+ `hosts/codex/hook.py::gate_reason`.
120
122
  4. **PostToolUse** — `hooks/projects-posttooluse.py::main` writes the evidence row
121
123
  through `note_tool` (`hooks/projects-posttooluse.py::main`), records the used
122
124
  kind for the status line (`tezgah_context.record`, `hooks/projects-posttooluse.py::main`),
@@ -158,6 +160,7 @@ lowest-value blocks are dropped in a fixed order rather than the rules
158
160
  | Session store (used marks) | the `used` rows of the cache dir's `tezgah.db`, one per session and kind (`hooks/tezgah_store.py::use`) | `record` (`hooks/tezgah_context.py::record`) from every adapter | nothing evidential: a convenience channel for a surface that has no transcript; an `orch` mark is also written to the evidence ledger (`hooks/tezgah_context.py::record`) so `fanout` can count it |
159
161
  | Snapshot store | the blobs under `<cache dir>/snapshots` (`hooks/tezgah_snapshot.py::_store`), their manifests the `snapshots` rows of `tezgah.db` (`hooks/tezgah_store.py::add_snapshot`) | `capture` on the write path (`hooks/tezgah_snapshot.py::capture`) | the pre-write bytes; the rollback source |
160
162
  | Turn stamp | the `turns` rows of the cache dir's `tezgah.db`, one per session (`hooks/tezgah_context.py::write_stamp`) | `write_stamp` (`hooks/tezgah_context.py::write_stamp`) | the comparison behind the one-line delta, nothing else |
163
+ | Slice journal | `<repo root>/.tezgah/slices/slices.db`, one row per delegated slice (`hooks/tezgah_task.py::slice_dir`) | `note_spawn` from the hosts' spawn calls (`hooks/tezgah_task.py::note_spawn`) and `tezgah-task slice` | which slices are open or stopped, how to resume them, and the `owns` lease (`hooks/tezgah_task.py::owned_globs`) - never evidence |
161
164
  | Installed config dir | `~/.config/tezgah` (`hooks/tezgah_paths.py::CONFIG_DIR`) | the installer and the user | which roots are armed, which kill switches are on |
162
165
  | Plugin copy | `~/.claude/plugins/cache/<marketplace>/tezgah/<version>/` (`bin/tezgah-setup::plugin_copies`) | `tezgah-setup --sync` (`bin/tezgah-setup::sync`) | what Claude Code actually executes — a copy, never this checkout |
163
166
 
@@ -205,7 +208,7 @@ fork and skips an entry whose checkout is gone, as git's own `prunable` does
205
208
  `context_for` returns `None` (`hooks/tezgah_context.py::context_for`), the gate only acts
206
209
  inside one (`hooks/tezgah_gate.py::decision`), and the status line is the single
207
210
  deliberate exception, because a globally loaded rules file must still show that
208
- it is armed (`hosts/omp/hook.py:40-43`).
211
+ it is armed (`hosts/omp/hook.py:41-44`).
209
212
  - **No cross-host drift.** A host shares the core, never a copy of it. Adding a
210
213
  rule means editing the policy; adding behaviour means editing `hooks/` and
211
214
  letting every adapter inherit it.
@@ -21,7 +21,7 @@
21
21
  "unjudged": 2
22
22
  },
23
23
  "docs/gate.md": {
24
- "unjudged": 40
24
+ "unjudged": 39
25
25
  },
26
26
  "docs/glossary.md": {
27
27
  "unjudged": 12
@@ -21,7 +21,7 @@
21
21
  "long_sentences": 42,
22
22
  "passive": 25,
23
23
  "replace_words": 0,
24
- "semicolons": 30
24
+ "semicolons": 29
25
25
  },
26
26
  "docs/environment.md": {
27
27
  "long_sentences": 3,
@@ -42,10 +42,10 @@
42
42
  "semicolons": 11
43
43
  },
44
44
  "docs/gate.md": {
45
- "long_sentences": 67,
46
- "passive": 32,
45
+ "long_sentences": 66,
46
+ "passive": 31,
47
47
  "replace_words": 1,
48
- "semicolons": 45
48
+ "semicolons": 44
49
49
  },
50
50
  "docs/glossary.md": {
51
51
  "long_sentences": 51,
@@ -84,7 +84,7 @@
84
84
  "semicolons": 42
85
85
  },
86
86
  "docs/research.md": {
87
- "long_sentences": 107,
87
+ "long_sentences": 105,
88
88
  "passive": 59,
89
89
  "replace_words": 1,
90
90
  "semicolons": 77
package/docs/contract.md CHANGED
@@ -144,6 +144,26 @@ looks for each clause's object phrase, the words a paraphrasing summary keeps
144
144
  precise rather than complete. `tests/constraint-fixtures.md` lists the real
145
145
  prompts the shapes ran on, the misses and the known false positives.
146
146
 
147
+ A grant is the positive half of the same reader (plan 086, S3). "bana sormadan
148
+ pushla", "without asking" and "onay veriyorum" let the session take an outward
149
+ step without asking again. The shapes live in each language pack's
150
+ `constraint.grant` list. `user_grants` (`hooks/tezgah_context.py::user_grants`)
151
+ reads the whole sentence for the class it names: `push`, `release`, `publish`,
152
+ `pr`, or `all` when it names none. Each prompt's grants land as `grant` rows in
153
+ the session's ledger. They come before the reminder switch, so `reminder-off`
154
+ keeps them. A `constraint.lift` shape ("artık bana sor", "ask me again",
155
+ "Don't push.", "şimdilik push yapma") lifts them. So does a later constraint that
156
+ names the class. A grant shape grants nothing in a sentence that ends in "?" or
157
+ that its own pack's rules take back (`hooks/tezgah_context.py::_refuses`): a
158
+ `constraint.grant_not` word such as "don't" or a complaint ("pushladın"), or a
159
+ `grant_not_verb` prohibition ("yapma") right after the shape or at the
160
+ sentence's end. A negated outward step lifts only the class it names. Quoted text and a subagent's prompt grant nothing.
161
+ `grants_in_force` (`hooks/tezgah_integrity.py::grants_in_force`) folds the rows
162
+ oldest first. The Stop rule's `needless ask` class and the ask-tool warning read
163
+ that fold. A grant never covers a never-default step: a force push, a delete, a
164
+ credential, or a spend over `spend_cap` in `~/.config/tezgah/config.json`. That
165
+ cap is a number of USD, and 0 when unset (`hooks/tezgah_paths.py::spend_cap`).
166
+
147
167
  One per-turn line is not a rule but a check on the turn's own evidence. When the
148
168
  ledger says every check that passed in this session ran in a scratch or stand-in
149
169
  path - `/tmp/`, `/var/folders/`, `$TMPDIR`, or a path segment naming a
@@ -167,27 +187,27 @@ change with an executable oracle - a checker, a gate rule, a parser - spends the
167
187
  same budget. Its round one is four direction-led reviewers, and each claim
168
188
  becomes a fixture that runs before it counts. That population is a tentative
169
189
  default, measured on two research lines with a written flip
170
- (`skills/tezgah-contract/SKILL.md:519-550`).
190
+ (`skills/tezgah-contract/SKILL.md:532-563`).
171
191
 
172
192
  ## Composition order of the always-on core
173
193
 
174
194
  Paragraphs are concatenated in the order they appear in `CORE` and identified by
175
195
  the bold label each starts with (`CORE_RULES`, `hooks/tezgah_context.py::CORE_RULES`).
176
196
  That order, with the line each label sits on in `hooks/tezgah_policy.py`:
177
- `**{REPLY_LANG}, BLUF.**` :649, `**Ponytail (minimal code).**` :657, `**Output shape:
178
- ADHD-friendly.**` :668, `**Deliver the whole ask; never the shortcut.**` :682,
179
- `**Integrity: evidence, or "doğrulanmadı".**` :696, `**Loop discipline.**` :713,
180
- `**Spec before building.**` :722 *(conditional)*, `**Lessons ledger: stop
181
- repeating mistakes.**` :736, `**Code discovery: graph first.**` :744
182
- *(conditional)*, `**Consult before irreversible.**` :753 *(conditional)*,
183
- `**Research: route it to OpenResearch.**` :769 *(conditional)*, `**Product
184
- analysis: five axes, one evidence class per finding.**` :785 *(conditional)*,
185
- `**No AI attribution, ever, on any host.**` :818, `**Identifiers and messages
186
- stay English.**` :828, `**Session scope: the user's repo, not tezgah.**` :834,
187
- ``**Workspace: `.tezgah/` only.**`` :843, `**Parallel by default: fan out
188
- independent items.**` :850 *(conditional)*,
189
- `**Kill switches:**` :865. The first label renders as `**Turkish, BLUF.**` under
190
- the default `reply_lang`.
197
+ `**{REPLY_LANG}, BLUF.**` :713, `**Ponytail (minimal code).**` :722, `**Output shape:
198
+ ADHD-friendly.**` :739, `**Deliver the whole ask; never the shortcut.**` :763,
199
+ `**Integrity: evidence, or a named blocker.**` :778, `**Loop discipline.**` :811,
200
+ `**Spec before building.**` :820 *(conditional)*, `**Lessons ledger: stop
201
+ repeating mistakes.**` :834, `**Code discovery: graph first.**` :842
202
+ *(conditional)*, `**Consult before irreversible.**` :851 *(conditional)*,
203
+ `**Research: route it to OpenResearch.**` :869 *(conditional)*, `**Product
204
+ analysis: five axes, one evidence class per finding.**` :896 *(conditional)*,
205
+ `**No AI attribution, ever, on any host.**` :931, `**Identifiers and messages
206
+ stay English.**` :941, `**Session scope: the user's repo, not tezgah.**` :947,
207
+ ``**Workspace: `.tezgah/` only.**`` :958, `**Parallel by default: fan out
208
+ independent items.**` :965 *(conditional)*,
209
+ `**Kill switches:**` :990. The first label renders as `**Turkish, BLUF.**` under
210
+ `language` `tr`.
191
211
 
192
212
  `always_on_core()` (`hooks/tezgah_context.py::always_on_core`) drops the six
193
213
  conditional paragraphs and appends `POINTERS` (`hooks/tezgah_policy.py::POINTERS`):
@@ -211,11 +231,11 @@ applied, and it also returns the names of the switches that fired.
211
231
 
212
232
  ## What each always-on rule is for
213
233
 
214
- - **Turkish, BLUF.** Every user-facing reply is Turkish even when the prompt is English, outcome first; code, commits, docs and subagent prompts stay English. That is the default. `reply_lang` in `~/.config/tezgah/config.json` picks another: `tr`, `en` or `any`. `tezgah-setup --install --reply-lang` sets it, and the label follows it. The value is the switch. The Stop rule checks the language only under `tr`: under `en` and `any` it judges none.
234
+ - **{Language}, BLUF.** Every user-facing reply is in the configured language, outcome first. Code, commits, docs and subagent prompts stay English. `language` in `~/.config/tezgah/config.json` picks it: `tr`, `en` or `any` (see [Reply language and language packs](#reply-language-and-language-packs)). The value is the switch, and the label follows it.
215
235
  - **Ponytail (minimal code).** Take the laziest rung that holds (YAGNI → reuse → stdlib → platform → installed dependency → one line), never simplify away validation, error handling or security. A non-default level rides the reminder (`hooks/tezgah_context.py::_pony_level_line`).
216
236
  - **Output shape: ADHD-friendly.** The action or answer is the first line, multi-step work is a numbered list whose position is restated, an estimate is in concrete units. Two upstream rules were rewritten rather than imported verbatim because they collided with rules already in force: the state restatement points at the todo list instead of duplicating it, and a time estimate can no longer be read as a measurement.
217
237
  - **Deliver the whole ask; never the shortcut.** The request is a floor: no cheaper stand-in, no silent scope cut, no token gesture reported as done.
218
- - **Integrity: evidence, or "doğrulanmadı".** A done/tested claim holds only if the check ran in this session and its output was seen; the mechanical half is enforced by [gate.md](gate.md).
238
+ - **Integrity: evidence, or a named blocker.** A done/tested claim holds only if the check ran in this session and its output was seen; "unverified" (tr: "doğrulanmadı") follows a check that ran and failed; leftover work is done, or stays as one of four blockers (DECISION, ACCESS, IRREVERSIBLE, ENV; tr: KARAR, YETKİ, GERİ ALINAMAZ, ORTAM) with its evidence. The mechanical half is enforced by [gate.md](gate.md), the residual half by the Stop rule's `residual work` class ([evidence.md](evidence.md)). Its **Ask policy** says what is asked at all. Work and process are never asked. An outward step runs on a grant. A never-default step waits for an answer. Every other question goes in one batch with a recommended default. The Stop rule's `needless ask` class refuses a reply that ends on a question the agent can decide, and the gate warns on an ask-tool question with no default ([gate.md](gate.md#ask-an-ask-tool-question-with-no-recommended-option)).
219
239
  - **Loop discipline.** Never repeat an identical failing command; three attempts is the ceiling, then report what is still unknown.
220
240
  - **Lessons ledger: stop repeating mistakes.** `.tezgah/lessons.md` lines are standing constraints; the recent ones are injected at session start, and up to three older ones a prompt shares words with ride that turn, each once per session when the host sends a session id (a compaction resets that, because the compacted context no longer holds them; without an id every matching turn carries them). Write a line rule first, `<rule> - <incident>` (`LESSON_SEPARATOR`, `hooks/tezgah_context.py::LESSON_SEPARATOR`). The session block counts the shown lines that do not open with their rule. A line ending `|| enforced_by: <rule|test>` leaves both blocks while that gate rule or test is on (`hooks/tezgah_lessons.py::lines`). That one reader serves the context, the gate and the CLI. It cuts that clause, a `|| check: ...` clause and an `@<sha> <path>` stamp off each line (`hooks/tezgah_lessons.py::parse`). None of them changes the key, the per-turn digest, the cut or the ranking. The gate records a line written after untrusted text in the same turn as a `lesson_tainted` row ([evidence.md](evidence.md)). That line still rides both blocks, behind a per-line data label (`hooks/tezgah_context.py::LESSON_TAINTED`). ADR 010 makes it a row, never a refusal. `bin/tezgah-lessons` proposes rewrites, merges and retirements, and never writes the file.
221
241
  - **No AI attribution, ever, on any host.** Nothing persisted or published may name a model, vendor or "AI" as author, co-author, generator or helper; one already in pushed history is reported.
@@ -223,6 +243,54 @@ applied, and it also returns the names of the switches that fired.
223
243
  - **Session scope: the user's repo, not tezgah.** The session never maintains tezgah itself; a missing capability is one line plus the documented fallback.
224
244
  - **Kill switches.** The switch list itself, so a session can tell the user how to disarm a rule it is asked to ignore; pinned against the shipped skill by `tests/test_skills.py::SkillStandards`.
225
245
 
246
+ ## Reply language and language packs
247
+
248
+ The reply language is `language` in `~/.config/tezgah/config.json`
249
+ (`hooks/tezgah_paths.py::language`). A config written before the key existed
250
+ may say `reply_lang` instead. It is still read and never rewritten. A config
251
+ with neither key reads as `tr`.
252
+
253
+ - **Install default.** `tezgah-setup --install --language <code>` stores it.
254
+ `--reply-lang` is the older spelling of the same flag. A first install with no
255
+ flag takes the environment's language. That is the first of `LANGUAGE`,
256
+ `LC_ALL`, `LC_MESSAGES` and `LANG` that is set, cut to its language code. A
257
+ code with no pack, `C` or `POSIX` gives `en`
258
+ (`hooks/tezgah_paths.py::env_language`). An install over a config that has
259
+ neither key writes `tr`. So an upgrade changes no one's reply language
260
+ (`bin/tezgah-setup::install_common`).
261
+ - **Language packs.** Each language is one data file, `hooks/lang/<code>.json`.
262
+ `en` and `tr` ship. A pack holds that language's half of every detection
263
+ class. That covers claims, admissions, openers, closers, residual labels,
264
+ blocker spellings, prompt hints, constraint shapes, ranking stopwords and
265
+ reply markers. `hooks/tezgah_paths.py::packs` reads them once. Every reader
266
+ compiles the union of all packs (`hooks/tezgah_paths.py::alts`). So a prompt
267
+ or a reply in any shipped language arms the same classes, whatever `language`
268
+ says. A pack's `word_order` picks the shape of its word-order hints. Turkish
269
+ is `object_first`, because its object comes before the verb
270
+ (`hooks/tezgah_context.py::_ui_ask`, `hooks/tezgah_context.py::_fanout`,
271
+ `hooks/tezgah_context.py::user_constraints`).
272
+ `tests/test_lang_packs.py` holds every pack to one key set and pins the probe
273
+ rows the packs must keep.
274
+ - **Contract words.** A pack's `say` map holds the words the contract asks the
275
+ model to write. Those are the admission (`unverified`), the blocker heading
276
+ (`Blockers`), the four blocker tokens and the opener examples. `render` fills
277
+ the placeholders with the configured language's words
278
+ (`hooks/tezgah_context.py::render`). The Stop refusals use the same map
279
+ (`hooks/tezgah_integrity.py::_said`). Under `en` and `any` the contract names
280
+ the canonical English tokens. Under `tr` it names the Turkish ones. There the
281
+ residual, ready-step and stopped-slice refusals open with a Turkish line,
282
+ because Claude Code shows a refusal's first line to the user. The Stop rule
283
+ reads every pack's spelling either way.
284
+ - **Reply check.** The Stop rule's `reply language` class judges only a
285
+ language whose pack ships reply `markers`, which today is `tr`
286
+ (`hooks/tezgah_integrity.py::reply_share`). `en` replies are not judged,
287
+ because an English reply often quotes the user's other language. `any` is
288
+ never judged.
289
+
290
+ Adding a language is one file. Copy `hooks/lang/en.json` and write the
291
+ language's words into each class and into `say`. Leave `markers` empty unless
292
+ its replies should be held to it.
293
+
226
294
  ## Which mechanism enforces each always-on rule
227
295
 
228
296
  `tezgah-status --enforcers` prints one row per always-on rule. A row names the
@@ -260,7 +328,7 @@ sentence of the rule.
260
328
 
261
329
  | rule | decision |
262
330
  |---|---|
263
- | exec | Only the reply language is refused, and only under `reply_lang` `tr`. Answer-first (BLUF) has no mechanism. |
331
+ | exec | Only the reply language is refused, and only for a language whose pack ships reply markers (`tr`). Answer-first (BLUF) has no mechanism. |
264
332
  | fidelity | Only the placating opener is refused, and only while the output-shape switches are on. Delivering the whole ask has no mechanism. |
265
333
  | loop | Only an identical repeat is refused, under `verify-off`. Re-running a check that already passed is not refused. |
266
334
  | ponytail | no decision |
@@ -276,7 +344,10 @@ sentence of the rule.
276
344
 
277
345
  `PROMPT_HINTS` (`hooks/tezgah_context.py::PROMPT_HINTS`) is one compiled pattern per key
278
346
  — `spec`, `consult`, `research`, `product`, `graph`, `fanout` — and `classify_prompt()` returns the keys
279
- a prompt matches (`hooks/tezgah_context.py::classify_prompt`). `fanout` arms on the
347
+ a prompt matches (`hooks/tezgah_context.py::classify_prompt`). It also adds `product`
348
+ for a data ask: a dashboard, a chart or a metric to choose. The check reads each
349
+ clause on its own (`hooks/tezgah_context.py::_data_ask`). A code marker in that
350
+ clause keeps the ask out: a file name, an identifier, SQL or a build word. `fanout` arms on the
280
351
  shape of the request, not a word. Two list entries on their own lines arm it.
281
352
  An entry starts with `1.`, `1)`, `1 -`, `1:`, `a)` or a bullet. An inline
282
353
  `1. ... 2. ...` arms it too. So do three files named as one list, "First, ...
@@ -303,6 +374,16 @@ telling the model to load `tezgah-contract` for the deep detail
303
374
  (`hooks/tezgah_context.py::context_for`), whose two appendixes apply only on a machine
304
375
  missing the code graph or every consult option (`hooks/tezgah_policy.py::NO_GRAPH`, `hooks/tezgah_policy.py::NO_CONSULT`).
305
376
 
377
+ The router's own context gets one line too (`hooks/tezgah_context.py::context_nudge`).
378
+ The prompt hook reads the newest main-thread step's input-side tokens from the
379
+ host transcript (`hooks/tezgah_context.py::context_tokens`). Claude and Codex
380
+ hand that path to every hook. omp's bridge hands over its session file. The
381
+ threshold is `CONTEXT_NUDGE_TOKENS` (`hooks/tezgah_paths.py::CONTEXT_NUDGE_TOKENS`),
382
+ 700 k, just under the measured p90 of 724 k. Above it, the line asks the router
383
+ to push heavy reads through a slice, or to run `/compact`. It fires once per session. A
384
+ compaction clears the `nudged` mark, and `orchestrate-off` silences it. A host
385
+ with no transcript pays nothing.
386
+
306
387
  ## How a rule is disarmed
307
388
 
308
389
  A kill switch removes the rule's text, not just a status mark
@@ -422,16 +503,22 @@ brief carries the same caller-list floor (`hooks/tezgah_agents.py`), and
422
503
 
423
504
  ## When the injected text grows too large
424
505
 
425
- Each event has a byte budget: `session_start` and `post_compact` 13000,
426
- `user_prompt` 6000, `subagent_start` 5500, anything else 13000
506
+ Each event has a byte budget: `session_start` and `post_compact` 15500,
507
+ `user_prompt` 6500, `subagent_start` 5500, anything else 13000
427
508
  (`hooks/tezgah_context.py::CONTEXT_BUDGET`). The rationale and the measured
428
509
  sizes sit beside it. In an empty fixture repo, `session_start` with the core
429
510
  builds 9842 B before the ponytail and ADHD rules went inline and 10884 B after
430
511
  (+1042 B). The fan-out pointer adds 112 B and the `tezgah-skill` sentence
431
- 113 B, so the always-on band is 10499 B (`tests/test_setup.py`). The healthy
432
- fixture with a lesson and a plan then built 12059 B. At the old 12000 B budget
433
- it lost its lessons and sibling checkouts to pay for core rules, so the budget
434
- moved to 13000 B. The subagent brief and the reminder stay the same size.
512
+ 113 B. The healthy fixture with a lesson and a plan then built 12059 B. At the
513
+ old 12000 B budget it lost its lessons and sibling checkouts to pay for core
514
+ rules, so the budget moved to 13000 B. The residual rule (plan 080) added
515
+ 1473 B to the core and 151 B to the reminder, so the always-on band is
516
+ 11972 B (`tests/test_setup.py`). At 13000 B the healthy fixture gave up its
517
+ lessons, plans and steer blocks. At 6000 B the busiest prompt turn gave up its
518
+ active-task line. The session budgets moved to 14500 B, the prompt budget to 6500 B.
519
+ The Ask policy (plan 086, S3) added 913 B to the core. At 14500 B the healthy
520
+ fixture then gave up a lessons, plans or knowledge block. The session budgets moved to 15500 B.
521
+ The subagent brief stays the same size.
435
522
  `SkillRulesInForceAtSessionStart` (`tests/test_context.py`) pins the rules
436
523
  themselves, not the size. A
437
524
  `user_prompt` that arms all five conditional rules