@r1z4x/tezgah 1.0.0 → 1.1.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 +71 -1
- package/MANIFEST +8 -0
- package/bin/tezgah-agents +10 -1
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-doctor +11 -0
- package/bin/tezgah-research +31 -14
- package/bin/tezgah-setup +154 -29
- package/bin/tezgah-skill +59 -0
- package/bin/tezgah-skill.py +59 -0
- package/bin/tezgah-status +62 -1
- package/bin/tezgah-triage +5 -3
- package/docs/README.md +1 -0
- package/docs/architecture.md +5 -2
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +9 -9
- package/docs/contract.md +68 -26
- package/docs/environment.md +6 -3
- package/docs/evidence.md +7 -0
- package/docs/gate.md +13 -7
- package/docs/glossary.md +1 -1
- package/docs/hosts.md +48 -1
- package/docs/index.json +29 -1
- package/docs/judge.md +61 -55
- package/docs/models.md +14 -8
- package/docs/operations.md +10 -5
- package/docs/orca.md +126 -0
- package/docs/research.md +38 -3
- package/docs/skills.md +74 -0
- package/docs/status-line.md +3 -1
- package/docs/testing.md +33 -21
- package/docs/vision.md +12 -8
- package/hooks/projects-pretooluse.py +17 -5
- package/hooks/tezgah_agents.py +135 -13
- package/hooks/tezgah_components.py +3 -0
- package/hooks/tezgah_context.py +65 -22
- package/hooks/tezgah_gate.py +133 -17
- package/hooks/tezgah_integrity.py +60 -13
- package/hooks/tezgah_judge.py +239 -56
- package/hooks/tezgah_models.py +41 -5
- package/hooks/tezgah_orca.py +144 -0
- package/hooks/tezgah_paths.py +40 -6
- package/hooks/tezgah_policy.py +109 -35
- package/hooks/tezgah_research.py +104 -8
- package/hooks/tezgah_skill_pick.py +459 -8
- package/hooks/tezgah_stack.py +382 -0
- package/hooks/tezgah_stopspec.py +2 -2
- package/hosts/codex/hook.py +7 -1
- package/hosts/cursor/hook.py +7 -4
- package/hosts/omp/hook.py +12 -6
- package/hosts/omp/tezgah-hook.ts.in +20 -7
- package/hosts/opencode/plugins/tezgah.js +18 -9
- package/output-styles/tezgah.md +34 -19
- package/package.json +1 -1
- package/skills/i-have-adhd/SKILL.md +2 -2
- package/skills/research/SKILL.md +60 -17
- package/skills/tezgah-contract/SKILL.md +55 -13
package/hooks/tezgah_context.py
CHANGED
|
@@ -15,6 +15,7 @@ import sys
|
|
|
15
15
|
import time
|
|
16
16
|
|
|
17
17
|
import tezgah_embed
|
|
18
|
+
import tezgah_orca
|
|
18
19
|
import tezgah_research
|
|
19
20
|
from tezgah_guard import import_crash_mark
|
|
20
21
|
from tezgah_integrity import (STEP_KINDS, _heredocs, _path as _ledger_path,
|
|
@@ -333,6 +334,15 @@ PROMPT_HINTS = (
|
|
|
333
334
|
r"nerede (kullanıl|çağrıl)\w*|tanım\w* nerede|kullanım yerleri|"
|
|
334
335
|
r"çağrı (zinciri|grafiği)|bağımlılık (grafiği|ağacı)|"
|
|
335
336
|
r"yeniden adlandır\w*)\b"),
|
|
337
|
+
# Two or more independent items: a numbered or bulleted list of at least
|
|
338
|
+
# two entries on their own lines, or the explicit delegation ask. The owner
|
|
339
|
+
# had to say "do everything with subagents and in parallel" on every task
|
|
340
|
+
# because nothing armed the fan-out rule from the shape of the request.
|
|
341
|
+
# `parallelize` alone is code work, so only the delegation phrasings count.
|
|
342
|
+
("fanout", r"(?ms:^[ \t]*\d+[.)][ \t]+\S.*?^[ \t]*\d+[.)][ \t]+\S)|"
|
|
343
|
+
r"(?ms:^[ \t]*[-*•][ \t]+\S.*?^[ \t]*[-*•][ \t]+\S)|"
|
|
344
|
+
r"\b(?:in parallel|paralel\w*|sub-?agents?|alt ?ajan\w*|"
|
|
345
|
+
r"worktrees?|fan[- ]?out|orchestrat\w*|orkestra\w*)\b"),
|
|
336
346
|
)
|
|
337
347
|
|
|
338
348
|
# the detached auto-index worker (lock-guarded, retrying); same dir as this file
|
|
@@ -363,6 +373,7 @@ CORE_RULES = (
|
|
|
363
373
|
("attribution", "**No AI attribution, ever, on any host.**"),
|
|
364
374
|
("lang", "**Identifiers and messages stay English.**"),
|
|
365
375
|
("workspace", "**Workspace: `.tezgah/` only.**"),
|
|
376
|
+
("fanout", "**Parallel by default: fan out independent items.**"),
|
|
366
377
|
)
|
|
367
378
|
|
|
368
379
|
# The per-turn reminder's clause for each switchable rule, matched against
|
|
@@ -1226,6 +1237,7 @@ def switches(cwd):
|
|
|
1226
1237
|
drop.add("product")
|
|
1227
1238
|
disabled.append("research-off")
|
|
1228
1239
|
if off("orchestrate-off"):
|
|
1240
|
+
drop.add("fanout")
|
|
1229
1241
|
disabled.append("orchestrate-off")
|
|
1230
1242
|
if off("lang-off"):
|
|
1231
1243
|
drop.add("lang")
|
|
@@ -1368,12 +1380,17 @@ def session_of(payload):
|
|
|
1368
1380
|
# 4000 B this rule's own 282 B dropped `consult`, held at 4400 B a 111 B core
|
|
1369
1381
|
# growth dropped it again, and at 5000 B the brief's operative clauses (the
|
|
1370
1382
|
# inter-agent language and the lessons instruction, plan 056) would have - bloat
|
|
1371
|
-
# paid for with a rule, which is the failure this bound exists to prevent.
|
|
1383
|
+
# paid for with a rule, which is the failure this bound exists to prevent. The
|
|
1384
|
+
# same reasoning moved the two session budgets on 2026-10-07: the ponytail and
|
|
1385
|
+
# ADHD rules inline from the first turn (+1042 B), the fan-out pointer (+112 B)
|
|
1386
|
+
# and the `tezgah-skill` line (+113 B) took the healthy fixture's session_start
|
|
1387
|
+
# from 10579 B to 12059 B, so at 12000 B a healthy repo gave up its lessons
|
|
1388
|
+
# and sibling checkouts to pay for core rules; 13000 B restores the headroom. A
|
|
1372
1389
|
# budget in bytes, not tokens: this file has no tokenizer and a wrong estimate
|
|
1373
1390
|
# would be worse than a bound.
|
|
1374
|
-
CONTEXT_BUDGET = {"session_start":
|
|
1391
|
+
CONTEXT_BUDGET = {"session_start": 13000, "post_compact": 13000,
|
|
1375
1392
|
"subagent_start": 5500, "user_prompt": 6000}
|
|
1376
|
-
DEFAULT_BUDGET =
|
|
1393
|
+
DEFAULT_BUDGET = 13000
|
|
1377
1394
|
# The blocks in the order they are given up when the budget is exceeded, lowest
|
|
1378
1395
|
# value first: text another surface already carries (the plan table lives in the
|
|
1379
1396
|
# plan-status skill, the sibling checkouts in `tezgah-research --all`, the
|
|
@@ -1395,7 +1412,7 @@ DEFAULT_BUDGET = 12000
|
|
|
1395
1412
|
# spend them would turn bloat into rule loss - which is the failure the budget
|
|
1396
1413
|
# exists to prevent, not one it may cause.
|
|
1397
1414
|
DROP_ORDER = ("knowledge", "worktrees", "lessons", "skill", "lessons_turn",
|
|
1398
|
-
"plans", "subagents", "steer", "consult", "research",
|
|
1415
|
+
"plans", "subagents", "steer", "orca", "consult", "research",
|
|
1399
1416
|
"research_broken", "graph", "offnote", "orchestrate", "index",
|
|
1400
1417
|
"resume", "scratch", "task", "delta", "pointer")
|
|
1401
1418
|
|
|
@@ -1987,9 +2004,14 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
1987
2004
|
# model as truncated one-liners, so which entry to look at first is the one
|
|
1988
2005
|
# thing this turn cannot work out for itself. One call, one line, cached per
|
|
1989
2006
|
# prompt - and nothing is appended when the judge is gone, the answer is
|
|
1990
|
-
# `none`, or the arming file is absent (hooks/tezgah_skill_pick).
|
|
2007
|
+
# `none`, or the arming file is absent (hooks/tezgah_skill_pick). When
|
|
2008
|
+
# it gives no line, the model-free section search may: default on, one
|
|
2009
|
+
# line at most, each section once per session (`section_hint`).
|
|
1991
2010
|
if prompt and tezgah_skill_pick:
|
|
1992
2011
|
hint = tezgah_skill_pick.suggest(prompt, session_id)
|
|
2012
|
+
if not hint:
|
|
2013
|
+
host = (payload or {}).get("host") if isinstance(payload, dict) else None
|
|
2014
|
+
hint = tezgah_skill_pick.section_hint(prompt, session_id, host=host)
|
|
1993
2015
|
if hint:
|
|
1994
2016
|
parts.append(("skill", hint))
|
|
1995
2017
|
# C1 and C3 ride this turn because it is the only channel a live state
|
|
@@ -2074,8 +2096,18 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2074
2096
|
# indexers on the same repo, so only the parent session triggers one.
|
|
2075
2097
|
status = (autoindex(root) if event != "subagent_start"
|
|
2076
2098
|
else "index handled by the parent session")
|
|
2077
|
-
|
|
2078
|
-
|
|
2099
|
+
# The tool by the name this host calls it: a session whose prompt arms
|
|
2100
|
+
# no graph rule otherwise never learns which tool the graph is, and
|
|
2101
|
+
# omp's own prompt sends code discovery to `find`. A subagent gets the
|
|
2102
|
+
# CLI (omp's MCP device refuses concurrent writes, see tezgah_agents).
|
|
2103
|
+
from tezgah_gate import graph_tool # lazy: keep hook import cost minimal
|
|
2104
|
+
how = ("`codegraph explore|callers|impact <symbol>` in a shell"
|
|
2105
|
+
if event == "subagent_start"
|
|
2106
|
+
else graph_tool((payload or {}).get("host")))
|
|
2107
|
+
parts.append(("graph", "Graph index: %s (project %s). Definitions, "
|
|
2108
|
+
"callers and blast radius: %s first; grep only for "
|
|
2109
|
+
"literal text, configs and docs."
|
|
2110
|
+
% (status, slug(root), how)))
|
|
2079
2111
|
else:
|
|
2080
2112
|
parts.append(("graph", "Graph: codegraph is not installed, so use "
|
|
2081
2113
|
"grep/find and say the answer came from text "
|
|
@@ -2131,6 +2163,11 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2131
2163
|
siblings = sibling_line(root) if root not in roots() else ""
|
|
2132
2164
|
if siblings:
|
|
2133
2165
|
parts.append(("worktrees", siblings))
|
|
2166
|
+
# Inside Orca a checkout or a long job belongs to Orca, or its sidebar
|
|
2167
|
+
# never shows it; env only, so this forks nothing.
|
|
2168
|
+
orca = tezgah_orca.context_line()
|
|
2169
|
+
if orca:
|
|
2170
|
+
parts.append(("orca", orca))
|
|
2134
2171
|
# The project's own rule files, agents and skills (tezgah-migrate's
|
|
2135
2172
|
# index): they stay where the project keeps them, and one line makes a
|
|
2136
2173
|
# session read the rows its task touches instead of never seeing them.
|
|
@@ -2538,30 +2575,36 @@ def index_notice(cwd):
|
|
|
2538
2575
|
says less than "↻" but still says something the turn needs: no stamp, or no
|
|
2539
2576
|
readable HEAD, means the graph's age is unknown rather than current, and
|
|
2540
2577
|
staying silent there is what let an unverifiable index answer with the
|
|
2541
|
-
index's authority.
|
|
2578
|
+
index's authority.
|
|
2579
|
+
|
|
2580
|
+
The line used to stop there ("re-index before trusting one, or say the
|
|
2581
|
+
answer came from text search") and start nothing: the index is stamped only
|
|
2582
|
+
by the session-start worker, so every turn after a commit told the model to
|
|
2583
|
+
leave the graph for grep. A stale or unknown stamp now starts the same
|
|
2584
|
+
lock-guarded incremental worker the session start runs (`autoindex`), and
|
|
2585
|
+
the line says so."""
|
|
2542
2586
|
base, _marks = repo_marks(cwd)
|
|
2543
2587
|
mark = index_mark(cwd, base) if base else ""
|
|
2544
|
-
if mark
|
|
2545
|
-
return ("Graph index: this session cannot compare the graph to HEAD (no "
|
|
2546
|
-
"readable index stamp), so the graph's age is unknown - it may "
|
|
2547
|
-
"describe code that has moved since. Re-index before trusting a "
|
|
2548
|
-
"graph answer, or say the answer came from text search.")
|
|
2549
|
-
if mark != "\u21bb":
|
|
2588
|
+
if mark not in ("?", "\u21bb"):
|
|
2550
2589
|
return ""
|
|
2590
|
+
status = autoindex(repo_root(cwd))
|
|
2591
|
+
keep = ("Keep using the graph for code discovery; a file changed since the "
|
|
2592
|
+
"last index may still show its older source there, so read that "
|
|
2593
|
+
"file itself before editing it.")
|
|
2594
|
+
if mark == "?":
|
|
2595
|
+
return ("Graph index: the graph's age is unknown (no readable index "
|
|
2596
|
+
"stamp); %s. %s" % (status, keep))
|
|
2551
2597
|
from tezgah_gate import index_slug # lazy: keep hook import cost minimal
|
|
2552
2598
|
slug = index_slug(cwd, base)
|
|
2553
2599
|
try:
|
|
2554
2600
|
with open(os.path.join(cache_dir(), slug), encoding="utf-8") as fh:
|
|
2555
2601
|
stamped = fh.read().strip()
|
|
2556
2602
|
except OSError:
|
|
2557
|
-
|
|
2558
|
-
head = git(repo_root(cwd), "rev-parse", "HEAD")
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
"behind, so a graph answer may describe code that has moved since. "
|
|
2563
|
-
"Re-index before trusting one, or say the answer came from text "
|
|
2564
|
-
"search." % (stamped[:7], head[:7]))
|
|
2603
|
+
stamped = ""
|
|
2604
|
+
head = git(repo_root(cwd), "rev-parse", "HEAD") or ""
|
|
2605
|
+
return ("Graph index: the graph is indexed at %s, HEAD is %s; %s "
|
|
2606
|
+
"(`codegraph sync`, incremental). %s"
|
|
2607
|
+
% (stamped[:7] or "?", head[:7] or "?", status, keep))
|
|
2565
2608
|
|
|
2566
2609
|
|
|
2567
2610
|
def plan_mark(cwd, base):
|
package/hooks/tezgah_gate.py
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
Rules, all only inside a tezgah root:
|
|
5
5
|
1. a grep-only explorer subagent is refused, with the graph tools named as the
|
|
6
6
|
replacement;
|
|
7
|
-
2. the FIRST
|
|
8
|
-
through a shell
|
|
9
|
-
|
|
7
|
+
2. the FIRST code-symbol search of a session (grep/search/find tool, or
|
|
8
|
+
grep/rg run through a shell; a plain word is literal text and passes) in a
|
|
9
|
+
repo whose own codegraph index exists is nudged toward the graph once;
|
|
10
|
+
every later one passes, with graph_advice's line beside it while the index
|
|
11
|
+
is current.
|
|
10
12
|
3. a git/gh command that writes an artifact carrying an AI/model credit
|
|
11
13
|
(Co-Authored-By, "Generated with", a robot emoji, ...) is refused.
|
|
12
14
|
4. an identical call that already failed LOOP_ATTEMPTS times in the current
|
|
@@ -170,7 +172,16 @@ try: # The language rule's detector: the letter set and the word list, in their
|
|
|
170
172
|
except ImportError: # pragma: no cover - only where the module has not landed
|
|
171
173
|
tezgah_lang = None
|
|
172
174
|
|
|
173
|
-
|
|
175
|
+
# A search for a code symbol, the shape the graph answers: an identifier with an
|
|
176
|
+
# inner underscore or a lower-to-upper step (snake_case, camelCase), or any
|
|
177
|
+
# identifier inside a definition or call pattern (`def run`, `run\(`). A plain
|
|
178
|
+
# word (`TODO`, `error`) is literal text, so it is never nudged or advised.
|
|
179
|
+
SEARCH_SYMBOL = re.compile(
|
|
180
|
+
r"(?:\\b)?(?:(def|class|function|func|fn|interface|struct|type)\s+)?"
|
|
181
|
+
r"([A-Za-z_][A-Za-z0-9_]{2,})(\\?\()?(?:\\b)?")
|
|
182
|
+
# omp's discovery tools: `find` takes a described behavior plus likely terms,
|
|
183
|
+
# `search` and `ast_grep` a pattern; `grep` is every host's regex tool.
|
|
184
|
+
PATTERN_SEARCH_TOOLS = ("grep", "search", "ast_grep")
|
|
174
185
|
# ponytail: flags with a separate value (grep -A 3 foo) shift the token and the
|
|
175
186
|
# search passes unnudged; not worth a real argv parser for a once-a-session hint.
|
|
176
187
|
BASH_SEARCH = re.compile(r"(?:^|[|;&(]\s*|\s)(?:grep|rg)\s+((?:-\S+\s+)*)(\S+)")
|
|
@@ -481,18 +492,35 @@ def explored(subagent_type):
|
|
|
481
492
|
return str(subagent_type or "").lower() in ("explore", "explorer")
|
|
482
493
|
|
|
483
494
|
|
|
484
|
-
def
|
|
485
|
-
"""The
|
|
486
|
-
|
|
495
|
+
def symbol_in(text):
|
|
496
|
+
"""The code symbol a search string names, or None for literal text."""
|
|
497
|
+
m = SEARCH_SYMBOL.fullmatch(str(text or "").strip().strip("'\""))
|
|
498
|
+
if not m:
|
|
499
|
+
return None
|
|
500
|
+
tok = m.group(2)
|
|
501
|
+
if (m.group(1) or m.group(3) or "_" in tok.strip("_")
|
|
502
|
+
or re.search(r"[a-z][A-Z]", tok)):
|
|
503
|
+
return tok
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def searched_symbol(tool, inp):
|
|
508
|
+
"""The code symbol this call searches for, or None. Host tool names differ
|
|
509
|
+
in case (Claude `Grep`/`Bash`, Codex/dsh/omp `bash`), so match lowercased.
|
|
510
|
+
omp's `find` is read too: its own prompt sends code discovery there first,
|
|
511
|
+
so a rule that read only grep never met an omp session."""
|
|
487
512
|
t = str(tool or "").lower()
|
|
488
|
-
if t
|
|
489
|
-
|
|
490
|
-
|
|
513
|
+
if t in PATTERN_SEARCH_TOOLS:
|
|
514
|
+
return symbol_in(inp.get("pattern", ""))
|
|
515
|
+
if t == "find" and "query" in inp:
|
|
516
|
+
words = list(inp.get("grep_keywords") or [])
|
|
517
|
+
words += re.findall(r"[A-Za-z_][A-Za-z0-9_]{2,}\(?", str(inp["query"]))
|
|
518
|
+
return next(filter(None, map(symbol_in, words)), None)
|
|
491
519
|
if t in ("bash", "shell"):
|
|
492
520
|
for m in BASH_SEARCH.finditer(str(inp.get("command", ""))):
|
|
493
|
-
|
|
494
|
-
if
|
|
495
|
-
return
|
|
521
|
+
symbol = symbol_in(m.group(2))
|
|
522
|
+
if symbol:
|
|
523
|
+
return symbol
|
|
496
524
|
return None
|
|
497
525
|
|
|
498
526
|
|
|
@@ -509,15 +537,56 @@ def index_slug(cwd, base):
|
|
|
509
537
|
one, so its own `.codegraph` is read) - and the name returned is still the
|
|
510
538
|
key the index worker stamps HEAD under, so index_mark and index_notice keep
|
|
511
539
|
reading the same file."""
|
|
540
|
+
d = index_dir(cwd, base)
|
|
541
|
+
return re.sub(r"[^A-Za-z0-9]+", "-", d).strip("-") if d else None
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def index_dir(cwd, base):
|
|
545
|
+
"""The directory index_slug names: the closest repo up to `base` that holds
|
|
546
|
+
its own codegraph index, or None (none, or a `.no-graph` met first)."""
|
|
512
547
|
d = os.path.realpath(cwd)
|
|
513
548
|
base, rs = os.path.realpath(base), roots()
|
|
514
549
|
while d.startswith(base) and (d != base or base not in rs):
|
|
550
|
+
if os.path.exists(os.path.join(d, ".no-graph")):
|
|
551
|
+
return None # the repo's own switch: no graph rule here at all
|
|
515
552
|
if os.path.isfile(os.path.join(d, ".codegraph", "codegraph.db")):
|
|
516
|
-
return
|
|
553
|
+
return d
|
|
517
554
|
d = os.path.dirname(d)
|
|
518
555
|
return None
|
|
519
556
|
|
|
520
557
|
|
|
558
|
+
def _read(path):
|
|
559
|
+
with open(path, encoding="utf-8") as fh:
|
|
560
|
+
return fh.read().strip()
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def git_head(repo):
|
|
564
|
+
"""HEAD's commit read from `repo`'s git files, or None - no `git` fork,
|
|
565
|
+
because the gate pays it on a tool call. Covers a plain checkout, a linked
|
|
566
|
+
worktree (`.git` file plus `commondir`) and a packed ref."""
|
|
567
|
+
try:
|
|
568
|
+
gd = os.path.join(repo, ".git")
|
|
569
|
+
if os.path.isfile(gd):
|
|
570
|
+
gd = os.path.join(repo, _read(gd).split("gitdir:", 1)[1].strip())
|
|
571
|
+
head = _read(os.path.join(gd, "HEAD"))
|
|
572
|
+
if not head.startswith("ref: "):
|
|
573
|
+
return head or None
|
|
574
|
+
ref = head[5:]
|
|
575
|
+
common = gd
|
|
576
|
+
if os.path.isfile(os.path.join(gd, "commondir")):
|
|
577
|
+
common = os.path.join(gd, _read(os.path.join(gd, "commondir")))
|
|
578
|
+
for top in (gd, common):
|
|
579
|
+
if os.path.isfile(os.path.join(top, ref)):
|
|
580
|
+
return _read(os.path.join(top, ref))
|
|
581
|
+
with open(os.path.join(common, "packed-refs"), encoding="utf-8") as fh:
|
|
582
|
+
for line in fh:
|
|
583
|
+
if line.rstrip("\n").endswith(" " + ref):
|
|
584
|
+
return line.split()[0]
|
|
585
|
+
except (OSError, IndexError):
|
|
586
|
+
pass
|
|
587
|
+
return None
|
|
588
|
+
|
|
589
|
+
|
|
521
590
|
def nudge_mark(session_id):
|
|
522
591
|
"""The once-per-session mark: sha1(id)[:16], the key opencode's oncePerSession writes."""
|
|
523
592
|
return os.path.join(cache_dir(), "nudged", hashlib.sha1(
|
|
@@ -540,12 +609,59 @@ def first_nudge(session_id):
|
|
|
540
609
|
return False
|
|
541
610
|
|
|
542
611
|
|
|
612
|
+
def graph_tool(host=None):
|
|
613
|
+
"""The code-graph tool by the name this host's model calls it: omp mounts an
|
|
614
|
+
MCP server's tools as `xd://` devices, Claude and Codex as `mcp__<server>__`
|
|
615
|
+
(`mcp__plugin_tezgah_codegraph__` on a Claude plugin install), Cursor by
|
|
616
|
+
the bare tool name. Unknown host: the two prefixed spellings, so the line is
|
|
617
|
+
right wherever it lands."""
|
|
618
|
+
if host == "omp":
|
|
619
|
+
return "`xd://mcp__codegraph_explore`"
|
|
620
|
+
if host == "cursor":
|
|
621
|
+
return "the codegraph MCP server's `codegraph_explore`"
|
|
622
|
+
claude = ("`mcp__codegraph__codegraph_explore` (plugin install: "
|
|
623
|
+
"`mcp__plugin_tezgah_codegraph__codegraph_explore`)")
|
|
624
|
+
return claude if host else "%s, on omp `xd://mcp__codegraph_explore`" % claude
|
|
625
|
+
|
|
626
|
+
|
|
543
627
|
def nudge_reason(symbol):
|
|
544
|
-
return ("Code graph index is ready for this repo.
|
|
628
|
+
return ("Code graph index is ready for this repo. Call the MCP tool %s with "
|
|
629
|
+
"the query `%s`, or run `codegraph explore %s` (source, callers, "
|
|
545
630
|
"blast radius in one call) or `codegraph callers %s` / `codegraph impact %s` from a shell; "
|
|
546
631
|
"an omp subagent must use this CLI, not the MCP device, because omp's MCP device refuses "
|
|
547
632
|
"concurrent writes. If you need literal text, re-run this search unchanged; it will pass - "
|
|
548
|
-
"this nudge fires once per session."
|
|
633
|
+
"this nudge fires once per session."
|
|
634
|
+
% (graph_tool(), symbol, symbol, symbol, symbol))
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def graph_advice(tool, inp, cwd, host=None):
|
|
638
|
+
"""One non-blocking line for a symbol search the current graph answers, or
|
|
639
|
+
None. The refusal above is spent once per session; every later symbol
|
|
640
|
+
search gets this line beside its result instead, so the graph is named at
|
|
641
|
+
the moment the session reaches for grep. Fresh only: the stamp the index
|
|
642
|
+
worker writes must equal HEAD, because pointing at a graph that lags the
|
|
643
|
+
tree would send the session to a stale answer. A literal search, a repo
|
|
644
|
+
without its own index, `.no-graph` and `pretooluse-off` get nothing."""
|
|
645
|
+
symbol = searched_symbol(tool, inp) # first: a regex, and most calls end here
|
|
646
|
+
if not symbol or off("pretooluse-off"):
|
|
647
|
+
return None
|
|
648
|
+
base = root_for(cwd)
|
|
649
|
+
repo = index_dir(cwd, base) if base else None
|
|
650
|
+
if not repo:
|
|
651
|
+
return None
|
|
652
|
+
try:
|
|
653
|
+
stamped = _read(os.path.join(cache_dir(), index_slug(cwd, base)))
|
|
654
|
+
except OSError:
|
|
655
|
+
return None
|
|
656
|
+
head = git_head(repo)
|
|
657
|
+
if not head or stamped != head:
|
|
658
|
+
return None
|
|
659
|
+
return ("Graph: `%s` is a code symbol and this repo's code graph is current "
|
|
660
|
+
"(HEAD %s). For its definition, callers or blast radius call %s "
|
|
661
|
+
"with the query `%s` (or `codegraph callers %s` / `codegraph impact "
|
|
662
|
+
"%s` in a shell): one call returns the source and the call path "
|
|
663
|
+
"that this text search cannot."
|
|
664
|
+
% (symbol, head[:7], graph_tool(host), symbol, symbol, symbol))
|
|
549
665
|
|
|
550
666
|
|
|
551
667
|
# The identical attempts a call gets before the loop guard refuses it, whatever
|
|
@@ -2637,7 +2753,7 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
|
|
|
2637
2753
|
reason = retry_reason(tool, inp, session_id, agent)
|
|
2638
2754
|
if reason:
|
|
2639
2755
|
return _deny(session_id, "retry", reason, tool, inp, base)
|
|
2640
|
-
if symbol :=
|
|
2756
|
+
if symbol := searched_symbol(tool, inp):
|
|
2641
2757
|
slug = index_slug(cwd, base)
|
|
2642
2758
|
if slug and first_nudge(session_id):
|
|
2643
2759
|
note(session_id, "nudge", slug, id=call_id(tool, inp),
|
|
@@ -276,6 +276,9 @@ VERIFIED = re.compile(
|
|
|
276
276
|
r"\b(tested|verified|i ran|ran the (?:tests?|suite|build|lint|checks?)|"
|
|
277
277
|
r"doğruladım|test ettim|kontrol ettim|denetledim|doğrulandı|test edildi)\b",
|
|
278
278
|
re.I)
|
|
279
|
+
# The DONE forms that say a check passed ("all tests pass", "testler yeşil"):
|
|
280
|
+
# such a claim rests on a pass even when the turn only edited prose.
|
|
281
|
+
TESTED_WORD = re.compile(r"test|check|suite", re.I)
|
|
279
282
|
# an explicit admission that removes the lie: an unverified claim is allowed
|
|
280
283
|
NEGATED = re.compile(
|
|
281
284
|
r"doğrulanmadı|doğrulamadım|unverified|not verified|could ?n[o']t verify|"
|
|
@@ -3587,6 +3590,43 @@ def _changed_write(row):
|
|
|
3587
3590
|
return "hash" in row and "changed" not in row
|
|
3588
3591
|
|
|
3589
3592
|
|
|
3593
|
+
# Prose a person reads and no program runs: a write tool's edit to one owes no
|
|
3594
|
+
# check and does not stale a pass (`_prose_edit`). The owner's rule (2026-10-07)
|
|
3595
|
+
# is "a non-code change needs no suite", and the stale branch on a NOTES.md
|
|
3596
|
+
# written after green moved no outcome (PREREGISTRATION-E8 F3: 17/25 against
|
|
3597
|
+
# 18/25). `.txt` is not here: requirements.txt is code by another name.
|
|
3598
|
+
PROSE_EXT = (".md", ".markdown", ".rst", ".adoc")
|
|
3599
|
+
# Markdown an agent loads as instructions is the product, not prose: these names
|
|
3600
|
+
# and anything under these directories stay a change like any code file.
|
|
3601
|
+
INSTRUCTION_NAMES = frozenset(("skill.md", "agents.md", "claude.md", "gemini.md",
|
|
3602
|
+
"copilot-instructions.md", "rules.md"))
|
|
3603
|
+
INSTRUCTION_DIRS = frozenset(("skills", "prompts", "templates", "agents",
|
|
3604
|
+
"commands", "output-styles", "hooks", ".claude",
|
|
3605
|
+
".cursor", ".codex", ".omp", ".github"))
|
|
3606
|
+
|
|
3607
|
+
|
|
3608
|
+
def _prose_edit(row):
|
|
3609
|
+
"""True for a write tool's `edit` row whose real target is prose
|
|
3610
|
+
(`PROSE_EXT`) and not an agent instruction file (`INSTRUCTION_NAMES`,
|
|
3611
|
+
`INSTRUCTION_DIRS`). The target is the realpath `note_tool` stored, so a
|
|
3612
|
+
`.md` link over a code file is judged by the file it writes; a row with no
|
|
3613
|
+
target is never prose. ponytail: a comment-only edit to a code file is
|
|
3614
|
+
still a change - telling it apart needs the file's text before and after,
|
|
3615
|
+
which the row does not carry."""
|
|
3616
|
+
target = row.get("target")
|
|
3617
|
+
if row.get("kind") != "edit" or not target:
|
|
3618
|
+
return False
|
|
3619
|
+
parts = str(target).replace("\\", "/").lower().split("/")
|
|
3620
|
+
return (parts[-1].endswith(PROSE_EXT) and parts[-1] not in INSTRUCTION_NAMES
|
|
3621
|
+
and not INSTRUCTION_DIRS.intersection(parts[:-1]))
|
|
3622
|
+
|
|
3623
|
+
|
|
3624
|
+
def _work_row(row):
|
|
3625
|
+
"""True when the row is one step of work a check is owed for: a `WORK_KINDS`
|
|
3626
|
+
row that is not a prose edit (`_prose_edit`)."""
|
|
3627
|
+
return str(row.get("kind")) in WORK_KINDS and not _prose_edit(row)
|
|
3628
|
+
|
|
3629
|
+
|
|
3590
3630
|
def _change_row(row):
|
|
3591
3631
|
"""True when this row is one the freshness fold counts as a change to the
|
|
3592
3632
|
tree: a write tool's `edit` row, a shell call that wrote a file (`run`), or
|
|
@@ -3602,9 +3642,9 @@ def _change_row(row):
|
|
|
3602
3642
|
check in one call (`sed -i ... && pytest`, `ruff format . && pytest`) records
|
|
3603
3643
|
as the check, so it is not read as a change; a write whose target is not a
|
|
3604
3644
|
redirect carries no captured state either (tezgah_gate.write_paths names
|
|
3605
|
-
both ceilings)."""
|
|
3645
|
+
both ceilings). A prose edit (`_prose_edit`) is never one either."""
|
|
3606
3646
|
kind = str(row.get("kind"))
|
|
3607
|
-
return kind in ("edit", "run") and (
|
|
3647
|
+
return kind in ("edit", "run") and not _prose_edit(row) and (
|
|
3608
3648
|
_changed_write(row)
|
|
3609
3649
|
or (kind == "run" and format_write(str(row.get("detail") or ""))))
|
|
3610
3650
|
|
|
@@ -3663,7 +3703,7 @@ def _stale_paths(rows):
|
|
|
3663
3703
|
with no file named at all."""
|
|
3664
3704
|
names = []
|
|
3665
3705
|
for row in rows[_last_pass(rows) + 1:]:
|
|
3666
|
-
if row.get("kind") == "edit" and
|
|
3706
|
+
if row.get("kind") == "edit" and _change_row(row):
|
|
3667
3707
|
name = str(row.get("detail") or "").strip()
|
|
3668
3708
|
if len(name) > 80:
|
|
3669
3709
|
name = "..." + name[-77:]
|
|
@@ -4441,7 +4481,7 @@ def _bookkeeping_turn(rows):
|
|
|
4441
4481
|
cut it there, and the command after the cut is unread (review F1). Nor is
|
|
4442
4482
|
one that holds a redaction marker: the marker can swallow the rest of a
|
|
4443
4483
|
word and what was glued to it (`echo token=x;./regen.sh`, review N3)."""
|
|
4444
|
-
steps = [r for r in rows if
|
|
4484
|
+
steps = [r for r in rows if _work_row(r)]
|
|
4445
4485
|
return bool(steps) and all(
|
|
4446
4486
|
r.get("kind") == "run" and not _change_row(r)
|
|
4447
4487
|
and len(str(r.get("detail") or "")) < DETAIL_MAX
|
|
@@ -4456,7 +4496,7 @@ def _settled(rows):
|
|
|
4456
4496
|
last = _last_pass(rows)
|
|
4457
4497
|
if last < 0:
|
|
4458
4498
|
return False
|
|
4459
|
-
after = [r for r in rows[last + 1:] if
|
|
4499
|
+
after = [r for r in rows[last + 1:] if _work_row(r)]
|
|
4460
4500
|
return not after or _bookkeeping_turn(after)
|
|
4461
4501
|
|
|
4462
4502
|
|
|
@@ -4528,7 +4568,6 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4528
4568
|
return ("evidence tampered", TAMPERED)
|
|
4529
4569
|
if (done or verified) and any(r.get(ORPHAN) for r in rows):
|
|
4530
4570
|
return ("evidence tampered", ORPHANED)
|
|
4531
|
-
ev = {str(entry.get("kind")) for entry in rows}
|
|
4532
4571
|
# The trigger is the turn's own evidence, not its words. The claim vocabulary
|
|
4533
4572
|
# below catches a claim-shaped reply; it missed the same unfounded state
|
|
4534
4573
|
# stated as a description ("the parser is wired up now"), which is what E2
|
|
@@ -4541,7 +4580,12 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4541
4580
|
# owes the check it never got - which is the `no verify_ok` floor below and
|
|
4542
4581
|
# not the partial-failure branch above it, because an interruption is no
|
|
4543
4582
|
# failure at all (the row carries no `exit`).
|
|
4544
|
-
|
|
4583
|
+
# A prose edit (`_prose_edit`) is no step a check is owed for - unless the
|
|
4584
|
+
# reply says something was tested or checked, a claim that rests on a pass.
|
|
4585
|
+
worked = {str(r.get("kind")) for r in rows if _work_row(r)}
|
|
4586
|
+
if not worked and any(_prose_edit(r) for r in rows) and (
|
|
4587
|
+
verified or any(TESTED_WORD.search(m.group(0)) for m in DONE.finditer(t))):
|
|
4588
|
+
worked = {"edit"}
|
|
4545
4589
|
if not worked:
|
|
4546
4590
|
# With no work, the words are the whole trigger, so a question or a
|
|
4547
4591
|
# negation is not read as the claim it names (`asserted_claims`).
|
|
@@ -4599,7 +4643,7 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4599
4643
|
# allowed on all five Stop hosts (audit INT-01 / M-2).
|
|
4600
4644
|
prior = _before_turn(events(session_id))
|
|
4601
4645
|
refused = fold(
|
|
4602
|
-
prior, {str(r.get("kind")) for r in prior
|
|
4646
|
+
prior, {str(r.get("kind")) for r in prior if _work_row(r)}, None,
|
|
4603
4647
|
"this session")
|
|
4604
4648
|
if refused[0]:
|
|
4605
4649
|
return refused
|
|
@@ -4624,7 +4668,7 @@ def _before_turn(rows):
|
|
|
4624
4668
|
if row.get("kind") == TURN_KIND:
|
|
4625
4669
|
pending = row
|
|
4626
4670
|
continue
|
|
4627
|
-
if pending is not None and
|
|
4671
|
+
if pending is not None and _work_row(row):
|
|
4628
4672
|
out.append(pending)
|
|
4629
4673
|
pending = None
|
|
4630
4674
|
out.append(row)
|
|
@@ -4732,8 +4776,9 @@ def _evidence_block(rows, worked, external, where="this turn"):
|
|
|
4732
4776
|
return ("stale evidence",
|
|
4733
4777
|
"Stale evidence: the newest check that passed ran before %s %s "
|
|
4734
4778
|
"written, so it verified an earlier revision of the tree than the "
|
|
4735
|
-
"one this reply is about. Re-run the check
|
|
4736
|
-
"
|
|
4779
|
+
"one this reply is about. Re-run the smallest check that covers "
|
|
4780
|
+
"the files written since (their own tests, not the whole suite) "
|
|
4781
|
+
"and report its output, or mark the claim \"doğrulanmadı\". "
|
|
4737
4782
|
"A green run over the previous revision does not cover this one."
|
|
4738
4783
|
% (shown or "a file this session wrote",
|
|
4739
4784
|
"was" if len(names) <= 1 else "were"))
|
|
@@ -4753,8 +4798,10 @@ def _evidence_block(rows, worked, external, where="this turn"):
|
|
|
4753
4798
|
"%s did work (edits or commands) and no check ran "
|
|
4754
4799
|
"successfully in it (nothing recorded as verify_ok with a "
|
|
4755
4800
|
"real result and an unmasked command), so nothing here supports "
|
|
4756
|
-
"calling it done, complete or verified. Run the
|
|
4757
|
-
"
|
|
4801
|
+
"calling it done, complete or verified. Run the smallest check "
|
|
4802
|
+
"that proves the change (the tests of what changed, not the "
|
|
4803
|
+
"whole suite) and report its output, or mark the claim "
|
|
4804
|
+
"\"doğrulanmadı\". Do not "
|
|
4758
4805
|
"describe a check you did not run as if it ran."
|
|
4759
4806
|
% where.capitalize())
|
|
4760
4807
|
# Last, so none of the classes above loses its turn to it: every shape this
|