@r1z4x/tezgah 1.6.0 → 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.
- package/CHANGELOG.md +80 -1
- package/MANIFEST +5 -0
- package/README.md +6 -3
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-research +234 -10
- package/bin/tezgah-route +48 -1
- package/bin/tezgah-setup +26 -13
- package/bin/tezgah-task +589 -17
- package/bin/tezgah-taste +48 -10
- package/docs/architecture.md +7 -4
- package/docs/clarity-baseline.json +2 -2
- package/docs/contract.md +108 -24
- package/docs/evidence.md +201 -28
- package/docs/feature-audit.md +1 -1
- package/docs/gate.md +60 -22
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +11 -5
- package/docs/judge.md +9 -3
- package/docs/models.md +35 -2
- package/docs/operations.md +103 -3
- package/docs/research.md +346 -13
- package/docs/skills.md +1 -1
- package/docs/status-line.md +5 -5
- package/docs/testing.md +2 -2
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +29 -10
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_context.py +425 -406
- package/hooks/tezgah_gate.py +583 -33
- package/hooks/tezgah_integrity.py +1480 -197
- package/hooks/tezgah_models.py +337 -15
- package/hooks/tezgah_paths.py +103 -10
- package/hooks/tezgah_policy.py +258 -126
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1294 -9
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +915 -6
- package/hosts/codex/hook.py +9 -10
- package/hosts/cursor/hook.py +21 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +102 -2
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/SKILL.md +5 -3
- package/skills/plan-sync/SKILL.md +5 -3
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/research/SKILL.md +89 -1
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +151 -56
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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":
|
|
300
|
-
"prompt": ti.redact(text)[:CUT],
|
|
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")
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
118
|
-
|
|
119
|
-
|
|
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:
|
|
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
|
"long_sentences": 42,
|
|
22
22
|
"passive": 25,
|
|
23
23
|
"replace_words": 0,
|
|
24
|
-
"semicolons":
|
|
24
|
+
"semicolons": 29
|
|
25
25
|
},
|
|
26
26
|
"docs/environment.md": {
|
|
27
27
|
"long_sentences": 3,
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"semicolons": 42
|
|
85
85
|
},
|
|
86
86
|
"docs/research.md": {
|
|
87
|
-
"long_sentences":
|
|
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:
|
|
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.**` :
|
|
178
|
-
ADHD-friendly.**` :
|
|
179
|
-
`**Integrity: evidence, or
|
|
180
|
-
`**Spec before building.**` :
|
|
181
|
-
repeating mistakes.**` :
|
|
182
|
-
*(conditional)*, `**Consult before irreversible.**` :
|
|
183
|
-
`**Research: route it to OpenResearch.**` :
|
|
184
|
-
analysis: five axes, one evidence class per finding.**` :
|
|
185
|
-
`**No AI attribution, ever, on any host.**` :
|
|
186
|
-
stay English.**` :
|
|
187
|
-
``**Workspace: `.tezgah/` only.**`` :
|
|
188
|
-
independent items.**` :
|
|
189
|
-
`**Kill switches:**` :
|
|
190
|
-
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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 |
|
|
@@ -306,6 +374,16 @@ telling the model to load `tezgah-contract` for the deep detail
|
|
|
306
374
|
(`hooks/tezgah_context.py::context_for`), whose two appendixes apply only on a machine
|
|
307
375
|
missing the code graph or every consult option (`hooks/tezgah_policy.py::NO_GRAPH`, `hooks/tezgah_policy.py::NO_CONSULT`).
|
|
308
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
|
+
|
|
309
387
|
## How a rule is disarmed
|
|
310
388
|
|
|
311
389
|
A kill switch removes the rule's text, not just a status mark
|
|
@@ -425,16 +503,22 @@ brief carries the same caller-list floor (`hooks/tezgah_agents.py`), and
|
|
|
425
503
|
|
|
426
504
|
## When the injected text grows too large
|
|
427
505
|
|
|
428
|
-
Each event has a byte budget: `session_start` and `post_compact`
|
|
429
|
-
`user_prompt`
|
|
506
|
+
Each event has a byte budget: `session_start` and `post_compact` 15500,
|
|
507
|
+
`user_prompt` 6500, `subagent_start` 5500, anything else 13000
|
|
430
508
|
(`hooks/tezgah_context.py::CONTEXT_BUDGET`). The rationale and the measured
|
|
431
509
|
sizes sit beside it. In an empty fixture repo, `session_start` with the core
|
|
432
510
|
builds 9842 B before the ponytail and ADHD rules went inline and 10884 B after
|
|
433
511
|
(+1042 B). The fan-out pointer adds 112 B and the `tezgah-skill` sentence
|
|
434
|
-
113 B
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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.
|
|
438
522
|
`SkillRulesInForceAtSessionStart` (`tests/test_context.py`) pins the rules
|
|
439
523
|
themselves, not the size. A
|
|
440
524
|
`user_prompt` that arms all five conditional rules
|