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