@r1z4x/tezgah 1.11.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 +507 -1
- package/MANIFEST +40 -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 +18 -10
- 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 +158 -42
- 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 +51 -10
- package/bin/tezgah-rollback +3 -0
- package/bin/tezgah-route +1 -1
- package/bin/tezgah-setup +1741 -95
- package/bin/tezgah-status +11 -1
- package/bin/tezgah-task +58 -17
- 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 +12 -6
- package/docs/contract.md +83 -34
- package/docs/environment.md +59 -24
- package/docs/evidence.md +128 -25
- package/docs/gate.md +77 -23
- package/docs/glossary.md +1 -1
- package/docs/hosts.md +343 -48
- package/docs/layers.md +2 -2
- package/docs/models.md +6 -0
- package/docs/operations.md +83 -29
- package/docs/research.md +37 -11
- package/docs/skills.md +31 -8
- package/docs/status-line.md +39 -23
- package/docs/testing.md +50 -17
- package/docs/vision.md +6 -2
- package/hooks/hooks.json +1 -1
- package/hooks/lang/en.json +16 -2
- package/hooks/lang/tr.json +14 -2
- package/hooks/projects-posttooluse.py +11 -3
- package/hooks/projects-stop.py +3 -2
- package/hooks/tezgah_agents.py +27 -3
- package/hooks/tezgah_attest.py +79 -10
- package/hooks/tezgah_context.py +61 -31
- package/hooks/tezgah_gate.py +480 -126
- package/hooks/tezgah_integrity.py +577 -133
- package/hooks/tezgah_lessons.py +5 -2
- package/hooks/tezgah_paths.py +60 -8
- package/hooks/tezgah_policy.py +48 -18
- package/hooks/tezgah_replay.py +8 -2
- package/hooks/tezgah_research.py +167 -24
- package/hooks/tezgah_skill_pick.py +1 -1
- package/hooks/tezgah_snapshot.py +6 -3
- package/hooks/tezgah_stack.py +236 -1
- package/hooks/tezgah_stopspec.py +2 -1
- package/hooks/tezgah_task.py +2 -1
- package/hosts/amp/hook.py +217 -0
- package/hosts/amp/tezgah-plugin.ts.in +396 -0
- package/hosts/antigravity/hook.py +255 -0
- package/hosts/codex/hook.py +3 -2
- package/hosts/commandcode/hook.py +239 -0
- package/hosts/copilot/hook.py +284 -0
- package/hosts/cursor/hook.py +4 -2
- package/hosts/droid/hook.py +259 -0
- package/hosts/dsh/hooks.json +1 -1
- package/hosts/gemini/hook.py +213 -0
- package/hosts/goose/hook.py +145 -0
- package/hosts/omp/hook.py +5 -2
- package/hosts/omp/tezgah-hook.ts.in +157 -42
- package/hosts/opencode/plugins/tezgah.js +209 -63
- package/hosts/qwen/hook.py +206 -0
- package/output-styles/tezgah.md +11 -6
- 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/harness/SKILL.md +2 -0
- package/skills/i-have-adhd/SKILL.md +10 -1
- package/skills/plan-add/SKILL.md +5 -1
- package/skills/plan-add/render_table.py +7 -0
- package/skills/pm-frameworks/LICENSE +21 -0
- package/skills/research/SKILL.md +22 -9
- package/skills/rl-env/LICENSE +201 -0
- package/skills/tezgah-contract/SKILL.md +21 -8
- package/statusline.py +20 -9
- 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/workflows/graph-review.js +14 -2
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
|
-
# cell reads: `hooks/tezgah_policy.py:
|
|
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
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
tezgah-research verdict <slug> <id> met|not-met|unanswerable --evidence "<ref>"
|
|
26
26
|
record one success criterion's verdict
|
|
27
27
|
tezgah-research conclude <slug> conclude a line whose criteria all carry a verdict
|
|
28
|
+
tezgah-research choose <slug> <decision> <variant> --ack "<the owner's words>"
|
|
29
|
+
record the owner's pick of a recommended variant
|
|
28
30
|
tezgah-research migrate-layout move flat research/<slug> into open/ or done/
|
|
29
31
|
tezgah-research close <slug> --limit "<reason>" conclude a line as a deliberate limit
|
|
30
32
|
tezgah-research seal <slug> [--history-lost] --ack "<owner decision>"
|
|
@@ -121,14 +123,20 @@ no status, a design/plan/analysis/code deliverable with no decision comparing it
|
|
|
121
123
|
variants - printing every open slug with its reasons. `--allow-open "<reason>"`
|
|
122
124
|
opens the new line anyway and writes the reason into its `log.md` as the first
|
|
123
125
|
entry; an empty reason is misuse, and the hatch is refused while any open line has
|
|
124
|
-
`check` errors
|
|
126
|
+
`check` errors other than a criterion whose verdict is still `pending` (a line in
|
|
127
|
+
flight). Both refusals say that a line another session is running goes to the
|
|
128
|
+
owner, not to this session. `close <slug> --limit "<reason>"` concludes a line on purpose,
|
|
125
129
|
writing the reasons it was still open into `state.json` `closed` and `log.md`.
|
|
126
130
|
`--supersedes <old>` opens a new version of an existing line, which then carries
|
|
127
131
|
the old deliverable as a variant; a line asking an existing line's exact question
|
|
128
132
|
without it is refused. `compare` appends one cell to
|
|
129
133
|
`decisions/<decision>/comparison.jsonl` under the rule `check` applies to it.
|
|
134
|
+
A rules-9 decision.md writes the line's pick as `recommended:`; `choose` records
|
|
135
|
+
the owner's `chosen:` with an `owner:` line holding their words, and `conclude`
|
|
136
|
+
names each recommendation still waiting for it.
|
|
130
137
|
`check` prints the same list of open lines after the per-line report, and so does
|
|
131
|
-
`status`, so neither state has to be discovered by being refused.
|
|
138
|
+
`status`, so neither state has to be discovered by being refused. Both also fail
|
|
139
|
+
a hidden dir under `research/`, which no line may live in.
|
|
132
140
|
|
|
133
141
|
`init --method exploratory` (rules 7) opens a campaign instead of a question:
|
|
134
142
|
`--arena` and `--budget` (and, for a measurable arena, `--evaluator` with its
|
|
@@ -286,6 +294,7 @@ def cmd_init(args):
|
|
|
286
294
|
print(" %s: %s" % (name, "; ".join(reasons)))
|
|
287
295
|
print('to open %s anyway, say why: tezgah-research init %s '
|
|
288
296
|
'--allow-open "<reason>"' % (slug, slug))
|
|
297
|
+
print(tr.OWNER_ASK)
|
|
289
298
|
return 1
|
|
290
299
|
broken = tr.broken_open_lines(repo) if unfinished else []
|
|
291
300
|
if broken:
|
|
@@ -297,6 +306,7 @@ def cmd_init(args):
|
|
|
297
306
|
print(" %s: %d error(s), first: %s" % (name, len(errors), errors[0]))
|
|
298
307
|
print('fix them, or record what is left: tezgah-research close <slug> '
|
|
299
308
|
'--limit "<reason>"')
|
|
309
|
+
print(tr.OWNER_ASK)
|
|
300
310
|
return 1
|
|
301
311
|
twins = [t for t in tr.serial_twins(repo, slug, {"question": question})
|
|
302
312
|
if t != supersedes]
|
|
@@ -486,26 +496,29 @@ def cmd_check(args):
|
|
|
486
496
|
known = tr.slugs(repo)
|
|
487
497
|
if rest and rest[0] not in known:
|
|
488
498
|
print("FAIL %s: no such research line (have: %s)"
|
|
489
|
-
% (rest[0], ", ".join(known) or "none")
|
|
499
|
+
% (rest[0], ", ".join(known) or "none"),
|
|
500
|
+
file=sys.stderr if as_json else sys.stdout)
|
|
490
501
|
return 1
|
|
491
502
|
problems = 0
|
|
492
503
|
report = {}
|
|
493
|
-
|
|
504
|
+
# under --json the rows already carry every FAIL/warn: stdout is the payload
|
|
505
|
+
say = (lambda _text: None) if as_json else print
|
|
506
|
+
if known or tr.stray_dirs(repo):
|
|
494
507
|
report = tr.check(repo, rest[0] if rest else None, strict=strict,
|
|
495
508
|
all_lines=all_lines, code=checkout_root(os.getcwd()),
|
|
496
509
|
flags={"online": online, "orx": ask_orx})
|
|
497
510
|
for slug, row in report.items():
|
|
498
511
|
for err in row["errors"]:
|
|
499
512
|
problems += 1
|
|
500
|
-
|
|
513
|
+
say("FAIL %s: %s" % (slug, err))
|
|
501
514
|
for warn in row["warnings"]:
|
|
502
|
-
|
|
515
|
+
say("warn %s: %s" % (slug, warn))
|
|
503
516
|
# rules 8's report lines (certainty, stats, effect, budget, ...): never
|
|
504
517
|
# problems, and inside the JSON row rather than before it
|
|
505
518
|
for line in [] if as_json else row.get("report", []):
|
|
506
519
|
print(line)
|
|
507
520
|
else:
|
|
508
|
-
|
|
521
|
+
say("no research line under %s" % tr.root(repo))
|
|
509
522
|
lines = len(report)
|
|
510
523
|
if ask_orx:
|
|
511
524
|
# The registry is the repository's, not a line's, so this runs whatever the
|
|
@@ -513,9 +526,9 @@ def cmd_check(args):
|
|
|
513
526
|
report["orx"] = tr.check_orx(repo, tp.orx_bin(), strict=strict)
|
|
514
527
|
for err in report["orx"]["errors"]:
|
|
515
528
|
problems += 1
|
|
516
|
-
|
|
529
|
+
say("FAIL orx: %s" % err)
|
|
517
530
|
for warn in report["orx"]["warnings"]:
|
|
518
|
-
|
|
531
|
+
say("warn orx: %s" % warn)
|
|
519
532
|
if as_json:
|
|
520
533
|
# The payload is the whole answer for a machine caller: a sentence after
|
|
521
534
|
# it is not JSON, and one before it is prose the reader never asked for.
|
|
@@ -1067,6 +1080,28 @@ def cmd_conclude(args):
|
|
|
1067
1080
|
print("FAIL conclude: %s" % problem)
|
|
1068
1081
|
return 1
|
|
1069
1082
|
print("concluded %s; it moved to research/done/%s" % (slug, slug))
|
|
1083
|
+
for decision, variant in tr.owner_waits(repo, slug):
|
|
1084
|
+
print("report to the owner: %s is recommended, not chosen; building it waits "
|
|
1085
|
+
"for the owner's answer (tezgah-research choose %s %s %s --ack "
|
|
1086
|
+
"\"<their words>\")" % (variant, slug, decision, variant))
|
|
1087
|
+
return 0
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def cmd_choose(args):
|
|
1091
|
+
"""Record the owner's pick among a decision's produced variants."""
|
|
1092
|
+
usage = 'choose <slug> <decision> <variant> --ack "<the owner\'s words>"'
|
|
1093
|
+
if len(args) != 5 or args[3] != "--ack":
|
|
1094
|
+
return misuse(usage)
|
|
1095
|
+
slug, decision, variant, _flag, ack = args
|
|
1096
|
+
repo = repo_root(os.getcwd())
|
|
1097
|
+
if slug not in tr.slugs(repo):
|
|
1098
|
+
return misuse("no such research line: %s" % slug)
|
|
1099
|
+
problem = tr.choose_line(repo, slug, decision, variant, ack,
|
|
1100
|
+
time.strftime("%Y-%m-%d"))
|
|
1101
|
+
if problem:
|
|
1102
|
+
print("FAIL choose: %s" % problem)
|
|
1103
|
+
return 1
|
|
1104
|
+
print("decisions/%s: the owner chose %s" % (decision, variant))
|
|
1070
1105
|
return 0
|
|
1071
1106
|
|
|
1072
1107
|
|
|
@@ -1184,6 +1219,9 @@ def main(argv):
|
|
|
1184
1219
|
print(__doc__, file=sys.stderr)
|
|
1185
1220
|
return 2
|
|
1186
1221
|
cmd, rest = argv[1], argv[2:]
|
|
1222
|
+
if cmd in ("-h", "--help"):
|
|
1223
|
+
print(__doc__.strip())
|
|
1224
|
+
return 0
|
|
1187
1225
|
if cmd == "--all":
|
|
1188
1226
|
return cmd_all(rest)
|
|
1189
1227
|
if cmd == "init":
|
|
@@ -1212,6 +1250,8 @@ def main(argv):
|
|
|
1212
1250
|
return cmd_verdict(rest)
|
|
1213
1251
|
if cmd == "conclude":
|
|
1214
1252
|
return cmd_conclude(rest)
|
|
1253
|
+
if cmd == "choose":
|
|
1254
|
+
return cmd_choose(rest)
|
|
1215
1255
|
if cmd == "migrate-layout":
|
|
1216
1256
|
return cmd_migrate_layout(rest)
|
|
1217
1257
|
if cmd == "close":
|
|
@@ -1237,7 +1277,8 @@ def main(argv):
|
|
|
1237
1277
|
if cmd in commands:
|
|
1238
1278
|
return commands[cmd](repo_root(os.getcwd()), rest)
|
|
1239
1279
|
return misuse("unknown command %r (init, commit, check, status, --all, claim, predict, "
|
|
1240
|
-
"components, migrate, source, compare, verdict, conclude,
|
|
1280
|
+
"components, migrate, source, compare, verdict, conclude, choose, "
|
|
1281
|
+
"migrate-layout, "
|
|
1241
1282
|
"close, seal, import, next, idea, stage, match, board, dedup, yield, cite, "
|
|
1242
1283
|
"fetch, search, instrument, gold, label, switch, release-check, "
|
|
1243
1284
|
"calibration)" % cmd)
|
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
|