@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +126 -1
  2. package/MANIFEST +11 -0
  3. package/README.md +6 -3
  4. package/bin/consult +13 -10
  5. package/bin/tezgah-docs +18 -16
  6. package/bin/tezgah-doctor +152 -0
  7. package/bin/tezgah-research +286 -12
  8. package/bin/tezgah-route +48 -1
  9. package/bin/tezgah-setup +27 -14
  10. package/bin/tezgah-task +771 -46
  11. package/bin/tezgah-taste +48 -10
  12. package/docs/architecture.md +7 -4
  13. package/docs/citations-baseline.json +1 -1
  14. package/docs/clarity-baseline.json +5 -5
  15. package/docs/contract.md +112 -25
  16. package/docs/evidence.md +201 -28
  17. package/docs/feature-audit.md +25 -4
  18. package/docs/gate.md +70 -33
  19. package/docs/glossary.md +4 -4
  20. package/docs/hosts.md +8 -8
  21. package/docs/index.json +30 -10
  22. package/docs/judge.md +9 -3
  23. package/docs/models.md +35 -2
  24. package/docs/operations.md +103 -3
  25. package/docs/research.md +392 -13
  26. package/docs/skills.md +24 -6
  27. package/docs/status-line.md +5 -5
  28. package/docs/testing.md +2 -2
  29. package/docs/vision.md +12 -4
  30. package/hooks/hooks.json +2 -2
  31. package/hooks/lang/en.json +323 -0
  32. package/hooks/lang/tr.json +344 -0
  33. package/hooks/projects-posttooluse.py +24 -5
  34. package/hooks/projects-pretooluse.py +9 -4
  35. package/hooks/projects-stop.py +28 -16
  36. package/hooks/tezgah_agents.py +29 -9
  37. package/hooks/tezgah_clarity.py +7 -3
  38. package/hooks/tezgah_components.py +2 -1
  39. package/hooks/tezgah_context.py +460 -390
  40. package/hooks/tezgah_gate.py +640 -104
  41. package/hooks/tezgah_integrity.py +1480 -197
  42. package/hooks/tezgah_models.py +337 -15
  43. package/hooks/tezgah_paths.py +103 -10
  44. package/hooks/tezgah_policy.py +286 -130
  45. package/hooks/tezgah_product.py +107 -0
  46. package/hooks/tezgah_rank.py +29 -27
  47. package/hooks/tezgah_replay.py +200 -30
  48. package/hooks/tezgah_research.py +1297 -12
  49. package/hooks/tezgah_skill_pick.py +58 -17
  50. package/hooks/tezgah_stopspec.py +7 -0
  51. package/hooks/tezgah_store.py +74 -27
  52. package/hooks/tezgah_task.py +917 -8
  53. package/hosts/codex/hook.py +9 -10
  54. package/hosts/cursor/hook.py +21 -14
  55. package/hosts/dsh/hooks.json +2 -2
  56. package/hosts/omp/hook.py +44 -23
  57. package/hosts/omp/tezgah-hook.ts.in +93 -7
  58. package/hosts/opencode/plugins/tezgah.js +119 -39
  59. package/output-styles/tezgah.md +76 -42
  60. package/package.json +1 -1
  61. package/skills/analyze-app/SKILL.md +57 -0
  62. package/skills/design-contract/EVALS.md +24 -1
  63. package/skills/design-contract/SKILL.md +17 -1
  64. package/skills/design-contract/data-views.md +86 -0
  65. package/skills/feature-audit/SKILL.md +32 -3
  66. package/skills/i-have-adhd/EVALS.md +22 -2
  67. package/skills/i-have-adhd/SKILL.md +12 -5
  68. package/skills/plan-add/EVALS.md +8 -8
  69. package/skills/plan-add/SKILL.md +22 -21
  70. package/skills/plan-sync/EVALS.md +2 -2
  71. package/skills/plan-sync/SKILL.md +14 -8
  72. package/skills/pm-frameworks/SKILL.md +6 -0
  73. package/skills/ponytail/EVALS.md +21 -2
  74. package/skills/ponytail/SKILL.md +4 -4
  75. package/skills/product-analysis/EVALS.md +23 -1
  76. package/skills/product-analysis/SKILL.md +50 -11
  77. package/skills/product-flow/EVALS.md +81 -0
  78. package/skills/product-flow/SKILL.md +91 -0
  79. package/skills/research/SKILL.md +99 -2
  80. package/skills/tezgah-contract/EVALS.md +52 -7
  81. 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, deduplicated on the file's realpath so a skill five hosts link
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
- path = os.path.realpath(os.path.join(root, name, "SKILL.md"))
362
- if not os.path.isfile(path):
363
- continue
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
- seen.add((path, start))
368
- out.append((name, path, title, start, end, text))
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: what a rebuild
494
- keys on. The file's own stat, not its directory's - installing, editing or
495
- removing one skill is exactly the change the cache must notice."""
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
- try:
504
- st = os.stat(os.path.join(root, name, "SKILL.md"))
505
- except OSError:
506
- continue
507
- sig.append([root, name, st.st_mtime_ns, st.st_size])
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
 
@@ -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
@@ -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 that
274
- differs from the recorded one). A file cut below what went in (rewritten in
275
- place) resumes at its end, so the rows appended after the cut still
276
- import."""
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.execute("SELECT bytes FROM imported WHERE name = ? AND dev = ? "
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
- return key, start, bool(found and found[0] != start)
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.execute("INSERT OR REPLACE INTO imported (name, dev, ino, bytes) "
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: kept as it was written, so a 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
- if not line.strip():
531
- return
532
- try:
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
- found = conn.execute("SELECT bytes FROM imported WHERE name = ? AND dev = ? AND ino = ?",
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 = ? "