@r1z4x/tezgah 1.5.1 → 1.7.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 +126 -1
- package/MANIFEST +11 -0
- package/README.md +6 -3
- package/bin/consult +13 -10
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-research +286 -12
- package/bin/tezgah-route +48 -1
- package/bin/tezgah-setup +27 -14
- package/bin/tezgah-task +771 -46
- package/bin/tezgah-taste +48 -10
- package/docs/architecture.md +7 -4
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +5 -5
- package/docs/contract.md +112 -25
- package/docs/evidence.md +201 -28
- package/docs/feature-audit.md +25 -4
- package/docs/gate.md +70 -33
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +30 -10
- package/docs/judge.md +9 -3
- package/docs/models.md +35 -2
- package/docs/operations.md +103 -3
- package/docs/research.md +392 -13
- package/docs/skills.md +24 -6
- package/docs/status-line.md +5 -5
- package/docs/testing.md +2 -2
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +29 -9
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +460 -390
- package/hooks/tezgah_gate.py +640 -104
- package/hooks/tezgah_integrity.py +1480 -197
- package/hooks/tezgah_models.py +337 -15
- package/hooks/tezgah_paths.py +103 -10
- package/hooks/tezgah_policy.py +286 -130
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1297 -12
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +917 -8
- package/hosts/codex/hook.py +9 -10
- package/hosts/cursor/hook.py +21 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +119 -39
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +22 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +14 -8
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +99 -2
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +204 -85
|
@@ -347,10 +347,40 @@ def skill_roots(extra=()):
|
|
|
347
347
|
return out
|
|
348
348
|
|
|
349
349
|
|
|
350
|
+
# One routing line of a skill's own INDEX.md: `name - what it makes - path`.
|
|
351
|
+
_INDEX_ROW = re.compile(r"^\S+ - .+ - (\S+/SKILL\.md)\s*$")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _entries(root, name):
|
|
355
|
+
"""[(skill, path)] for one skill directory: its SKILL.md, then every entry its
|
|
356
|
+
INDEX.md routes to. design-library keeps 39 entries under
|
|
357
|
+
`<plugin>/skills/<entry>/SKILL.md` and reaches them only through that index,
|
|
358
|
+
so a search over top-level files alone never saw a word of them. A nested
|
|
359
|
+
entry is addressed `<skill>/<path>`, the form `skill://` resolves."""
|
|
360
|
+
base = os.path.join(root, name)
|
|
361
|
+
out = [(name, os.path.join(base, "SKILL.md"))]
|
|
362
|
+
try:
|
|
363
|
+
with open(os.path.join(base, "INDEX.md"), encoding="utf-8") as fh:
|
|
364
|
+
lines = fh.read().splitlines()
|
|
365
|
+
except (OSError, UnicodeDecodeError):
|
|
366
|
+
return out
|
|
367
|
+
# A row is followed only inside the skill's own directory: an absolute path,
|
|
368
|
+
# a `..` or a symlink out would index a file the skill does not ship.
|
|
369
|
+
inside = os.path.realpath(base) + os.sep
|
|
370
|
+
for line in lines:
|
|
371
|
+
hit = _INDEX_ROW.match(line)
|
|
372
|
+
if not hit or os.path.isabs(hit.group(1)):
|
|
373
|
+
continue
|
|
374
|
+
path = os.path.join(base, hit.group(1))
|
|
375
|
+
if os.path.realpath(path).startswith(inside):
|
|
376
|
+
out.append(("%s/%s" % (name, hit.group(1)), path))
|
|
377
|
+
return out
|
|
378
|
+
|
|
379
|
+
|
|
350
380
|
def _rows(roots=None):
|
|
351
381
|
"""[(skill, path, title, start, end, text)] over every installed skill's
|
|
352
|
-
sections,
|
|
353
|
-
is one entry, not five."""
|
|
382
|
+
sections, and the entries a skill's INDEX.md routes to, deduplicated on the
|
|
383
|
+
file's realpath so a skill five hosts link is one entry, not five."""
|
|
354
384
|
out, seen = [], set()
|
|
355
385
|
for root in (skill_roots() if roots is None else roots):
|
|
356
386
|
try:
|
|
@@ -358,14 +388,23 @@ def _rows(roots=None):
|
|
|
358
388
|
except OSError:
|
|
359
389
|
continue
|
|
360
390
|
for name in names:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for title, start, end, text in sections(path):
|
|
365
|
-
if (path, start) in seen:
|
|
391
|
+
for skill, raw in _entries(root, name):
|
|
392
|
+
path = os.path.realpath(raw)
|
|
393
|
+
if not os.path.isfile(path):
|
|
366
394
|
continue
|
|
367
|
-
|
|
368
|
-
|
|
395
|
+
secs = sections(path)
|
|
396
|
+
if "/" in skill and secs:
|
|
397
|
+
# A routed entry is one row, the whole file: its short
|
|
398
|
+
# sections out-scored whole skills on two common words
|
|
399
|
+
# ("give", "thing") and silenced the right hint, while the
|
|
400
|
+
# entry as a whole still carries its topic's words.
|
|
401
|
+
secs = [(secs[0][0], secs[0][1], secs[-1][2],
|
|
402
|
+
"\n".join(s[3] for s in secs))]
|
|
403
|
+
for title, start, end, text in secs:
|
|
404
|
+
if (path, start) in seen:
|
|
405
|
+
continue
|
|
406
|
+
seen.add((path, start))
|
|
407
|
+
out.append((skill, path, title, start, end, text))
|
|
369
408
|
return out
|
|
370
409
|
|
|
371
410
|
|
|
@@ -490,9 +529,10 @@ def rank_postings(query, posts, lengths, k):
|
|
|
490
529
|
|
|
491
530
|
|
|
492
531
|
def _signature():
|
|
493
|
-
"""[[root, skill, mtime_ns, size]] per installed SKILL.md
|
|
494
|
-
keys on. The file's own stat, not its
|
|
495
|
-
removing one skill is exactly the
|
|
532
|
+
"""[[root, skill, mtime_ns, size]] per installed SKILL.md and per entry an
|
|
533
|
+
INDEX.md routes to: what a rebuild keys on. The file's own stat, not its
|
|
534
|
+
directory's - installing, editing or removing one skill is exactly the
|
|
535
|
+
change the cache must notice."""
|
|
496
536
|
sig = []
|
|
497
537
|
for root in skill_roots():
|
|
498
538
|
try:
|
|
@@ -500,11 +540,12 @@ def _signature():
|
|
|
500
540
|
except OSError:
|
|
501
541
|
continue
|
|
502
542
|
for name in names:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
543
|
+
for skill, path in _entries(root, name):
|
|
544
|
+
try:
|
|
545
|
+
st = os.stat(path)
|
|
546
|
+
except OSError:
|
|
547
|
+
continue
|
|
548
|
+
sig.append([root, skill, st.st_mtime_ns, st.st_size])
|
|
508
549
|
return sig
|
|
509
550
|
|
|
510
551
|
|
package/hooks/tezgah_stopspec.py
CHANGED
|
@@ -102,6 +102,13 @@ NOT_COUNTED = (
|
|
|
102
102
|
"constants over one trace, read off the reply, not a row"),
|
|
103
103
|
("evidence tampered", "a trace-validity guard in the selector, before the fold"),
|
|
104
104
|
)
|
|
105
|
+
# Plan 080 (2026-10-11), after the pre-registration and not a change to it: the
|
|
106
|
+
# admission ("doğrulanmadı") is applied by the selector to whichever fold it is
|
|
107
|
+
# given (`tezgah_integrity._admitted`: a check-repair class cleared after a check
|
|
108
|
+
# that failed since the newest change), and `residual work` is a selector verdict
|
|
109
|
+
# asked at every exit that would end the turn (`_residual_block`). Both reach the
|
|
110
|
+
# spec's verdict through the shared selector, so neither is a formula here.
|
|
111
|
+
|
|
105
112
|
# GO 2's live floor: shadow rows needed before the 99% bar is read. At 400 a
|
|
106
113
|
# 99% rate is 4 disagreements, so one more or less moves it by 0.25 pp.
|
|
107
114
|
MIN_LIVE_N = 400
|
package/hooks/tezgah_store.py
CHANGED
|
@@ -269,17 +269,58 @@ def _taste_put(table):
|
|
|
269
269
|
return put
|
|
270
270
|
|
|
271
271
|
|
|
272
|
+
def _recorded(conn, name, st):
|
|
273
|
+
"""(the bytes `imported` counts for the file `name` with stat `st`, the
|
|
274
|
+
device id of the row they were found under when it is not the current one)
|
|
275
|
+
or None. A volume remount can renumber the device of a file whose name and
|
|
276
|
+
inode are unchanged, and a store from before the fix imported such a file
|
|
277
|
+
again from offset 0 under the new id, so every row for (name, inode) is one
|
|
278
|
+
import stream. The longest is the file's offset under any id - this id's
|
|
279
|
+
own row included, when the id comes back to a shorter stream - so no byte
|
|
280
|
+
goes in twice."""
|
|
281
|
+
found = conn.execute("SELECT bytes, dev FROM imported WHERE name = ? AND ino = ? "
|
|
282
|
+
"ORDER BY bytes DESC, dev = ? DESC LIMIT 1",
|
|
283
|
+
(name, st.st_ino, st.st_dev)).fetchone()
|
|
284
|
+
if found is None:
|
|
285
|
+
return None
|
|
286
|
+
return found[0], None if found[1] == st.st_dev else found[1]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _record(conn, key, start, end, stale):
|
|
290
|
+
"""The file `key` in up to byte `end`, its import having resumed at `start`.
|
|
291
|
+
An import resumed from a row under another device id (`stale`) moves that
|
|
292
|
+
row to the current key, and a shorter stream's row already under the
|
|
293
|
+
current key takes the stale id: the two swap, so one row per import stream
|
|
294
|
+
stays, which is what the doctor's repair counts a double import by
|
|
295
|
+
(`tezgah-doctor --repair-evidence`). A file cut below what went in (a row
|
|
296
|
+
past `start`) no longer holds what its imports read: it goes on as one
|
|
297
|
+
stream, which resumes at its end and which the repair leaves alone."""
|
|
298
|
+
name, dev, ino = key
|
|
299
|
+
if conn.execute("SELECT 1 FROM imported WHERE name = ? AND ino = ? AND bytes > ?",
|
|
300
|
+
(name, ino, start)).fetchone():
|
|
301
|
+
conn.execute("DELETE FROM imported WHERE name = ? AND ino = ?", (name, ino))
|
|
302
|
+
elif stale is not None:
|
|
303
|
+
conn.execute("DELETE FROM imported WHERE name = ? AND dev = ? AND ino = ?",
|
|
304
|
+
(name, stale, ino))
|
|
305
|
+
conn.execute("UPDATE imported SET dev = ? WHERE name = ? AND dev = ? AND ino = ?",
|
|
306
|
+
(stale, name, dev, ino))
|
|
307
|
+
conn.execute("INSERT OR REPLACE INTO imported (name, dev, ino, bytes) "
|
|
308
|
+
"VALUES (?, ?, ?, ?)", key + (end,))
|
|
309
|
+
|
|
310
|
+
|
|
272
311
|
def _offset(conn, name, fh):
|
|
273
|
-
"""(the file's key, the byte offset its import resumes at, whether
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
312
|
+
"""(the file's key, the byte offset its import resumes at, whether the
|
|
313
|
+
recorded row must be rewritten, the device id of the row it resumes from
|
|
314
|
+
when that is not the current one (`_recorded`)). The row is rewritten when
|
|
315
|
+
its offset differs or it is under another id: a file cut below what went
|
|
316
|
+
in (rewritten in place) resumes at its end, so the rows appended after the
|
|
317
|
+
cut still import."""
|
|
277
318
|
st = os.fstat(fh.fileno())
|
|
278
319
|
key = (name, st.st_dev, st.st_ino)
|
|
279
|
-
found = conn
|
|
280
|
-
"AND ino = ?", key).fetchone()
|
|
320
|
+
found = _recorded(conn, name, st)
|
|
281
321
|
start = min(found[0], st.st_size) if found else 0
|
|
282
|
-
|
|
322
|
+
stale = found[1] if found else None
|
|
323
|
+
return key, start, bool(found and found[0] != start) or stale is not None, stale
|
|
283
324
|
|
|
284
325
|
|
|
285
326
|
def _import_rows(conn, name, src, st, put=None, keep=False):
|
|
@@ -303,14 +344,14 @@ def _import_rows(conn, name, src, st, put=None, keep=False):
|
|
|
303
344
|
_failed(conn, name, st)
|
|
304
345
|
return
|
|
305
346
|
with fh: # closing it releases the flock
|
|
306
|
-
key, start, cut = _offset(conn, name, fh)
|
|
347
|
+
key, start, cut, _stale = _offset(conn, name, fh)
|
|
307
348
|
fh.seek(start)
|
|
308
349
|
raw = fh.read()
|
|
309
350
|
if b"\n" in raw or cut:
|
|
310
351
|
with transaction(conn):
|
|
311
352
|
if not _locked(fh):
|
|
312
353
|
return # its writer holds it: the next open takes it
|
|
313
|
-
key, start, cut = _offset(conn, name, fh)
|
|
354
|
+
key, start, cut, stale = _offset(conn, name, fh)
|
|
314
355
|
fh.seek(start)
|
|
315
356
|
raw = fh.read()
|
|
316
357
|
whole = raw[:raw.rfind(b"\n") + 1]
|
|
@@ -318,8 +359,7 @@ def _import_rows(conn, name, src, st, put=None, keep=False):
|
|
|
318
359
|
put = put or _taste_put(name[:-len(".jsonl")])
|
|
319
360
|
for line in whole.split(b"\n"):
|
|
320
361
|
put(conn, line)
|
|
321
|
-
conn
|
|
322
|
-
"VALUES (?, ?, ?, ?)", key + (start + len(whole),))
|
|
362
|
+
_record(conn, key, start, start + len(whole), stale)
|
|
323
363
|
# aside only when every byte went in (no fragment past the last newline)
|
|
324
364
|
# and no writer appended past the read (the old writer appends unlocked
|
|
325
365
|
# once its own wait runs out), under the flock so no writer is inside
|
|
@@ -523,27 +563,36 @@ def _insert(conn, session, text):
|
|
|
523
563
|
ts if ts is not None and -2 ** 63 <= ts < 2 ** 63 else None, text))
|
|
524
564
|
|
|
525
565
|
|
|
566
|
+
def _line_text(line):
|
|
567
|
+
"""The row text one legacy ledger line (no newline) imports as, None for a
|
|
568
|
+
blank one: kept as it was written, so a line that does not parse stays a
|
|
569
|
+
line the reader names as damage."""
|
|
570
|
+
if not line.strip():
|
|
571
|
+
return None
|
|
572
|
+
try:
|
|
573
|
+
return line.decode("utf-8")
|
|
574
|
+
except UnicodeDecodeError:
|
|
575
|
+
# the marker `tezgah_integrity._parse` names as damage, hashed over the
|
|
576
|
+
# line with its newline as the file reader hashed it, so a damage row
|
|
577
|
+
# it wrote still names the same line
|
|
578
|
+
return "\x00not utf-8 %s" % hashlib.sha1(line + b"\n").hexdigest()[:12]
|
|
579
|
+
|
|
580
|
+
|
|
526
581
|
def _evidence_put(session):
|
|
527
|
-
"""The import of one legacy ledger line
|
|
528
|
-
that does not parse stays a line the reader names as damage."""
|
|
582
|
+
"""The import of one legacy ledger line (`_line_text`)."""
|
|
529
583
|
def put(conn, line):
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
text = line.decode("utf-8")
|
|
534
|
-
except UnicodeDecodeError:
|
|
535
|
-
# the marker `tezgah_integrity._parse` names as damage, hashed
|
|
536
|
-
# over the line with its newline as the file reader hashed it, so
|
|
537
|
-
# a damage row it wrote still names the same line
|
|
538
|
-
text = "\x00not utf-8 %s" % hashlib.sha1(line + b"\n").hexdigest()[:12]
|
|
539
|
-
_insert(conn, session, text)
|
|
584
|
+
text = _line_text(line)
|
|
585
|
+
if text is not None:
|
|
586
|
+
_insert(conn, session, text)
|
|
540
587
|
return put
|
|
541
588
|
|
|
542
589
|
|
|
543
590
|
def _sync(held, path):
|
|
544
591
|
"""Import the legacy JSONL at `path` past the bytes already in. Without a
|
|
545
592
|
file, or with nothing new in it, this is one stat (and one lookup the first
|
|
546
|
-
time a process sees it).
|
|
593
|
+
time a process sees it). A file whose offset sits under another device id
|
|
594
|
+
(`_recorded`) goes through the import even with nothing new, whose write
|
|
595
|
+
re-keys it."""
|
|
547
596
|
try:
|
|
548
597
|
st = os.stat(path)
|
|
549
598
|
except OSError:
|
|
@@ -553,9 +602,7 @@ def _sync(held, path):
|
|
|
553
602
|
if seen.get(path) == mark:
|
|
554
603
|
return
|
|
555
604
|
name = os.path.basename(path)
|
|
556
|
-
|
|
557
|
-
(name, st.st_dev, st.st_ino)).fetchone()
|
|
558
|
-
if found and found[0] == st.st_size:
|
|
605
|
+
if _recorded(conn, name, st) == (st.st_size, None):
|
|
559
606
|
seen[path] = mark
|
|
560
607
|
return
|
|
561
608
|
if conn.execute("SELECT 1 FROM import_failed WHERE name = ? AND mtime_ns = ? "
|