@r1z4x/tezgah 1.3.0 → 1.5.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 (53) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/MANIFEST +3 -0
  3. package/agents/tezgah-reviewer.md +19 -10
  4. package/bin/tezgah-docs +3 -3
  5. package/bin/tezgah-doctor +75 -18
  6. package/bin/tezgah-route +5 -2
  7. package/bin/tezgah-setup +30 -18
  8. package/bin/tezgah-status +3 -1
  9. package/bin/tezgah-task +130 -37
  10. package/bin/tezgah-taste +74 -55
  11. package/bin/tezgah-triage +12 -7
  12. package/docs/architecture.md +4 -4
  13. package/docs/clarity-baseline.json +9 -9
  14. package/docs/contract.md +22 -12
  15. package/docs/environment.md +4 -1
  16. package/docs/evidence.md +71 -34
  17. package/docs/gate.md +21 -21
  18. package/docs/glossary.md +5 -5
  19. package/docs/hosts.md +8 -8
  20. package/docs/judge.md +93 -27
  21. package/docs/operations.md +69 -30
  22. package/docs/skills.md +2 -2
  23. package/docs/status-line.md +7 -7
  24. package/docs/vision.md +10 -5
  25. package/hooks/tezgah_agents.py +22 -11
  26. package/hooks/tezgah_attest.py +15 -22
  27. package/hooks/tezgah_context.py +84 -124
  28. package/hooks/tezgah_gate.py +26 -23
  29. package/hooks/tezgah_guard.py +9 -11
  30. package/hooks/tezgah_integrity.py +77 -178
  31. package/hooks/tezgah_judge.py +270 -122
  32. package/hooks/tezgah_lessons.py +18 -63
  33. package/hooks/tezgah_paths.py +183 -55
  34. package/hooks/tezgah_policy.py +35 -8
  35. package/hooks/tezgah_replay.py +57 -33
  36. package/hooks/tezgah_shapes.py +4 -4
  37. package/hooks/tezgah_skill_pick.py +6 -35
  38. package/hooks/tezgah_snapshot.py +25 -46
  39. package/hooks/tezgah_store.py +1268 -0
  40. package/hooks/tezgah_task.py +146 -40
  41. package/hooks/tezgah_taste.py +21 -18
  42. package/hooks/tezgah_taste_ledger.py +63 -88
  43. package/hooks/tezgah_update.py +6 -26
  44. package/hosts/codex/hook.py +5 -0
  45. package/hosts/cursor/hook.py +19 -31
  46. package/hosts/opencode/plugins/tezgah.js +309 -108
  47. package/package.json +1 -1
  48. package/skills/harness/SKILL.md +4 -0
  49. package/skills/plan-add/SKILL.md +23 -7
  50. package/skills/plan-add/render_table.py +3 -2
  51. package/skills/plan-status/SKILL.md +12 -4
  52. package/skills/plan-sync/SKILL.md +19 -9
  53. package/skills/tezgah-contract/SKILL.md +25 -12
@@ -27,6 +27,7 @@ import json
27
27
  import os
28
28
 
29
29
  import tezgah_paths as tp
30
+ import tezgah_store
30
31
 
31
32
  ROOT = tp.PLUGIN_ROOT
32
33
  CONTRACT_SHA = os.path.join(tp.CONFIG_DIR, "contract.sha256")
@@ -194,13 +195,13 @@ def drift(host, root=ROOT, shas=None):
194
195
  return found, unknown
195
196
 
196
197
 
197
- def drift_mark(session_id, host):
198
- """The mark one host's drifted session start leaves; its text is the drift
199
- list. Keyed by session AND host: two hosts can share a session id (a
200
- `tezgah-context attest` run by hand, a host whose ids collide), and a clean
201
- start on one must not clear the other's mark."""
202
- return os.path.join(tp.cache_dir(), "harness-drift", "%s.%s" % (
203
- hashlib.sha256(str(session_id).encode()).hexdigest()[:16], host))
198
+ def drift_key(session_id, host):
199
+ """The key of the mark one host's drifted session start leaves in the
200
+ `harness_drift` store; its value is the drift list. Keyed by session AND
201
+ host: two hosts can share a session id (a `tezgah-context attest` run by
202
+ hand, a host whose ids collide), and a clean start on one must not clear
203
+ the other's mark."""
204
+ return "%s.%s" % (hashlib.sha256(str(session_id).encode()).hexdigest()[:16], host)
204
205
 
205
206
 
206
207
  def mark_text(session_id):
@@ -209,11 +210,8 @@ def mark_text(session_id):
209
210
  return ""
210
211
  texts = []
211
212
  for host in CODE_TARGETS:
212
- try:
213
- with open(drift_mark(session_id, host), encoding="utf-8") as fh:
214
- text = fh.read().strip()
215
- except OSError:
216
- continue
213
+ text = tezgah_store.doc("harness_drift", drift_key(session_id, host))
214
+ text = text.strip() if isinstance(text, str) else ""
217
215
  if text and text not in texts:
218
216
  texts.append(text)
219
217
  return "; ".join(texts)
@@ -242,14 +240,9 @@ def run(host, session_id, cwd=None):
242
240
  import tezgah_integrity
243
241
  tezgah_integrity.note(session_id, "attest", detail, host=host,
244
242
  switches=",".join(switches()) or None)
245
- mark = drift_mark(session_id, host)
246
- try:
247
- if found:
248
- os.makedirs(os.path.dirname(mark), exist_ok=True)
249
- with open(mark, "w", encoding="utf-8") as fh:
250
- fh.write("; ".join(found) + "\n")
251
- elif os.path.exists(mark):
252
- os.remove(mark)
253
- except OSError:
254
- pass
243
+ key = drift_key(session_id, host)
244
+ if found:
245
+ tezgah_store.put_doc("harness_drift", key, "; ".join(found))
246
+ else:
247
+ tezgah_store.drop("harness_drift", key)
255
248
  return found
@@ -17,7 +17,8 @@ import time
17
17
  import tezgah_embed
18
18
  import tezgah_orca
19
19
  import tezgah_research
20
- from tezgah_guard import import_crash_mark
20
+ import tezgah_store
21
+ from tezgah_guard import import_crash_key
21
22
  from tezgah_integrity import (STEP_KINDS, _heredocs, _path as _ledger_path,
22
23
  _shell_segments, bind_session,
23
24
  changed_files, cut, last_check, note,
@@ -25,8 +26,9 @@ from tezgah_integrity import (STEP_KINDS, _heredocs, _path as _ledger_path,
25
26
  UNTRUSTED_CHANNEL)
26
27
  from tezgah_lessons import (lesson_key, lines as lesson_lines,
27
28
  tainted as tainted_lessons)
28
- from tezgah_policy import (CONDITIONAL_KEYS, CORE, POINTERS, PROMPT_REMINDER,
29
- REPLY_LANG_TEXT, open_lines_note, pony_level_line)
29
+ from tezgah_policy import (CONDITIONAL_KEYS, CORE, FANOUT_ACTION, POINTERS,
30
+ PROMPT_REMINDER, REPLY_LANG_TEXT, open_lines_note,
31
+ pony_level_line)
30
32
  from tezgah_paths import (CACHE, REPO_MARKS, SWITCHES, ai_research_dir, armed,
31
33
  cache_dir, codegraph_bin, consult_options,
32
34
  ensure_workspace, fallback_cache, have_judge_key, off,
@@ -105,6 +107,21 @@ def _tr_ui_ask(word):
105
107
  return (r"(?:\b(?:%s)\w*[^.!?\n]{0,40}\b(?:%s))" % (TR_UI_OBJECTS, word))
106
108
 
107
109
 
110
+ # The fan-out hint's parts (PROMPT_HINTS["fanout"]). A list item's marker:
111
+ # `1.`, `1)`, `1 -`, `1:`, `a)`, or a `-`/`*`/`+`/`•` bullet, then its text.
112
+ _FAN_ITEM = r"(?:\d+[ \t]*[.):\u2013-]|[a-z]\)|[-*+\u2022])[ \t]+\S"
113
+ # A file a prompt names: a path with a directory, or a name with an extension.
114
+ # Anchored at a token start: unanchored, a long run of word characters made
115
+ # every position a start and the search quadratic (100 KB took minutes).
116
+ _FAN_FILE = (r"(?<![\w./-])[`'\"]?(?:(?:[\w.-]+/)+[\w.-]*\w|"
117
+ r"\w[\w-]*\.[a-z]{1,4}\b)[`'\"]?")
118
+ # The imperative that opens an English clause and closes a Turkish one.
119
+ _FAN_VERB_EN = (r"(?:fix|add|update|write|remove|delete|rename|move|refactor|"
120
+ r"document|bump|create|implement|migrate|replace)")
121
+ _FAN_VERB_TR = (r"(?:düzelt|güncelle|ekle|yaz|sil|kaldır|taşı|bitir|tamamla|"
122
+ r"incele|oluştur|değiştir)")
123
+
124
+
108
125
  PROMPT_HINTS = (
109
126
  ("spec", r"\b(normal (user )?behaviou?r|clean ui|nicer|more intuitive|"
110
127
  r"un?professional|polish(ed)?|improve the (ui|ux)|make it (better|"
@@ -339,9 +356,22 @@ PROMPT_HINTS = (
339
356
  # had to say "do everything with subagents and in parallel" on every task
340
357
  # because nothing armed the fan-out rule from the shape of the request.
341
358
  # `parallelize` alone is code work, so only the delegation phrasings count.
342
- ("fanout", r"(?ms:^[ \t]*\d+[.)][ \t]+\S.*?^[ \t]*\d+[.)][ \t]+\S)|"
343
- r"(?ms:^[ \t]*[-*•][ \t]+\S.*?^[ \t]*[-*•][ \t]+\S)|"
344
- r"\b(?:in parallel|paralel\w*|sub-?agents?|alt ?ajan\w*|"
359
+ # The owner's own shapes armed nothing until 2026-10-09: `1 - `, `1- `,
360
+ # `1:`, `a)`, `+` items, an inline `1. ... 2. ...`, "subagentlerle", three
361
+ # files named as one list, "First, ... Second, ...", and three imperative
362
+ # clauses in one sentence (clause-initial in English, clause-final in
363
+ # Turkish). A timestamp, a range and a traceback stay out (the corpus).
364
+ ("fanout", r"(?ms:^[ \t]*" + _FAN_ITEM + r".*?^[ \t]*" + _FAN_ITEM + r")|"
365
+ r"(?<![\w.])1[ \t]*[.)-][ \t]+\S[^\n]*?(?<![\w.])2[ \t]*[.)-][ \t]+\S|"
366
+ + _FAN_FILE + r"(?:[ \t]*(?:,[ \t]*(?:(?:and|ve)[ \t]+)?|"
367
+ r"(?:and|ve|&)[ \t]+)" + _FAN_FILE + r"){2,}|"
368
+ r"(?s:\bfirst(?:ly)?[,:].*?\bsecond(?:ly)?[,:])|"
369
+ r"(?s:\bbirinci(?:si)?[,:].*?\bikinci(?:si)?[,:])|"
370
+ r"(?m:(?:(?:^|[,;]|\band\b)[ \t]*(?:and[ \t]+)?" + _FAN_VERB_EN
371
+ + r"\b[^\n]*?){3})|"
372
+ r"(?m:(?:\b" + _FAN_VERB_TR + r"\w{0,4}[ \t]*(?:[,;.]|\bve\b|$)"
373
+ r"[^\n]*?){3})|"
374
+ r"\b(?:in parallel|paralel\w*|sub-?agent\w*|alt ?ajan\w*|"
345
375
  r"worktrees?|fan[- ]?out|orchestrat\w*|orkestra\w*)\b"),
346
376
  )
347
377
 
@@ -1055,32 +1085,18 @@ def state_stamp(root):
1055
1085
  "lessons": _lessons_state(root)}
1056
1086
 
1057
1087
 
1058
- def _stamp_path(session_id):
1059
- return os.path.join(cache_dir(), "turns", slug(str(session_id)) + ".json")
1060
-
1061
-
1062
1088
  def read_stamp(session_id):
1063
1089
  """The stamp this session's previous turn wrote, or None when there is none."""
1064
1090
  if not session_id:
1065
1091
  return None
1066
- try:
1067
- with open(_stamp_path(session_id), encoding="utf-8") as fh:
1068
- got = json.load(fh)
1069
- except (OSError, ValueError):
1070
- return None
1092
+ got = tezgah_store.doc("turns", slug(str(session_id)))
1071
1093
  return got if isinstance(got, dict) and got.get("root") else None
1072
1094
 
1073
1095
 
1074
1096
  def write_stamp(session_id, root, stamp):
1075
1097
  """Remember this turn's stamp. Best effort: a host may sandbox hook writes."""
1076
- if not session_id:
1077
- return
1078
- try:
1079
- os.makedirs(os.path.join(cache_dir(), "turns"), exist_ok=True)
1080
- with open(_stamp_path(session_id), "w", encoding="utf-8") as fh:
1081
- json.dump(dict(stamp, root=root), fh)
1082
- except OSError:
1083
- pass
1098
+ if session_id:
1099
+ tezgah_store.put_doc("turns", slug(str(session_id)), dict(stamp, root=root))
1084
1100
 
1085
1101
 
1086
1102
  def forget_seen(session_id):
@@ -1107,8 +1123,13 @@ ARMED_RESURFACE = 20
1107
1123
  def armed_again(key):
1108
1124
  """The one line a matching prompt pays for a paragraph this session was
1109
1125
  already shown. The research rule keeps its `{OPEN_LINES}` slot: the open
1110
- lines are a fact about the repo now, not rule text the session holds."""
1126
+ lines are a fact about the repo now, not rule text the session holds. The
1127
+ fan-out rule keeps its action (`FANOUT_ACTION`): the rule is a thing to do
1128
+ on this turn's list, and a bare reminder that it exists was not done."""
1111
1129
  label = dict(CORE_RULES)[key]
1130
+ if key == "fanout":
1131
+ return ("%s Armed again: %s The full rule is in the `tezgah-contract` "
1132
+ "skill." % (label, FANOUT_ACTION))
1112
1133
  return ("%s Armed again: the full rule was given earlier this session and "
1113
1134
  "is in the `tezgah-contract` skill.%s"
1114
1135
  % (label, "{OPEN_LINES}" if key == "research" else ""))
@@ -1627,10 +1648,11 @@ POST_TOOL_ROWS = frozenset(k.encode() for k in STEP_KINDS + ("external", "unknow
1627
1648
 
1628
1649
 
1629
1650
  def _ledger_lines(path):
1651
+ """The ledger's rows at `path` as their JSON bytes, oldest first."""
1630
1652
  try:
1631
- with open(path, "rb") as fh:
1632
- return fh.read().split(b"\n")
1633
- except OSError:
1653
+ return [text.encode("utf-8", "replace")
1654
+ for text in tezgah_store.evidence_rows(path)]
1655
+ except tezgah_store.ERRORS:
1634
1656
  return []
1635
1657
 
1636
1658
 
@@ -1672,12 +1694,10 @@ def _ledger_since(session_id):
1672
1694
  return None, False
1673
1695
 
1674
1696
 
1675
- def _gate_mark(session_id):
1676
- return os.path.join(cache_dir(), "gate-inactive", slug(str(session_id)))
1677
-
1678
-
1679
- def _switch_baseline(session_id):
1680
- return os.path.join(cache_dir(), "switches", slug(str(session_id)) + ".json")
1697
+ def gate_marked(session_id):
1698
+ """True when the prompt hook left the disarmed-gate mark for this session
1699
+ (`gate_inactive`)."""
1700
+ return tezgah_store.marked("gate_inactive", slug(str(session_id)))
1681
1701
 
1682
1702
 
1683
1703
  def disarmed(session_id):
@@ -1694,24 +1714,17 @@ def disarmed(session_id):
1694
1714
  # the files alone (`armed`, unlatched): a switch the latch ignores is still
1695
1715
  # the tamper shape this records
1696
1716
  now = sorted(n for n in SWITCHES if armed(n))
1697
- path = _switch_baseline(session_id)
1717
+ state = tezgah_store.doc("switches", slug(str(session_id)))
1698
1718
  try:
1699
- with open(path, encoding="utf-8") as fh:
1700
- state = json.load(fh)
1701
1719
  start, seen = list(state["start"]), list(state["seen"])
1702
- except (OSError, ValueError, KeyError, TypeError):
1720
+ except (KeyError, TypeError):
1703
1721
  start, seen = now, []
1704
1722
  moved = [n for n in now if n not in start]
1705
1723
  for name in moved:
1706
1724
  if name not in seen:
1707
1725
  note(session_id, "disarm", name)
1708
1726
  seen.append(name)
1709
- try:
1710
- os.makedirs(os.path.dirname(path), exist_ok=True)
1711
- with open(path, "w", encoding="utf-8") as fh:
1712
- json.dump({"start": start, "seen": seen}, fh)
1713
- except OSError:
1714
- pass
1727
+ tezgah_store.put_doc("switches", slug(str(session_id)), {"start": start, "seen": seen})
1715
1728
  return moved
1716
1729
 
1717
1730
 
@@ -1730,14 +1743,13 @@ def gate_inactive(session_id, payload):
1730
1743
  calls = 0 if since is None or rows else _transcript_calls(
1731
1744
  p.get("transcript_path"), since)
1732
1745
  inactive = calls >= GATE_MIN_CALLS
1733
- mark = _gate_mark(session_id)
1746
+ key = slug(str(session_id))
1734
1747
  try:
1735
1748
  if inactive or moved:
1736
- os.makedirs(os.path.dirname(mark), exist_ok=True)
1737
- open(mark, "w", encoding="utf-8").close()
1738
- elif os.path.exists(mark):
1739
- os.remove(mark)
1740
- except OSError:
1749
+ tezgah_store.mark("gate_inactive", key)
1750
+ else:
1751
+ tezgah_store.drop("gate_inactive", key)
1752
+ except tezgah_store.ERRORS:
1741
1753
  pass
1742
1754
  return GATE_INACTIVE % calls if inactive else ""
1743
1755
 
@@ -2210,6 +2222,10 @@ def context_for(event, cwd, payload=None, with_core=True):
2210
2222
  tezgah_taste.learn_later(taste_root)
2211
2223
  except Exception:
2212
2224
  pass
2225
+ # The legacy JSONL ledgers into the evidence database: a detached
2226
+ # bulk import at most daily (tezgah_store.import_later).
2227
+ if event == "session_start":
2228
+ tezgah_store.import_later(cache_dir())
2213
2229
  broken = tezgah_research.failing(root) if not off("research-off") else []
2214
2230
  if broken:
2215
2231
  line_slug, err = broken[0]
@@ -2408,62 +2424,22 @@ def record(session_id, kind):
2408
2424
  if kind == "orch":
2409
2425
  note(session_id, "orch")
2410
2426
  try:
2411
- d = os.path.join(cache_dir(), "sessions")
2412
- os.makedirs(d, exist_ok=True)
2413
- with open(os.path.join(d, slug(session_id) + ".jsonl"), "a", encoding="utf-8") as fh:
2414
- fh.write(json.dumps({"kind": kind}) + "\n")
2415
- except OSError:
2427
+ tezgah_store.use(slug(session_id), kind)
2428
+ except tezgah_store.ERRORS:
2416
2429
  pass
2417
2430
 
2418
2431
 
2419
- # Past this size `used()` compacts the store to one row per kind. Every reader
2420
- # of the store takes it as a set, so nothing is lost, and the next read is a
2421
- # few hundred bytes however long the session ran.
2422
- USED_MAX_BYTES = 65536
2423
-
2424
-
2425
2432
  def used(session_id):
2426
- if not session_id:
2427
- return set()
2428
- out = set()
2429
- path = os.path.join(cache_dir(), "sessions", slug(session_id) + ".jsonl")
2430
- try:
2431
- with open(path, encoding="utf-8") as fh:
2432
- size = os.fstat(fh.fileno()).st_size
2433
- for line in fh:
2434
- try:
2435
- out.add(json.loads(line)["kind"])
2436
- except (ValueError, KeyError):
2437
- pass
2438
- # a row appended since the open changes the size: skip, never lose it
2439
- if size > USED_MAX_BYTES and os.path.getsize(path) == size:
2440
- tmp = path + ".tmp"
2441
- with open(tmp, "w", encoding="utf-8") as fh:
2442
- fh.writelines(json.dumps({"kind": k}) + "\n"
2443
- for k in sorted(k for k in out
2444
- if isinstance(k, str)))
2445
- os.replace(tmp, path)
2446
- except OSError:
2447
- pass
2448
- return out
2433
+ """The kinds this session recorded, a set (one `used` row per kind)."""
2434
+ return tezgah_store.used(slug(session_id)) if session_id else set()
2449
2435
 
2450
2436
 
2451
- # How many of the newest recorded session files a fitness report reads. A row is
2452
- # `{"kind": kind}` and carries no timestamp, so the window can only be files by
2453
- # mtime - the moment the session last recorded anything, which for a used-kind
2454
- # store is its last event.
2437
+ # How many of the sessions that recorded last a fitness report reads, by the
2438
+ # time each last recorded anything, which for a used-kind store is its last
2439
+ # event.
2455
2440
  FITNESS_WINDOW = 200
2456
2441
 
2457
2442
 
2458
- def _mtime(path):
2459
- """A file's mtime, or 0 when it is gone: the window is a sort key, and a
2460
- session that vanished mid-report must not crash the report."""
2461
- try:
2462
- return os.path.getmtime(path)
2463
- except OSError:
2464
- return 0.0
2465
-
2466
-
2467
2443
  def skill_fitness(window=FITNESS_WINDOW):
2468
2444
  """Which shipped skills the recorded sessions opened, and which opened none.
2469
2445
 
@@ -2474,45 +2450,29 @@ def skill_fitness(window=FITNESS_WINDOW):
2474
2450
  skill a read opened). This is a measurement, not a gate - it names the
2475
2451
  candidates, and retiring one stays the owner's call.
2476
2452
 
2477
- The window is the newest `window` session files by mtime, because the rows
2478
- carry no timestamp. A session counts as having opened a skill when it
2453
+ The window is the `window` sessions that recorded last. A session counts
2454
+ as having opened a skill when it
2479
2455
  recorded that skill's kind: `skill:<name>`, or the mark the status line draws
2480
2456
  for the two marked skills, so reads recorded before `SKILL_KIND` existed
2481
2457
  still count. Sessions on a host that cannot see a read record neither, and
2482
2458
  the report cannot tell that session from one that read nothing - the number
2483
2459
  is a floor, not a census.
2484
2460
 
2485
- Returns `sessions` and `recorded` (the files read and the files present),
2486
- `skills` (the opened ones ranked by session count, each with the mark the
2487
- line draws for it, or None), and `never` (the shipped skills no session in
2488
- the window opened)."""
2461
+ Returns `sessions` and `recorded` (the sessions read and the sessions
2462
+ recorded), `skills` (the opened ones ranked by session count, each with the
2463
+ mark the line draws for it, or None), and `never` (the shipped skills no
2464
+ session in the window opened)."""
2489
2465
  catalog = shipped_skills()
2490
- d = os.path.join(cache_dir(), "sessions")
2491
- try:
2492
- files = sorted((os.path.join(d, n) for n in os.listdir(d)
2493
- if n.endswith(".jsonl")),
2494
- key=_mtime, reverse=True)
2495
- except OSError:
2496
- files = []
2466
+ window_kinds, recorded = tezgah_store.used_sessions(window)
2497
2467
  opened = {}
2498
- for path in files[:window]:
2499
- kinds = set()
2500
- try:
2501
- with open(path, encoding="utf-8") as fh:
2502
- for line in fh:
2503
- try:
2504
- kinds.add(json.loads(line)["kind"])
2505
- except (ValueError, KeyError, TypeError):
2506
- pass # a torn line, the way `used` reads one
2507
- except OSError:
2508
- continue
2468
+ for kinds in window_kinds:
2509
2469
  for name in catalog:
2510
2470
  mark = SKILL_MARKS.get(name)
2511
2471
  if (SKILL_KIND + name) in kinds or (mark and mark in kinds):
2512
2472
  opened[name] = opened.get(name, 0) + 1
2513
2473
  return {
2514
- "sessions": len(files[:window]),
2515
- "recorded": len(files),
2474
+ "sessions": len(window_kinds),
2475
+ "recorded": recorded,
2516
2476
  "skills": [{"name": name, "mark": SKILL_MARKS.get(name),
2517
2477
  "sessions": count}
2518
2478
  for name, count in sorted(opened.items(),
@@ -2801,7 +2761,7 @@ def health_segments(cwd, session_id=None, used_override=None, idx_override=None,
2801
2761
  segs = with_update_notice(version_segment())
2802
2762
  # Shown only when the prompt hook found the gate disarmed (gate_inactive):
2803
2763
  # first, because it says every other mark is not being enforced.
2804
- if session_id and os.path.exists(_gate_mark(session_id)):
2764
+ if session_id and gate_marked(session_id):
2805
2765
  segs.append({"key": "gate", "state": "off", "glyph": GLYPHS["off"],
2806
2766
  "text": "gate", "group": 0})
2807
2767
  # Shown when this session's start found tezgah's own hook entries changed
@@ -2819,7 +2779,7 @@ def health_segments(cwd, session_id=None, used_override=None, idx_override=None,
2819
2779
  "text": "drift", "group": 0})
2820
2780
  # A hook of this session could not import its core (tezgah_guard.
2821
2781
  # import_failed): it failed open, so what it guards did not run.
2822
- if session_id and os.path.exists(import_crash_mark(session_id)):
2782
+ if session_id and tezgah_store.doc("import_crash", import_crash_key(session_id)) is not None:
2823
2783
  segs.append({"key": "crash", "state": "off", "glyph": GLYPHS["off"],
2824
2784
  "text": "crash", "group": 0})
2825
2785
  for name, on, meas in flags:
@@ -98,6 +98,8 @@ import re
98
98
  import shlex
99
99
  import time
100
100
 
101
+ import tezgah_store
102
+
101
103
  from tezgah_integrity import (BASH_TOOLS, BEGAN_KIND, ENV_WORD, GIT_VALUE_OPTS,
102
104
  HEREDOC_RISK, HOOKS_KEY, PATCH_FILE,
103
105
  PLAIN_TAIL, SECRET_PREFIXED, STEP_KINDS,
@@ -587,25 +589,22 @@ def git_head(repo):
587
589
  return None
588
590
 
589
591
 
590
- def nudge_mark(session_id):
591
- """The once-per-session mark: sha1(id)[:16], the key opencode's oncePerSession writes."""
592
- return os.path.join(cache_dir(), "nudged", hashlib.sha1(
593
- (session_id or "nosession").encode("utf-8")).hexdigest()[:16])
592
+ def nudge_key(session_id):
593
+ """The once-per-session mark's key in the `nudged` store: sha1(id)[:16],
594
+ the key opencode's oncePerSession writes."""
595
+ return hashlib.sha1((session_id or "nosession").encode("utf-8")).hexdigest()[:16]
594
596
 
595
597
 
596
598
  def first_nudge(session_id):
597
599
  """Consume the once-per-session nudge. False when already spent/unwritable.
598
600
 
599
- The mark lands in cache_dir(), so a sandboxed host (dsh) still gets the
600
- one-shot nudge instead of the write failing open."""
601
- mark = nudge_mark(session_id)
602
- if os.path.exists(mark):
603
- return False
601
+ The mark lands in cache_dir()'s database, so a sandboxed host (dsh) still
602
+ gets the one-shot nudge instead of the write failing open. One INSERT, so
603
+ of two calls at once exactly one is first."""
604
604
  try:
605
- os.makedirs(os.path.dirname(mark), exist_ok=True)
606
- open(mark, "w", encoding="utf-8").close() # consume BEFORE denying: later greps pass
607
- return True
608
- except OSError:
605
+ # consume BEFORE denying: later greps pass
606
+ return tezgah_store.mark("nudged", nudge_key(session_id))
607
+ except tezgah_store.ERRORS:
609
608
  return False
610
609
 
611
610
 
@@ -1841,11 +1840,13 @@ HOOK_ENTRIES = frozenset(("projects-auto-init", "projects-pretooluse",
1841
1840
  HOOK_HOSTS = frozenset(("codex", "cursor", "omp"))
1842
1841
  # Programs that change a path named in their arguments. Every positional is a
1843
1842
  # target, except for the copiers, whose last one is (`cp switch /tmp` reads it).
1844
- # `sed`/`perl` count only with an in-place flag.
1843
+ # `sed`/`perl` count only with an in-place flag. `sqlite3` rewrites the cache
1844
+ # stores, rows of `tezgah.db`, as `rm` would remove the files they were; its
1845
+ # SQL argument names no protected path.
1845
1846
  CONTROL_PROGRAMS = frozenset(("touch", "rm", "rmdir", "unlink", "shred",
1846
1847
  "truncate", "chmod", "chown", "chgrp", "mv",
1847
1848
  "mkdir", "tee", "cp", "ln", "install", "rsync",
1848
- "dd", "sed", "perl"))
1849
+ "dd", "sed", "perl", "sqlite3"))
1849
1850
  CONTROL_COPIERS = frozenset(("cp", "mv", "ln", "install", "rsync"))
1850
1851
  # The programs that remove, disable or move a path. A directory that holds
1851
1852
  # protected state, a repository's husky hooks and the open plans (evidence) are
@@ -1867,13 +1868,15 @@ SHELL_NAMES = frozenset(("sh", "bash", "zsh", "dash", "ksh"))
1867
1868
  SHELL_KEYWORDS = frozenset(("if", "then", "elif", "else", "do", "while",
1868
1869
  "until", "!", "{", "time"))
1869
1870
  SHELL_OPS = re.compile(r"^[;&|()<>]+$")
1870
- # The cache subtrees whose rows the rules read: the evidence ledger, the session
1871
- # store, the switch baseline (tezgah_context.disarmed), the gate mark, and the
1872
- # marks the status line reads for a drifted hook entry
1873
- # (tezgah_attest.drift_mark) and a core that failed to import
1874
- # (tezgah_guard.import_crash_mark).
1875
- CONTROL_CACHE = ("evidence", "sessions", "switches", "gate-inactive",
1876
- "harness-drift", "import-crash", "workspace-index.json")
1871
+ # The cache subtrees whose rows the rules read: the cache database with its WAL
1872
+ # files (the evidence ledger, the used-kind marks, the switch baselines of
1873
+ # tezgah_context.disarmed, the gate mark, and the marks the status line reads
1874
+ # for a drifted hook entry and a core that failed to import), the evidence
1875
+ # JSONL files it imports, and the files the other stores it imports once
1876
+ # (tezgah_store.CACHE_LEGACY) had, until that import reads them.
1877
+ CONTROL_CACHE = ("evidence", "tezgah.db", "tezgah.db-wal", "tezgah.db-shm", "sessions",
1878
+ "switches", "gate-inactive", "harness-drift", "import-crash",
1879
+ "workspace-index.json")
1877
1880
  # Top-level keys tezgah writes into Claude's settings (bin/tezgah-setup
1878
1881
  # wire_claude_statusline, wire_claude_attribution), and the one that turns every
1879
1882
  # hook off at once; any other key or entry is tezgah's only when it names tezgah.
@@ -2830,7 +2833,7 @@ def _dry_decision(tool, inp, cwd, session_id, agent=None):
2830
2833
 
2831
2834
  g.update(note=lambda *a, **k: None, write_taint=lambda *a, **k: None,
2832
2835
  capture=None,
2833
- first_nudge=lambda sid: not os.path.exists(nudge_mark(sid)))
2836
+ first_nudge=lambda sid: not tezgah_store.marked("nudged", nudge_key(sid)))
2834
2837
  try:
2835
2838
  return decision(tool, inp, cwd, session_id, agent=agent)
2836
2839
  finally:
@@ -135,13 +135,11 @@ def attest_session(host, session_id, cwd):
135
135
  return tezgah_attest.run(host, session_id, cwd)
136
136
 
137
137
 
138
- def import_crash_mark(session_id):
139
- """The mark an import failure leaves for the status line, one per session.
140
- Built here from stdlib alone (the same `~/.cache/tezgah` as
141
- `tezgah_paths.CACHE`), because the module that failed may be tezgah_paths."""
138
+ def import_crash_key(session_id):
139
+ """The key of the mark an import failure leaves for the status line, one
140
+ per session: the `import_crash` store of the cache database."""
142
141
  import hashlib
143
- return os.path.join(os.path.expanduser("~"), ".cache", "tezgah", "import-crash",
144
- hashlib.sha256(str(session_id).encode()).hexdigest()[:16])
142
+ return hashlib.sha256(str(session_id).encode()).hexdigest()[:16]
145
143
 
146
144
 
147
145
  def import_failed(exc, code=0):
@@ -154,7 +152,7 @@ def import_failed(exc, code=0):
154
152
  the rename that crashed every omp hook). A hook fails open (`code` 0) like
155
153
  every other caught fault, so the dead core must be visible elsewhere: one
156
154
  stderr line always, a `crash` row when the ledger's own modules still
157
- import, and a mark the status line draws (`import_crash_mark`). A CLI a
155
+ import, and a mark the status line draws (`import_crash_key`). A CLI a
158
156
  person or a tool asks for a verdict (`tezgah-gate check`) passes a non-zero
159
157
  `code`: an empty answer from it would read as a pass."""
160
158
  detail = "import: %s: %s" % (type(exc).__name__, str(exc)[:120])
@@ -163,10 +161,10 @@ def import_failed(exc, code=0):
163
161
  % (os.path.basename(sys.argv[0] if sys.argv else "hook"), detail))
164
162
  session = _payload_session()
165
163
  try:
166
- mark = import_crash_mark(session)
167
- os.makedirs(os.path.dirname(mark), exist_ok=True)
168
- with open(mark, "w", encoding="utf-8") as fh:
169
- fh.write(detail + "\n")
164
+ # the store's own import may be what failed: then no mark, as no
165
+ # status line that imports it can draw one either
166
+ import tezgah_store
167
+ tezgah_store.put_doc("import_crash", import_crash_key(session), detail)
170
168
  except Exception:
171
169
  pass
172
170
  try: