@r1z4x/tezgah 1.0.0 → 1.2.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 +92 -1
- package/MANIFEST +10 -0
- package/bin/tezgah-agents +10 -1
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-doctor +11 -0
- package/bin/tezgah-research +31 -14
- package/bin/tezgah-setup +154 -29
- package/bin/tezgah-skill +59 -0
- package/bin/tezgah-skill.py +59 -0
- package/bin/tezgah-status +62 -1
- package/bin/tezgah-taste +504 -5
- package/bin/tezgah-triage +5 -3
- package/docs/README.md +1 -0
- package/docs/architecture.md +5 -2
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +9 -9
- package/docs/contract.md +68 -26
- package/docs/environment.md +6 -3
- package/docs/evidence.md +7 -0
- package/docs/gate.md +13 -7
- package/docs/glossary.md +1 -1
- package/docs/hosts.md +48 -1
- package/docs/index.json +34 -3
- package/docs/judge.md +75 -60
- package/docs/models.md +14 -8
- package/docs/operations.md +80 -25
- package/docs/orca.md +126 -0
- package/docs/research.md +38 -3
- package/docs/skills.md +74 -0
- package/docs/status-line.md +3 -1
- package/docs/testing.md +33 -21
- package/docs/vision.md +12 -8
- package/hooks/projects-posttooluse.py +7 -0
- package/hooks/projects-pretooluse.py +17 -5
- package/hooks/tezgah_agents.py +135 -13
- package/hooks/tezgah_components.py +3 -0
- package/hooks/tezgah_context.py +84 -22
- package/hooks/tezgah_gate.py +133 -17
- package/hooks/tezgah_integrity.py +60 -13
- package/hooks/tezgah_judge.py +288 -66
- package/hooks/tezgah_models.py +41 -5
- package/hooks/tezgah_orca.py +144 -0
- package/hooks/tezgah_paths.py +40 -6
- package/hooks/tezgah_policy.py +109 -35
- package/hooks/tezgah_research.py +104 -8
- package/hooks/tezgah_skill_pick.py +459 -8
- package/hooks/tezgah_stack.py +382 -0
- package/hooks/tezgah_stopspec.py +2 -2
- package/hooks/tezgah_taste.py +35 -9
- package/hooks/tezgah_taste_ledger.py +628 -0
- package/hosts/codex/hook.py +12 -1
- package/hosts/cursor/hook.py +12 -4
- package/hosts/omp/hook.py +18 -6
- package/hosts/omp/tezgah-hook.ts.in +20 -7
- package/hosts/opencode/plugins/tezgah.js +28 -15
- package/output-styles/tezgah.md +34 -19
- package/package.json +1 -1
- package/skills/i-have-adhd/SKILL.md +2 -2
- package/skills/research/SKILL.md +60 -17
- package/skills/tezgah-contract/SKILL.md +55 -13
package/hooks/tezgah_research.py
CHANGED
|
@@ -136,12 +136,20 @@ SOURCE_CLASSES = ("formal", "grey", "agent-report")
|
|
|
136
136
|
# those warns on it instead of refusing - its rows were written before the rule
|
|
137
137
|
# existed. Each later set is gated on its own number, so a bump never demotes an
|
|
138
138
|
# older set: 2 the standards rules, 3 the ask contract (keyed on the `ask` field),
|
|
139
|
-
# 4 the protocol sections (`_check_protocol_sections`)
|
|
139
|
+
# 4 the protocol sections (`_check_protocol_sections`), 5 the scope lock
|
|
140
|
+
# (`scope_problems`, `trace_problem`, the method-gated evaluation and the analysis
|
|
141
|
+
# that owes no variants). A `rules` above `RULES` is
|
|
140
142
|
# a line a later tezgah wrote, and every reader and writer here refuses it by name
|
|
141
143
|
# (`newer_rules`) rather than judging it by rules it does not know.
|
|
142
144
|
STANDARDS_RULES = 2
|
|
143
145
|
PROTOCOL_RULES = 4
|
|
144
|
-
|
|
146
|
+
SCOPE_RULES = 5
|
|
147
|
+
RULES = SCOPE_RULES
|
|
148
|
+
# How a rules-5 line answers its question. `qualitative` is a source or
|
|
149
|
+
# reference review and owes no metric; `measured` runs experiments and locks
|
|
150
|
+
# one. The question picks it, not the tooling: a qualitative ask answered with a
|
|
151
|
+
# metric table is the metric-first drift this field exists against.
|
|
152
|
+
METHODS = ("qualitative", "measured")
|
|
145
153
|
|
|
146
154
|
# What a line delivers, and how many compared variants that takes. `finding` is a
|
|
147
155
|
# line whose deliverable is its claims; the other four are an artifact a reader
|
|
@@ -925,6 +933,7 @@ def _check_state(base, errors, warnings, strict):
|
|
|
925
933
|
errors.append("state.json direction %r is not one of %s"
|
|
926
934
|
% (state.get("direction"), ", ".join(DIRECTIONS)))
|
|
927
935
|
_check_evaluation(state, errors, warnings, strict)
|
|
936
|
+
_check_scope(base, state, errors, warnings, strict)
|
|
928
937
|
_check_hypotheses(state, errors)
|
|
929
938
|
_check_sessions(state, errors)
|
|
930
939
|
_check_success(state, errors, warnings, strict)
|
|
@@ -1012,6 +1021,8 @@ def _check_evaluation(state, errors, warnings, strict):
|
|
|
1012
1021
|
is written where a session reads it (`skills/research/SKILL.md`,
|
|
1013
1022
|
`docs/research.md`); what it cannot catch is a tolerance its own author sets
|
|
1014
1023
|
so wide that every candidate passes it."""
|
|
1024
|
+
if _qualitative(state):
|
|
1025
|
+
return
|
|
1015
1026
|
phase = state.get("phase")
|
|
1016
1027
|
locked = phase != "bootstrap"
|
|
1017
1028
|
evaluation = state.get("evaluation")
|
|
@@ -1045,6 +1056,77 @@ def _check_evaluation(state, errors, warnings, strict):
|
|
|
1045
1056
|
"move: fill it, or omit the two-gate pair" % (counter,))
|
|
1046
1057
|
|
|
1047
1058
|
|
|
1059
|
+
# --- scope lock (rules 5) ------------------------------------------------------
|
|
1060
|
+
# A line answers the question the user asked, by the method that question takes.
|
|
1061
|
+
# `scope.in` lists what the answer covers (S1, S2, ... in order) and `scope.out`
|
|
1062
|
+
# what it deliberately leaves alone; every claim's `trace` names an in-scope item,
|
|
1063
|
+
# so a finding about something nobody asked is refused instead of padding the
|
|
1064
|
+
# report. Owner's report, 2026-10-07: research turned every question into
|
|
1065
|
+
# metrics, grew past the ask, and wandered into unrelated topics.
|
|
1066
|
+
|
|
1067
|
+
def _qualitative(state):
|
|
1068
|
+
return _rules(state) >= SCOPE_RULES and state.get("method") == "qualitative"
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _scope_items(state):
|
|
1072
|
+
scope = state.get("scope")
|
|
1073
|
+
items = scope.get("in") if isinstance(scope, dict) else None
|
|
1074
|
+
return [i for i in items if isinstance(i, str) and i.strip()] \
|
|
1075
|
+
if isinstance(items, list) else []
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def scope_problems(state):
|
|
1079
|
+
"""What a rules-5 line is missing to be locked to its question; [] otherwise."""
|
|
1080
|
+
if _rules(state) < SCOPE_RULES:
|
|
1081
|
+
return []
|
|
1082
|
+
problems = []
|
|
1083
|
+
scope = state.get("scope") if isinstance(state.get("scope"), dict) else {}
|
|
1084
|
+
for side, what in (("in", "what the answer covers"),
|
|
1085
|
+
("out", "what it deliberately leaves alone")):
|
|
1086
|
+
items = scope.get(side)
|
|
1087
|
+
if not isinstance(items, list) or not [
|
|
1088
|
+
i for i in items if isinstance(i, str) and i.strip()]:
|
|
1089
|
+
problems.append("state.json scope.%s names nothing: list %s, from the "
|
|
1090
|
+
"user's own question" % (side, what))
|
|
1091
|
+
if state.get("method") not in METHODS:
|
|
1092
|
+
problems.append("state.json method %r is not one of %s: the question picks "
|
|
1093
|
+
"it - a source or reference question is qualitative and "
|
|
1094
|
+
"owes no metric" % (state.get("method"), ", ".join(METHODS)))
|
|
1095
|
+
return problems
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def trace_problem(cid, claim, state):
|
|
1099
|
+
"""The refusal for a rules-5 claim that answers no in-scope item, or None.
|
|
1100
|
+
The write path (`claim_problems`) and the checker (`_check_claims`) both
|
|
1101
|
+
call it, so they cannot disagree about what is on topic."""
|
|
1102
|
+
if _rules(state) < SCOPE_RULES:
|
|
1103
|
+
return None
|
|
1104
|
+
items = _scope_items(state)
|
|
1105
|
+
trace = claim.get("trace")
|
|
1106
|
+
for entry in trace if isinstance(trace, list) else [trace]:
|
|
1107
|
+
if not isinstance(entry, str):
|
|
1108
|
+
continue
|
|
1109
|
+
entry = entry.strip()
|
|
1110
|
+
if any(entry in ("S%d" % n, item.strip()) for n, item in enumerate(items, 1)):
|
|
1111
|
+
return None
|
|
1112
|
+
return ("claim %s traces to no in-scope item: its `trace` names S<n> or the "
|
|
1113
|
+
"text of a state.json scope.in entry - a finding that answers nothing "
|
|
1114
|
+
"the user asked is dropped, not reported" % cid)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _check_scope(base, state, errors, warnings, strict):
|
|
1118
|
+
for problem in scope_problems(state):
|
|
1119
|
+
_soft(errors, warnings, strict or state.get("phase") != "bootstrap", problem)
|
|
1120
|
+
if _qualitative(state):
|
|
1121
|
+
ran = [h for h in _experiment_names(base)
|
|
1122
|
+
if os.path.isfile(os.path.join(base, "experiments", h, "results.jsonl"))]
|
|
1123
|
+
if ran:
|
|
1124
|
+
errors.append("state.json method is qualitative and experiment(s) %s "
|
|
1125
|
+
"recorded results: a measured question is `method: "
|
|
1126
|
+
"measured` with its evaluation locked first"
|
|
1127
|
+
% ", ".join(ran))
|
|
1128
|
+
|
|
1129
|
+
|
|
1048
1130
|
def _check_hypotheses(state, errors):
|
|
1049
1131
|
"""Every hypothesis states something.
|
|
1050
1132
|
|
|
@@ -1563,7 +1645,8 @@ def _check_claims(base, errors, warnings, roots=(), strict=False):
|
|
|
1563
1645
|
superseded.update(_supersedes(give)[0])
|
|
1564
1646
|
# the token-savings rule is a `PROTOCOL_RULES` rule, so an older line keeps
|
|
1565
1647
|
# the rules it was opened under
|
|
1566
|
-
|
|
1648
|
+
line_state = _state(base)
|
|
1649
|
+
sections = _rules(line_state) >= PROTOCOL_RULES
|
|
1567
1650
|
parsed = []
|
|
1568
1651
|
for n, raw in enumerate(rows, 1):
|
|
1569
1652
|
raw = raw.strip()
|
|
@@ -1599,6 +1682,9 @@ def _check_claims(base, errors, warnings, roots=(), strict=False):
|
|
|
1599
1682
|
errors.append("claim %s status %r is not one of %s"
|
|
1600
1683
|
% (cid, claim.get("status"), ", ".join(STATUSES)))
|
|
1601
1684
|
_check_claim_scope(cid, claim, base, repo, errors, warnings, strict)
|
|
1685
|
+
problem = trace_problem(cid, claim, line_state)
|
|
1686
|
+
if problem:
|
|
1687
|
+
errors.append(problem)
|
|
1602
1688
|
# A superseded claim is the historical row: the line has corrected it in
|
|
1603
1689
|
# the row that supersedes it, and that row is the one whose numbers are
|
|
1604
1690
|
# held to their proof here. Warning on the historical statement too would
|
|
@@ -1907,6 +1993,9 @@ def claim_problems(claim, base, repo, held=None):
|
|
|
1907
1993
|
known = _claim_ids(base) if held is None else held
|
|
1908
1994
|
problems.extend("claim %s supersedes %s, and no claim in this line carries "
|
|
1909
1995
|
"that id" % (cid, ref) for ref in ids if ref not in known)
|
|
1996
|
+
problem = trace_problem(cid, claim, _state(base))
|
|
1997
|
+
if problem:
|
|
1998
|
+
problems.append(problem)
|
|
1910
1999
|
# The scope rules the checker decides, so the write path cannot record a claim
|
|
1911
2000
|
# `check` would refuse. The warn class (a claim over fixture rows that declares
|
|
1912
2001
|
# nothing) is deliberately not repeated: a writer stricter than the checker
|
|
@@ -2848,7 +2937,7 @@ def _check_results_append_only(repo, base, state, errors, warnings, strict):
|
|
|
2848
2937
|
# 2006), and serial versions fixate on the first option (Dow et al. 2010); the
|
|
2849
2938
|
# layer used to hold exactly that, v3 -> v4 -> v5 of one plan never compared.
|
|
2850
2939
|
|
|
2851
|
-
def deliverable_problems(deliverable):
|
|
2940
|
+
def deliverable_problems(deliverable, rules=RULES):
|
|
2852
2941
|
"""(problems, variants needed) for `state.json`'s deliverable declaration."""
|
|
2853
2942
|
if not isinstance(deliverable, dict):
|
|
2854
2943
|
return (["state.json deliverable is a %s, not the object naming what the line "
|
|
@@ -2862,7 +2951,11 @@ def deliverable_problems(deliverable):
|
|
|
2862
2951
|
if ask is not None and (not isinstance(ask, list) or not all(
|
|
2863
2952
|
isinstance(item, str) and item.strip() for item in ask)):
|
|
2864
2953
|
problems.append("state.json deliverable ask is not a list of the ask's items")
|
|
2865
|
-
|
|
2954
|
+
# rules 5: an analysis answers its question once; variants are for a design,
|
|
2955
|
+
# plan or code the user will choose between, not a way to triple the answer
|
|
2956
|
+
kinds = VARIANT_KINDS if rules < SCOPE_RULES else tuple(
|
|
2957
|
+
k for k in VARIANT_KINDS if k != "analysis")
|
|
2958
|
+
default = {"code": 2}.get(kind, 3) if kind in kinds else 0
|
|
2866
2959
|
need = deliverable.get("min_variants", default)
|
|
2867
2960
|
if not isinstance(need, int) or isinstance(need, bool) or need < 0:
|
|
2868
2961
|
problems.append("state.json deliverable min_variants %r is not a count"
|
|
@@ -3250,7 +3343,7 @@ def _check_decisions(repo, slug, base, state, errors, warnings, strict, git):
|
|
|
3250
3343
|
"line delivers a design, plan, analysis or code that is owed "
|
|
3251
3344
|
"compared variants, or only findings")
|
|
3252
3345
|
else:
|
|
3253
|
-
problems, need = deliverable_problems(deliverable)
|
|
3346
|
+
problems, need = deliverable_problems(deliverable, _rules(state))
|
|
3254
3347
|
errors.extend(problems)
|
|
3255
3348
|
decisions = _decision_dirs(base)
|
|
3256
3349
|
if need > 1 and not decisions:
|
|
@@ -3910,7 +4003,7 @@ def _variants_owed(base, state):
|
|
|
3910
4003
|
deliverable = state.get("deliverable")
|
|
3911
4004
|
if not isinstance(deliverable, dict):
|
|
3912
4005
|
return None
|
|
3913
|
-
_problems, need = deliverable_problems(deliverable)
|
|
4006
|
+
_problems, need = deliverable_problems(deliverable, _rules(state))
|
|
3914
4007
|
if need < 2:
|
|
3915
4008
|
return None
|
|
3916
4009
|
for _name, ddir in _decision_dirs(base):
|
|
@@ -4688,7 +4781,8 @@ def verdict_problems(state, rows):
|
|
|
4688
4781
|
return problems
|
|
4689
4782
|
|
|
4690
4783
|
|
|
4691
|
-
def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFAULT_TIER
|
|
4784
|
+
def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFAULT_TIER,
|
|
4785
|
+
method="", scope_in=(), scope_out=()):
|
|
4692
4786
|
"""Scaffold a research line. Returns the paths created (never overwrites).
|
|
4693
4787
|
|
|
4694
4788
|
Refuses a slug `slugs()` cannot list - see `valid_slug` - so no caller can
|
|
@@ -4702,6 +4796,8 @@ def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFA
|
|
|
4702
4796
|
path = os.path.join(base, sub)
|
|
4703
4797
|
os.makedirs(path, exist_ok=True)
|
|
4704
4798
|
state = dict(STATE_TEMPLATE)
|
|
4799
|
+
state["method"] = method
|
|
4800
|
+
state["scope"] = {"in": list(scope_in), "out": list(scope_out)}
|
|
4705
4801
|
state["question"] = question
|
|
4706
4802
|
state["created"] = created
|
|
4707
4803
|
# every new line is opened under the newest rule set (`RULES` in the
|