@r1z4x/tezgah 1.2.0 → 1.3.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 CHANGED
@@ -6,6 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2026-10-07
10
+
11
+ ### Added
12
+
13
+ - **Armed taste learns by itself.** Session start runs `tezgah-taste learn` in
14
+ the background when the repository's signals grew, at most once an hour. It
15
+ runs only when a TypeSafe key resolves, with the new `--no-fallback`, so it
16
+ asks no other provider. It does not run on Windows yet. Its log goes under
17
+ the cache dir, in `taste-learn/`.
18
+
9
19
  ## [1.2.0] - 2026-10-07
10
20
 
11
21
  ### Added
@@ -823,7 +833,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
823
833
  the copy could not find `hooks/` and died on import. It now links into the
824
834
  installed tree, and also installs the command as `tezgah`.
825
835
 
826
- [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.2.0...HEAD
836
+ [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.3.0...HEAD
837
+ [1.3.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.3.0
827
838
  [1.2.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.2.0
828
839
  [1.1.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.1.0
829
840
  [1.0.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.0.0
package/bin/tezgah-taste CHANGED
@@ -5,7 +5,7 @@
5
5
  tezgah-taste measure --samples FILE --labels FILE [--model M] [--json]
6
6
  tezgah-taste rate --in MINED [--limit N] [--json]
7
7
  tezgah-taste learn [--repo DIR] [--from-transcripts] [--host omp|claude|all]
8
- [--limit N] [--json]
8
+ [--limit N] [--no-fallback] [--json]
9
9
  tezgah-taste list [--repo DIR] [--all] [--json]
10
10
  tezgah-taste show|accept|reject ID [--repo DIR]
11
11
  tezgah-taste edit ID --text TEXT [--repo DIR]
@@ -701,7 +701,8 @@ def cmd_learn(args):
701
701
  "preference": 0, "defect": 0, "none": 0}
702
702
  tokens = [0, 0]
703
703
  for sig in todo:
704
- dec, result = decide(sig, held(ledgers), fallback=sig["id"] not in decided)
704
+ dec, result = decide(sig, held(ledgers),
705
+ fallback=not args.no_fallback and sig["id"] not in decided)
705
706
  if dec is None:
706
707
  counts["unanswered"] += 1
707
708
  continue
@@ -729,7 +730,9 @@ def cmd_learn(args):
729
730
  for lid, learning in tl.every(ledgers).items():
730
731
  if learning["state"] == "active" and lid not in before \
731
732
  and not learning.get("written") and not learning.get("edited"):
732
- line = write_line(learning)
733
+ # --no-fallback asks nothing of a generative provider: the user's
734
+ # own words stay the learning's line
735
+ line = None if args.no_fallback else write_line(learning)
733
736
  if line:
734
737
  learning.update(text=" ".join(line.split())[:400], written=True)
735
738
  tl.save(repo, ledgers)
@@ -878,6 +881,8 @@ def main(argv):
878
881
  learn.add_argument("--from-transcripts", action="store_true")
879
882
  learn.add_argument("--host", choices=("omp", "claude", "all"), default="all")
880
883
  learn.add_argument("--limit", type=int)
884
+ learn.add_argument("--no-fallback", action="store_true",
885
+ help="TypeSafe only: never pay another provider for a decision")
881
886
  listing = sub.add_parser("list")
882
887
  listing.add_argument("--all", action="store_true")
883
888
  for name in ("show", "accept", "reject", "edit"):
@@ -60,7 +60,7 @@
60
60
  "semicolons": 18
61
61
  },
62
62
  "docs/judge.md": {
63
- "long_sentences": 23,
63
+ "long_sentences": 22,
64
64
  "passive": 15,
65
65
  "replace_words": 0,
66
66
  "semicolons": 14
package/docs/judge.md CHANGED
@@ -59,10 +59,13 @@ certificates, destructive data or history operations, privilege and security.
59
59
  The prompt-path skill hint sends the user's prompt through the same redactor. It
60
60
  also cuts the prompt at 2,000 characters (`PROMPT_MAX`,
61
61
  `hooks/tezgah_skill_pick.py::PROMPT_MAX`), because a prompt can quote a token and
62
- nobody chose to send it. Four of the callers are explicit -
63
- a person runs the tool - and the fifth, `bin/tezgah-route`, is what the
64
- ORCHESTRATE paragraph tells the router to run before every delegation, which is
65
- why its redaction is not optional. The switches below are the off buttons.
62
+ nobody chose to send it. Three of the callers are explicit - a person runs
63
+ the tool. `bin/tezgah-route` is what the ORCHESTRATE paragraph tells the router
64
+ to run before every delegation, which is why its redaction is not optional.
65
+ `bin/tezgah-taste learn` also starts by itself at session start once the user
66
+ armed `taste-on`. It then sends redacted signal text to TypeSafe only, and only
67
+ when a TypeSafe key resolves (`learn_later`, `hooks/tezgah_taste.py::learn_later`).
68
+ The switches below are the off buttons.
66
69
 
67
70
  ## The five callers
68
71
 
@@ -78,7 +81,9 @@ The third caller is the one no shell row can see: it runs on the prompt path,
78
81
  caches one answer per `(session, prompt)` (`_remember`,
79
82
  `hooks/tezgah_skill_pick.py::_remember`; `suggest`,
80
83
  `hooks/tezgah_skill_pick.py::suggest`), and asks at all only when its own marker is
81
- armed. The other four are bin tools a session runs by name.
84
+ armed. `bin/tezgah-triage`, `bin/tezgah-docs` and `bin/tezgah-route` are bin
85
+ tools a session runs by name, and so is `bin/tezgah-taste`, except for the
86
+ background `learn` that armed taste starts.
82
87
 
83
88
  ## The providers and their order
84
89
 
@@ -442,6 +442,14 @@ contradicts, narrows or unrelated. When another provider answers, `learn` writes
442
442
  the decision to `decisions.jsonl` as `unverified` and never applies it. A defect
443
443
  goes to `defects.jsonl` and never into the ledger.
444
444
 
445
+ Armed taste also learns by itself. At session start, `learn` starts in the
446
+ background when `signals.jsonl` grew since the last start, at most once an hour
447
+ per repository (`learn_later`, `hooks/tezgah_taste.py::learn_later`). It runs
448
+ with `--no-fallback` and only when a TypeSafe key resolves, so it never pays a
449
+ generative provider for a decision it could not apply. It does not run on
450
+ Windows, which has no ledger lock yet. The stamp and the log live under the
451
+ cache dir, in `taste-learn/`.
452
+
445
453
  The ledger is `<repo>/.tezgah/taste/ledger.json`, plus
446
454
  `~/.config/tezgah/taste/ledger.json` for user-scope learnings
447
455
  (`hooks/tezgah_taste_ledger.py`). Confidence is (r+1)/(r+s+2) over supporting
@@ -2202,6 +2202,14 @@ def context_for(event, cwd, payload=None, with_core=True):
2202
2202
  taste = ""
2203
2203
  if taste:
2204
2204
  parts.append(("taste", taste))
2205
+ # Armed taste learns by itself: a detached `learn` when the signals grew,
2206
+ # at most hourly (tezgah_taste.learn_later); session start only, with the
2207
+ # root `enabled` already resolved above.
2208
+ if taste_root and event == "session_start":
2209
+ try:
2210
+ tezgah_taste.learn_later(taste_root)
2211
+ except Exception:
2212
+ pass
2205
2213
  broken = tezgah_research.failing(root) if not off("research-off") else []
2206
2214
  if broken:
2207
2215
  line_slug, err = broken[0]
@@ -15,15 +15,24 @@ PostToolUse hot paths, so the switch is read before anything else is.
15
15
 
16
16
  Learning lives in `bin/tezgah-taste learn` and `tezgah_taste_ledger`; the one
17
17
  thing fed back from here is `write_note`, the learnings in scope for a write.
18
+ `learn_later` starts that `learn` in the background at session start.
18
19
  """
19
20
  import datetime
20
21
  import json
21
22
  import os
23
+ import re
24
+ import subprocess
25
+ import sys
26
+ import time
22
27
 
23
28
  import tezgah_integrity as ti
29
+ import tezgah_paths
24
30
  from tezgah_paths import _toplevel, armed, root_for, workspace_from_repo
25
31
 
26
32
  ARM = "taste-on"
33
+ LEARN_EVERY = 3600 # seconds between two background `learn` starts per repo
34
+ BIN = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
35
+ "bin", "tezgah-taste")
27
36
  MARK = ".no-taste"
28
37
  VERSION = 1
29
38
  PROMPT_MAX = 2000
@@ -203,6 +212,48 @@ def write_note(session_id, inp, cwd, tool=None):
203
212
  return ""
204
213
 
205
214
 
215
+ def learn_later(root):
216
+ """Start `tezgah-taste learn --repo <root> --no-fallback` detached when the
217
+ signals of `root` (the git root `enabled` returned) grew since the last
218
+ start and an hour has passed. Only when TypeSafe resolves: the automatic run
219
+ never pays a generative provider for a decision it may not apply. Not on a
220
+ machine without flock (Windows), where two background runs in two
221
+ repositories would race on the user ledger. ponytail: no Windows lock; add
222
+ one (msvcrt.locking) before enabling it there. Total; the stamp is written
223
+ before the spawn, so a failed start waits an hour."""
224
+ try:
225
+ if not root:
226
+ return
227
+ size = os.path.getsize(os.path.join(root, ".tezgah", "taste", "signals.jsonl"))
228
+ base = os.path.join(tezgah_paths.cache_dir(), "taste-learn")
229
+ name = re.sub(r"[^A-Za-z0-9]+", "-", root).strip("-")
230
+ stamp = os.path.join(base, name + ".json")
231
+ try:
232
+ with open(stamp, encoding="utf-8") as fh:
233
+ last = json.load(fh)
234
+ except (OSError, ValueError):
235
+ last = {}
236
+ now = time.time()
237
+ if size <= last.get("size", -1) or now - last.get("at", 0) < LEARN_EVERY:
238
+ return
239
+ # the costlier checks run only once a start is due: at most hourly
240
+ import tezgah_judge
241
+ import tezgah_taste_ledger
242
+ if tezgah_taste_ledger.fcntl is None or tezgah_paths.off("judge-off") \
243
+ or not tezgah_judge.named(("typesafe",)):
244
+ return
245
+ os.makedirs(base, exist_ok=True)
246
+ with open(stamp, "w", encoding="utf-8") as fh:
247
+ json.dump({"size": size, "at": now}, fh)
248
+ with open(os.path.join(base, name + ".log"), "ab") as log:
249
+ subprocess.Popen([sys.executable, BIN, "learn", "--repo", root,
250
+ "--no-fallback"],
251
+ cwd=root, stdin=subprocess.DEVNULL, stdout=log,
252
+ stderr=log, start_new_session=True)
253
+ except Exception:
254
+ pass
255
+
256
+
206
257
  def main(argv):
207
258
  """`tezgah_taste.py '<json>'`: `note_write`, then `write_note`, for a host
208
259
  that cannot call them in process (opencode's plugin is JavaScript). The one
@@ -223,5 +274,4 @@ def main(argv):
223
274
 
224
275
 
225
276
  if __name__ == "__main__":
226
- import sys
227
277
  sys.exit(main(sys.argv[1:]))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r1z4x/tezgah",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "One shared working contract for every AI coding assistant you run: Claude Code, Codex, Cursor, opencode, dsh and oh-my-pi (omp).",
5
5
  "license": "MIT",
6
6
  "repository": {