@r1z4x/tezgah 1.3.0 → 1.4.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,37 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.0] - 2026-10-08
10
+
11
+ ### Changed
12
+
13
+ - **Taste state lives in SQLite.** The repository's signals, decisions,
14
+ defects, labels, injections, gate state, learnings and write-note history
15
+ move to `.tezgah/taste/taste.db`. User-scope learnings move to
16
+ `~/.config/tezgah/taste/taste.db`. Opening a store imports the old JSON and
17
+ JSONL files and renames each to `<name>.imported`. It also takes rows an
18
+ older install appends to an old file later. `taste.md` stays a text file.
19
+ - **The evidence ledger lives in SQLite.** Session rows move from
20
+ `~/.cache/tezgah/evidence/<session>.jsonl` to the `evidence` table of
21
+ `~/.cache/tezgah/tezgah.db`. Each row is one INSERT, so no reader meets a
22
+ torn line. A read or write of a session imports its old JSONL file first.
23
+ Session start imports the rest in the background once a day, and
24
+ `tezgah-doctor --import-evidence` imports them now. opencode's plugin uses
25
+ the same database through node:sqlite. On node before 22.5 it calls the
26
+ store's `evidence` CLI. The old files stay in place: an opencode process
27
+ started before the upgrade may still append to one. `tezgah-doctor --clean`
28
+ deletes a session idle past the retention window.
29
+ - **The small cache stores live in the same database.** They move from files
30
+ under `~/.cache/tezgah/` to tables of `~/.cache/tezgah/tezgah.db`. These are
31
+ the turn stamps, switch baselines, used-tool marks and status marks. The
32
+ nudge marks, the judge's marks, the skill hints and the update check move
33
+ too. So do the lesson-taint index, Cursor's marks and replies, the snapshot
34
+ and replay indexes and the taste-learn stamps. The first open of the
35
+ database imports each store's old files once and deletes them. A later open
36
+ deletes, unread, a file an older release still running writes there. Snapshot
37
+ blobs, replay runs and logs stay files. `tezgah-doctor --clean` sweeps the
38
+ stamps and marks there.
39
+
9
40
  ## [1.3.0] - 2026-10-07
10
41
 
11
42
  ### Added
@@ -833,7 +864,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
833
864
  the copy could not find `hooks/` and died on import. It now links into the
834
865
  installed tree, and also installs the command as `tezgah`.
835
866
 
836
- [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.3.0...HEAD
867
+ [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.4.0...HEAD
868
+ [1.4.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.4.0
837
869
  [1.3.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.3.0
838
870
  [1.2.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.2.0
839
871
  [1.1.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.1.0
package/MANIFEST CHANGED
@@ -142,6 +142,7 @@ hooks/tezgah_skill_pick.py
142
142
  hooks/tezgah_snapshot.py
143
143
  hooks/tezgah_stack.py
144
144
  hooks/tezgah_stopspec.py
145
+ hooks/tezgah_store.py
145
146
  hooks/tezgah_task.py
146
147
  hooks/tezgah_taste.py
147
148
  hooks/tezgah_taste_ledger.py
@@ -670,6 +671,7 @@ tests/test_ai_research_library.py
670
671
  tests/test_apps_registry.py
671
672
  tests/test_attest.py
672
673
  tests/test_bash_agreement.py
674
+ tests/test_cache_store.py
673
675
  tests/test_cheap_source.py
674
676
  tests/test_clarity.py
675
677
  tests/test_claude_adapters.py
@@ -694,6 +696,7 @@ tests/test_dsh_statusline.py
694
696
  tests/test_embed.py
695
697
  tests/test_encoding.py
696
698
  tests/test_enforcers.py
699
+ tests/test_evidence_store.py
697
700
  tests/test_fuzz_shell.py
698
701
  tests/test_gate.py
699
702
  tests/test_gate_vectors.py
package/bin/tezgah-doctor CHANGED
@@ -34,12 +34,14 @@ this tree.
34
34
  tezgah-doctor --coverage the tracked files the graph index does not hold
35
35
  tezgah-doctor --coverage --repo ~/Projects/x
36
36
  tezgah-doctor --clean vacuum the opencode database, and delete the
37
- hook state (evidence ledgers, turns/,
38
- sessions/, classify.log, context-drops.log,
39
- debug.log) idle > 30 days
40
- (--retention-days N); $TEZGAH_SESSION's
41
- own files are always kept
37
+ hook state (evidence ledgers, turn
38
+ stamps, used-tool marks, classify.log,
39
+ context-drops.log, debug.log) idle > 30
40
+ days (--retention-days N);
41
+ $TEZGAH_SESSION's own state is always kept
42
42
  tezgah-doctor --prune-sessions 30 delete sessions idle > 30 days, then vacuum
43
+ tezgah-doctor --import-evidence import the legacy JSONL evidence ledgers
44
+ into the evidence database now
43
45
  """
44
46
  import argparse
45
47
  import importlib.machinery
@@ -79,7 +81,11 @@ GRAPH_DB = CODEGRAPH + ".db"
79
81
  # The per-session state the hooks write and nothing else removes: the evidence
80
82
  # ledgers, the per-turn stamps, the used-tool marks and the two capped logs.
81
83
  # Audit L-11 (GAP-10) measured 2,000 of each, 400 days old, surviving a full
82
- # cycle and `--clean`. Snapshots are not here: tezgah_snapshot caps them itself.
84
+ # cycle and `--clean`. The stamps and the marks are rows of the cache database
85
+ # (`tezgah_store.sweep_cache`); the evidence directory holds the legacy ledger
86
+ # files its import has not taken, and `turns/` and `sessions/` the files a
87
+ # writer from an older release still running beside this one adds there.
88
+ # Snapshots are not here: tezgah_snapshot caps them itself.
83
89
  STATE_DIRS = ("evidence", "turns", "sessions")
84
90
  STATE_LOGS = ("classify.log", "context-drops.log", "debug.log")
85
91
  RETENTION_DAYS = 30
@@ -277,20 +283,36 @@ def prune_sessions(days):
277
283
 
278
284
  def sweep_state(days, session=None):
279
285
  """Delete the hook state not modified for `days`, in the global cache and
280
- the temp fallback; (removed, bytes freed).
281
-
282
- By mtime because every writer appends or rewrites in place, so a file's
283
- mtime is its session's last activity. The current session's files
284
- (`TEZGAH_SESSION`, the id omp and the judgement CLIs export) are kept
285
- whatever their age: a session idle past the window and then resumed must
286
- not lose its ledger mid-flight."""
286
+ the temp fallback; (files removed, bytes freed, evidence sessions deleted,
287
+ evidence rows deleted, turn-stamp and used-mark rows deleted).
288
+
289
+ The evidence ledger is swept in its database (`tezgah_store.sweep_evidence`,
290
+ after importing the legacy JSONL files): every session whose newest row is
291
+ older than the cutoff loses its rows. The turn stamps and the used-tool
292
+ marks are swept there too, by the time each was last written
293
+ (`tezgah_store.sweep_cache`). The other files go by mtime, because
294
+ every writer appends or rewrites in place, so a file's mtime is its
295
+ session's last activity. The current session (`TEZGAH_SESSION`, the id omp
296
+ and the judgement CLIs export) is kept whatever its age: a session idle past
297
+ the window and then resumed must not lose its ledger mid-flight."""
298
+ import tezgah_store
287
299
  from tezgah_context import slug
288
300
  from tezgah_integrity import _slug
289
301
  keep = ({_slug(session) + ".jsonl", slug(session) + ".json",
290
302
  slug(session) + ".jsonl"} if session else set())
291
303
  cutoff = time.time() - days * 86400
292
- removed = freed = 0
304
+ removed = freed = sessions = rows = marks = 0
293
305
  for base in dict.fromkeys((CACHE, fallback_cache())):
306
+ try:
307
+ swept = tezgah_store.sweep_evidence(base, cutoff,
308
+ _slug(session) if session else None)
309
+ except tezgah_store.ERRORS:
310
+ swept = (0, 0)
311
+ sessions, rows = sessions + swept[0], rows + swept[1]
312
+ try:
313
+ marks += tezgah_store.sweep_cache(base, cutoff, slug(session) if session else None)
314
+ except tezgah_store.ERRORS:
315
+ pass
294
316
  paths = [os.path.join(base, name) for name in STATE_LOGS]
295
317
  for sub in STATE_DIRS:
296
318
  try:
@@ -310,7 +332,15 @@ def sweep_state(days, session=None):
310
332
  continue
311
333
  removed += 1
312
334
  freed += st.st_size
313
- return removed, freed
335
+ return removed, freed, sessions, rows, marks
336
+
337
+
338
+ def import_evidence():
339
+ """The bulk import of the legacy JSONL ledgers into the evidence database
340
+ of the cache and of the temp fallback: {cache: files seen}."""
341
+ import tezgah_store
342
+ return {base: tezgah_store.import_evidence(base)
343
+ for base in dict.fromkeys((CACHE, fallback_cache()))}
314
344
 
315
345
 
316
346
 
@@ -421,7 +451,7 @@ def main():
421
451
  help="a saved `codegraph files -j` dump to read for --coverage")
422
452
  ap.add_argument("--clean", action="store_true",
423
453
  help="vacuum the opencode database and delete tezgah hook "
424
- "state (ledgers, turns/, sessions/, classify.log, "
454
+ "state (ledgers, turn stamps, used-tool marks, classify.log, "
425
455
  "context-drops.log, debug.log) not "
426
456
  "modified for --retention-days; the current session "
427
457
  "($TEZGAH_SESSION) is kept (opt-in)")
@@ -431,6 +461,10 @@ def main():
431
461
  "%(default)s days)")
432
462
  ap.add_argument("--prune-sessions", type=int, metavar="DAYS",
433
463
  help="delete sessions idle longer than DAYS, then vacuum")
464
+ ap.add_argument("--import-evidence", action="store_true",
465
+ help="import the legacy JSONL evidence ledgers into the "
466
+ "evidence database now, instead of at the next "
467
+ "session start's daily run")
434
468
  args = ap.parse_args()
435
469
  if args.retention_days < 1:
436
470
  ap.error("--retention-days wants a positive day count")
@@ -439,6 +473,19 @@ def main():
439
473
  repo = os.path.realpath(os.path.expanduser(args.repo[0]) if args.repo
440
474
  else os.getcwd())
441
475
  return coverage_report(repo, args.dump)
476
+ if args.import_evidence:
477
+ import tezgah_store
478
+ try:
479
+ seen = import_evidence()
480
+ except tezgah_store.ERRORS as exc:
481
+ print("evidence import failed: %s" % exc, file=sys.stderr)
482
+ return 1
483
+ if args.json:
484
+ print(json.dumps({"imported": seen}, indent=2))
485
+ else:
486
+ for base, n in seen.items():
487
+ print("%s: %d legacy ledger file(s) imported" % (base, n))
488
+ return 0
442
489
  if args.stack:
443
490
  import tezgah_stack
444
491
  repo = os.path.realpath(os.path.expanduser(args.repo[0]) if args.repo
@@ -470,10 +517,13 @@ def main():
470
517
  except sqlite3.Error as exc:
471
518
  cleaned["vacuum"] = "failed: %s" % exc
472
519
  if args.clean:
473
- removed, freed = sweep_state(args.retention_days,
474
- os.environ.get("TEZGAH_SESSION"))
520
+ removed, freed, sessions, rows, marks = sweep_state(
521
+ args.retention_days, os.environ.get("TEZGAH_SESSION"))
475
522
  cleaned["state"] = {"removed": removed, "bytes": freed,
476
523
  "days": args.retention_days}
524
+ cleaned["evidence"] = {"sessions": sessions, "rows": rows,
525
+ "days": args.retention_days}
526
+ cleaned["marks"] = {"rows": marks, "days": args.retention_days}
477
527
  rep = collect(pairs)
478
528
 
479
529
  if args.json:
@@ -499,6 +549,13 @@ def main():
499
549
  if st:
500
550
  print(" hook state %d file(s) older than %d days removed, %s"
501
551
  % (st["removed"], st["days"], human(st["bytes"])))
552
+ ev = cleaned.get("evidence")
553
+ if ev:
554
+ print(" evidence %d session(s) idle > %d days deleted (%d rows)"
555
+ % (ev["sessions"], ev["days"], ev["rows"]))
556
+ mk = cleaned.get("marks")
557
+ if mk:
558
+ print(" stamps/marks %d row(s) idle > %d days deleted" % (mk["rows"], mk["days"]))
502
559
  elif rep["opencode_db_bytes"] > 500 * 1024 * 1024:
503
560
  print("\nrun with --clean to vacuum; --prune-sessions DAYS to shrink the db")
504
561
  return 0
package/bin/tezgah-setup CHANGED
@@ -59,6 +59,7 @@ import tezgah_paths as tp # noqa: E402
59
59
  import tezgah_policy as policy # noqa: E402
60
60
  import tezgah_agents # noqa: E402
61
61
  import tezgah_integrity # noqa: E402
62
+ import tezgah_store # noqa: E402
62
63
  import tezgah_attest as ta # noqa: E402
63
64
  from tezgah_policy import ( # noqa: E402
64
65
  ADHD, CODEGRAPH_RULE, CODEGRAPH_STATIC, CONSULT, EXEC, NO_CONSULT,
@@ -4161,17 +4162,6 @@ def _denied(host, stdout):
4161
4162
  return (got.get("hookSpecificOutput") or {}).get("permissionDecision") == "deny"
4162
4163
 
4163
4164
 
4164
- def _live_cleanup(slug):
4165
- """Remove every cache file the synthetic session left (named by its slug)."""
4166
- for dirpath, _dirs, files in os.walk(tp.cache_dir()):
4167
- for name in files:
4168
- if slug in name:
4169
- try:
4170
- os.remove(os.path.join(dirpath, name))
4171
- except OSError:
4172
- pass
4173
-
4174
-
4175
4165
  def live_probe(host, deny=False):
4176
4166
  """(state, line) for one host: ok, miss or unverified. `deny` sends the
4177
4167
  PreToolUse probe: the host's deny envelope and a deny row must both appear;
@@ -4211,15 +4201,14 @@ def live_probe(host, deny=False):
4211
4201
  capture_output=True, text=True, env=env,
4212
4202
  cwd=root, timeout=LIVE_TIMEOUT)
4213
4203
  ms = int((time.monotonic() - started) * 1000)
4214
- try:
4215
- with open(ledger, encoding="utf-8") as fh:
4216
- kinds = [json.loads(ln).get("kind") for ln in fh if ln.strip()]
4217
- except (OSError, ValueError):
4218
- kinds = []
4204
+ kinds = [row.get("kind") for row in tezgah_integrity.events_path(ledger)]
4219
4205
  except (OSError, subprocess.SubprocessError) as exc:
4220
4206
  return "miss", "live %s: the hook did not run (%s)" % (who, exc)
4221
4207
  finally:
4222
- _live_cleanup(slug)
4208
+ try:
4209
+ tezgah_store.forget_session(ledger)
4210
+ except tezgah_store.ERRORS:
4211
+ pass
4223
4212
  shutil.rmtree(root, ignore_errors=True)
4224
4213
  err = (proc.stderr or "").strip().splitlines()
4225
4214
  tail = " (exit %d%s)" % (proc.returncode, ": " + err[-1][:160] if err else "")
package/bin/tezgah-taste CHANGED
@@ -34,15 +34,15 @@ judge's provider (`hooks/tezgah_judge.py` names it); with no credential or
34
34
  `judge-off` set they send nothing and exit 2.
35
35
 
36
36
  `learn` is taste-1's pipeline on tezgah (research line taste-architecture,
37
- design T1P). Each prompt after a writing turn - from the capture rows in
38
- `<repo>/.tezgah/taste/signals.jsonl`, or with `--from-transcripts` from this
39
- repository's omp and Claude transcripts - becomes one structured signal built by
40
- code: the prompt, the files written, the agent's edits and the user's own edit
41
- between two agent writes. One typed decision per signal, required from TypeSafe
42
- (`only=("typesafe",)`): preference, defect or none; the category; the scope; and
43
- how it relates to each learning held. A decision any other provider gave is
44
- recorded `unverified` and never applied. A defect goes to `defects.jsonl`, never
45
- into the ledger. The ledger itself is `hooks/tezgah_taste_ledger.py`. A
37
+ design T1P). Each prompt after a writing turn - from the capture rows in the
38
+ `signals` table of `<repo>/.tezgah/taste/taste.db`, or with `--from-transcripts`
39
+ from this repository's omp and Claude transcripts - becomes one structured signal
40
+ built by code: the prompt, the files written, the agent's edits and the user's
41
+ own edit between two agent writes. One typed decision per signal, required from
42
+ TypeSafe (`only=("typesafe",)`): preference, defect or none; the category; the
43
+ scope; and how it relates to each learning held. A decision any other provider
44
+ gave is recorded `unverified` and never applied. A defect goes to the `defects`
45
+ table, never into the ledger. The ledger itself is `hooks/tezgah_taste_ledger.py`. A
46
46
  learning that becomes active gets one readable line from a generative provider
47
47
  (a `text` question; the user's own words when none answers).
48
48
 
@@ -70,6 +70,7 @@ sys.path.insert(0, os.path.join(HERE, "hooks"))
70
70
  import tezgah_integrity as ti # noqa: E402
71
71
  import tezgah_judge # noqa: E402
72
72
  import tezgah_models # noqa: E402
73
+ import tezgah_paths # noqa: E402
73
74
  import tezgah_taste_ledger as tl # noqa: E402
74
75
 
75
76
  WRITE_TOOLS = {"omp": {"edit", "write"},
@@ -466,18 +467,13 @@ def iso_day(ts):
466
467
 
467
468
 
468
469
  def _snapshots():
469
- """{absolute path: [(ts, id, after)]} over the snapshot store, oldest first."""
470
- import tezgah_snapshot as ts
470
+ """{absolute path: [(ts, id, after)]} over the snapshot index, oldest first."""
471
+ import tezgah_store
471
472
  out = {}
472
- for d in ts._dirs():
473
- try:
474
- with open(os.path.join(d, "meta.json"), encoding="utf-8") as fh:
475
- meta = json.load(fh)
476
- except (OSError, ValueError):
477
- continue
478
- if isinstance(meta, dict) and meta.get("path"):
473
+ for meta in tezgah_store.snapshots():
474
+ if isinstance(meta, dict) and meta.get("path") and meta.get("id"):
479
475
  out.setdefault(meta["path"], []).append(
480
- (int(meta.get("ts") or 0), os.path.basename(d), bool(meta.get("after"))))
476
+ (int(meta.get("ts") or 0), meta["id"], bool(meta.get("after"))))
481
477
  for rows in out.values():
482
478
  rows.sort()
483
479
  return out
@@ -522,7 +518,7 @@ def user_diff(repo, rel, after_sid, snaps):
522
518
  def capture_signals(repo):
523
519
  """One signal per prompt that followed a writing turn, from the capture rows."""
524
520
  snaps, turns, out = None, {}, []
525
- for row in tl.rows(os.path.join(tl.store_dir(repo), "signals.jsonl")):
521
+ for row in tl.rows(repo, "signals"):
526
522
  session = row.get("session") or ""
527
523
  turn = turns.setdefault(session, {"edits": [], "after": []})
528
524
  if row.get("kind") == "edit":
@@ -666,9 +662,15 @@ def write_line(learning):
666
662
 
667
663
 
668
664
  def store_or_exit(args):
665
+ """The repository whose taste store a command opens. Refused when its
666
+ `.tezgah` came with the clone: those files are the project's data, and
667
+ opening the store would import, rename or create files in them."""
669
668
  repo = repo_root(os.path.abspath(args.repo or os.getcwd()))
670
669
  if not repo:
671
670
  raise SystemExit(misuse("not inside a git repository"))
671
+ if tezgah_paths.workspace_from_repo(repo):
672
+ raise SystemExit(misuse("%s/.tezgah came with the repository; its taste "
673
+ "store is not opened" % repo))
672
674
  return repo
673
675
 
674
676
 
@@ -689,7 +691,7 @@ def cmd_learn(args):
689
691
  signals = (transcript_signals(repo, args.host) if args.from_transcripts
690
692
  else capture_signals(repo))
691
693
  decided = {}
692
- for r in tl.rows(os.path.join(tl.store_dir(repo), "decisions.jsonl")):
694
+ for r in tl.rows(repo, "decisions"):
693
695
  decided[r.get("id")] = decided.get(r.get("id")) or r.get("provider") == "typesafe"
694
696
  # a typed decision is final; an unverified one is retried, TypeSafe only
695
697
  todo = [s for s in signals if not decided.get(s["id"])]
@@ -709,7 +711,7 @@ def cmd_learn(args):
709
711
  tokens[0] += result["usage"]["input_tokens"]
710
712
  tokens[1] += result["usage"]["output_tokens"]
711
713
  day = tl.now_day()
712
- tl.append(os.path.join(tl.store_dir(repo), "decisions.jsonl"), {
714
+ tl.append(repo, "decisions", {
713
715
  "id": sig["id"], "day": day, "at": sig.get("at"), "session": sig["session"],
714
716
  "provider": result.get("provider"), "model": result.get("model"),
715
717
  "verified": dec["verified"], "kind": dec["kind"][0], "p": dec["kind"][1],
@@ -722,7 +724,7 @@ def cmd_learn(args):
722
724
  counts["typesafe"] += 1
723
725
  counts[dec["kind"][0] if dec["kind"][0] in CLASSES else "none"] += 1
724
726
  if dec["kind"][0] == "defect":
725
- tl.append(os.path.join(tl.store_dir(repo), "defects.jsonl"),
727
+ tl.append(repo, "defects",
726
728
  {"id": sig["id"], "day": day, "session": sig["session"],
727
729
  "text": ti.redact(sig["text"])[:600], "paths": sig["paths"][:8]},
728
730
  strict=True)
@@ -806,17 +808,16 @@ def cmd_label(args):
806
808
  sample of unlabelled typed decisions - drawn without regard to what was
807
809
  injected, so the calibration bound rests on an unselected sample."""
808
810
  repo = store_or_exit(args)
809
- store = tl.store_dir(repo)
810
- decided = {r["id"]: r for r in tl.rows(os.path.join(store, "decisions.jsonl"))
811
- if r.get("provider") == "typesafe" and r.get("id")}
812
- labelled = {r.get("id") for r in tl.rows(os.path.join(store, "labels.jsonl"))}
811
+ decided = {r["id"]: r for r in tl.rows(repo, "decisions", "WHERE provider = ?",
812
+ ("typesafe",)) if r.get("id")}
813
+ labelled = {r.get("id") for r in tl.rows(repo, "labels")}
813
814
  if args.id:
814
815
  if args.label not in CLASSES:
815
816
  return misuse("label is one of %s" % ", ".join(CLASSES))
816
817
  row = decided.get(args.id)
817
818
  if row is None:
818
819
  return misuse("no typed decision %s" % args.id)
819
- tl.append(os.path.join(store, "labels.jsonl"),
820
+ tl.append(repo, "labels",
820
821
  {"id": args.id, "label": args.label, "decided": row.get("kind"),
821
822
  "provider": row.get("provider"), "day": tl.now_day()}, strict=True)
822
823
  print("%s labelled %s (decided %s)" % (args.id, args.label, row.get("kind")))
@@ -910,7 +911,7 @@ def main(argv):
910
911
  "gate": cmd_gate}[args.cmd]
911
912
  if args.cmd not in ("learn", "accept", "reject", "edit", "label"):
912
913
  return handler(args)
913
- # the commands that rewrite ledger.json: one at a time, or the last one
914
+ # the commands that rewrite the ledgers: one at a time, or the last one
914
915
  # to finish would silently drop what the other wrote
915
916
  try:
916
917
  repo = store_or_exit(args)
@@ -154,10 +154,10 @@ lowest-value blocks are dropped in a fixed order rather than the rules
154
154
 
155
155
  | Store | Path | Writer | Authoritative for |
156
156
  |---|---|---|---|
157
- | Evidence ledger | `~/.cache/tezgah/evidence/<session>.jsonl` (`hooks/tezgah_integrity.py::_path`) | `note_tool` from each host's PostToolUse (`hooks/tezgah_integrity.py::note_tool`) | what a session actually ran, and therefore the Stop verdict |
158
- | Session store (used marks) | the chosen cache dir, `sessions/<session>.jsonl` (`hooks/tezgah_context.py::record`) | `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
- | Snapshot store | `<cache dir>/snapshots` (`hooks/tezgah_snapshot.py::_store`) | `capture` on the write path (`hooks/tezgah_snapshot.py::capture`) | the pre-write bytes; the rollback source |
160
- | Turn stamp | `<cache dir>/turns/<session>.json` (`hooks/tezgah_context.py::_stamp_path`) | `write_stamp` (`hooks/tezgah_context.py::write_stamp`) | the comparison behind the one-line delta, nothing else |
157
+ | Evidence ledger | the `evidence` rows of `~/.cache/tezgah/tezgah.db`, keyed by the session's `evidence/<session>.jsonl` path (`hooks/tezgah_integrity.py::_path`, `hooks/tezgah_store.py::EVIDENCE_SCHEMA`); an old JSONL file at that path is imported (`hooks/tezgah_store.py::_sync`) | `note_tool` from each host's PostToolUse (`hooks/tezgah_integrity.py::note_tool`) | what a session actually ran, and therefore the Stop verdict |
158
+ | 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
+ | 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
+ | 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 |
161
161
  | 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
162
  | 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
163
 
@@ -30,10 +30,10 @@
30
30
  "semicolons": 3
31
31
  },
32
32
  "docs/evidence.md": {
33
- "long_sentences": 109,
34
- "passive": 57,
33
+ "long_sentences": 108,
34
+ "passive": 55,
35
35
  "replace_words": 1,
36
- "semicolons": 40
36
+ "semicolons": 39
37
37
  },
38
38
  "docs/feature-audit.md": {
39
39
  "long_sentences": 20,
package/docs/contract.md CHANGED
@@ -43,7 +43,7 @@ answers it - TypeSafe, or the OpenRouter fallback when no TypeSafe key resolves
43
43
  skill names plus `none`, and one Noul
44
44
  asking whether the turn wants a skill at all - and the winner is appended after
45
45
  the armed paragraphs as a hint to look at first, never as an instruction to load
46
- (`hooks/tezgah_skill_pick.py:41-75` is the whole configuration, wired into
46
+ (`hooks/tezgah_skill_pick.py:41-76` is the whole configuration, wired into
47
47
  `context_for` at `hooks/tezgah_context.py::context_for`). Measured cost of the line
48
48
  itself: 455-523 characters, about 115-130 tokens of prompt (the section address
49
49
  added ~150 characters over the name-only line; `bin/tezgah-skill` is the same
@@ -122,6 +122,7 @@ the kill switches are files, not variables, and live in
122
122
  | Variable | Read by | Default | Effect |
123
123
  |---|---|---|---|
124
124
  | `TEZGAH_E2E_STRICT` | `tests/e2e_*.py`, `.github/workflows/ci.yml` | unset (a missing prerequisite is a SKIP) | `1` turns an end-to-end script's SKIP into a FAIL; CI sets it. |
125
+ | `TEZGAH_OPENCODE_NO_SQLITE` | `hosts/opencode/plugins/tezgah.js` | unset (node:sqlite when the runtime has it) | `1` makes the opencode plugin read and write the evidence ledger through `python3 hooks/tezgah_store.py evidence ...`, the path a runtime without node:sqlite takes; a test sets it. |
125
126
 
126
127
  ## Debugging
127
128