@r1z4x/tezgah 1.6.0 → 1.8.2
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 +140 -1
- package/MANIFEST +7 -0
- package/README.md +6 -3
- package/bin/tezgah-decisions +6 -14
- package/bin/tezgah-design +21 -5
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-lessons +4 -1
- package/bin/tezgah-research +249 -17
- package/bin/tezgah-route +70 -8
- package/bin/tezgah-setup +26 -13
- package/bin/tezgah-task +877 -60
- package/bin/tezgah-taste +56 -15
- package/docs/architecture.md +28 -11
- package/docs/clarity-baseline.json +2 -2
- package/docs/contract.md +108 -24
- package/docs/environment.md +3 -1
- package/docs/evidence.md +209 -28
- package/docs/feature-audit.md +1 -1
- package/docs/gate.md +78 -23
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +11 -5
- package/docs/judge.md +9 -3
- package/docs/models.md +75 -14
- package/docs/operations.md +133 -3
- package/docs/research.md +349 -15
- package/docs/skills.md +1 -1
- package/docs/status-line.md +5 -5
- package/docs/testing.md +20 -13
- 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-auto-init.py +29 -1
- 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 +53 -17
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_context.py +489 -438
- package/hooks/tezgah_gate.py +996 -80
- package/hooks/tezgah_integrity.py +1547 -197
- package/hooks/tezgah_models.py +427 -45
- package/hooks/tezgah_paths.py +147 -16
- package/hooks/tezgah_policy.py +261 -129
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1303 -14
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +1201 -40
- package/hooks/tezgah_taste.py +8 -4
- package/hosts/codex/hook.py +32 -13
- package/hosts/cursor/hook.py +32 -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 +273 -6
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/SKILL.md +27 -11
- package/skills/plan-add/render_table.py +8 -3
- package/skills/plan-sync/SKILL.md +5 -3
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/research/SKILL.md +89 -1
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +154 -59
package/hooks/tezgah_paths.py
CHANGED
|
@@ -70,7 +70,8 @@ OFF_DIRS = (CONFIG_DIR, os.path.join(HOME, ".claude"))
|
|
|
70
70
|
# the opencode plugin's own `off()` and the gate's control rule all answer from
|
|
71
71
|
# it, and tests/test_paths.py fails when any of them names a switch it lacks.
|
|
72
72
|
# CORE_SWITCHES are the sixteen the always-on contract names. ADR 007 leaves
|
|
73
|
-
# UNCLASSIFIED_SWITCHES open -
|
|
73
|
+
# UNCLASSIFIED_SWITCHES open - three `off()` names CORE does not list (the
|
|
74
|
+
# third, `slices-off`, stops the slice journal: hooks/tezgah_task.py) and two
|
|
74
75
|
# opt-in markers read with `armed()` - and until the owner classifies them the
|
|
75
76
|
# control rule protects them like the rest (bin/tezgah-docs carries the same
|
|
76
77
|
# set for its inventory check; the test pins the two equal).
|
|
@@ -78,7 +79,7 @@ CORE_SWITCHES = ("adhd-off", "consult-off", "docs-judge-off", "exec-mode.off",
|
|
|
78
79
|
"judge-off", "lang-off", "orchestrate-off", "ponytail-auto.off",
|
|
79
80
|
"pretooluse-off", "reminder-off", "research-off", "spec-off",
|
|
80
81
|
"task-off", "triage-off", "verify-off", "workspace-off")
|
|
81
|
-
UNCLASSIFIED_SWITCHES = ("agents-off", "skill-suggest-on", "taste-on",
|
|
82
|
+
UNCLASSIFIED_SWITCHES = ("agents-off", "skill-suggest-on", "slices-off", "taste-on",
|
|
82
83
|
"update-check-off")
|
|
83
84
|
SWITCHES = CORE_SWITCHES + UNCLASSIFIED_SWITCHES
|
|
84
85
|
REPO_MARKS = (".no-adhd", ".no-graph", ".no-lessons", ".no-plan-gate",
|
|
@@ -254,14 +255,15 @@ def root_for(path):
|
|
|
254
255
|
|
|
255
256
|
|
|
256
257
|
def _toplevel(real):
|
|
257
|
-
"""The nearest directory at or above `real` holding a `.git` entry, else
|
|
258
|
+
"""The nearest directory at or above `real` holding a `.git` entry, else
|
|
259
|
+
None - the project for a path inside its `.tezgah` (`project_top`)."""
|
|
258
260
|
cur = real
|
|
259
261
|
while not os.path.lexists(os.path.join(cur, ".git")):
|
|
260
262
|
parent = os.path.dirname(cur)
|
|
261
263
|
if parent == cur:
|
|
262
264
|
return None
|
|
263
265
|
cur = parent
|
|
264
|
-
return cur
|
|
266
|
+
return project_top(cur)
|
|
265
267
|
|
|
266
268
|
|
|
267
269
|
def linked_main(path):
|
|
@@ -737,17 +739,99 @@ def codex_bin():
|
|
|
737
739
|
return which_user(os.environ.get("TEZGAH_CODEX_BIN") or "codex")
|
|
738
740
|
|
|
739
741
|
|
|
740
|
-
# The
|
|
741
|
-
#
|
|
742
|
-
#
|
|
743
|
-
|
|
742
|
+
# The reply language (config.json `language`; an older config's `reply_lang`
|
|
743
|
+
# still reads and is never rewritten). The value is the switch: a code with a
|
|
744
|
+
# pack under hooks/lang/ asks for replies in that language, `any` for the
|
|
745
|
+
# user's own, and only a pack that ships reply `markers` (tr) is judged by the
|
|
746
|
+
# Stop language check. A config naming neither key reads as `tr`, the default
|
|
747
|
+
# every install made before the packs keeps; a first install writes the
|
|
748
|
+
# environment's language instead (`env_language`, bin/tezgah-setup).
|
|
749
|
+
LANG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lang")
|
|
750
|
+
_PACKS = []
|
|
744
751
|
|
|
745
752
|
|
|
746
|
-
def
|
|
747
|
-
"""The
|
|
753
|
+
def languages():
|
|
754
|
+
"""The values `language` takes: every shipped pack's code, then `any`."""
|
|
755
|
+
return tuple(packs()) + ("any",)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def language():
|
|
759
|
+
"""The configured reply language, `tr` when unset or not one of `languages()`."""
|
|
760
|
+
cfg = config()
|
|
761
|
+
for key in ("language", "reply_lang"):
|
|
762
|
+
value = cfg.get(key) if isinstance(cfg, dict) else None
|
|
763
|
+
if value in languages():
|
|
764
|
+
return value
|
|
765
|
+
return "tr"
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def spend_cap():
|
|
769
|
+
"""The standing spend cap in USD, `spend_cap` in config.json: a paid run at
|
|
770
|
+
or under it is a process parameter the agent picks, one over it is asked
|
|
771
|
+
and waits (the ask policy's never-default class). 0 when unset, negative
|
|
772
|
+
or not a number, so every spend is asked."""
|
|
748
773
|
cfg = config()
|
|
749
|
-
value = cfg.get("
|
|
750
|
-
|
|
774
|
+
value = cfg.get("spend_cap") if isinstance(cfg, dict) else None
|
|
775
|
+
ok = isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
776
|
+
return float(value) if ok and value > 0 else 0.0
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def env_language(environ=None):
|
|
780
|
+
"""The language a first install asks for: the first of LANGUAGE (its own
|
|
781
|
+
first `:` entry), LC_ALL, LC_MESSAGES and LANG that is set and not empty -
|
|
782
|
+
GNU gettext's order - cut to its language code (`tr_TR.UTF-8` -> `tr`), or
|
|
783
|
+
`en` when that code has no pack (`C`, `POSIX`, a language not shipped)."""
|
|
784
|
+
environ = os.environ if environ is None else environ
|
|
785
|
+
for key in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"):
|
|
786
|
+
value = (environ.get(key) or "").split(":")[0]
|
|
787
|
+
if value:
|
|
788
|
+
code = value.split(".")[0].split("@")[0].split("_")[0].lower()
|
|
789
|
+
return code if code in packs() else "en"
|
|
790
|
+
return "en"
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def packs():
|
|
794
|
+
"""{code: pack} for every hooks/lang/<code>.json, read once per process.
|
|
795
|
+
|
|
796
|
+
A pack is data: one language's half of every detection class (S5's
|
|
797
|
+
inventory) and the contract's own words (`say`), keyed alike in every pack
|
|
798
|
+
(tests/test_lang_packs.py). The detectors compile the union of all packs,
|
|
799
|
+
so a reply or prompt in any shipped language arms the same classes whatever
|
|
800
|
+
`language` says; a language is added by one file, never a regex edit."""
|
|
801
|
+
if not _PACKS:
|
|
802
|
+
found = {}
|
|
803
|
+
for name in sorted(os.listdir(LANG_DIR)):
|
|
804
|
+
if name.endswith(".json"):
|
|
805
|
+
with open(os.path.join(LANG_DIR, name), encoding="utf-8") as fh:
|
|
806
|
+
found[name[:-5]] = json.load(fh)
|
|
807
|
+
_PACKS.append(found)
|
|
808
|
+
return _PACKS[0]
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def pack_values(*path):
|
|
812
|
+
"""Each pack's value at `path` (nested keys), in pack order, skipping a
|
|
813
|
+
pack that lacks it."""
|
|
814
|
+
for pack in packs().values():
|
|
815
|
+
node = pack
|
|
816
|
+
for key in path:
|
|
817
|
+
node = node.get(key) if isinstance(node, dict) else None
|
|
818
|
+
if node is not None:
|
|
819
|
+
yield node
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def alts(*path):
|
|
823
|
+
"""The fragments every pack lists at `path`, joined into one alternation;
|
|
824
|
+
`(?!)`, which never matches, when no pack lists one."""
|
|
825
|
+
return "|".join(f for frags in pack_values(*path) for f in frags) or "(?!)"
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def say(code=None):
|
|
829
|
+
"""The contract's words for `code` (the configured language by default):
|
|
830
|
+
the English pack's `say` map with the code's own pack laid over it, so a
|
|
831
|
+
language with no pack, or `any`, speaks the canonical English tokens."""
|
|
832
|
+
words = dict(packs().get("en", {}).get("say", {}))
|
|
833
|
+
words.update(packs().get(code or language(), {}).get("say", {}))
|
|
834
|
+
return words
|
|
751
835
|
|
|
752
836
|
|
|
753
837
|
# Switch latching (plan 051, ADR 018). A hook process that knows its session
|
|
@@ -861,6 +945,16 @@ def pony_level():
|
|
|
861
945
|
return value if value in PONY_LEVELS else "full"
|
|
862
946
|
|
|
863
947
|
|
|
948
|
+
# The router's input-side tokens (input plus cache reads and writes) on its
|
|
949
|
+
# newest step, above which the per-turn reminder asks it to push heavy reads
|
|
950
|
+
# through a slice (tezgah_context.context_nudge). Measured on 12,576 omp
|
|
951
|
+
# main-thread steps (orchestrator-flow-audit S2 F1): median 396 k, p90 724 k. The
|
|
952
|
+
# median is the normal working state and would fire on every other turn, so the
|
|
953
|
+
# line sits just under p90: it marks the heaviest tenth of steps and stays rare
|
|
954
|
+
# enough to be read.
|
|
955
|
+
CONTEXT_NUDGE_TOKENS = 700000
|
|
956
|
+
|
|
957
|
+
|
|
864
958
|
def tool(name):
|
|
865
959
|
"""Stable installed path for a tezgah CLI, else the plugin's own copy, so
|
|
866
960
|
the text injected into a session names something that actually exists."""
|
|
@@ -897,6 +991,26 @@ def workspace(repo):
|
|
|
897
991
|
return os.path.join(repo, WORKSPACE)
|
|
898
992
|
|
|
899
993
|
|
|
994
|
+
def project_top(path):
|
|
995
|
+
"""`path`, or the project owning the workspace when the nearest `.git` at or
|
|
996
|
+
above `path` is a `.tezgah`'s own repository.
|
|
997
|
+
|
|
998
|
+
git and the `.git` walk both answer the workspace for a path inside it, and a
|
|
999
|
+
caller that joins `.tezgah` to that answer or sets the project up there wrote
|
|
1000
|
+
`<x>/.tezgah/.tezgah/plans/README.md`, `.tezgah/.cursor` and `.tezgah/.codegraph`.
|
|
1001
|
+
A repository cloned inside the workspace answers for itself. Every resolver
|
|
1002
|
+
(`_toplevel`, the hooks' and CLIs' `repo_root`) passes its answer through here."""
|
|
1003
|
+
cur = path
|
|
1004
|
+
while not os.path.lexists(os.path.join(cur, ".git")):
|
|
1005
|
+
parent = os.path.dirname(cur)
|
|
1006
|
+
if parent == cur:
|
|
1007
|
+
return path
|
|
1008
|
+
cur = parent
|
|
1009
|
+
if os.path.basename(cur) == WORKSPACE:
|
|
1010
|
+
return project_top(os.path.dirname(cur))
|
|
1011
|
+
return path
|
|
1012
|
+
|
|
1013
|
+
|
|
900
1014
|
# The workspace paths whose text is injected as a standing constraint: each is
|
|
901
1015
|
# checked for a symlink, because a clone can carry one pointing into its own tree.
|
|
902
1016
|
WORKSPACE_INJECTED = (".tezgah", os.path.join(".tezgah", "lessons.md"),
|
|
@@ -1252,15 +1366,32 @@ def seed_identity(repo, ws=None):
|
|
|
1252
1366
|
pass
|
|
1253
1367
|
|
|
1254
1368
|
|
|
1369
|
+
# What git says when another git process holds the private repository's index
|
|
1370
|
+
# or a ref: every worktree commits into the main checkout's one `.tezgah/.git`,
|
|
1371
|
+
# so two closes or a close and a research commit collide (plan 097: 10 of 19
|
|
1372
|
+
# concurrent rounds).
|
|
1373
|
+
_GIT_LOCKED = (".lock': File exists", "cannot lock ref")
|
|
1374
|
+
# The waits between tries, ~1.5 s in all: a commit there takes milliseconds,
|
|
1375
|
+
# and a lock still held after it is a crashed git, which the caller reports.
|
|
1376
|
+
_LOCK_WAITS = (0.1, 0.2, 0.4, 0.8)
|
|
1377
|
+
|
|
1378
|
+
|
|
1255
1379
|
def ws_git(repo, *args, **kw):
|
|
1256
1380
|
"""`git -C <repo>/.tezgah <args>` with tezgah's commit identity, as a
|
|
1257
|
-
CompletedProcess (text, captured unless `kw` says otherwise)
|
|
1258
|
-
|
|
1381
|
+
CompletedProcess (text, captured unless `kw` says otherwise), tried again
|
|
1382
|
+
while another git holds a lock there (`_GIT_LOCKED`). Raises OSError when
|
|
1383
|
+
git is not installed, like subprocess.run."""
|
|
1259
1384
|
import subprocess # deferred: see ensure_workspace
|
|
1385
|
+
import time
|
|
1260
1386
|
kw.setdefault("capture_output", True)
|
|
1261
1387
|
kw.setdefault("text", True)
|
|
1262
|
-
|
|
1263
|
-
|
|
1388
|
+
cmd = ["git", "-C", workspace(repo)] + list(WS_IDENTITY) + list(args)
|
|
1389
|
+
for wait in _LOCK_WAITS + (None,):
|
|
1390
|
+
done = subprocess.run(cmd, **kw)
|
|
1391
|
+
err = done.stderr if isinstance(done.stderr, str) else ""
|
|
1392
|
+
if not done.returncode or wait is None or not any(s in err for s in _GIT_LOCKED):
|
|
1393
|
+
return done
|
|
1394
|
+
time.sleep(wait)
|
|
1264
1395
|
|
|
1265
1396
|
|
|
1266
1397
|
def guarded_opener(drop_auth=False):
|