@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
@@ -22,6 +22,7 @@ import sys
22
22
  import time
23
23
 
24
24
  import tezgah_paths as tp
25
+ import tezgah_store
25
26
 
26
27
  REPO = os.environ.get("TEZGAH_REPO") or "r1z4x/tezgah"
27
28
  CHECK_EVERY = 24 * 3600
@@ -30,10 +31,6 @@ SEMVER = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$")
30
31
  ARROW = "\u2191" # ↑: East Asian width Ambiguous, one cell, never an emoji
31
32
 
32
33
 
33
- def cache_path():
34
- return os.path.join(tp.cache_dir(), "update.json")
35
-
36
-
37
34
  def endpoint():
38
35
  """Where the newest release is read, repointed by TEZGAH_UPDATE_URL (tests).
39
36
 
@@ -73,32 +70,15 @@ def disabled():
73
70
 
74
71
 
75
72
  def read_cache():
76
- try:
77
- with open(cache_path(), encoding="utf-8") as fh:
78
- data = json.load(fh)
79
- return data if isinstance(data, dict) else {}
80
- except Exception:
81
- return {}
73
+ """The `update_check` store's one document, `{checked, latest}`, or {}."""
74
+ data = tezgah_store.doc("update_check", "")
75
+ return data if isinstance(data, dict) else {}
82
76
 
83
77
 
84
78
  def write_cache(data):
85
- """Write the cache whole, owner-only, through a rename, so a redraw reading it
79
+ """Write the cache whole, in one statement, so a redraw reading it
86
80
  mid-write sees the old answer or the new one and never half of either."""
87
- path = cache_path()
88
- tmp = "%s.%d.tmp" % (path, os.getpid())
89
- try:
90
- os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
91
- fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
92
- with os.fdopen(fd, "w", encoding="utf-8") as fh:
93
- json.dump(data, fh)
94
- os.replace(tmp, path)
95
- return True
96
- except OSError:
97
- try:
98
- os.remove(tmp)
99
- except OSError:
100
- pass
101
- return False
81
+ return tezgah_store.put_doc("update_check", "", data)
102
82
 
103
83
 
104
84
  # Every 0.x ever published, retired by 1.0.0: npm served 0.17.0 through 0.32.0
@@ -158,6 +158,11 @@ def main():
158
158
  event = payload.get("hook_event_name") or "SessionStart"
159
159
  cwd = payload.get("cwd") or os.getcwd()
160
160
  session_id = payload.get("session_id")
161
+ # codex hands its hooks its own env, which carries no codex marker (only the
162
+ # shell tool gets CODEX_THREAD_ID), so the judge seam's session_cli() could
163
+ # not tell this is a codex session without it.
164
+ if session_id:
165
+ os.environ.setdefault("CODEX_THREAD_ID", str(session_id))
161
166
  safe(session_id, bind_session, session_id) # the switch latch (tezgah_paths.off)
162
167
  if event == "SessionStart":
163
168
  safe(session_id, attest_session, "codex", session_id, cwd)
@@ -52,7 +52,8 @@ try:
52
52
  from tezgah_integrity import ( # noqa: E402
53
53
  SUBAGENT_CHANNEL, bind_session, note, note_tool, ran_nothing, report_bytes,
54
54
  stop_reason)
55
- from tezgah_paths import cache_dir, off # noqa: E402
55
+ from tezgah_paths import off # noqa: E402
56
+ import tezgah_store # noqa: E402
56
57
  from tezgah_taste import write_note # noqa: E402
57
58
  from tezgah_untrusted import marks # noqa: E402
58
59
  except Exception as exc:
@@ -215,25 +216,21 @@ def first_time(session_id, tag):
215
216
  """True the first time a tag is seen for a session; fail-open when unwritable."""
216
217
  if not session_id:
217
218
  return True
218
- mark = os.path.join(cache_dir(), "reinforced", slug(str(session_id)), tag)
219
- if os.path.exists(mark):
220
- return False
221
219
  try:
222
- os.makedirs(os.path.dirname(mark), exist_ok=True)
223
- open(mark, "w", encoding="utf-8").close()
224
- except OSError:
225
- pass
226
- return True
220
+ return tezgah_store.mark("cursor_reinforced", "%s/%s" % (slug(str(session_id)), tag))
221
+ except tezgah_store.ERRORS:
222
+ return True
227
223
 
228
224
 
229
- def answer_path(session_id):
230
- """Where the last assistant text of a conversation is kept.
225
+ def answer_key(session_id):
226
+ """The key of the last assistant text of a conversation in the
227
+ `cursor_answer` store.
231
228
 
232
229
  Cursor's `stop` payload carries only `status`/`loop_count`, so the reply the
233
230
  Stop rule has to read arrives on the earlier `afterAgentResponse`, which
234
- documents `{"text": "<assistant final text>"}`. One file per conversation,
235
- overwritten."""
236
- return os.path.join(cache_dir(), "answer", slug(str(session_id or "nosession")))
231
+ documents `{"text": "<assistant final text>"}`. One document per
232
+ conversation, overwritten."""
233
+ return slug(str(session_id or "nosession"))
237
234
 
238
235
 
239
236
  # A long reply is kept by its two ends rather than by one slice: the Stop rule's
@@ -255,29 +252,20 @@ def remember_answer(session_id, text):
255
252
  count of what was dropped, then `ANSWER_TAIL` characters of its tail - so
256
253
  both the opener the Stop rule reads and the closing marker it must not lose
257
254
  survive the round trip. A reply with no `conversation_id` is not stored; a
258
- file that cannot be written is not fatal."""
255
+ store that cannot be written is not fatal."""
259
256
  if not session_id or not text:
260
257
  return
261
- path = answer_path(session_id)
262
- try:
263
- os.makedirs(os.path.dirname(path), exist_ok=True)
264
- text = str(text)
265
- dropped = len(text) - ANSWER_HEAD - ANSWER_TAIL
266
- if dropped > 0:
267
- text = text[:ANSWER_HEAD] + ANSWER_CUT % dropped + text[-ANSWER_TAIL:]
268
- with open(path, "w", encoding="utf-8") as fh:
269
- fh.write(text)
270
- except OSError:
271
- pass
258
+ text = str(text)
259
+ dropped = len(text) - ANSWER_HEAD - ANSWER_TAIL
260
+ if dropped > 0:
261
+ text = text[:ANSWER_HEAD] + ANSWER_CUT % dropped + text[-ANSWER_TAIL:]
262
+ tezgah_store.put_doc("cursor_answer", answer_key(session_id), text)
272
263
 
273
264
 
274
265
  def last_answer(session_id):
275
266
  """The reply remembered for this conversation, or ""."""
276
- try:
277
- with open(answer_path(session_id), encoding="utf-8") as fh:
278
- return fh.read()
279
- except OSError:
280
- return ""
267
+ text = tezgah_store.doc("cursor_answer", answer_key(session_id))
268
+ return text if isinstance(text, str) else ""
281
269
 
282
270
 
283
271
  def dispatch(payload):