@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.
@@ -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, root_for, roots, say, tool,
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 with its open plan and
536
- research line counts, or "" when it has one checkout. Fork-free (the pinned
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. Each checkout keeps its own `.tezgah`;
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
- state = ("no workspace yet" if not os.path.isdir(ws) else
550
- "%d open plan(s), %d research line(s)"
551
- % (len(glob.glob(os.path.join(ws, "plans", "open", "*.md"))),
552
- len(tezgah_research.slugs(checkout))))
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, each with its own "
556
- "`.tezgah`: %s. Detail: `%s --all`."
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
- paths = sorted(glob.glob(os.path.join(root, ".tezgah", "plans",
651
- "open", "*.md")))
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 (SubagentStart writes it too), the omp fork's
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(root)))))
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(root, prompt, seen)
2085
- if relevant and workspace_from_repo(root):
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 root not in roots():
2225
- ensure_workspace(root)
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(root))
2273
+ from_repo.append(workspace_from_repo(ws))
2242
2274
  return from_repo[0]
2243
- plans = open_plans(root)
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(root, ".tezgah", "analysis",
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(root)
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(root)[-LESSON_LINES:]
2304
+ recent = lesson_lines(ws)[-LESSON_LINES:]
2273
2305
  injected = list(zip(recent, _lesson_shown(
2274
- recent, tainted_lessons(root))))
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(root) if not off("research-off") else []
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",