@r1z4x/tezgah 1.5.0 → 1.6.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 +96 -1
- package/MANIFEST +6 -1
- package/README.md +6 -4
- package/bin/consult +13 -10
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-research +53 -3
- package/bin/tezgah-route +8 -5
- package/bin/tezgah-setup +45 -25
- package/bin/tezgah-task +194 -41
- package/docs/architecture.md +1 -1
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +4 -4
- package/docs/contract.md +4 -1
- package/docs/feature-audit.md +24 -3
- package/docs/gate.md +10 -11
- package/docs/hosts.md +9 -0
- package/docs/index.json +21 -7
- package/docs/judge.md +3 -1
- package/docs/layers.md +1 -1
- package/docs/models.md +17 -7
- package/docs/operations.md +0 -1
- package/docs/research.md +46 -0
- package/docs/skills.md +23 -5
- package/docs/testing.md +1 -1
- package/hooks/tezgah_agents.py +130 -96
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +67 -15
- package/hooks/tezgah_gate.py +62 -76
- package/hooks/tezgah_judge.py +15 -8
- package/hooks/tezgah_models.py +50 -3
- package/hooks/tezgah_paths.py +26 -11
- package/hooks/tezgah_policy.py +47 -19
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_research.py +3 -3
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_task.py +16 -8
- package/hosts/opencode/plugins/tezgah.js +17 -37
- package/package.json +1 -2
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +20 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +11 -7
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +10 -1
- package/skills/tezgah-contract/SKILL.md +67 -41
- package/agents/tezgah-reviewer.md +0 -79
package/hooks/tezgah_agents.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Claude Code
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
"""Subagent definitions, generated from the detected infrastructure.
|
|
3
|
+
|
|
4
|
+
One role set, rendered into every host surface that actually exists, at the
|
|
5
|
+
place each host loads it from:
|
|
6
|
+
|
|
7
|
+
Claude Code `~/.claude/agents/*.md` user-level, written at install
|
|
8
|
+
Codex `$CODEX_HOME/agents/*.toml` user-level, written at install
|
|
9
|
+
omp `~/.omp/agent/agents/*.md` user-level, written at install
|
|
10
|
+
Cursor `.cursor/agents/*.md` per repo, at session start
|
|
11
|
+
opencode `.opencode/agents/*.md` (fallback) + config injection, per repo
|
|
12
|
+
|
|
13
|
+
Claude and Codex miss a repo agents dir written during their first session
|
|
14
|
+
(Claude does not watch a dir that was absent at start; Codex reads roles at
|
|
15
|
+
config load and ignores `.codex/agents` in an untrusted repo), so their roles
|
|
16
|
+
live user-level; a repo's `.claude/agents` and `.codex/agents` files an older
|
|
17
|
+
tezgah wrote are swept. Cursor's CLI reads no user dir, so it keeps a per-repo
|
|
18
|
+
dir. A role is only emitted when the capability it needs is present (the
|
|
19
|
+
reviewer needs the code graph); the three tier workers are emitted in every root,
|
|
20
|
+
because `tezgah-route` names one of them for every delegated task. The per-repo
|
|
21
|
+
set is regenerated when either this manifest
|
|
14
22
|
or the repo's infrastructure changes, so the definitions stay updatable instead
|
|
15
23
|
of drifting. Hosts with no such surface (dsh) get nothing here; the orchestrator
|
|
16
24
|
directive in the injected contract already covers them. The generated dirs are
|
|
@@ -30,16 +38,20 @@ import subprocess
|
|
|
30
38
|
import tezgah_models as tm
|
|
31
39
|
from tezgah_paths import (CONFIG_DIR, HOME, codegraph_bin, host_installed, off,
|
|
32
40
|
root_for)
|
|
41
|
+
from tezgah_paths import HOST_DIRS as HOST_HOMES
|
|
33
42
|
|
|
34
43
|
MARKER = "# tezgah: managed by tezgah-agents; do not edit"
|
|
35
44
|
STATE = os.path.join(CONFIG_DIR, "agents.state.json")
|
|
36
|
-
# hosts with a file-based custom-agent surface
|
|
37
|
-
#
|
|
38
|
-
# both when either is installed; the *detection* of whether a host is installed
|
|
39
|
-
# lives in tezgah_paths.host_installed, not here.
|
|
45
|
+
# hosts with a file-based custom-agent surface; the *detection* of whether a
|
|
46
|
+
# host is installed lives in tezgah_paths.host_installed, not here.
|
|
40
47
|
FILE_HOSTS = ("claude", "opencode", "codex", "cursor")
|
|
48
|
+
# the file hosts whose roles live in their user dir (user_agents), written by
|
|
49
|
+
# the installer; the others get a per-repo dir from sync_root
|
|
50
|
+
USER_HOSTS = ("claude", "codex")
|
|
51
|
+
# the repo-relative agent dirs. Claude's and Codex's are no longer written, only
|
|
52
|
+
# swept: a copy an older tezgah left there would sit beside the user-level one.
|
|
41
53
|
HOST_DIRS = {"claude": os.path.join(".claude", "agents"),
|
|
42
|
-
"cursor": os.path.join(".
|
|
54
|
+
"cursor": os.path.join(".cursor", "agents"),
|
|
43
55
|
"opencode": os.path.join(".opencode", "agents"),
|
|
44
56
|
"codex": os.path.join(".codex", "agents")}
|
|
45
57
|
# Stack markers -> a short hint. Exact commands are left to the repo's own docs
|
|
@@ -61,10 +73,11 @@ MCP_PREFIXES = ("mcp__codegraph__", "mcp__plugin_tezgah_codegraph__")
|
|
|
61
73
|
GRAPH_SELECT = ",".join(p + t for t in GRAPH_MCP for p in MCP_PREFIXES)
|
|
62
74
|
GRAPH_CLI = ("callers", "callees", "impact", "node", "files")
|
|
63
75
|
GRAPH_TOOLS = ", ".join("`codegraph %s`" % verb for verb in GRAPH_CLI)
|
|
64
|
-
# A role with no shell (Claude `disallowedTools` Bash, opencode `bash: deny
|
|
76
|
+
# A role with no shell (Claude `disallowedTools` Bash, opencode `bash: deny`;
|
|
77
|
+
# Cursor's `readonly` documents no shell scope, so it is held to the same recipe)
|
|
65
78
|
# cannot run `git diff` or the CLI, so its blast radius comes from the caller's
|
|
66
79
|
# brief and the MCP impact tool; a role with a shell runs the diff itself.
|
|
67
|
-
NO_SHELL = ("claude", "opencode")
|
|
80
|
+
NO_SHELL = ("claude", "opencode", "cursor")
|
|
68
81
|
# The floor every graph role carries (gortex edge provenance): the index holds
|
|
69
82
|
# only the edges its parser saw, so an absence of callers is a coverage gap
|
|
70
83
|
# until shown otherwise, never a safety verdict.
|
|
@@ -114,7 +127,7 @@ def _graph_howto(host):
|
|
|
114
127
|
"`codegraph status`). Do not call the `xd://mcp__codegraph_*`\n"
|
|
115
128
|
"devices: omp refuses them with \"Concurrent write refused\"\n"
|
|
116
129
|
"while the parent session holds the index." % GRAPH_TOOLS)
|
|
117
|
-
if host
|
|
130
|
+
if host in ("opencode", "cursor"):
|
|
118
131
|
return ("codegraph is registered as an MCP server (%s). You have no\n"
|
|
119
132
|
"shell, so the codegraph CLI is out of reach: use those MCP tools."
|
|
120
133
|
% ", ".join("`%s`" % t for t in GRAPH_MCP))
|
|
@@ -209,8 +222,9 @@ def _reviewer_body(host):
|
|
|
209
222
|
"Scope and budget. One review per plan, over its whole diff in the\n"
|
|
210
223
|
"verification phase; at most two rounds - only a confirmed critical or\n"
|
|
211
224
|
"major finding opens round two, a minor or suggestion one is recorded\n"
|
|
212
|
-
"fix-later, and after round two what is left
|
|
213
|
-
"recorded fix-later, never a third
|
|
225
|
+
"fix-later, and after round two what is left is fixed by the session\n"
|
|
226
|
+
"and closed with `--resolved`, or recorded fix-later, never a third\n"
|
|
227
|
+
"round. A round may be a fan-out of\n"
|
|
214
228
|
"reviewers spawned at once, one per dimension (correctness and contract,\n"
|
|
215
229
|
"security, tests and performance) or per file group: when your brief\n"
|
|
216
230
|
"names a dimension or files, review only those - a sibling owns the rest.\n"
|
|
@@ -440,10 +454,8 @@ def _claude_model(model):
|
|
|
440
454
|
|
|
441
455
|
def _model_lines(name, family, model_fmt, effort_fmt):
|
|
442
456
|
"""The model (and effort) lines the table gives this agent on a family;
|
|
443
|
-
`model: inherit` on Claude
|
|
444
|
-
|
|
445
|
-
(`claude-opus-5[effort=high]`, cursor.com/docs/subagents) - unverified for
|
|
446
|
-
the ids this table names, so Cursor is documented, not guessed at."""
|
|
457
|
+
`model: inherit` on Claude for an agent with no slot. Cursor gets
|
|
458
|
+
`model: inherit` always (render_md_cursor)."""
|
|
447
459
|
picked = tm.pick(name, family)
|
|
448
460
|
if not picked:
|
|
449
461
|
return ["model: inherit"] if family == "anthropic" else []
|
|
@@ -462,35 +474,34 @@ def _fold(desc, width=74):
|
|
|
462
474
|
|
|
463
475
|
|
|
464
476
|
def render_md(name, description, readonly, body):
|
|
465
|
-
"""Claude Code /
|
|
466
|
-
|
|
477
|
+
"""Claude Code markdown (`~/.claude/agents`), which the Cursor editor also
|
|
478
|
+
reads (cursor.com/docs/subagents). The description is one line: Cursor's
|
|
479
|
+
line parser reads a folded `>` block as ">", and Claude's parser re-quotes
|
|
480
|
+
a plain value YAML rejects (a `: ` inside it). Claude denies the write/shell
|
|
481
|
+
tools explicitly; `readonly` is Cursor's key, which Claude ignores."""
|
|
467
482
|
lines = ["---", MARKER + " (manifest %s)" % manifest_sha(),
|
|
468
|
-
"name: %s" % name, "description:
|
|
469
|
-
lines += _fold(description)
|
|
483
|
+
"name: %s" % name, "description: %s" % " ".join(description.split())]
|
|
470
484
|
lines += _model_lines(name, "anthropic", "model: %s", "effort: %s")
|
|
471
485
|
if readonly:
|
|
472
|
-
# `disallowedTools` is Claude's; `readonly` is Cursor's. Both parsers
|
|
473
|
-
# ignore the other's field.
|
|
474
486
|
lines.append("readonly: true")
|
|
475
487
|
lines.append("disallowedTools: Write, Edit, NotebookEdit, Bash, Agent")
|
|
476
488
|
lines.append("---")
|
|
477
489
|
return "\n".join(lines) + "\n\n" + body.strip() + "\n"
|
|
478
490
|
|
|
479
491
|
|
|
480
|
-
def
|
|
481
|
-
"""
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return out
|
|
492
|
+
def render_md_cursor(name, description, readonly, body):
|
|
493
|
+
"""Cursor `.cursor/agents/*.md`. cursor-agent splits each frontmatter line
|
|
494
|
+
at its first ':' (no YAML), passes `model` through as a raw id and has no
|
|
495
|
+
`effort` key, so: a one-line description, `model: inherit` (the only id
|
|
496
|
+
portable across Cursor accounts), `readonly`, and no `tools` line - its
|
|
497
|
+
comma split would cut a Claude `Agent(a, b)` list into broken names."""
|
|
498
|
+
lines = ["---", MARKER + " (manifest %s)" % manifest_sha(),
|
|
499
|
+
"name: %s" % name, "description: %s" % " ".join(description.split()),
|
|
500
|
+
"model: inherit"]
|
|
501
|
+
if readonly:
|
|
502
|
+
lines.append("readonly: true")
|
|
503
|
+
lines.append("---")
|
|
504
|
+
return "\n".join(lines) + "\n\n" + body.strip() + "\n"
|
|
494
505
|
|
|
495
506
|
|
|
496
507
|
def render_md_opencode(name, description, readonly, body):
|
|
@@ -529,13 +540,19 @@ def render_md_omp(name, description, body, tools=("read", "grep", "glob", "bash"
|
|
|
529
540
|
return "\n".join(lines) + "\n\n" + body.strip() + "\n"
|
|
530
541
|
|
|
531
542
|
|
|
543
|
+
def _active_roles(root):
|
|
544
|
+
"""The roles a user-level dir holds: none under `agents-off`, else those
|
|
545
|
+
whose capability the machine has (`~/.no-graph` counts as the repo's)."""
|
|
546
|
+
if off("agents-off"):
|
|
547
|
+
return []
|
|
548
|
+
infra = detect_infra(os.path.expanduser(root))
|
|
549
|
+
return [r for r in ROLES if r[2](infra)]
|
|
550
|
+
|
|
551
|
+
|
|
532
552
|
def omp_user_agents(root="~"):
|
|
533
553
|
"""{filename: text} for omp's user agent dir (~/.omp/agent/agents); empty
|
|
534
554
|
under `agents-off`, so the installer's sweep removes the files."""
|
|
535
|
-
|
|
536
|
-
return {}
|
|
537
|
-
infra = detect_infra(os.path.expanduser(root))
|
|
538
|
-
active = [r for r in ROLES if r[2](infra)]
|
|
555
|
+
active = _active_roles(root)
|
|
539
556
|
names = [r[0] for r in active]
|
|
540
557
|
if not names:
|
|
541
558
|
return {}
|
|
@@ -550,20 +567,50 @@ def omp_user_agents(root="~"):
|
|
|
550
567
|
return out
|
|
551
568
|
|
|
552
569
|
|
|
570
|
+
def user_agent_dir(host):
|
|
571
|
+
"""Where a USER_HOSTS host loads agents from in every session: Claude's
|
|
572
|
+
`~/.claude/agents`, Codex's `$CODEX_HOME/agents` (default ~/.codex)."""
|
|
573
|
+
return os.path.join(HOST_HOMES[host], "agents")
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def user_agents(host, root="~"):
|
|
577
|
+
"""{filename: text} for a USER_HOSTS host's user agent dir, each file
|
|
578
|
+
carrying MARKER so the installer sweeps only tezgah's; empty under
|
|
579
|
+
`agents-off`, so that sweep removes them. Codex gets no orchestrator: the
|
|
580
|
+
main thread orchestrates there."""
|
|
581
|
+
active = _active_roles(root)
|
|
582
|
+
names = [r[0] for r in active]
|
|
583
|
+
if not names:
|
|
584
|
+
return {}
|
|
585
|
+
if host == "codex":
|
|
586
|
+
return {name + ".toml": render_toml(name, desc, readonly, body("codex"))
|
|
587
|
+
for name, desc, _cap, body, readonly in active}
|
|
588
|
+
out = {name + ".md": render_md(name, desc, readonly, body("claude"))
|
|
589
|
+
for name, desc, _cap, body, readonly in active}
|
|
590
|
+
out["tezgah-orchestrator.md"] = render_orch_md(names)
|
|
591
|
+
return out
|
|
592
|
+
|
|
593
|
+
|
|
553
594
|
def steering(root, host=None):
|
|
554
595
|
"""One line naming the generated specialists this host can spawn, or None.
|
|
555
596
|
|
|
556
597
|
Nothing else in the contract points the main agent at them, so a model
|
|
557
598
|
reached for the host's generic explorer instead. Existence is read from
|
|
558
599
|
disk, not from the capabilities: a specialist is named only when its file
|
|
559
|
-
is there for the host to load -
|
|
560
|
-
|
|
561
|
-
|
|
600
|
+
is there for the host to load - the user agent dir on omp, Claude and
|
|
601
|
+
Codex, the repo's own dir on Cursor and opencode, and all of them for a
|
|
602
|
+
host that does not say which it is. A repo's `.no-graph` turns the graph
|
|
603
|
+
off there, so a graph role is not named in it even when a user-level file
|
|
562
604
|
exists."""
|
|
563
605
|
if host == "omp":
|
|
564
606
|
dirs = [os.path.join(HOME, ".omp", "agent", "agents")]
|
|
607
|
+
elif host in USER_HOSTS:
|
|
608
|
+
dirs = [user_agent_dir(host)]
|
|
609
|
+
elif host in HOST_DIRS:
|
|
610
|
+
dirs = [os.path.join(root, HOST_DIRS[host])]
|
|
565
611
|
else:
|
|
566
|
-
dirs = [
|
|
612
|
+
dirs = ([user_agent_dir(h) for h in USER_HOSTS]
|
|
613
|
+
+ [os.path.join(root, HOST_DIRS[h]) for h in ("cursor", "opencode")])
|
|
567
614
|
have = {os.path.splitext(os.path.basename(p))[0]
|
|
568
615
|
for d in dirs for p in glob.glob(os.path.join(d, "tezgah-*.*"))}
|
|
569
616
|
if os.path.exists(os.path.join(root, ".no-graph")):
|
|
@@ -656,14 +703,18 @@ def _write_if_changed(path, text):
|
|
|
656
703
|
return True
|
|
657
704
|
|
|
658
705
|
|
|
659
|
-
def
|
|
660
|
-
"""Delete only tezgah-managed files no longer in the wanted set
|
|
706
|
+
def remove_stale(directory, wanted):
|
|
707
|
+
"""Delete only tezgah-managed files no longer in the wanted set; returns
|
|
708
|
+
the paths removed."""
|
|
709
|
+
removed = []
|
|
661
710
|
for path in glob.glob(os.path.join(directory, "tezgah-*.*")):
|
|
662
711
|
if os.path.basename(path) not in wanted and _is_managed(_read(path)):
|
|
663
712
|
try:
|
|
664
713
|
os.remove(path)
|
|
714
|
+
removed.append(path)
|
|
665
715
|
except OSError:
|
|
666
716
|
pass
|
|
717
|
+
return removed
|
|
667
718
|
|
|
668
719
|
|
|
669
720
|
BLOCK_BEGIN = ("# tezgah: generated agents (managed; removed by "
|
|
@@ -796,66 +847,49 @@ def sync_root(root, report_steady=False):
|
|
|
796
847
|
return None
|
|
797
848
|
disabled = off("agents-off")
|
|
798
849
|
infra = detect_infra(root)
|
|
799
|
-
|
|
850
|
+
# Claude's and Codex's roles are user-level (user_agents): only Cursor and
|
|
851
|
+
# opencode get a dir here
|
|
852
|
+
hosts = set() if disabled else set(infra["hosts"]) - set(USER_HOSTS)
|
|
800
853
|
active = [] if disabled else [r for r in ROLES if r[2](infra)]
|
|
801
854
|
names = [r[0] for r in active]
|
|
802
855
|
|
|
803
|
-
|
|
804
|
-
md_dir = os.path.join(root, HOST_DIRS["claude"]) if hosts & {"claude", "cursor"} else None
|
|
856
|
+
cursor_dir = os.path.join(root, HOST_DIRS["cursor"]) if "cursor" in hosts else None
|
|
805
857
|
oc_dir = os.path.join(root, HOST_DIRS["opencode"]) if "opencode" in hosts else None
|
|
806
|
-
codex_dir = os.path.join(root, HOST_DIRS["codex"]) if "codex" in hosts else None
|
|
807
858
|
|
|
808
859
|
written, removed = 0, 0
|
|
809
860
|
paths = []
|
|
861
|
+
wanted = {h: set() for h in HOST_DIRS}
|
|
810
862
|
|
|
811
|
-
def emit(
|
|
863
|
+
def emit(host, name, text):
|
|
812
864
|
nonlocal written
|
|
813
|
-
wanted.add(name)
|
|
814
|
-
path = os.path.join(
|
|
865
|
+
wanted[host].add(name)
|
|
866
|
+
path = os.path.join(root, HOST_DIRS[host], name)
|
|
815
867
|
if _write_if_changed(path, text):
|
|
816
868
|
written += 1
|
|
817
869
|
paths.append(path)
|
|
818
870
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
nonlocal removed
|
|
823
|
-
before = set(os.path.basename(p) for p in glob.glob(os.path.join(directory, "tezgah-*.*")))
|
|
824
|
-
_remove_stale(directory, wanted)
|
|
825
|
-
after = set(os.path.basename(p) for p in glob.glob(os.path.join(directory, "tezgah-*.*")))
|
|
826
|
-
removed += len(before - after)
|
|
827
|
-
|
|
828
|
-
if md_dir and names: # Claude Code + Cursor
|
|
871
|
+
# Cursor gets no orchestrator: its files are subagents the main thread
|
|
872
|
+
# delegates to, and the orchestrator is a primary agent
|
|
873
|
+
if cursor_dir and names:
|
|
829
874
|
for name, desc, _cap, body, readonly in active:
|
|
830
|
-
emit(
|
|
831
|
-
|
|
875
|
+
emit("cursor", name + ".md",
|
|
876
|
+
render_md_cursor(name, desc, readonly, body("cursor")))
|
|
832
877
|
|
|
833
878
|
if oc_dir and names: # opencode
|
|
834
879
|
for name, desc, _cap, body, readonly in active:
|
|
835
|
-
emit(
|
|
880
|
+
emit("opencode", name + ".md",
|
|
836
881
|
render_md_opencode(name, desc, readonly, body("opencode")))
|
|
837
|
-
emit(
|
|
838
|
-
|
|
839
|
-
if codex_dir and names: # Codex (no orchestrator agent; the main thread orchestrates)
|
|
840
|
-
for name, desc, _cap, body, readonly in active:
|
|
841
|
-
emit(codex_dir, wanted_codex, name + ".toml",
|
|
842
|
-
render_toml(name, desc, readonly, body("codex")))
|
|
882
|
+
emit("opencode", "tezgah-orchestrator.md", render_orch_md_opencode(names))
|
|
843
883
|
|
|
844
|
-
# The sweep runs over every dir this module
|
|
845
|
-
# ones: a host dropped from the config,
|
|
846
|
-
#
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
884
|
+
# The sweep runs over every dir this module ever wrote, not only the
|
|
885
|
+
# selected ones: a host dropped from the config, a capability that
|
|
886
|
+
# disappeared, or a `.claude`/`.codex` copy from before the roles moved
|
|
887
|
+
# user-level must not stay behind for a host to keep loading.
|
|
888
|
+
for host, directory in HOST_DIRS.items():
|
|
889
|
+
removed += len(remove_stale(os.path.join(root, directory), wanted[host]))
|
|
850
890
|
|
|
851
891
|
if paths:
|
|
852
|
-
dirs =
|
|
853
|
-
if md_dir:
|
|
854
|
-
dirs.add(HOST_DIRS["claude"])
|
|
855
|
-
if oc_dir:
|
|
856
|
-
dirs.add(HOST_DIRS["opencode"])
|
|
857
|
-
if codex_dir:
|
|
858
|
-
dirs.add(HOST_DIRS["codex"])
|
|
892
|
+
dirs = {HOST_DIRS[h] for h in ("cursor", "opencode") if wanted[h]}
|
|
859
893
|
ex = ensure_exclude(root, dirs)
|
|
860
894
|
if ex:
|
|
861
895
|
paths.append(ex)
|
|
@@ -868,7 +902,7 @@ def sync_root(root, report_steady=False):
|
|
|
868
902
|
% (len(names), ", %d removed" % removed if removed else ""))
|
|
869
903
|
if removed:
|
|
870
904
|
return "%d stale agent file(s) removed" % removed
|
|
871
|
-
if report_steady and names and (
|
|
905
|
+
if report_steady and names and (cursor_dir or oc_dir):
|
|
872
906
|
return "%d agent(s) current" % len(names)
|
|
873
907
|
return None
|
|
874
908
|
|
|
@@ -103,7 +103,8 @@ COMPONENTS = [
|
|
|
103
103
|
"switch": "research-off", "editable": False, "revert": "git"},
|
|
104
104
|
{"key": "product", "label": "product",
|
|
105
105
|
"files": ["hooks/tezgah_policy.py", "skills/product-analysis/SKILL.md",
|
|
106
|
-
"skills/feature-audit/SKILL.md"
|
|
106
|
+
"skills/feature-audit/SKILL.md", "skills/product-flow/SKILL.md",
|
|
107
|
+
"hooks/tezgah_product.py"],
|
|
107
108
|
"switch": "research-off", "editable": True, "revert": "git"},
|
|
108
109
|
{"key": "attribution", "label": "attribution",
|
|
109
110
|
"files": ["hooks/tezgah_policy.py", "hooks/tezgah_gate.py"],
|
package/hooks/tezgah_context.py
CHANGED
|
@@ -690,7 +690,8 @@ def autoindex(root):
|
|
|
690
690
|
|
|
691
691
|
|
|
692
692
|
def sync_agents(root):
|
|
693
|
-
"""Generate/refresh this repo's
|
|
693
|
+
"""Generate/refresh this repo's Cursor and opencode subagent files (best
|
|
694
|
+
effort); Claude's and Codex's roles are user-level, written at install."""
|
|
694
695
|
try:
|
|
695
696
|
from tezgah_agents import sync_root
|
|
696
697
|
return sync_root(root)
|
|
@@ -786,16 +787,16 @@ def task_line(task):
|
|
|
786
787
|
and refuses a write the phase or the allowlist excludes, but a refusal
|
|
787
788
|
costs a turn - so the phase rides every user turn, and the refusal is never
|
|
788
789
|
the first the session hears of it. One line because it is paid every turn;
|
|
789
|
-
the allowlist is the globs as written, and the phase is stated as the
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
refuses is an invitation to a loop."""
|
|
790
|
+
the allowlist is the globs as written, and the phase is stated as the
|
|
791
|
+
orchestrator's own move: the session runs `tezgah-task` itself when the
|
|
792
|
+
work reaches the next phase and closes the plan when it is done, never
|
|
793
|
+
handing those steps to the user."""
|
|
794
794
|
paths = ", ".join(task.get("allowed_paths") or []) or "any path in the repo"
|
|
795
|
-
return ("Active task %s is in phase `%s`; writes allowed on: %s.
|
|
796
|
-
"
|
|
797
|
-
"
|
|
798
|
-
|
|
795
|
+
return ("Active task %s is in phase `%s`; writes allowed on: %s. Move the "
|
|
796
|
+
"phase yourself with `tezgah-task phase <next>` when the work reaches "
|
|
797
|
+
"it, and `tezgah-task close %s done` when it is merged - never hand "
|
|
798
|
+
"these steps to the user." % (task.get("id"), task.get("phase"),
|
|
799
|
+
paths, task.get("id")))
|
|
799
800
|
|
|
800
801
|
|
|
801
802
|
# --- the resume block: the live turn state a compact loses -------------------
|
|
@@ -1185,10 +1186,61 @@ def constraint_notice(cwd, session_id):
|
|
|
1185
1186
|
return " ".join(render(subagent_core(core_for(cwd)[0])).split())
|
|
1186
1187
|
|
|
1187
1188
|
|
|
1189
|
+
# A data ask said by its output: a dashboard, a chart, a KPI, a metric to choose.
|
|
1190
|
+
# "Bu veriye uygun bir metric dashboard tasarla" armed nothing before plan 072.
|
|
1191
|
+
# These words are code words as often as product words (`dashboard.json`, a
|
|
1192
|
+
# metrics endpoint, a progress `gösterge`, the clipboard `pano`), and two rounds
|
|
1193
|
+
# of noun denylists inside the product regex kept missing the next code sense.
|
|
1194
|
+
# So the ask is judged per clause: it arms `product` only when no code marker -
|
|
1195
|
+
# a file name, an identifier, SQL, a build, test or runtime word - shares its
|
|
1196
|
+
# clause (decision 006: qualify the stem, never drop it).
|
|
1197
|
+
DATA_ASK = re.compile(
|
|
1198
|
+
r"\b(?:(?:design|create|make|build|sketch|propose|need|want)\w* (?:\w+ ){0,4}"
|
|
1199
|
+
r"(?:dashboard|charts?|visuali[sz]ation)|"
|
|
1200
|
+
r"(?:metri[ck]s?|kpis?|data|sales|revenue|veri\w*|satış\w*|gelir\w*) "
|
|
1201
|
+
r"(?:\w+ ){0,2}(?:dashboard|pano)|"
|
|
1202
|
+
r"(?:dashboard|gösterge panel)\w* (?:\w+ ){0,4}(?:tasarla|oluştur|hazırla|"
|
|
1203
|
+
r"yap|göster)\w*|"
|
|
1204
|
+
r"(?:what|which) (?:\w+ ){0,3}(?:metri[ck]s|kpis|charts?) (?:\w+ ){0,3}"
|
|
1205
|
+
r"(?:should|to|do|matter|would)|"
|
|
1206
|
+
r"what should (?:\w+ ){0,3}dashboard show|"
|
|
1207
|
+
r"(?:choose|pick|define|track|design)(?:s|ing)? (?:\w+ ){0,3}(?:metri[ck]s|kpis)|"
|
|
1208
|
+
r"(?:metri[ck]s|kpis) (?:should|to|do|would) (?:\w+ ){0,2}"
|
|
1209
|
+
r"(?:track|measure|use|watch)|success metrics?|"
|
|
1210
|
+
r"(?:suggest|recommend)\w* (?:\w+ ){0,3}charts?|öner\w* (?:\w+ ){0,3}grafi\w*|"
|
|
1211
|
+
r"hangi (?:metri[ck]\w*|kpi\w*|grafi\w*|göstergeleri?)|"
|
|
1212
|
+
r"(?:metri[ck]\w*|kpi\w*) (?:\w+ ){0,3}(?:tasarla|seç|tanımla|belirle|takip|izle)\w*|"
|
|
1213
|
+
r"gösterge panel\w*|visuali[sz]\w* (?:\w+ ){0,3}(?:data|numbers|veri\w*)|"
|
|
1214
|
+
r"data visuali[sz]ation|veri\w* (?:\w+ ){0,3}görselleştir\w*)")
|
|
1215
|
+
CODE_SENSE = re.compile(
|
|
1216
|
+
r"\w\.(?:py|tsx?|jsx?|json|go|rs|ya?ml|sql|java|rb|css|html)\b|\b\w+_\w+\b|"
|
|
1217
|
+
r"/metrics\b|\bselect\b.{0,200}?\bfrom\b|\b(?:tables?|columns?|schema|sql|queries|"
|
|
1218
|
+
r"query|bundle|webpack|vite|tests?|ci|counters?|registry|pipeline|backend|"
|
|
1219
|
+
r"handlers?|structs?|endpoints?|exporters?|export\w*|servers?|librar\w*|"
|
|
1220
|
+
r"packages?|modules?|database|index\w*|indeks\w*|"
|
|
1221
|
+
r"middleware|debugger|data (?:structure|flow)s?|clipboard|spinner|threads?|"
|
|
1222
|
+
r"commit|git|functions?|class|component|prop|hooks?|prometheus|grafana|"
|
|
1223
|
+
r"kibana|statsd|opentelemetry|config\w*|provision\w*|api|routes?|snake_case|"
|
|
1224
|
+
r"veri yapı\w*|fonksiyon\w*|bileşen\w*|modül\w*|dosya\w*|kopyala\w*|"
|
|
1225
|
+
r"seçici\w*|yükleme|ilerleme|paket\w*)\b")
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _data_ask(low):
|
|
1229
|
+
"""Does one clause of the prompt ask for a data view or a metric choice with
|
|
1230
|
+
no code marker beside it? A clause ends at `!`, `?`, `;`, a newline or a full
|
|
1231
|
+
stop followed by a space, so `dashboard.json` stays one clause."""
|
|
1232
|
+
return any(DATA_ASK.search(clause) and not CODE_SENSE.search(clause)
|
|
1233
|
+
for clause in re.split(r"[!?;\n]|\.\s", low))
|
|
1234
|
+
|
|
1235
|
+
|
|
1188
1236
|
def classify_prompt(text):
|
|
1189
|
-
"""The conditional rule keys a prompt arms, from the shared hint table
|
|
1237
|
+
"""The conditional rule keys a prompt arms, from the shared hint table, plus
|
|
1238
|
+
`product` for a data ask (DATA_ASK, gated per clause by CODE_SENSE)."""
|
|
1190
1239
|
low = (text or "").lower()
|
|
1191
|
-
|
|
1240
|
+
keys = {key for key, pattern in PROMPT_HINTS if re.search(pattern, low)}
|
|
1241
|
+
if "product" not in keys and _data_ask(low):
|
|
1242
|
+
keys.add("product")
|
|
1243
|
+
return keys
|
|
1192
1244
|
|
|
1193
1245
|
|
|
1194
1246
|
def prompt_text(payload):
|
|
@@ -1434,8 +1486,8 @@ DEFAULT_BUDGET = 13000
|
|
|
1434
1486
|
# exists to prevent, not one it may cause.
|
|
1435
1487
|
DROP_ORDER = ("knowledge", "worktrees", "lessons", "skill", "lessons_turn",
|
|
1436
1488
|
"taste", "plans", "subagents", "steer", "orca", "consult", "research",
|
|
1437
|
-
"research_broken", "graph", "offnote", "orchestrate",
|
|
1438
|
-
"resume", "scratch", "task", "delta", "pointer")
|
|
1489
|
+
"research_broken", "graph", "offnote", "orchestrate",
|
|
1490
|
+
"index", "resume", "scratch", "task", "delta", "pointer")
|
|
1439
1491
|
|
|
1440
1492
|
|
|
1441
1493
|
def _first_item(text):
|
|
@@ -1576,7 +1628,7 @@ GATE_INACTIVE = (
|
|
|
1576
1628
|
"tezgah gate inactive on this host: %d gated tool call(s) since this "
|
|
1577
1629
|
"session's first turn and not one row from the gate, so the shortcut "
|
|
1578
1630
|
"denials and the Stop check are not running. Say so before claiming a check "
|
|
1579
|
-
"passed,
|
|
1631
|
+
"passed, run `tezgah-setup --report` yourself and quote its MISS rows.")
|
|
1580
1632
|
|
|
1581
1633
|
|
|
1582
1634
|
def _iso_epoch(value):
|