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