@r1z4x/tezgah 1.7.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/MANIFEST +2 -0
- package/bin/tezgah-decisions +6 -14
- package/bin/tezgah-design +21 -5
- package/bin/tezgah-lessons +4 -1
- package/bin/tezgah-research +15 -7
- package/bin/tezgah-route +22 -7
- package/bin/tezgah-task +320 -75
- package/bin/tezgah-taste +8 -5
- package/docs/architecture.md +21 -7
- package/docs/environment.md +3 -1
- package/docs/evidence.md +9 -1
- package/docs/gate.md +24 -7
- package/docs/models.md +40 -12
- package/docs/operations.md +31 -1
- package/docs/research.md +3 -2
- package/docs/testing.md +18 -11
- package/hooks/projects-auto-init.py +29 -1
- package/hooks/tezgah_agents.py +24 -7
- package/hooks/tezgah_context.py +67 -35
- package/hooks/tezgah_gate.py +430 -64
- package/hooks/tezgah_integrity.py +108 -41
- package/hooks/tezgah_models.py +90 -30
- package/hooks/tezgah_paths.py +44 -6
- package/hooks/tezgah_policy.py +3 -3
- package/hooks/tezgah_research.py +9 -5
- package/hooks/tezgah_task.py +319 -67
- package/hooks/tezgah_taste.py +8 -4
- package/hosts/codex/hook.py +23 -3
- package/hosts/cursor/hook.py +11 -0
- package/hosts/opencode/plugins/tezgah.js +171 -4
- package/package.json +1 -1
- package/skills/plan-add/SKILL.md +23 -9
- package/skills/plan-add/render_table.py +8 -3
- package/skills/tezgah-contract/SKILL.md +3 -3
package/hooks/tezgah_context.py
CHANGED
|
@@ -33,7 +33,8 @@ from tezgah_paths import (CACHE, CONTEXT_NUDGE_TOKENS, REPO_MARKS, SWITCHES,
|
|
|
33
33
|
ai_research_dir, alts, armed,
|
|
34
34
|
cache_dir, codegraph_bin, consult_options,
|
|
35
35
|
ensure_workspace, fallback_cache, have_judge_key, language,
|
|
36
|
-
off, orx_bin, packs, pony_level,
|
|
36
|
+
off, orx_bin, packs, pony_level, project_top, root_for,
|
|
37
|
+
roots, say, tool,
|
|
37
38
|
workspace, workspace_from_repo, worktrees, writable_dir)
|
|
38
39
|
|
|
39
40
|
try: # The task record is the active plan's frontmatter (see tezgah_task), read
|
|
@@ -383,10 +384,12 @@ def git(root, *args):
|
|
|
383
384
|
|
|
384
385
|
def repo_root(cwd):
|
|
385
386
|
"""The unit of work: the git top-level under a root, else the first path
|
|
386
|
-
component below the root (so a non-git project still gets one slug).
|
|
387
|
+
component below the root (so a non-git project still gets one slug). Inside
|
|
388
|
+
`.tezgah` git answers the workspace's own repository; the unit is the
|
|
389
|
+
project around it, or the session start sets the project up in `.tezgah`."""
|
|
387
390
|
top = git(cwd, "rev-parse", "--show-toplevel")
|
|
388
391
|
if top and under(top):
|
|
389
|
-
return os.path.realpath(top)
|
|
392
|
+
return project_top(os.path.realpath(top))
|
|
390
393
|
base = root_for(cwd)
|
|
391
394
|
if not base:
|
|
392
395
|
return os.path.realpath(cwd)
|
|
@@ -532,28 +535,49 @@ def open_plans(root):
|
|
|
532
535
|
|
|
533
536
|
|
|
534
537
|
def sibling_line(root):
|
|
535
|
-
"""One line naming every checkout of this repository
|
|
536
|
-
|
|
538
|
+
"""One line naming every checkout of this repository, or "" when it has one.
|
|
539
|
+
The main checkout's `.tezgah` is the repository's one workspace (plan 097),
|
|
540
|
+
named with its open plan and research line counts. A linked checkout whose
|
|
541
|
+
own `.tezgah` still holds data is named with what it holds, because `git
|
|
542
|
+
worktree remove` deletes it with the checkout. Fork-free (the pinned
|
|
537
543
|
session-start git budget): `worktrees` reads `.git/worktrees/*/gitdir`, and
|
|
538
|
-
the counts are directory listings.
|
|
539
|
-
this only makes the others visible."""
|
|
544
|
+
the counts are directory listings."""
|
|
540
545
|
checkouts = worktrees(root)
|
|
541
546
|
here = os.path.realpath(root)
|
|
542
547
|
if here not in checkouts:
|
|
543
548
|
checkouts.append(here)
|
|
544
549
|
if len(checkouts) < 2:
|
|
545
550
|
return ""
|
|
551
|
+
main = checkouts[0]
|
|
546
552
|
items = []
|
|
547
553
|
for checkout in checkouts:
|
|
548
554
|
ws = workspace(checkout)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
555
|
+
if checkout == main:
|
|
556
|
+
state = ("no workspace yet" if not os.path.isdir(ws) else
|
|
557
|
+
"%d open plan(s), %d research line(s)"
|
|
558
|
+
% (len(glob.glob(os.path.join(ws, "plans", "open", "*.md"))),
|
|
559
|
+
len(tezgah_research.slugs(checkout))))
|
|
560
|
+
else:
|
|
561
|
+
try:
|
|
562
|
+
# a `.tezgah` that links to the main checkout's is that workspace
|
|
563
|
+
# (review of plan 097, F3): removing the worktree drops the link
|
|
564
|
+
held = ([] if os.path.realpath(ws) == os.path.realpath(workspace(main))
|
|
565
|
+
else sorted(set(os.listdir(ws)) - {".git"}))
|
|
566
|
+
except OSError:
|
|
567
|
+
held = []
|
|
568
|
+
lines = tezgah_research.slugs(checkout) if held else []
|
|
569
|
+
state = ("uses the main checkout's `.tezgah`" if not held else
|
|
570
|
+
"its own `.tezgah` still holds %s, which `git worktree remove` "
|
|
571
|
+
"deletes: move each research line with `%s import %s <slug>` "
|
|
572
|
+
"from the main checkout, and the rest by hand"
|
|
573
|
+
% (", ".join(held), tool("tezgah-research"), checkout))
|
|
574
|
+
if lines:
|
|
575
|
+
state += " (lines: %s)" % ", ".join(lines)
|
|
553
576
|
items.append("%s (%s%s)" % (checkout, "this checkout, " if checkout == here
|
|
554
577
|
else "", state))
|
|
555
|
-
return ("Worktrees: %d checkouts of this repository
|
|
556
|
-
"`.tezgah
|
|
578
|
+
return ("Worktrees: %d checkouts of this repository share the main checkout's "
|
|
579
|
+
"`.tezgah` - plans, lessons, research and analysis are written there, "
|
|
580
|
+
"never into a worktree's own: %s. Detail: `%s --all`."
|
|
557
581
|
% (len(checkouts), "; ".join(items), tool("tezgah-research")))
|
|
558
582
|
|
|
559
583
|
|
|
@@ -647,8 +671,10 @@ def _active_plan(root):
|
|
|
647
671
|
report of another plan's State/Next read as the active one is the block
|
|
648
672
|
inventing a task, which is worse than the line it costs. (None, False) when
|
|
649
673
|
the repo keeps no open plan."""
|
|
650
|
-
|
|
651
|
-
|
|
674
|
+
from tezgah_paths import linked_main
|
|
675
|
+
# a linked worktree's plans are its main checkout's (plan 097)
|
|
676
|
+
paths = sorted(glob.glob(os.path.join(linked_main(root) or root, ".tezgah",
|
|
677
|
+
"plans", "open", "*.md")))
|
|
652
678
|
if not paths:
|
|
653
679
|
return None, False
|
|
654
680
|
branch = git(root, "rev-parse", "--abbrev-ref", "HEAD")
|
|
@@ -884,11 +910,12 @@ def _lessons_state(root):
|
|
|
884
910
|
lines, tainted_lessons(root))).encode()).hexdigest()[:8]]
|
|
885
911
|
|
|
886
912
|
|
|
887
|
-
def state_stamp(root):
|
|
888
|
-
"""The comparable state of the repo this turn is about
|
|
913
|
+
def state_stamp(root, ws=None):
|
|
914
|
+
"""The comparable state of the repo this turn is about: the checkout's HEAD,
|
|
915
|
+
the plans and lessons of its workspace `ws` (the main checkout's)."""
|
|
889
916
|
return {"head": git(root, "rev-parse", "HEAD") or "nogit",
|
|
890
|
-
"plans": _plan_ids(root),
|
|
891
|
-
"lessons": _lessons_state(root)}
|
|
917
|
+
"plans": _plan_ids(ws or root),
|
|
918
|
+
"lessons": _lessons_state(ws or root)}
|
|
892
919
|
|
|
893
920
|
|
|
894
921
|
def read_stamp(session_id):
|
|
@@ -1543,8 +1570,8 @@ def context_nudge(payload, session_id):
|
|
|
1543
1570
|
# The ledger kinds a hook other than the tool hooks writes: the prompt hook's
|
|
1544
1571
|
# turn marker, judge row, lesson rows and disarm row, the Stop hook's claim,
|
|
1545
1572
|
# refusal, after_block, shape and stop_spec rows, the subagent-end hook's subagent_end row,
|
|
1546
|
-
# compaction, the subagent mark
|
|
1547
|
-
# `forked` row, the guard's crash
|
|
1573
|
+
# compaction, the subagent mark and the `subagent_start` lineage row (both from
|
|
1574
|
+
# SubagentStart), the omp fork's `forked` row, the guard's crash
|
|
1548
1575
|
# row (any hook), the damage row any reader writes and the session start's
|
|
1549
1576
|
# attest row. Every other kind - deny, nudge, began, snapshot, ... - can only
|
|
1550
1577
|
# come from a tool hook.
|
|
@@ -1555,7 +1582,7 @@ NOT_TOOL_HOOK = frozenset((b"turn", b"judge", b"claim", b"refusal",
|
|
|
1555
1582
|
b"after_block", b"subagent_end", b"stop_spec",
|
|
1556
1583
|
b"shape", b"compact", b"orch", b"crash", b"route",
|
|
1557
1584
|
b"spawned", b"forked", b"lesson", b"disarm", b"ledger_damage",
|
|
1558
|
-
b"attest"))
|
|
1585
|
+
b"attest", b"subagent_start"))
|
|
1559
1586
|
# The rows PostToolUse writes (`note_tool`): they prove the host ran the call,
|
|
1560
1587
|
# not that the gate saw it - a PostToolUse hook keeps writing them while a
|
|
1561
1588
|
# broken PreToolUse hook lets every call through - so they are no proof either.
|
|
@@ -1955,6 +1982,11 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
1955
1982
|
if not under(cwd):
|
|
1956
1983
|
return None
|
|
1957
1984
|
root = repo_root(cwd)
|
|
1985
|
+
# The repository's one workspace: a linked worktree reads and creates the
|
|
1986
|
+
# main checkout's `.tezgah`, where `git worktree remove` cannot take it and
|
|
1987
|
+
# where bin/tezgah-task already keeps the plans (plan 097).
|
|
1988
|
+
from tezgah_paths import linked_main
|
|
1989
|
+
ws = linked_main(root) or root
|
|
1958
1990
|
ACTIVE_ROOT[0] = root_for(cwd) or ""
|
|
1959
1991
|
# What the compaction kept, recorded before the block is built: the summary
|
|
1960
1992
|
# is the host's own text and this path is the only place it is handed over, so
|
|
@@ -2045,7 +2077,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2045
2077
|
# only here: a conditional paragraph reaches no static file. The
|
|
2046
2078
|
# one-line repeat carries it too: it is a fact, not rule text.
|
|
2047
2079
|
parts.append(("armed", render("\n\n".join(armed).replace(
|
|
2048
|
-
"{OPEN_LINES}", open_lines_note(
|
|
2080
|
+
"{OPEN_LINES}", open_lines_note(ws)))))
|
|
2049
2081
|
else:
|
|
2050
2082
|
audit_classification(set(), 0)
|
|
2051
2083
|
# One cheap judgement in front of the skill choice: the roster reaches the
|
|
@@ -2068,7 +2100,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2068
2100
|
# state_delta/constraint_notice), and the glance says whether the graph
|
|
2069
2101
|
# is behind that move - a comparison that used to end in a status glyph
|
|
2070
2102
|
# the model never reads.
|
|
2071
|
-
stamp = state_stamp(root)
|
|
2103
|
+
stamp = state_stamp(root, ws)
|
|
2072
2104
|
delta = state_delta(root, previous, stamp)
|
|
2073
2105
|
if delta:
|
|
2074
2106
|
parts.append(("delta", delta))
|
|
@@ -2081,8 +2113,8 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2081
2113
|
if (previous or {}).get("root") == root else []
|
|
2082
2114
|
relevant, keys = "", []
|
|
2083
2115
|
if prompt and ".no-lessons" not in repo_marks(cwd)[1]:
|
|
2084
|
-
relevant, keys = relevant_lessons(
|
|
2085
|
-
if relevant and workspace_from_repo(
|
|
2116
|
+
relevant, keys = relevant_lessons(ws, prompt, seen)
|
|
2117
|
+
if relevant and workspace_from_repo(ws):
|
|
2086
2118
|
relevant, keys = (("", []) if "provided" in seen else
|
|
2087
2119
|
(repo_provided(".tezgah/lessons.md"), ["provided"]))
|
|
2088
2120
|
if relevant:
|
|
@@ -2221,8 +2253,8 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2221
2253
|
# The workspace every per-project artifact lands in: created, ignored by
|
|
2222
2254
|
# the project and given its private git before anything writes there.
|
|
2223
2255
|
# A root is not a project, and the helper returns None outside a work tree.
|
|
2224
|
-
if
|
|
2225
|
-
ensure_workspace(
|
|
2256
|
+
if ws not in roots():
|
|
2257
|
+
ensure_workspace(ws)
|
|
2226
2258
|
# The live turn state, first in this region: on a compacted or resumed
|
|
2227
2259
|
# session it is the one thing the rest of the block cannot re-derive.
|
|
2228
2260
|
resume = resume_state(root, session_of(payload))
|
|
@@ -2238,9 +2270,9 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2238
2270
|
|
|
2239
2271
|
def provided():
|
|
2240
2272
|
if not from_repo:
|
|
2241
|
-
from_repo.append(workspace_from_repo(
|
|
2273
|
+
from_repo.append(workspace_from_repo(ws))
|
|
2242
2274
|
return from_repo[0]
|
|
2243
|
-
plans = open_plans(
|
|
2275
|
+
plans = open_plans(ws)
|
|
2244
2276
|
if plans:
|
|
2245
2277
|
parts.append(("plans", repo_provided(".tezgah/plans/open/")
|
|
2246
2278
|
if provided() else plans))
|
|
@@ -2255,7 +2287,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2255
2287
|
# The project's own rule files, agents and skills (tezgah-migrate's
|
|
2256
2288
|
# index): they stay where the project keeps them, and one line makes a
|
|
2257
2289
|
# session read the rows its task touches instead of never seeing them.
|
|
2258
|
-
if os.path.isfile(os.path.join(
|
|
2290
|
+
if os.path.isfile(os.path.join(ws, ".tezgah", "analysis",
|
|
2259
2291
|
"project-knowledge.md")):
|
|
2260
2292
|
parts.append(("knowledge",
|
|
2261
2293
|
"Project knowledge: this repo keeps its own rule files, "
|
|
@@ -2264,14 +2296,14 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2264
2296
|
"the task touches before starting; a nested AGENTS.md or "
|
|
2265
2297
|
"CLAUDE.md binds the subtree it sits in."))
|
|
2266
2298
|
if ".no-lessons" not in marks:
|
|
2267
|
-
past = lessons(
|
|
2299
|
+
past = lessons(ws)
|
|
2268
2300
|
if past:
|
|
2269
2301
|
parts.append(("lessons", repo_provided(".tezgah/lessons.md")
|
|
2270
2302
|
if provided() else past))
|
|
2271
2303
|
if not provided():
|
|
2272
|
-
recent = lesson_lines(
|
|
2304
|
+
recent = lesson_lines(ws)[-LESSON_LINES:]
|
|
2273
2305
|
injected = list(zip(recent, _lesson_shown(
|
|
2274
|
-
recent, tainted_lessons(
|
|
2306
|
+
recent, tainted_lessons(ws))))
|
|
2275
2307
|
# The opt-in taste learnings (hooks/tezgah_taste_ledger.block): the
|
|
2276
2308
|
# active ones as rules or hints, read from the ledger with no call out;
|
|
2277
2309
|
# off, `enabled` costs the marker stat. The injection is recorded below,
|
|
@@ -2297,7 +2329,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2297
2329
|
# bulk import at most daily (tezgah_store.import_later).
|
|
2298
2330
|
if event == "session_start":
|
|
2299
2331
|
tezgah_store.import_later(cache_dir())
|
|
2300
|
-
broken = tezgah_research.failing(
|
|
2332
|
+
broken = tezgah_research.failing(ws) if not off("research-off") else []
|
|
2301
2333
|
if broken:
|
|
2302
2334
|
line_slug, err = broken[0]
|
|
2303
2335
|
parts.append(("research_broken",
|