@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/bin/tezgah-lessons
CHANGED
|
@@ -112,7 +112,10 @@ def merged(lines, groups):
|
|
|
112
112
|
|
|
113
113
|
def main(argv):
|
|
114
114
|
ap = argparse.ArgumentParser(prog="tezgah-lessons", description=__doc__.split("\n")[0])
|
|
115
|
-
|
|
115
|
+
# a linked worktree's lessons are the main checkout's (plan 097)
|
|
116
|
+
from tezgah_paths import linked_main
|
|
117
|
+
here = tc.repo_root(os.getcwd())
|
|
118
|
+
ap.add_argument("--root", default=linked_main(here) or here)
|
|
116
119
|
ap.add_argument("--fixture")
|
|
117
120
|
args = ap.parse_args(argv)
|
|
118
121
|
lines = tezgah_lessons.lines(args.root)
|
package/bin/tezgah-research
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
"""tezgah-research - the research workspace CLI.
|
|
3
3
|
|
|
4
4
|
tezgah-research init <slug> --ask "<the user's words>" [--question "..."]
|
|
5
|
-
[--method qualitative|measured] [--in "<covered>"]...
|
|
6
|
-
[--allow-open "<why>"] [--supersedes <slug>]
|
|
5
|
+
[--method qualitative|measured|exploratory] [--in "<covered>"]...
|
|
6
|
+
[--out "<left alone>"]... [--allow-open "<why>"] [--supersedes <slug>]
|
|
7
|
+
[--arena "<problem>" --budget "<budget>" [--evaluator <path> --metric "<m>"]]
|
|
8
|
+
scaffold a line
|
|
7
9
|
tezgah-research commit <slug> "<message>" commit the line in .tezgah's own repo
|
|
8
10
|
tezgah-research check [<slug>] [--json] [--strict] [--orx] [--all-lines] [-- <slug>]
|
|
9
11
|
the discipline checks
|
|
@@ -27,6 +29,14 @@
|
|
|
27
29
|
bring lines from another checkout, with history
|
|
28
30
|
tezgah-research next --topic <t> [--flow metric-dashboard|feature] [--json]
|
|
29
31
|
the product-flow steps of a topic and the next one
|
|
32
|
+
tezgah-research idea <slug> record one idea of an exploratory line from stdin
|
|
33
|
+
tezgah-research stage <slug> <idea> <stage> [--evidence "<ref or JSON>"] [--reason "<why>"]
|
|
34
|
+
move an idea to its next stage
|
|
35
|
+
tezgah-research match <slug> record one judged tournament match from stdin
|
|
36
|
+
tezgah-research board <slug> [--json] each idea's stage, score and next action
|
|
37
|
+
tezgah-research dedup <slug> [--threshold <(0,1]>] [--json]
|
|
38
|
+
cluster the proposed ideas, write nothing
|
|
39
|
+
tezgah-research yield [--json] [--all-lines] how far ideas got, per operator and family
|
|
30
40
|
|
|
31
41
|
A research line lives at <repo>/.tezgah/research/<slug>/ and holds the state an
|
|
32
42
|
auditable investigation needs: the question, the decision log, the findings, the
|
|
@@ -95,10 +105,18 @@ without it is refused. `compare` appends one cell to
|
|
|
95
105
|
`check` prints the same list of open lines after the per-line report, and so does
|
|
96
106
|
`status`, so neither state has to be discovered by being refused.
|
|
97
107
|
|
|
108
|
+
`init --method exploratory` (rules 7) opens a campaign instead of a question:
|
|
109
|
+
`--arena` and `--budget` (and, for a measurable arena, `--evaluator` with its
|
|
110
|
+
`--metric`, whose sha256 `init` records) are the lock committed before the
|
|
111
|
+
first idea. `idea`, `stage` and `match` append the campaign's rows under the
|
|
112
|
+
lock, each judged by the one function `check` applies to it; a `prior-art`
|
|
113
|
+
stage whose verdict is `known` takes `--reason` and is killed in the same
|
|
114
|
+
append. `board`, `dedup` and `yield` are reports: they write nothing and exit 0.
|
|
115
|
+
|
|
98
116
|
Exit codes: 0 clean, 1 problems found - a broken line for `check`, a refused
|
|
99
|
-
claim for `claim`, a refused row for `predict`,
|
|
100
|
-
one reason per problem, or a new line refused for
|
|
101
|
-
misuse.
|
|
117
|
+
claim for `claim`, a refused row for `predict`, `idea`, `stage` or `match`, which
|
|
118
|
+
writes nothing and prints one reason per problem, or a new line refused for
|
|
119
|
+
`init` over unfinished ones - 2 misuse.
|
|
102
120
|
"""
|
|
103
121
|
import json
|
|
104
122
|
import os
|
|
@@ -113,20 +131,27 @@ import tezgah_product as tp_product # noqa: E402
|
|
|
113
131
|
import tezgah_research as tr # noqa: E402
|
|
114
132
|
|
|
115
133
|
|
|
116
|
-
def
|
|
134
|
+
def checkout_root(path):
|
|
135
|
+
"""The checkout `path` is in: git's top level, the project around it inside
|
|
136
|
+
`.tezgah`, else the directory itself."""
|
|
117
137
|
try:
|
|
118
138
|
out = subprocess.run(["git", "-C", path, "rev-parse", "--show-toplevel"],
|
|
119
139
|
capture_output=True, text=True)
|
|
120
140
|
if out.returncode == 0 and out.stdout.strip():
|
|
121
|
-
|
|
122
|
-
# inside `.tezgah` git answers with the private repository; the line
|
|
123
|
-
# belongs to the project around it
|
|
124
|
-
return os.path.dirname(top) if os.path.basename(top) == tp.WORKSPACE else top
|
|
141
|
+
return tp.project_top(out.stdout.strip())
|
|
125
142
|
except OSError:
|
|
126
143
|
pass
|
|
127
144
|
return os.path.realpath(path)
|
|
128
145
|
|
|
129
146
|
|
|
147
|
+
def repo_root(path):
|
|
148
|
+
"""The checkout whose `.tezgah` holds the lines: the main checkout, from a
|
|
149
|
+
linked worktree too, as for bin/tezgah-task - a line written into a
|
|
150
|
+
worktree's own `.tezgah` went with `git worktree remove` (plan 097)."""
|
|
151
|
+
top = checkout_root(path)
|
|
152
|
+
return tp.linked_main(top) or top
|
|
153
|
+
|
|
154
|
+
|
|
130
155
|
def misuse(message):
|
|
131
156
|
"""Misuse exits 2, so a caller can tell it from a line that fails the checks."""
|
|
132
157
|
print("tezgah-research: %s" % message, file=sys.stderr)
|
|
@@ -145,6 +170,7 @@ def cmd_init(args):
|
|
|
145
170
|
"and dashes" % args[0])
|
|
146
171
|
question, allow, supersedes = "", None, None
|
|
147
172
|
ask, tier, method, scope_in, scope_out = "", str(tr.DEFAULT_TIER), "", [], []
|
|
173
|
+
arena = {}
|
|
148
174
|
rest = args[1:]
|
|
149
175
|
while rest:
|
|
150
176
|
flag = rest.pop(0)
|
|
@@ -164,11 +190,34 @@ def cmd_init(args):
|
|
|
164
190
|
allow = rest.pop(0)
|
|
165
191
|
elif flag == "--supersedes" and rest:
|
|
166
192
|
supersedes = rest.pop(0)
|
|
193
|
+
elif flag in ("--arena", "--budget", "--evaluator", "--metric") and rest \
|
|
194
|
+
and flag not in arena:
|
|
195
|
+
arena[flag] = rest.pop(0)
|
|
167
196
|
else:
|
|
168
197
|
return misuse('init <slug> --ask "<the user\'s words>" [--tier quick|study|program]'
|
|
169
|
-
' [--method qualitative|measured] [--in "<covered>"]...'
|
|
198
|
+
' [--method qualitative|measured|exploratory] [--in "<covered>"]...'
|
|
170
199
|
' [--out "<left alone>"]... [--question "..."]'
|
|
171
|
-
' [--allow-open "<reason>"] [--supersedes <slug>]'
|
|
200
|
+
' [--allow-open "<reason>"] [--supersedes <slug>]'
|
|
201
|
+
' [--arena "<problem>" --budget "<budget>"'
|
|
202
|
+
' [--evaluator <path> --metric "<metric>"]]')
|
|
203
|
+
if method == "exploratory":
|
|
204
|
+
# The arena is the lock an exploratory line commits before its first idea,
|
|
205
|
+
# so a line without one is a call that cannot work, not a line to repair.
|
|
206
|
+
if not all(arena.get(f, "").strip() for f in ("--arena", "--budget")):
|
|
207
|
+
return misuse('--method exploratory needs --arena "<the problem, verbatim>" '
|
|
208
|
+
'and --budget "<ideas, pilots, full runs, wall time>"')
|
|
209
|
+
if bool(arena.get("--evaluator", "").strip()) != bool(
|
|
210
|
+
arena.get("--metric", "").strip()):
|
|
211
|
+
return misuse("--evaluator and --metric go together: the file that scores a "
|
|
212
|
+
"candidate and the number it returns")
|
|
213
|
+
evaluator = arena.get("--evaluator", "").strip()
|
|
214
|
+
if evaluator and (os.path.isabs(evaluator) or ".." in evaluator.split("/")
|
|
215
|
+
or not os.path.isfile(os.path.join(repo_root(os.getcwd()),
|
|
216
|
+
evaluator))):
|
|
217
|
+
return misuse("--evaluator %r is not a file under the repository" % evaluator)
|
|
218
|
+
elif arena:
|
|
219
|
+
return misuse("--arena, --budget, --evaluator and --metric belong to "
|
|
220
|
+
"--method exploratory")
|
|
172
221
|
problems = tr.ask_problems(ask, tier)
|
|
173
222
|
if problems:
|
|
174
223
|
for problem in problems:
|
|
@@ -211,9 +260,14 @@ def cmd_init(args):
|
|
|
211
260
|
"init %s --supersedes %s" % (", ".join(twins), slug, twins[0]))
|
|
212
261
|
return 1
|
|
213
262
|
created = time.strftime("%Y-%m-%d")
|
|
263
|
+
record = None
|
|
264
|
+
if method == "exploratory":
|
|
265
|
+
record = tr.arena_record(repo, arena["--arena"], arena["--budget"],
|
|
266
|
+
arena.get("--evaluator", "").strip(),
|
|
267
|
+
arena.get("--metric", "").strip())
|
|
214
268
|
made = tr.init(repo, slug, question=question, created=created,
|
|
215
269
|
supersedes=supersedes, ask=ask, tier=tier, method=method,
|
|
216
|
-
scope_in=scope_in, scope_out=scope_out)
|
|
270
|
+
scope_in=scope_in, scope_out=scope_out, arena=record)
|
|
217
271
|
if allow is not None:
|
|
218
272
|
problem = tr.note_open(repo, slug, allow, created)
|
|
219
273
|
if problem:
|
|
@@ -224,7 +278,14 @@ def cmd_init(args):
|
|
|
224
278
|
print(" wrote %s" % path)
|
|
225
279
|
for problem in tr.scope_problems(tr.line_state(repo, slug)):
|
|
226
280
|
print(" owed: %s" % problem)
|
|
227
|
-
if method == "
|
|
281
|
+
if method == "exploratory":
|
|
282
|
+
print("\nnext: commit the arena now, before any idea - `tezgah-research commit "
|
|
283
|
+
"%s \"arena\"`; an idea committed with it or before it is refused. Then "
|
|
284
|
+
"record ideas (`tezgah-research idea %s`, one JSON row on stdin, each "
|
|
285
|
+
"naming its operator, generator and kill result), commit them, and "
|
|
286
|
+
"follow `tezgah-research board %s`: dedup, prior art, matches, pilots, a "
|
|
287
|
+
"full run, a confirmation on unseen data." % (slug, slug, slug))
|
|
288
|
+
elif method == "qualitative":
|
|
228
289
|
print("\nnext: read the primary sources for each state.json scope.in item "
|
|
229
290
|
"(`orx discover`, `orx paper`), save each to literature/ as you read "
|
|
230
291
|
"it, and record every claim with a `trace` naming the S<n> it answers "
|
|
@@ -301,7 +362,7 @@ def cmd_check(args):
|
|
|
301
362
|
report = {}
|
|
302
363
|
if known:
|
|
303
364
|
report = tr.check(repo, rest[0] if rest else None, strict=strict,
|
|
304
|
-
all_lines=all_lines)
|
|
365
|
+
all_lines=all_lines, code=checkout_root(os.getcwd()))
|
|
305
366
|
for slug, row in report.items():
|
|
306
367
|
for err in row["errors"]:
|
|
307
368
|
problems += 1
|
|
@@ -589,6 +650,172 @@ def cmd_compare(args):
|
|
|
589
650
|
return 0
|
|
590
651
|
|
|
591
652
|
|
|
653
|
+
def _campaign_slug(args, usage, count=1):
|
|
654
|
+
"""(repo, slug, None) for a campaign command's line, or (None, None, exit code)."""
|
|
655
|
+
if len(args) < count:
|
|
656
|
+
return None, None, misuse(usage)
|
|
657
|
+
repo = repo_root(os.getcwd())
|
|
658
|
+
known = tr.slugs(repo)
|
|
659
|
+
if args[0] not in known:
|
|
660
|
+
return None, None, misuse("no such research line (have: %s)"
|
|
661
|
+
% (", ".join(known) or "none"))
|
|
662
|
+
return repo, args[0], None
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _stdin_row(what):
|
|
666
|
+
"""(row, None) for the one JSON object on stdin, or (None, exit code)."""
|
|
667
|
+
try:
|
|
668
|
+
body = sys.stdin.buffer.read().decode("utf-8")
|
|
669
|
+
row = json.loads(body)
|
|
670
|
+
except (UnicodeDecodeError, ValueError):
|
|
671
|
+
return None, misuse("%s needs one JSON object on stdin" % what)
|
|
672
|
+
if not isinstance(row, dict):
|
|
673
|
+
return None, misuse("%s needs one JSON object on stdin" % what)
|
|
674
|
+
return row, None
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _refused(slug, problems):
|
|
678
|
+
for problem in problems:
|
|
679
|
+
print("FAIL %s: %s" % (slug, problem))
|
|
680
|
+
return 1 if problems else 0
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def cmd_idea(args):
|
|
684
|
+
"""Record one idea of an exploratory line from stdin (`append_idea`)."""
|
|
685
|
+
repo, slug, code = _campaign_slug(args, "idea <slug>")
|
|
686
|
+
if code is not None or len(args) != 1:
|
|
687
|
+
return code if code is not None else misuse("idea <slug>")
|
|
688
|
+
row, code = _stdin_row("idea")
|
|
689
|
+
if code is not None:
|
|
690
|
+
return code
|
|
691
|
+
try:
|
|
692
|
+
iid, problems = tr.append_idea(repo, slug, row)
|
|
693
|
+
except OSError as exc:
|
|
694
|
+
iid, problems = row.get("id"), ["cannot append to ideas.jsonl: %s" % exc]
|
|
695
|
+
if _refused(slug, problems):
|
|
696
|
+
return 1
|
|
697
|
+
print("idea %s recorded" % iid)
|
|
698
|
+
return 0
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def cmd_stage(args):
|
|
702
|
+
"""Move one idea to a stage, with the evidence that stage takes (a path or a
|
|
703
|
+
JSON object) and the reason a kill - or a known prior art - owes."""
|
|
704
|
+
usage = ('stage <slug> <idea> <stage> [--evidence "<ref or JSON>"] '
|
|
705
|
+
'[--reason "<why>"]')
|
|
706
|
+
repo, slug, code = _campaign_slug(args, usage, 3)
|
|
707
|
+
if code is not None:
|
|
708
|
+
return code
|
|
709
|
+
row = {"idea": args[1], "stage": args[2]}
|
|
710
|
+
reason, rest = None, args[3:]
|
|
711
|
+
while rest:
|
|
712
|
+
flag = rest.pop(0)
|
|
713
|
+
if flag == "--evidence" and rest and "evidence" not in row:
|
|
714
|
+
text = rest.pop(0)
|
|
715
|
+
try:
|
|
716
|
+
row["evidence"] = json.loads(text) if text.lstrip()[:1] in "{[" \
|
|
717
|
+
and text.strip() else text
|
|
718
|
+
except ValueError:
|
|
719
|
+
return misuse("--evidence starts like JSON and does not parse")
|
|
720
|
+
elif flag == "--reason" and rest and reason is None:
|
|
721
|
+
reason = rest.pop(0)
|
|
722
|
+
else:
|
|
723
|
+
return misuse(usage)
|
|
724
|
+
row["date"] = time.strftime("%Y-%m-%d")
|
|
725
|
+
kill_reason = None
|
|
726
|
+
if row["stage"] == "killed":
|
|
727
|
+
row["reason"] = reason
|
|
728
|
+
else:
|
|
729
|
+
kill_reason = reason
|
|
730
|
+
try:
|
|
731
|
+
problems = tr.append_stage(repo, slug, {k: v for k, v in row.items()
|
|
732
|
+
if v is not None}, kill_reason)
|
|
733
|
+
except OSError as exc:
|
|
734
|
+
problems = ["cannot append to stages.jsonl: %s" % exc]
|
|
735
|
+
if _refused(slug, problems):
|
|
736
|
+
return 1
|
|
737
|
+
killed = row["stage"] == "prior-art" and kill_reason \
|
|
738
|
+
and isinstance(row.get("evidence"), dict) \
|
|
739
|
+
and row["evidence"].get("verdict") == "known"
|
|
740
|
+
print("%s: %s recorded%s" % (row["idea"], row["stage"],
|
|
741
|
+
" and killed (%s)" % kill_reason if killed else ""))
|
|
742
|
+
return 0
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def cmd_match(args):
|
|
746
|
+
"""Record one judged tournament match from stdin (`append_match`)."""
|
|
747
|
+
repo, slug, code = _campaign_slug(args, "match <slug>")
|
|
748
|
+
if code is not None or len(args) != 1:
|
|
749
|
+
return code if code is not None else misuse("match <slug>")
|
|
750
|
+
row, code = _stdin_row("match")
|
|
751
|
+
if code is not None:
|
|
752
|
+
return code
|
|
753
|
+
try:
|
|
754
|
+
problems = tr.append_match(repo, slug, row)
|
|
755
|
+
except OSError as exc:
|
|
756
|
+
problems = ["cannot append to tournament.jsonl: %s" % exc]
|
|
757
|
+
if _refused(slug, problems):
|
|
758
|
+
return 1
|
|
759
|
+
print("match %s v %s recorded" % (row.get("a"), row.get("b")))
|
|
760
|
+
return 0
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def cmd_board(args):
|
|
764
|
+
"""The campaign's board: a report a fresh session resumes from, exit 0."""
|
|
765
|
+
flags = [a for a in args if a != "--json"]
|
|
766
|
+
repo, slug, code = _campaign_slug(flags, "board <slug> [--json]")
|
|
767
|
+
if code is not None or len(flags) != 1:
|
|
768
|
+
return code if code is not None else misuse("board <slug> [--json]")
|
|
769
|
+
report = tr.board(repo, slug)
|
|
770
|
+
if "--json" in args:
|
|
771
|
+
print(json.dumps(report, indent=2))
|
|
772
|
+
return 0
|
|
773
|
+
for line in tr.board_text(report):
|
|
774
|
+
print(line)
|
|
775
|
+
return 0
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def cmd_dedup(args):
|
|
779
|
+
"""Cluster the proposed ideas by statement; writes nothing, exit 0."""
|
|
780
|
+
usage = "dedup <slug> [--threshold <(0,1]>] [--json]"
|
|
781
|
+
rest, threshold, as_json = list(args), None, False
|
|
782
|
+
if "--json" in rest:
|
|
783
|
+
rest.remove("--json")
|
|
784
|
+
as_json = True
|
|
785
|
+
if "--threshold" in rest:
|
|
786
|
+
i = rest.index("--threshold")
|
|
787
|
+
try:
|
|
788
|
+
threshold = float(rest[i + 1])
|
|
789
|
+
except (IndexError, ValueError):
|
|
790
|
+
return misuse(usage)
|
|
791
|
+
if not 0 < threshold <= 1:
|
|
792
|
+
return misuse("--threshold is a similarity in (0, 1]")
|
|
793
|
+
rest = rest[:i] + rest[i + 2:]
|
|
794
|
+
repo, slug, code = _campaign_slug(rest, usage)
|
|
795
|
+
if code is not None or len(rest) != 1:
|
|
796
|
+
return code if code is not None else misuse(usage)
|
|
797
|
+
report = tr.dedup(repo, slug, threshold)
|
|
798
|
+
if as_json:
|
|
799
|
+
print(json.dumps(report, indent=2))
|
|
800
|
+
return 0
|
|
801
|
+
for line in tr.dedup_text(report, slug):
|
|
802
|
+
print(line)
|
|
803
|
+
return 0
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def cmd_yield(args):
|
|
807
|
+
"""Per operator and generator family, how far the ideas got; exit 0."""
|
|
808
|
+
if [a for a in args if a not in ("--json", "--all-lines")]:
|
|
809
|
+
return misuse("yield [--json] [--all-lines]")
|
|
810
|
+
report = tr.yield_report(repo_root(os.getcwd()), all_lines="--all-lines" in args)
|
|
811
|
+
if "--json" in args:
|
|
812
|
+
print(json.dumps(report, indent=2))
|
|
813
|
+
return 0
|
|
814
|
+
for line in tr.yield_text(report):
|
|
815
|
+
print(line)
|
|
816
|
+
return 0
|
|
817
|
+
|
|
818
|
+
|
|
592
819
|
def cmd_next(args):
|
|
593
820
|
"""The product-flow steps of one topic with their status, then the first
|
|
594
821
|
missing or due step's skill and command; a report, so exit 0."""
|
|
@@ -784,7 +1011,8 @@ def cmd_import(args):
|
|
|
784
1011
|
def cmd_all(args):
|
|
785
1012
|
if args:
|
|
786
1013
|
return misuse("--all takes no arguments")
|
|
787
|
-
|
|
1014
|
+
# every checkout's own `.tezgah`, marking the one this runs in
|
|
1015
|
+
for row in tr.across(checkout_root(os.getcwd())):
|
|
788
1016
|
print(row)
|
|
789
1017
|
return 0
|
|
790
1018
|
|
|
@@ -830,9 +1058,13 @@ def main(argv):
|
|
|
830
1058
|
return cmd_seal(rest)
|
|
831
1059
|
if cmd == "import":
|
|
832
1060
|
return cmd_import(rest)
|
|
1061
|
+
campaign = {"idea": cmd_idea, "stage": cmd_stage, "match": cmd_match,
|
|
1062
|
+
"board": cmd_board, "dedup": cmd_dedup, "yield": cmd_yield}
|
|
1063
|
+
if cmd in campaign:
|
|
1064
|
+
return campaign[cmd](rest)
|
|
833
1065
|
return misuse("unknown command %r (init, commit, check, status, --all, claim, predict, "
|
|
834
1066
|
"components, migrate, source, compare, verdict, conclude, migrate-layout, "
|
|
835
|
-
"close, seal, import, next)" % cmd)
|
|
1067
|
+
"close, seal, import, next, idea, stage, match, board, dedup, yield)" % cmd)
|
|
836
1068
|
|
|
837
1069
|
|
|
838
1070
|
if __name__ == "__main__":
|
package/bin/tezgah-route
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
tezgah-route --refresh re-read prices/ids (OpenRouter),
|
|
7
7
|
resolve opencode selectors
|
|
8
8
|
tezgah-route --mode auto|anthropic|any omp's family, then re-apply omp
|
|
9
|
+
tezgah-route --health probe the other funded families;
|
|
10
|
+
one at a limit leaves the chain
|
|
11
|
+
until its reset; re-apply omp
|
|
12
|
+
tezgah-route --fallback vendor|any|none what a chain falls back to, then
|
|
13
|
+
re-apply omp
|
|
9
14
|
tezgah-route --report each route's first check, by tier
|
|
10
15
|
|
|
11
16
|
A brief naming stored data, a migration, credentials, the gate or security goes to
|
|
@@ -21,7 +26,10 @@ model through TypeSafe, OpenRouter System One, Cloudflare Workers AI or a
|
|
|
21
26
|
compatible endpoint (TEZGAH_JEV_PROVIDER picks one), else the OpenRouter chat
|
|
22
27
|
fallback - so write it in English, the language the judge reads best.
|
|
23
28
|
|
|
24
|
-
Every routed call writes one `route` row to the session's ledger
|
|
29
|
+
Every routed call writes one `route` row to the session's ledger, named by
|
|
30
|
+
TEZGAH_SESSION (omp, opencode, Claude), CODEX_THREAD_ID (Codex) or
|
|
31
|
+
CURSOR_CONVERSATION_ID (Cursor); when two of them name different sessions (a host
|
|
32
|
+
started from another host's shell) it writes none. The row holds the
|
|
25
33
|
tier, agent, what set the tier (`via`: rule|jev|phase|default), the static
|
|
26
34
|
table's tier when --phase was given, the worker's model, and `override` (user: an
|
|
27
35
|
omp entry the user owns; mode: no entry, so the session model). `--report` joins
|
|
@@ -31,28 +39,33 @@ order only where no spawn could be swapped - and counts a route it cannot prove
|
|
|
31
39
|
as `unjoined`. That outcome is what the V2-as-default
|
|
32
40
|
decision waits for (docs/models.md).
|
|
33
41
|
|
|
34
|
-
A
|
|
35
|
-
|
|
42
|
+
A report missing an output field, or an `ESCALATE:`, gets one narrower re-brief
|
|
43
|
+
of the same role naming that field or judgement; only a second miss is restarted
|
|
44
|
+
on tezgah-frontier with the original brief, never continued. Exit codes: 0 routed / current, 1 the
|
|
36
45
|
table is stale or flagged (`--check`) or the refresh failed, 2 misuse.
|
|
37
46
|
"""
|
|
38
47
|
import argparse
|
|
39
48
|
import json
|
|
40
49
|
import os
|
|
41
50
|
import sys
|
|
51
|
+
import time
|
|
42
52
|
|
|
43
53
|
HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
44
54
|
sys.path.insert(0, os.path.join(HERE, "hooks"))
|
|
45
55
|
import tezgah_integrity as ti # noqa: E402
|
|
46
56
|
import tezgah_judge # noqa: E402
|
|
47
57
|
import tezgah_models as tm # noqa: E402
|
|
58
|
+
import tezgah_paths as tp # noqa: E402
|
|
48
59
|
|
|
49
60
|
|
|
50
61
|
def main(argv):
|
|
51
62
|
parser = argparse.ArgumentParser(
|
|
52
63
|
prog="tezgah-route",
|
|
53
64
|
description="Name the tier worker a delegated task goes to, and keep the model table.",
|
|
54
|
-
epilog="A
|
|
55
|
-
"the same
|
|
65
|
+
epilog="A report missing an output field, or an `ESCALATE:`, gets one "
|
|
66
|
+
"narrower re-brief of the same role naming that field or judgement; "
|
|
67
|
+
"only a second miss is restarted on tezgah-frontier with the original "
|
|
68
|
+
"brief, never continued.")
|
|
56
69
|
parser.add_argument("brief", nargs="*", help="the delegation brief, in English")
|
|
57
70
|
parser.add_argument("--phase", choices=sorted(tm.PHASE_TIER),
|
|
58
71
|
help="the static table's row when no judgement is available")
|
|
@@ -62,6 +75,12 @@ def main(argv):
|
|
|
62
75
|
parser.add_argument("--refresh", action="store_true",
|
|
63
76
|
help="re-read prices and ids, resolve opencode selectors")
|
|
64
77
|
parser.add_argument("--mode", choices=tm.MODES, help="omp's family, then re-apply")
|
|
78
|
+
parser.add_argument("--health", action="store_true",
|
|
79
|
+
help="probe the other funded families, record the ones at a "
|
|
80
|
+
"limit until their reset, then re-apply omp's chain")
|
|
81
|
+
parser.add_argument("--fallback", choices=tp.FALLBACKS,
|
|
82
|
+
help="which families a chain falls back to (config.json "
|
|
83
|
+
"`fallback`), then re-apply")
|
|
65
84
|
parser.add_argument("--report", action="store_true",
|
|
66
85
|
help="every ledger's routes joined to the first check after each")
|
|
67
86
|
args = parser.parse_args(argv[1:])
|
|
@@ -89,6 +108,40 @@ def main(argv):
|
|
|
89
108
|
print(line("%s %s" % (via, tier), counts))
|
|
90
109
|
print(line("jev-below-static", below))
|
|
91
110
|
return 0
|
|
111
|
+
if args.fallback:
|
|
112
|
+
# config.json `fallback` has one other writer: none. A config that does not
|
|
113
|
+
# parse is refused, not replaced (like tezgah-setup --jev).
|
|
114
|
+
try:
|
|
115
|
+
with open(tp.CONFIG, encoding="utf-8") as fh:
|
|
116
|
+
cfg = json.load(fh)
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
cfg = {}
|
|
119
|
+
except (OSError, ValueError):
|
|
120
|
+
print("%s is not valid JSON; fallback not set" % tp.CONFIG, file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
if cfg.get("fallback") != args.fallback:
|
|
123
|
+
cfg["fallback"] = args.fallback
|
|
124
|
+
os.makedirs(os.path.dirname(tp.CONFIG), exist_ok=True)
|
|
125
|
+
tmp = tp.CONFIG + ".tmp"
|
|
126
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
127
|
+
json.dump(cfg, fh, indent=2, sort_keys=True)
|
|
128
|
+
fh.write("\n")
|
|
129
|
+
os.replace(tmp, tp.CONFIG)
|
|
130
|
+
print("fallback: %s (%s)" % (args.fallback, tp.CONFIG))
|
|
131
|
+
print(tm.apply_omp() or "omp not answering; the chain applies at the next setup")
|
|
132
|
+
return 0
|
|
133
|
+
if args.health:
|
|
134
|
+
mode = tm.omp_mode()
|
|
135
|
+
data = tm.refresh_health(mode)
|
|
136
|
+
for family in tm.funded_families(live=False):
|
|
137
|
+
row = data["down"].get(family)
|
|
138
|
+
state = ("the mode's own family, not probed" if family == mode else
|
|
139
|
+
"down until %s - %s" % (time.strftime("%Y-%m-%d %H:%M",
|
|
140
|
+
time.localtime(row["until"])),
|
|
141
|
+
row.get("reason", "")) if row else "up")
|
|
142
|
+
print("%s: %s" % (family, state))
|
|
143
|
+
print(tm.apply_omp() or "omp not answering; the chain applies at the next setup")
|
|
144
|
+
return 0
|
|
92
145
|
if args.mode:
|
|
93
146
|
data = tm.overlay()
|
|
94
147
|
data.pop("mode", None)
|
|
@@ -102,11 +155,21 @@ def main(argv):
|
|
|
102
155
|
return 0
|
|
103
156
|
if not brief:
|
|
104
157
|
print("usage: tezgah-route \"<brief>\" [--phase P] [--json] | --check | --refresh "
|
|
105
|
-
"| --mode M | --report", file=sys.stderr)
|
|
158
|
+
"| --mode M | --health | --fallback F | --report", file=sys.stderr)
|
|
106
159
|
return 2
|
|
107
160
|
ask = tezgah_judge.ask if tezgah_judge.available() else None
|
|
108
161
|
out = tm.route(brief, args.phase, ask)
|
|
109
162
|
judged = out.pop("judged")
|
|
163
|
+
# The session the row goes under: omp, opencode and Claude (CLAUDE_ENV_FILE)
|
|
164
|
+
# export TEZGAH_SESSION, a Codex shell carries CODEX_THREAD_ID (the root
|
|
165
|
+
# thread its hooks' session_id names), a Cursor agent shell
|
|
166
|
+
# CURSOR_CONVERSATION_ID (its hooks' conversation_id). A host started from
|
|
167
|
+
# another host's agent shell inherits that host's id and markers alike, so
|
|
168
|
+
# two different ids name no host: no row, and the route is never written
|
|
169
|
+
# into another host's ledger.
|
|
170
|
+
ids = {os.environ.get(v) for v in ("TEZGAH_SESSION", "CODEX_THREAD_ID",
|
|
171
|
+
"CURSOR_CONVERSATION_ID")} - {None, ""}
|
|
172
|
+
session = ids.pop() if len(ids) == 1 else None
|
|
110
173
|
if judged:
|
|
111
174
|
usage = judged.get("usage")
|
|
112
175
|
usage = usage if isinstance(usage, dict) else {}
|
|
@@ -114,8 +177,7 @@ def main(argv):
|
|
|
114
177
|
judged.get("model", tezgah_judge.MODEL), out["tier"],
|
|
115
178
|
usage.get("input_tokens"), usage.get("output_tokens"),
|
|
116
179
|
judged.get("latency_ms"))
|
|
117
|
-
ti.note(
|
|
118
|
-
session = os.environ.get("TEZGAH_SESSION")
|
|
180
|
+
ti.note(session, "judge", detail)
|
|
119
181
|
if session: # the omp read in worker_model costs a process; skip it with no ledger
|
|
120
182
|
ti.note(session, "route", tm.route_detail(out, args.phase,
|
|
121
183
|
*tm.worker_model(out["agent"])))
|
package/bin/tezgah-setup
CHANGED
|
@@ -233,8 +233,9 @@ MATERIALISED = os.path.join(tp.CONFIG_DIR, "materialised")
|
|
|
233
233
|
# name too - a switch left behind is a rule that stays silent with no file saying why.
|
|
234
234
|
OFF_SWITCH_NAMES = ("adhd-off", "agents-off", "consult-off", "docs-judge-off", "exec-mode.off",
|
|
235
235
|
"judge-off", "lang-off", "orchestrate-off", "ponytail-auto.off",
|
|
236
|
-
"pretooluse-off", "reminder-off", "research-off", "
|
|
237
|
-
"
|
|
236
|
+
"pretooluse-off", "reminder-off", "research-off", "slices-off",
|
|
237
|
+
"spec-off", "task-off", "triage-off", "update-check-off", "verify-off",
|
|
238
|
+
"workspace-off")
|
|
238
239
|
# The ~/.config/tezgah entries this installer generates. A full uninstall
|
|
239
240
|
# removes exactly these (plus any `*.off` switch and the `.tezgah-bak` of one of
|
|
240
241
|
# these names) and keeps anything else: a file the user put there is not
|
|
@@ -314,9 +315,11 @@ CURSOR_HOOK_EVENTS = {
|
|
|
314
315
|
# makes a config written before this existed behave exactly as it used to.
|
|
315
316
|
# --enable/--disable/--devtools and the wizard set it before anything is written.
|
|
316
317
|
FEATURES = None
|
|
317
|
-
# The `
|
|
318
|
-
#
|
|
319
|
-
|
|
318
|
+
# The `language` this run stores (--language, or its older spelling
|
|
319
|
+
# --reply-lang); None keeps what config.json says. A first install with no flag
|
|
320
|
+
# takes the environment's (`tp.env_language`); a config made before the key
|
|
321
|
+
# existed is pinned to `tr`, the default it ran under (tezgah_paths.language).
|
|
322
|
+
LANGUAGE = None
|
|
320
323
|
OK, NO = " ok ", " MISS "
|
|
321
324
|
|
|
322
325
|
|
|
@@ -1028,6 +1031,10 @@ def install_common(new_roots, hosts):
|
|
|
1028
1031
|
cfg = read_config(tp.CONFIG, {})
|
|
1029
1032
|
own = cfg is not None
|
|
1030
1033
|
cfg = cfg if own else {}
|
|
1034
|
+
# a first install is a config no install wrote yet: `--jev` alone writes
|
|
1035
|
+
# the file too, so its existence says nothing; an install always writes
|
|
1036
|
+
# `roots` and `prefix`
|
|
1037
|
+
first = not any(key in cfg for key in ("roots", "prefix"))
|
|
1031
1038
|
cfg["roots"] = [expand(r) for r in new_roots] if new_roots else \
|
|
1032
1039
|
(tp.roots() or [tp.DEFAULT_ROOT])
|
|
1033
1040
|
if tp.codegraph_bin():
|
|
@@ -1044,8 +1051,13 @@ def install_common(new_roots, hosts):
|
|
|
1044
1051
|
# anywhere, so it moves the tree the install actually used instead of the
|
|
1045
1052
|
# XDG default. An explicit --prefix or TEZGAH_PREFIX still wins over it.
|
|
1046
1053
|
cfg["prefix"] = INSTALL_PREFIX
|
|
1047
|
-
if
|
|
1048
|
-
cfg["
|
|
1054
|
+
if LANGUAGE:
|
|
1055
|
+
cfg["language"] = LANGUAGE
|
|
1056
|
+
elif not any(key in cfg for key in ("language", "reply_lang")):
|
|
1057
|
+
# an existing `reply_lang` is never rewritten; only a config with
|
|
1058
|
+
# neither key gets one: the locale's on a first install, `tr` on an
|
|
1059
|
+
# older one, so an upgrade changes no one's reply language
|
|
1060
|
+
cfg["language"] = tp.env_language() if first else "tr"
|
|
1049
1061
|
if own:
|
|
1050
1062
|
write_json(tp.CONFIG, cfg)
|
|
1051
1063
|
print("%s %s roots=%s" % (OK, tp.CONFIG, cfg["roots"]))
|
|
@@ -5520,7 +5532,7 @@ def upgrade(version, dry_run):
|
|
|
5520
5532
|
so its farm links would point back into it. The hosts and roots come from
|
|
5521
5533
|
config.json, so an upgrade does not re-ask the install questions. Nothing
|
|
5522
5534
|
else calls this: a flag never upgrades tezgah as a side effect of another
|
|
5523
|
-
(hooks/tezgah_policy.py:
|
|
5535
|
+
(hooks/tezgah_policy.py:812). Prints every step before running it, and
|
|
5524
5536
|
`--dry-run` stops after the printing."""
|
|
5525
5537
|
cfg = tp.config()
|
|
5526
5538
|
hosts = cfg.get("hosts") or detected_hosts()
|
|
@@ -5713,10 +5725,11 @@ def main(argv):
|
|
|
5713
5725
|
version=tezgah_context.version() or "unknown")
|
|
5714
5726
|
ap.add_argument("--roots")
|
|
5715
5727
|
ap.add_argument("--hosts")
|
|
5716
|
-
ap.add_argument("--reply-lang", choices=tp.
|
|
5728
|
+
ap.add_argument("--language", "--reply-lang", choices=tp.languages(),
|
|
5717
5729
|
help="with --install: the language replies are asked for and "
|
|
5718
|
-
"the Stop rule checks (config.json `
|
|
5719
|
-
"
|
|
5730
|
+
"the Stop rule checks (config.json `language`; a first "
|
|
5731
|
+
"install takes LANGUAGE/LC_ALL/LC_MESSAGES/LANG, `en` "
|
|
5732
|
+
"when none names a pack; `any` checks none)")
|
|
5720
5733
|
ap.add_argument("--jev", type=tp.jev_name, choices=tp.JEV_CHOICES,
|
|
5721
5734
|
help="the carrier the judgement seam reaches the Jev model "
|
|
5722
5735
|
"through (config.json `jev`; `auto` tries every one "
|
|
@@ -5736,9 +5749,9 @@ def main(argv):
|
|
|
5736
5749
|
# so it is taken off before the flags are parsed (`--dry-run` still reads)
|
|
5737
5750
|
command = argv[1:2] == ["update"]
|
|
5738
5751
|
args = ap.parse_args(argv[2:] if command else argv[1:])
|
|
5739
|
-
global FEATURES, INSTALL_PREFIX, HOST_DEPS,
|
|
5752
|
+
global FEATURES, INSTALL_PREFIX, HOST_DEPS, LANGUAGE
|
|
5740
5753
|
HOST_DEPS = args.host_deps
|
|
5741
|
-
|
|
5754
|
+
LANGUAGE = args.language
|
|
5742
5755
|
select_code = 0
|
|
5743
5756
|
# --devtools is --enable <id> and nothing else: one code path, so the flag
|
|
5744
5757
|
# cannot reach the selection in a way the explicit command does not.
|