@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +92 -1
  2. package/MANIFEST +10 -0
  3. package/bin/tezgah-agents +10 -1
  4. package/bin/tezgah-docs +1 -1
  5. package/bin/tezgah-doctor +11 -0
  6. package/bin/tezgah-research +31 -14
  7. package/bin/tezgah-setup +154 -29
  8. package/bin/tezgah-skill +59 -0
  9. package/bin/tezgah-skill.py +59 -0
  10. package/bin/tezgah-status +62 -1
  11. package/bin/tezgah-taste +504 -5
  12. package/bin/tezgah-triage +5 -3
  13. package/docs/README.md +1 -0
  14. package/docs/architecture.md +5 -2
  15. package/docs/citations-baseline.json +1 -1
  16. package/docs/clarity-baseline.json +9 -9
  17. package/docs/contract.md +68 -26
  18. package/docs/environment.md +6 -3
  19. package/docs/evidence.md +7 -0
  20. package/docs/gate.md +13 -7
  21. package/docs/glossary.md +1 -1
  22. package/docs/hosts.md +48 -1
  23. package/docs/index.json +34 -3
  24. package/docs/judge.md +75 -60
  25. package/docs/models.md +14 -8
  26. package/docs/operations.md +80 -25
  27. package/docs/orca.md +126 -0
  28. package/docs/research.md +38 -3
  29. package/docs/skills.md +74 -0
  30. package/docs/status-line.md +3 -1
  31. package/docs/testing.md +33 -21
  32. package/docs/vision.md +12 -8
  33. package/hooks/projects-posttooluse.py +7 -0
  34. package/hooks/projects-pretooluse.py +17 -5
  35. package/hooks/tezgah_agents.py +135 -13
  36. package/hooks/tezgah_components.py +3 -0
  37. package/hooks/tezgah_context.py +84 -22
  38. package/hooks/tezgah_gate.py +133 -17
  39. package/hooks/tezgah_integrity.py +60 -13
  40. package/hooks/tezgah_judge.py +288 -66
  41. package/hooks/tezgah_models.py +41 -5
  42. package/hooks/tezgah_orca.py +144 -0
  43. package/hooks/tezgah_paths.py +40 -6
  44. package/hooks/tezgah_policy.py +109 -35
  45. package/hooks/tezgah_research.py +104 -8
  46. package/hooks/tezgah_skill_pick.py +459 -8
  47. package/hooks/tezgah_stack.py +382 -0
  48. package/hooks/tezgah_stopspec.py +2 -2
  49. package/hooks/tezgah_taste.py +35 -9
  50. package/hooks/tezgah_taste_ledger.py +628 -0
  51. package/hosts/codex/hook.py +12 -1
  52. package/hosts/cursor/hook.py +12 -4
  53. package/hosts/omp/hook.py +18 -6
  54. package/hosts/omp/tezgah-hook.ts.in +20 -7
  55. package/hosts/opencode/plugins/tezgah.js +28 -15
  56. package/output-styles/tezgah.md +34 -19
  57. package/package.json +1 -1
  58. package/skills/i-have-adhd/SKILL.md +2 -2
  59. package/skills/research/SKILL.md +60 -17
  60. package/skills/tezgah-contract/SKILL.md +55 -13
@@ -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`). A `rules` above `RULES` is
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
- RULES = PROTOCOL_RULES
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
- sections = _rules(_state(base)) >= PROTOCOL_RULES
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
- default = {"code": 2}.get(kind, 3) if kind in VARIANT_KINDS else 0
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