@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 +33 -1
- package/MANIFEST +3 -0
- package/bin/tezgah-doctor +75 -18
- package/bin/tezgah-setup +6 -17
- package/bin/tezgah-taste +30 -29
- package/docs/architecture.md +4 -4
- package/docs/clarity-baseline.json +3 -3
- package/docs/contract.md +1 -1
- package/docs/environment.md +1 -0
- package/docs/evidence.md +67 -31
- package/docs/gate.md +21 -21
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/operations.md +48 -20
- package/docs/skills.md +1 -1
- package/docs/status-line.md +7 -7
- package/hooks/tezgah_attest.py +15 -22
- package/hooks/tezgah_context.py +44 -118
- package/hooks/tezgah_gate.py +26 -23
- package/hooks/tezgah_guard.py +9 -11
- package/hooks/tezgah_integrity.py +77 -178
- package/hooks/tezgah_judge.py +25 -43
- package/hooks/tezgah_lessons.py +18 -63
- package/hooks/tezgah_paths.py +18 -28
- package/hooks/tezgah_replay.py +31 -18
- package/hooks/tezgah_shapes.py +4 -4
- package/hooks/tezgah_skill_pick.py +6 -35
- package/hooks/tezgah_snapshot.py +25 -46
- package/hooks/tezgah_store.py +1268 -0
- package/hooks/tezgah_taste.py +20 -17
- package/hooks/tezgah_taste_ledger.py +52 -86
- package/hooks/tezgah_update.py +6 -26
- package/hosts/cursor/hook.py +19 -31
- package/hosts/opencode/plugins/tezgah.js +309 -108
- package/package.json +1 -1
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.
|
|
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,
|
|
38
|
-
|
|
39
|
-
debug.log) idle > 30
|
|
40
|
-
(--retention-days N);
|
|
41
|
-
own
|
|
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`.
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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,
|
|
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(
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
39
|
-
repository's omp and Claude transcripts - becomes one structured signal
|
|
40
|
-
code: the prompt, the files written, the agent's edits and the user's
|
|
41
|
-
between two agent writes. One typed decision per signal, required from
|
|
42
|
-
(`only=("typesafe",)`): preference, defect or none; the category; the
|
|
43
|
-
how it relates to each learning held. A decision any other provider
|
|
44
|
-
recorded `unverified` and never applied. A defect goes to `defects
|
|
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
|
|
470
|
-
import
|
|
470
|
+
"""{absolute path: [(ts, id, after)]} over the snapshot index, oldest first."""
|
|
471
|
+
import tezgah_store
|
|
471
472
|
out = {}
|
|
472
|
-
for
|
|
473
|
-
|
|
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),
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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(
|
|
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
|
|
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)
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
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 |
|
|
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":
|
|
34
|
-
"passive":
|
|
33
|
+
"long_sentences": 108,
|
|
34
|
+
"passive": 55,
|
|
35
35
|
"replace_words": 1,
|
|
36
|
-
"semicolons":
|
|
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-
|
|
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
|
package/docs/environment.md
CHANGED
|
@@ -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
|
|