@r1z4x/tezgah 1.13.0 → 1.14.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 +175 -1
- package/MANIFEST +18 -0
- package/NOTICE +152 -0
- package/README.de.md +41 -20
- package/README.es.md +39 -19
- package/README.fr.md +41 -20
- package/README.md +15 -8
- package/README.tr.md +39 -21
- package/bin/codegen +9 -2
- package/bin/consult +6 -0
- package/bin/tezgah-adhd +3 -0
- package/bin/tezgah-agents +3 -0
- package/bin/tezgah-capture +3 -0
- package/bin/tezgah-design +329 -54
- package/bin/tezgah-docs +157 -41
- package/bin/tezgah-dsh +1 -1
- package/bin/tezgah-gate +7 -2
- package/bin/tezgah-import-ai-research +16 -4
- package/bin/tezgah-index +3 -0
- package/bin/tezgah-lessons +13 -4
- package/bin/tezgah-mcp +4 -0
- package/bin/tezgah-migrate +3 -0
- package/bin/tezgah-ocr-review +2 -1
- package/bin/tezgah-optimize +4 -1
- package/bin/tezgah-pony +3 -0
- package/bin/tezgah-research +12 -6
- package/bin/tezgah-rollback +3 -0
- package/bin/tezgah-route +1 -1
- package/bin/tezgah-setup +596 -157
- package/bin/tezgah-status +11 -1
- package/bin/tezgah-task +3 -0
- package/bin/tezgah.js +10 -3
- package/commands/adhd.md +1 -1
- package/docs/README.md +19 -8
- package/docs/architecture.md +2 -2
- package/docs/citations-baseline.json +11 -17
- package/docs/clarity-baseline.json +3 -3
- package/docs/contract.md +77 -31
- package/docs/environment.md +58 -24
- package/docs/evidence.md +49 -19
- package/docs/gate.md +71 -21
- package/docs/hosts.md +65 -52
- package/docs/layers.md +1 -1
- package/docs/operations.md +67 -24
- package/docs/research.md +1 -1
- package/docs/skills.md +31 -8
- package/docs/status-line.md +33 -23
- package/docs/testing.md +48 -17
- package/docs/vision.md +6 -2
- package/hooks/lang/en.json +5 -2
- package/hooks/lang/tr.json +3 -2
- package/hooks/projects-posttooluse.py +11 -3
- package/hooks/projects-stop.py +1 -1
- package/hooks/tezgah_context.py +40 -23
- package/hooks/tezgah_gate.py +404 -106
- package/hooks/tezgah_integrity.py +367 -97
- package/hooks/tezgah_lessons.py +5 -2
- package/hooks/tezgah_paths.py +17 -6
- package/hooks/tezgah_policy.py +13 -5
- package/hooks/tezgah_skill_pick.py +1 -1
- package/hooks/tezgah_snapshot.py +6 -3
- package/hooks/tezgah_stopspec.py +2 -1
- package/hosts/amp/hook.py +1 -1
- package/hosts/amp/tezgah-plugin.ts.in +13 -3
- package/hosts/antigravity/hook.py +1 -1
- package/hosts/codex/hook.py +1 -1
- package/hosts/commandcode/hook.py +1 -1
- package/hosts/copilot/hook.py +1 -1
- package/hosts/cursor/hook.py +2 -1
- package/hosts/droid/hook.py +1 -1
- package/hosts/gemini/hook.py +1 -1
- package/hosts/goose/hook.py +1 -1
- package/hosts/omp/hook.py +1 -1
- package/hosts/omp/tezgah-hook.ts.in +127 -35
- package/hosts/opencode/plugins/tezgah.js +209 -63
- package/hosts/qwen/hook.py +1 -1
- package/output-styles/tezgah.md +1 -1
- package/package.json +6 -3
- package/packaging/build.sh +4 -4
- package/packaging/install.sh +11 -0
- package/packaging/upgrade.sh +3 -0
- package/skills/ai-research/LICENSE +21 -0
- package/skills/design-contract/SKILL.md +27 -6
- package/skills/design-library/LICENSE +21 -0
- package/skills/plan-add/render_table.py +7 -0
- package/skills/pm-frameworks/LICENSE +21 -0
- package/skills/rl-env/LICENSE +201 -0
- package/skills/tezgah-contract/SKILL.md +2 -2
- package/third_party/open-code-review/LICENSE +201 -0
- package/third_party/open-code-review/README.md +15 -0
- package/third_party/open-code-review/prompts/main_task_system.md +26 -0
- package/third_party/open-code-review/prompts/review_filter_task_system.md +13 -0
- package/workflows/graph-map.js +14 -5
package/bin/tezgah-docs
CHANGED
|
@@ -99,22 +99,48 @@ def matches(query):
|
|
|
99
99
|
# captured output) is data, not a citation, and is not read.
|
|
100
100
|
|
|
101
101
|
CODE_FILES = ("hooks/*.py", "bin/*", "statusline.py", "hosts/*/*.py",
|
|
102
|
-
"hosts/*/*.ts.in", "hosts/*/plugins/*.js", "
|
|
102
|
+
"hosts/*/*.ts.in", "hosts/*/plugins/*.js", "hosts/*/tui/*.tsx",
|
|
103
|
+
"hosts/*/statusline/lib/*.js", "tests/*.py")
|
|
103
104
|
STRING = re.compile(r'"""(?:.|\n)*?"""|\'\'\'(?:.|\n)*?\'\'\'')
|
|
104
105
|
TOP = re.compile(r"^(?:def|class)\s+(\w+)|^([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=")
|
|
106
|
+
# A JS/TS file's top-level `const`/`let`/`function`/`class`, exported or not.
|
|
107
|
+
JS_FILES = (".js", ".mjs", ".ts", ".tsx", ".ts.in")
|
|
108
|
+
JS_TOP = re.compile(r"^(?:export\s+(?:default\s+)?)?(?:async\s+)?"
|
|
109
|
+
r"(?:function\*?|const|let|class)\s+([A-Za-z_$][\w$]*)")
|
|
105
110
|
RUN = re.compile(r"((?:`[A-Za-z_][A-Za-z0-9_]*(?:\(\))?`\s*[/,]?\s*){1,5})$")
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
# A cited path: one with a `/`, a `name.ext`, or a dot file (`.gitignore`); its
|
|
112
|
+
# lines a range or a comma list of them (x.py:12,40-41). The path is optional:
|
|
113
|
+
# a backticked colon and lines alone is a bare range of the path before it.
|
|
114
|
+
PATH = r"[\w.-]+(?:/[\w.-]+)+|\.?[\w-]+\.\w+|\.[\w-]+"
|
|
115
|
+
SPEC = r"\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*"
|
|
116
|
+
CITE = re.compile(r"`(%s)?:(%s)`" % (PATH, SPEC))
|
|
117
|
+
SCRIPTS = (".sh", ".ps1", ".py", ".js", ".mjs", ".ts", ".tsx", ".in")
|
|
108
118
|
# The symbol written after the citation instead of before it, the way a table
|
|
109
119
|
# cell reads: `hooks/tezgah_policy.py:819-1158` (`CORE`).
|
|
110
120
|
AFTER = re.compile(r"\s*\(`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`\)")
|
|
111
121
|
# Any `path:line`, backticked or not: what CITE does not match of these (a
|
|
112
|
-
# bare token
|
|
122
|
+
# bare token such as x.py:12) is counted as unjudged, so a citation
|
|
113
123
|
# written in a shape the audit cannot read is still a number someone sees.
|
|
114
124
|
# Scanned as one token run and then shape-checked: the shape alternation as
|
|
115
125
|
# the scanner (nested `+` under a look-behind) cost a third of a second a pass.
|
|
116
126
|
LOOSE = re.compile(r"(?<![\w./:-])([\w./-]*\w):(\d+)(?:-(\d+))?(?!\w)")
|
|
117
127
|
LOOSE_SHAPE = re.compile(r"(?:[\w.-]+/)+[\w.-]*\w|[\w-]+\.\w+")
|
|
128
|
+
# A path a bare range can follow: backticked, with or without its line or
|
|
129
|
+
# symbol (a loose `path:line` counts too). The bare ranges themselves:
|
|
130
|
+
# `(8-9)` or `(14,34-38)` - a lone `(300)` is a value, not a line - and an
|
|
131
|
+
# unquoted :8-9; the backticked one is CITE's second half. Each one is read
|
|
132
|
+
# against the path before it in the same sentence or table row, never further.
|
|
133
|
+
MARK = re.compile(r"`(%s)(?::%s|::[\w.]+)?`" % (PATH, SPEC))
|
|
134
|
+
BARE = re.compile(r"(?<![\w(])\((\d+-\d+|\d+(?:-\d+)?(?:,\d+(?:-\d+)?)+)\)"
|
|
135
|
+
r"|(?<=[\s(,]):(\d+(?:-\d+)?)(?![\w:-])")
|
|
136
|
+
# A comment source masks its code as NUL: the code between two comments ends
|
|
137
|
+
# the sentence too.
|
|
138
|
+
SENTENCE_END = re.compile(r"[.!?](?:\s|$)|\n\s*\n|\n\s*[|*-]|\0")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def spec_parts(spec):
|
|
142
|
+
"""[(lo, hi)] of a SPEC such as `12,40-41`."""
|
|
143
|
+
return [(int(a), int(b or a)) for a, _, b in (p.partition("-") for p in spec.split(","))]
|
|
118
144
|
|
|
119
145
|
|
|
120
146
|
def loose(text):
|
|
@@ -179,12 +205,17 @@ def anchor_span(names, qual):
|
|
|
179
205
|
|
|
180
206
|
|
|
181
207
|
CITATION_BASELINE = os.path.join("docs", "citations-baseline.json")
|
|
208
|
+
# The gitignored, private workspace: a CI checkout or a worktree has no copy, so
|
|
209
|
+
# a citation into it is unjudged in every checkout, never judged in one alone.
|
|
210
|
+
# ponytail: one fixed prefix, not a .gitignore reader; add another if one lands.
|
|
211
|
+
PRIVATE = (".tezgah/",)
|
|
182
212
|
|
|
183
213
|
|
|
184
214
|
def code_symbols(path):
|
|
185
215
|
"""{name: [(start, end)]} for this file's top-level defs, classes and
|
|
186
|
-
assignments. A triple-quoted body is
|
|
187
|
-
|
|
216
|
+
assignments (a JS/TS file's `JS_TOP` declarations). A triple-quoted body is
|
|
217
|
+
masked first, so a line inside a contract string cannot read as a
|
|
218
|
+
definition and cut another one short."""
|
|
188
219
|
text = open(path, encoding="utf-8", errors="replace").read()
|
|
189
220
|
chars = list(text)
|
|
190
221
|
for m in STRING.finditer(text):
|
|
@@ -192,8 +223,9 @@ def code_symbols(path):
|
|
|
192
223
|
if chars[i] != "\n":
|
|
193
224
|
chars[i] = " "
|
|
194
225
|
lines = "".join(chars).splitlines()
|
|
195
|
-
|
|
196
|
-
|
|
226
|
+
top = JS_TOP if path.endswith(JS_FILES) else TOP
|
|
227
|
+
starts = [(i, m.group(1) or m.group(top.groups))
|
|
228
|
+
for i, line in enumerate(lines, 1) if (m := top.match(line))]
|
|
197
229
|
out = {}
|
|
198
230
|
for k, (start, name) in enumerate(starts):
|
|
199
231
|
end = starts[k + 1][0] - 1 if k + 1 < len(starts) else len(lines)
|
|
@@ -264,6 +296,11 @@ def comment_text(path):
|
|
|
264
296
|
lead = (tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT, tokenize.ENCODING)
|
|
265
297
|
real = [t for t in tokens if t.type not in (tokenize.NL, tokenize.COMMENT)]
|
|
266
298
|
kept = [span(t, 1) for t in tokens if t.type == tokenize.COMMENT]
|
|
299
|
+
# a comment's `#` is a space, not code: two comment lines stay one sentence
|
|
300
|
+
chars = list(masked)
|
|
301
|
+
for lo, _ in kept:
|
|
302
|
+
chars[lo - 1] = " "
|
|
303
|
+
masked = "".join(chars)
|
|
267
304
|
kept += [span(t) for k, t in enumerate(real)
|
|
268
305
|
if t.type == tokenize.STRING and (k == 0 or real[k - 1].type in lead)
|
|
269
306
|
and k + 1 < len(real)
|
|
@@ -298,58 +335,134 @@ def citations(known):
|
|
|
298
335
|
`note_tool` in `hooks/x.py`, so a same-named symbol elsewhere is not the
|
|
299
336
|
thing being cited and the row is left unjudged rather than misjudged. A bare
|
|
300
337
|
file name (`tezgah_gate.py:12`) is that path when exactly one code file has
|
|
301
|
-
the name,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
338
|
+
the name, else the repository-root file of that name, and unjudged
|
|
339
|
+
otherwise; a path under `PRIVATE` is unjudged in every checkout. A bare
|
|
340
|
+
range (`BARE`, or CITE's second
|
|
341
|
+
half) takes the path written before it in the same sentence (`MARK`); with
|
|
342
|
+
none, a backticked one is judged against the one code file defining its
|
|
343
|
+
symbol. A range past the end of its file, or over blank lines only, is
|
|
344
|
+
flagged as (`file:line`, token, why, -1, -1) whether or not it names a
|
|
345
|
+
symbol; a bare range naming none is otherwise left alone, a backticked one
|
|
346
|
+
counted unjudged as before. A symbol anchor (`SYMCITE`) is always judged:
|
|
347
|
+
against the qualnames() of the file it names, a JS/TS file's
|
|
348
|
+
code_symbols()."""
|
|
349
|
+
names, code, local, lines_of = {}, set(code_files()), {}, {}
|
|
350
|
+
for rel in code:
|
|
305
351
|
names.setdefault(os.path.basename(rel), []).append(rel)
|
|
306
352
|
|
|
307
353
|
def resolve(path):
|
|
354
|
+
if path.startswith(PRIVATE):
|
|
355
|
+
return None
|
|
308
356
|
if "/" in path:
|
|
309
357
|
return path
|
|
310
358
|
found = names.get(path, ())
|
|
311
|
-
|
|
359
|
+
if found:
|
|
360
|
+
return found[0] if len(found) == 1 else None
|
|
361
|
+
return path if os.path.isfile(os.path.join(HERE, path)) else None
|
|
362
|
+
|
|
363
|
+
def named_at(text, start, end):
|
|
364
|
+
# RUN is anchored at the end and five names fit in far fewer than
|
|
365
|
+
# 600 characters: slicing the whole page per citation was the cost.
|
|
366
|
+
pre = text[max(0, start - 600):start].rstrip().rstrip("(") \
|
|
367
|
+
.rstrip().rstrip(",").rstrip()
|
|
368
|
+
run = RUN.search(pre) if pre.endswith("`") else None
|
|
369
|
+
if run:
|
|
370
|
+
return re.findall(r"`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`", run.group(1))
|
|
371
|
+
after = AFTER.match(text, end)
|
|
372
|
+
return [after.group(1)] if after else []
|
|
373
|
+
|
|
374
|
+
def path_before(text, marks, start):
|
|
375
|
+
k = bisect.bisect_right(marks, (start,)) - 1
|
|
376
|
+
while k >= 0 and marks[k][1] > start:
|
|
377
|
+
k -= 1
|
|
378
|
+
if k < 0 or SENTENCE_END.search(text, marks[k][1], start):
|
|
379
|
+
return None
|
|
380
|
+
return marks[k][2]
|
|
381
|
+
|
|
382
|
+
def judge(rel, text, m, path, parts, count):
|
|
383
|
+
"""Judge one citation `m` of `parts` [(lo, hi)] into `path` (None: the
|
|
384
|
+
known symbol's file); `count` it as unjudged when it names no symbol."""
|
|
385
|
+
nonlocal judged
|
|
386
|
+
|
|
387
|
+
def line():
|
|
388
|
+
return "%s:%d" % (rel, text.count("\n", 0, m.start()) + 1)
|
|
389
|
+
full = os.path.join(HERE, path) if path else None
|
|
390
|
+
if full and os.path.isfile(full):
|
|
391
|
+
if path not in lines_of:
|
|
392
|
+
with open(full, encoding="utf-8", errors="replace") as fh:
|
|
393
|
+
lines_of[path] = fh.read().splitlines()
|
|
394
|
+
body = lines_of[path]
|
|
395
|
+
if any(lo < 1 or hi > len(body) for lo, hi in parts):
|
|
396
|
+
if not count and max(hi for _, hi in parts) > 10 * len(body):
|
|
397
|
+
return # a bare value span such as a year range, not lines
|
|
398
|
+
flagged.append((line(), m.group(0), "%s has %d lines" % (path, len(body)),
|
|
399
|
+
-1, -1))
|
|
400
|
+
return
|
|
401
|
+
if not any(body[i].strip() for lo, hi in parts for i in range(lo - 1, hi)):
|
|
402
|
+
flagged.append((line(), m.group(0), "%s is blank there" % path, -1, -1))
|
|
403
|
+
return
|
|
404
|
+
if path not in code:
|
|
405
|
+
# A file with no symbol table: its range is judged, and in a
|
|
406
|
+
# script (packaging/build.sh) a name written beside it must be
|
|
407
|
+
# on those lines; a data or prose file is read for its range.
|
|
408
|
+
judged += 1
|
|
409
|
+
said = named_at(text, m.start(), m.end()) if path.endswith(SCRIPTS) else []
|
|
410
|
+
cut = "\n".join(s for lo, hi in parts for s in body[lo - 1:hi])
|
|
411
|
+
if said and not any(re.search(r"\b%s\b" % re.escape(n), cut) for n in said):
|
|
412
|
+
flagged.append((line(), m.group(0), "`%s` is not on %s there"
|
|
413
|
+
% ("`, `".join(said), path), -1, -1))
|
|
414
|
+
return
|
|
415
|
+
spans, where = [], path
|
|
416
|
+
for name in named_at(text, m.start(), m.end()):
|
|
417
|
+
if path:
|
|
418
|
+
if path not in local:
|
|
419
|
+
local[path] = code_symbols(full) if path in code else {}
|
|
420
|
+
spans += local[path].get(name, [])
|
|
421
|
+
elif name in known:
|
|
422
|
+
where, spans = known[name][0], spans + known[name][1]
|
|
423
|
+
if not spans:
|
|
424
|
+
if count:
|
|
425
|
+
unjudged[rel] = unjudged.get(rel, 0) + 1
|
|
426
|
+
return
|
|
427
|
+
low, high = min(s for s, _ in spans), max(e for _, e in spans)
|
|
428
|
+
judged += 1
|
|
429
|
+
if any(lo < low or hi > high for lo, hi in parts):
|
|
430
|
+
flagged.append((line(), m.group(0), where, low, high))
|
|
312
431
|
|
|
313
432
|
flagged, judged, unjudged, invisible, tables = [], 0, {}, 0, {}
|
|
314
433
|
for rel, text in sources():
|
|
434
|
+
marks = [(p.start(), p.end(), resolve(p.group(1)))
|
|
435
|
+
for p in list(MARK.finditer(text)) + list(loose(text))]
|
|
436
|
+
marks = sorted(mk for mk in marks
|
|
437
|
+
if mk[2] and os.path.isfile(os.path.join(HERE, mk[2])))
|
|
315
438
|
read = []
|
|
316
439
|
for m in CITE.finditer(text):
|
|
317
440
|
read.append(m.span())
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
named = re.findall(r"`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`",
|
|
324
|
-
run.group(1)) if run else []
|
|
325
|
-
after = AFTER.match(text, m.end())
|
|
326
|
-
if not named and after:
|
|
327
|
-
named = [after.group(1)]
|
|
328
|
-
cited = resolve(m.group(1)) if m.group(1) else None
|
|
329
|
-
spans, where = [], None
|
|
330
|
-
for name in named:
|
|
331
|
-
hit = known.get(name)
|
|
332
|
-
if not hit or (m.group(1) and hit[0] != cited):
|
|
441
|
+
parts = spec_parts(m.group(2))
|
|
442
|
+
if m.group(1):
|
|
443
|
+
path = resolve(m.group(1))
|
|
444
|
+
if path is None:
|
|
445
|
+
unjudged[rel] = unjudged.get(rel, 0) + 1
|
|
333
446
|
continue
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
line = text.count("\n", 0, m.start()) + 1
|
|
345
|
-
flagged.append(("%s:%d" % (rel, line), m.group(0), where, low, high))
|
|
447
|
+
else:
|
|
448
|
+
path = path_before(text, marks, m.start())
|
|
449
|
+
judge(rel, text, m, path, parts, True)
|
|
450
|
+
for m in BARE.finditer(text):
|
|
451
|
+
path = path_before(text, marks, m.start())
|
|
452
|
+
if path:
|
|
453
|
+
parts = spec_parts(m.group(1) or m.group(2))
|
|
454
|
+
if any(lo < 1 or lo > hi for lo, hi in parts):
|
|
455
|
+
continue # a value range such as 0-100, not lines
|
|
456
|
+
judge(rel, text, m, path, parts, False)
|
|
346
457
|
for m in SYMCITE.finditer(text):
|
|
347
458
|
judged += 1
|
|
348
459
|
path = resolve(m.group(1))
|
|
349
460
|
full = os.path.join(HERE, path) if path else None
|
|
350
461
|
if full and os.path.isfile(full):
|
|
351
462
|
if path not in tables:
|
|
352
|
-
tables[path] =
|
|
463
|
+
tables[path] = {n: (s[0][0], s[-1][1], False) for n, s in
|
|
464
|
+
code_symbols(full).items()} \
|
|
465
|
+
if path.endswith(JS_FILES) else qualnames(full) or {}
|
|
353
466
|
if anchor_span(tables[path], m.group(2)):
|
|
354
467
|
continue
|
|
355
468
|
line = text.count("\n", 0, m.start()) + 1
|
|
@@ -1136,6 +1249,9 @@ def main(argv):
|
|
|
1136
1249
|
if "--citations" in flags:
|
|
1137
1250
|
flagged, judged, unjudged, invisible = citations(symbols())
|
|
1138
1251
|
for where, token, symbol_in, low, high in flagged:
|
|
1252
|
+
if low < 0:
|
|
1253
|
+
print("%s %s -> %s" % (where, token, symbol_in))
|
|
1254
|
+
continue
|
|
1139
1255
|
if not low:
|
|
1140
1256
|
print("%s %s -> %s defines no such symbol" % (where, token, symbol_in))
|
|
1141
1257
|
continue
|
package/bin/tezgah-dsh
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
set -eu
|
|
17
17
|
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
|
18
18
|
BIN="$DSH_HOME/profiles/node_modules/@deepseek-ai/dsh/lib/bin.js"
|
|
19
|
-
INDEX="${TEZGAH_INDEX_BIN:-$HOME/.config/tezgah/bin/tezgah-index}"
|
|
19
|
+
INDEX="${TEZGAH_INDEX_BIN:-${XDG_CONFIG_HOME:-$HOME/.config}/tezgah/bin/tezgah-index}"
|
|
20
20
|
warm_index() {
|
|
21
21
|
if [ -x "$INDEX" ]; then
|
|
22
22
|
"$INDEX" "$PWD" >/dev/null 2>&1 || true
|
package/bin/tezgah-gate
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
`--sheet` draws the blind label sample (hooks/tezgah_replay.py)
|
|
11
11
|
|
|
12
12
|
Reads the host's own call as {"tool":..., "input":{...}, "cwd":...,
|
|
13
|
-
"session_id":...}
|
|
13
|
+
"session_id":..., "parent":...} (`parent`: the session that spawned this one,
|
|
14
|
+
as the host named it) and prints what hooks/tezgah_gate.decision() returns for it,
|
|
14
15
|
or nothing when the call passes. A host whose plugin is written in another
|
|
15
16
|
language (opencode's tezgah.js) asks `decide` instead of mirroring a rule: that
|
|
16
17
|
mirror is documented as incomplete and divergent, so a new rule reaches such a
|
|
@@ -49,6 +50,9 @@ def main(argv):
|
|
|
49
50
|
if len(argv) < 2:
|
|
50
51
|
print(__doc__, file=sys.stderr)
|
|
51
52
|
return 2
|
|
53
|
+
if argv[1] in ("-h", "--help"):
|
|
54
|
+
print(__doc__.strip())
|
|
55
|
+
return 0
|
|
52
56
|
if argv[1] == "replay":
|
|
53
57
|
import tezgah_replay
|
|
54
58
|
return tezgah_replay.main(argv[2:])
|
|
@@ -65,7 +69,8 @@ def main(argv):
|
|
|
65
69
|
inp = payload.get("input")
|
|
66
70
|
reason = decision(payload.get("tool"), inp if isinstance(inp, dict) else {},
|
|
67
71
|
payload.get("cwd") or os.getcwd(),
|
|
68
|
-
payload.get("session_id"), record=argv[1] == "decide"
|
|
72
|
+
payload.get("session_id"), record=argv[1] == "decide",
|
|
73
|
+
parent=payload.get("parent"))
|
|
69
74
|
if reason:
|
|
70
75
|
sys.stdout.write(reason if reason.endswith("\n") else reason + "\n")
|
|
71
76
|
return 0
|
|
@@ -23,6 +23,8 @@ the point of keeping it.
|
|
|
23
23
|
|
|
24
24
|
`skills/ai-research/SKILL.md` is hand-written and is not in the manifest: it is
|
|
25
25
|
the entry point, not vendored content. `library.json` is the manifest itself.
|
|
26
|
+
`LICENSE` is upstream's root licence, copied verbatim at the vendored revision
|
|
27
|
+
and, like SOURCE, outside the manifest.
|
|
26
28
|
|
|
27
29
|
Exit codes: 0 ok, 1 drift or an upstream surprise, 2 misuse.
|
|
28
30
|
"""
|
|
@@ -358,6 +360,14 @@ def cmd_import(source, rev, out):
|
|
|
358
360
|
texts = dict(files)
|
|
359
361
|
texts.update(generated)
|
|
360
362
|
texts["SOURCE"] = source_text(manifest, rev)
|
|
363
|
+
# upstream's licence, bytes verbatim at the vendored revision (NOTICE, OQ-21)
|
|
364
|
+
proc = subprocess.run(("git", "-C", source, "show", "%s:LICENSE" % rev),
|
|
365
|
+
capture_output=True)
|
|
366
|
+
if proc.returncode != 0:
|
|
367
|
+
raise SystemExit("tezgah-import-ai-research: upstream has no LICENSE at %s: %s"
|
|
368
|
+
% (rev, proc.stderr.decode(errors="replace").strip()
|
|
369
|
+
or proc.returncode))
|
|
370
|
+
licence = proc.stdout
|
|
361
371
|
library = json.dumps(manifest, indent=1, sort_keys=True) + "\n"
|
|
362
372
|
|
|
363
373
|
# SKILL.md is tezgah's own prose, not generated: the import refreshes what it
|
|
@@ -376,6 +386,8 @@ def cmd_import(source, rev, out):
|
|
|
376
386
|
fh.write(text)
|
|
377
387
|
with open(os.path.join(out, "library.json"), "w", encoding="utf-8") as fh:
|
|
378
388
|
fh.write(library)
|
|
389
|
+
with open(os.path.join(out, "LICENSE"), "wb") as fh:
|
|
390
|
+
fh.write(licence)
|
|
379
391
|
if kept is not None:
|
|
380
392
|
with open(entry, "w", encoding="utf-8") as fh:
|
|
381
393
|
fh.write(kept)
|
|
@@ -411,10 +423,10 @@ def cmd_check(out):
|
|
|
411
423
|
for name in sorted(names):
|
|
412
424
|
rel = os.path.relpath(os.path.join(root, name), out)
|
|
413
425
|
# library.json is the manifest itself, SKILL.md is tezgah's own entry
|
|
414
|
-
# point,
|
|
415
|
-
# that it names the manifest's revision)
|
|
416
|
-
# content, so none carries a digest.
|
|
417
|
-
if rel in ("library.json", "SKILL.md", "SOURCE") or rel in expected:
|
|
426
|
+
# point, SOURCE is prose derived from the manifest (a test pins
|
|
427
|
+
# that it names the manifest's revision), and LICENSE is upstream's
|
|
428
|
+
# licence file; none is vendored skill content, so none carries a digest.
|
|
429
|
+
if rel in ("library.json", "SKILL.md", "SOURCE", "LICENSE") or rel in expected:
|
|
418
430
|
continue
|
|
419
431
|
problems.append("unlisted %s" % rel)
|
|
420
432
|
if problems:
|
package/bin/tezgah-index
CHANGED
|
@@ -21,6 +21,9 @@ def main():
|
|
|
21
21
|
if "-h" in args or "--help" in args:
|
|
22
22
|
print(__doc__.strip())
|
|
23
23
|
return 0
|
|
24
|
+
if any(a.startswith("-") for a in args):
|
|
25
|
+
print("usage: tezgah-index [PATH]", file=sys.stderr)
|
|
26
|
+
return 2
|
|
24
27
|
cwd = os.path.realpath(args[0] if args else os.getcwd())
|
|
25
28
|
if not under(cwd):
|
|
26
29
|
return 0
|
package/bin/tezgah-lessons
CHANGED
|
@@ -118,6 +118,18 @@ def main(argv):
|
|
|
118
118
|
ap.add_argument("--root", default=linked_main(here) or here)
|
|
119
119
|
ap.add_argument("--fixture")
|
|
120
120
|
args = ap.parse_args(argv)
|
|
121
|
+
prompts = None
|
|
122
|
+
if args.fixture:
|
|
123
|
+
# read before any proposal prints: an unreadable fixture is usage (2),
|
|
124
|
+
# never exit 1, which means the merges would lower recall
|
|
125
|
+
try:
|
|
126
|
+
with open(args.fixture, encoding="utf-8") as fh:
|
|
127
|
+
prompts = [(p["q"], [g - 1 for g in p["gt"]])
|
|
128
|
+
for p in json.load(fh)["lessons_prompts"]]
|
|
129
|
+
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
130
|
+
print("tezgah-lessons: cannot read --fixture %s: %s" % (args.fixture, exc),
|
|
131
|
+
file=sys.stderr)
|
|
132
|
+
return 2
|
|
121
133
|
lines = tezgah_lessons.lines(args.root)
|
|
122
134
|
print("Proposals for %s (nothing is written; apply what you agree with by hand):"
|
|
123
135
|
% os.path.join(args.root, ".tezgah", "lessons.md"))
|
|
@@ -133,11 +145,8 @@ def main(argv):
|
|
|
133
145
|
for needle, by in RETIRE:
|
|
134
146
|
if needle in ln:
|
|
135
147
|
print("retire %d: append `|| enforced_by: %s`" % (n, by))
|
|
136
|
-
if
|
|
148
|
+
if prompts is None:
|
|
137
149
|
return 0
|
|
138
|
-
with open(args.fixture, encoding="utf-8") as fh:
|
|
139
|
-
prompts = [(p["q"], [g - 1 for g in p["gt"]])
|
|
140
|
-
for p in json.load(fh)["lessons_prompts"]]
|
|
141
150
|
after_lines, where = merged(lines, groups)
|
|
142
151
|
before = recall(lines, prompts)
|
|
143
152
|
after = recall(after_lines, [(q, sorted({where[g] for g in gt if g in where}))
|
package/bin/tezgah-mcp
CHANGED
|
@@ -312,6 +312,10 @@ def serve(stdin, stdout):
|
|
|
312
312
|
|
|
313
313
|
|
|
314
314
|
def main():
|
|
315
|
+
if sys.argv[1:]:
|
|
316
|
+
print("tezgah-mcp: takes no arguments; it is a stdio MCP server a host "
|
|
317
|
+
"starts", file=sys.stderr)
|
|
318
|
+
return 2
|
|
315
319
|
try:
|
|
316
320
|
return serve(sys.stdin, sys.stdout)
|
|
317
321
|
except BrokenPipeError:
|
package/bin/tezgah-migrate
CHANGED
|
@@ -299,6 +299,9 @@ def discover():
|
|
|
299
299
|
def main(argv):
|
|
300
300
|
apply, repos, i = False, [], 0
|
|
301
301
|
while i < len(argv):
|
|
302
|
+
if argv[i] in ("-h", "--help"):
|
|
303
|
+
print(__doc__.strip())
|
|
304
|
+
return 0
|
|
302
305
|
if argv[i] == "--apply":
|
|
303
306
|
apply = True
|
|
304
307
|
elif argv[i] == "--repo" and i + 1 < len(argv):
|
package/bin/tezgah-ocr-review
CHANGED
|
@@ -19,7 +19,8 @@ points at ~/.gitconfig, XDG_CONFIG_HOME at the user's config dir, and a
|
|
|
19
19
|
core.excludesFile resolved with the user's HOME is passed in absolute. The omp
|
|
20
20
|
child keeps the user's env - it needs its own credentials.
|
|
21
21
|
|
|
22
|
-
Exit 0 ok; 2
|
|
22
|
+
Exit 0 ok; 2 on misuse (an unknown flag, --commit beside --from/--to) or a missing
|
|
23
|
+
tool (`ocr` or `omp`); 3 when a path OCR could read looks
|
|
23
24
|
like a secret (its file_read could send it to the model; --allow-secrets skips
|
|
24
25
|
the check) - the working tree with untracked and ignored files, or the tree of
|
|
25
26
|
--to / --commit; 4 when the runner's own setup fails (not a git repository, git
|
package/bin/tezgah-optimize
CHANGED
|
@@ -302,7 +302,7 @@ def cmd_push(args):
|
|
|
302
302
|
"it out, or push an explicit ref)" % root])
|
|
303
303
|
at = time.time()
|
|
304
304
|
for row in reversed(recorded(root, "pushes")):
|
|
305
|
-
if row.get("sha")
|
|
305
|
+
if not same_commit(row.get("sha"), sha) or not isinstance(row.get("at"), (int, float)):
|
|
306
306
|
continue
|
|
307
307
|
waited = at - float(row["at"])
|
|
308
308
|
if waited < PUSH_WINDOW:
|
|
@@ -324,6 +324,9 @@ def main(argv):
|
|
|
324
324
|
if len(argv) < 2:
|
|
325
325
|
print(__doc__, file=sys.stderr)
|
|
326
326
|
return 2
|
|
327
|
+
if argv[1] in ("-h", "--help"):
|
|
328
|
+
print(__doc__.strip())
|
|
329
|
+
return 0
|
|
327
330
|
cmd, rest = argv[1], argv[2:]
|
|
328
331
|
if cmd == "next":
|
|
329
332
|
return cmd_next(rest)
|
package/bin/tezgah-pony
CHANGED
|
@@ -23,6 +23,9 @@ def main(argv):
|
|
|
23
23
|
print(__doc__.strip())
|
|
24
24
|
return 0
|
|
25
25
|
args = [a for a in argv if not a.startswith("-")]
|
|
26
|
+
if len(args) != len(argv):
|
|
27
|
+
print("%s\nlevels: %s" % (USAGE, ", ".join(PONY_LEVELS)), file=sys.stderr)
|
|
28
|
+
return 2
|
|
26
29
|
if not args:
|
|
27
30
|
print(pony_level())
|
|
28
31
|
return 0
|
package/bin/tezgah-research
CHANGED
|
@@ -496,10 +496,13 @@ def cmd_check(args):
|
|
|
496
496
|
known = tr.slugs(repo)
|
|
497
497
|
if rest and rest[0] not in known:
|
|
498
498
|
print("FAIL %s: no such research line (have: %s)"
|
|
499
|
-
% (rest[0], ", ".join(known) or "none")
|
|
499
|
+
% (rest[0], ", ".join(known) or "none"),
|
|
500
|
+
file=sys.stderr if as_json else sys.stdout)
|
|
500
501
|
return 1
|
|
501
502
|
problems = 0
|
|
502
503
|
report = {}
|
|
504
|
+
# under --json the rows already carry every FAIL/warn: stdout is the payload
|
|
505
|
+
say = (lambda _text: None) if as_json else print
|
|
503
506
|
if known or tr.stray_dirs(repo):
|
|
504
507
|
report = tr.check(repo, rest[0] if rest else None, strict=strict,
|
|
505
508
|
all_lines=all_lines, code=checkout_root(os.getcwd()),
|
|
@@ -507,15 +510,15 @@ def cmd_check(args):
|
|
|
507
510
|
for slug, row in report.items():
|
|
508
511
|
for err in row["errors"]:
|
|
509
512
|
problems += 1
|
|
510
|
-
|
|
513
|
+
say("FAIL %s: %s" % (slug, err))
|
|
511
514
|
for warn in row["warnings"]:
|
|
512
|
-
|
|
515
|
+
say("warn %s: %s" % (slug, warn))
|
|
513
516
|
# rules 8's report lines (certainty, stats, effect, budget, ...): never
|
|
514
517
|
# problems, and inside the JSON row rather than before it
|
|
515
518
|
for line in [] if as_json else row.get("report", []):
|
|
516
519
|
print(line)
|
|
517
520
|
else:
|
|
518
|
-
|
|
521
|
+
say("no research line under %s" % tr.root(repo))
|
|
519
522
|
lines = len(report)
|
|
520
523
|
if ask_orx:
|
|
521
524
|
# The registry is the repository's, not a line's, so this runs whatever the
|
|
@@ -523,9 +526,9 @@ def cmd_check(args):
|
|
|
523
526
|
report["orx"] = tr.check_orx(repo, tp.orx_bin(), strict=strict)
|
|
524
527
|
for err in report["orx"]["errors"]:
|
|
525
528
|
problems += 1
|
|
526
|
-
|
|
529
|
+
say("FAIL orx: %s" % err)
|
|
527
530
|
for warn in report["orx"]["warnings"]:
|
|
528
|
-
|
|
531
|
+
say("warn orx: %s" % warn)
|
|
529
532
|
if as_json:
|
|
530
533
|
# The payload is the whole answer for a machine caller: a sentence after
|
|
531
534
|
# it is not JSON, and one before it is prose the reader never asked for.
|
|
@@ -1216,6 +1219,9 @@ def main(argv):
|
|
|
1216
1219
|
print(__doc__, file=sys.stderr)
|
|
1217
1220
|
return 2
|
|
1218
1221
|
cmd, rest = argv[1], argv[2:]
|
|
1222
|
+
if cmd in ("-h", "--help"):
|
|
1223
|
+
print(__doc__.strip())
|
|
1224
|
+
return 0
|
|
1219
1225
|
if cmd == "--all":
|
|
1220
1226
|
return cmd_all(rest)
|
|
1221
1227
|
if cmd == "init":
|
package/bin/tezgah-rollback
CHANGED
package/bin/tezgah-route
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
tezgah-route --check the table's age and refresh flags
|
|
6
6
|
tezgah-route --refresh re-read prices/ids (OpenRouter),
|
|
7
7
|
resolve opencode selectors
|
|
8
|
-
tezgah-route --mode auto|anthropic|zai|any|openai|off
|
|
8
|
+
tezgah-route --mode auto|anthropic|zai|any|experientiallabs|openai|off
|
|
9
9
|
omp's family, then re-apply omp
|
|
10
10
|
tezgah-route --health probe the other funded families;
|
|
11
11
|
one at a limit leaves the chain
|