methodproof 0.8.7__tar.gz → 0.8.8__tar.gz

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.
Files changed (91) hide show
  1. {methodproof-0.8.7 → methodproof-0.8.8}/CHANGELOG.md +5 -0
  2. {methodproof-0.8.7 → methodproof-0.8.8}/PKG-INFO +1 -1
  3. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/agents/watcher.py +33 -7
  4. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/repos.py +2 -1
  5. {methodproof-0.8.7 → methodproof-0.8.8}/pyproject.toml +1 -1
  6. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_repos.py +18 -0
  7. methodproof-0.8.8/tests/test_watcher_git.py +41 -0
  8. {methodproof-0.8.7 → methodproof-0.8.8}/.github/workflows/ci.yml +0 -0
  9. {methodproof-0.8.7 → methodproof-0.8.8}/.gitignore +0 -0
  10. {methodproof-0.8.7 → methodproof-0.8.8}/LICENSE +0 -0
  11. {methodproof-0.8.7 → methodproof-0.8.8}/README.md +0 -0
  12. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/__init__.py +0 -0
  13. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/__main__.py +0 -0
  14. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/_daemon.py +0 -0
  15. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/agents/__init__.py +0 -0
  16. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/agents/base.py +0 -0
  17. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/agents/music.py +0 -0
  18. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/agents/terminal.py +0 -0
  19. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/analysis.py +0 -0
  20. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/binding.py +0 -0
  21. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/bip39.py +0 -0
  22. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/bridge.py +0 -0
  23. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/cli.py +0 -0
  24. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/config.py +0 -0
  25. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/crypto.py +0 -0
  26. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/e2e.py +0 -0
  27. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/graph.py +0 -0
  28. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hook.py +0 -0
  29. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/__init__.py +0 -0
  30. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/claude_code.py +0 -0
  31. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/claude_code.sh +0 -0
  32. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/cline_hook.sh +0 -0
  33. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/codex_hook.sh +0 -0
  34. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/gemini_hook.sh +0 -0
  35. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/install.py +0 -0
  36. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/kiro_hook.sh +0 -0
  37. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/mcp_register.py +0 -0
  38. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/model_cache.py +0 -0
  39. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/openclaw/HOOK.md +0 -0
  40. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/openclaw/handler.ts +0 -0
  41. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/openclaw_install.py +0 -0
  42. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/opencode_plugin.js +0 -0
  43. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/hooks/wrappers.py +0 -0
  44. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/integrity.py +0 -0
  45. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/kdf.py +0 -0
  46. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/keychain.py +0 -0
  47. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/live.py +0 -0
  48. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/lock.py +0 -0
  49. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/mcp.py +0 -0
  50. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/migrate_db.py +0 -0
  51. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/proxy.py +0 -0
  52. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/proxy_daemon.py +0 -0
  53. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/skills/methodproof/SKILL.md +0 -0
  54. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/store.py +0 -0
  55. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/sync.py +0 -0
  56. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/__init__.py +0 -0
  57. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/consent.py +0 -0
  58. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/init.py +0 -0
  59. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/log.py +0 -0
  60. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/login_success.py +0 -0
  61. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/review.py +0 -0
  62. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/start.py +0 -0
  63. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/status.py +0 -0
  64. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/tui/theme.py +0 -0
  65. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/viewer.py +0 -0
  66. {methodproof-0.8.7 → methodproof-0.8.8}/methodproof/wordlist.py +0 -0
  67. {methodproof-0.8.7 → methodproof-0.8.8}/test_windows_compat.py +0 -0
  68. {methodproof-0.8.7 → methodproof-0.8.8}/tests/__init__.py +0 -0
  69. {methodproof-0.8.7 → methodproof-0.8.8}/tests/conftest.py +0 -0
  70. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_analysis.py +0 -0
  71. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_cli_auth.py +0 -0
  72. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_cli_config.py +0 -0
  73. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_cli_helpers.py +0 -0
  74. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_cli_session.py +0 -0
  75. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_cli_share.py +0 -0
  76. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_cli_start.py +0 -0
  77. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_cli_update.py +0 -0
  78. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_e2e_integration.py +0 -0
  79. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_graph.py +0 -0
  80. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_hooks.py +0 -0
  81. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_live.py +0 -0
  82. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_model_cache.py +0 -0
  83. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_openclaw_hooks.py +0 -0
  84. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_profiles.py +0 -0
  85. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_security.py +0 -0
  86. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_store.py +0 -0
  87. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_sync.py +0 -0
  88. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_viewer.py +0 -0
  89. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_watcher_ignore.py +0 -0
  90. {methodproof-0.8.7 → methodproof-0.8.8}/tests/test_wrappers.py +0 -0
  91. {methodproof-0.8.7 → methodproof-0.8.8}/uv.lock +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.8] — 2026-05-30
4
+
5
+ ### Fixed
6
+ - **Git telemetry inside worktrees** — the watcher and sub-repo enumeration both assumed `.git` is a directory, but in a `git worktree` it is a file pointing at the shared git dir. As a result, running `mp start` from a worktree captured zero `git_commit`/`git_branch_switch` events (the poller raised `NotADirectoryError` reading `.git/refs` and `.git/HEAD`, swallowed silently) and `mp push`/`mp resync` skipped the worktree's repo entirely. The poller now resolves the per-worktree git dir (HEAD) and shared common dir (refs) via `git rev-parse --absolute-git-dir --git-common-dir` (`methodproof/agents/watcher.py`), and sub-repo enumeration detects worktrees via `os.path.exists` rather than `os.path.isdir` (`methodproof/repos.py`).
7
+
3
8
  ## [0.8.7] — 2026-05-15
4
9
 
5
10
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.8.7
3
+ Version: 0.8.8
4
4
  Summary: See how you code. Capture and visualize your engineering process.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -268,13 +268,39 @@ def _read_branch(head_file: Path) -> str:
268
268
  return ""
269
269
 
270
270
 
271
+ def _resolve_git_dirs(watch_dir: str) -> tuple[Path, Path] | None:
272
+ """Resolve (git_dir, common_dir) for `watch_dir`, or None if not a git repo.
273
+
274
+ HEAD lives in git_dir; shared refs live in common_dir. The two diverge
275
+ inside a git worktree, where `.git` is a file rather than a directory.
276
+ """
277
+ try:
278
+ out = subprocess.run(
279
+ ["git", "-C", watch_dir, "rev-parse", "--absolute-git-dir", "--git-common-dir"],
280
+ capture_output=True, text=True, timeout=5,
281
+ )
282
+ except (FileNotFoundError, subprocess.TimeoutExpired):
283
+ return None
284
+ if out.returncode != 0:
285
+ return None
286
+ lines = out.stdout.splitlines()
287
+ if len(lines) < 2:
288
+ return None
289
+ git_dir = Path(lines[0].strip())
290
+ common_dir = Path(lines[1].strip())
291
+ if not common_dir.is_absolute():
292
+ common_dir = (Path(watch_dir) / common_dir).resolve()
293
+ return git_dir, common_dir
294
+
295
+
271
296
  def _poll_git(watch_dir: str, stop: threading.Event) -> None:
272
- """Poll .git/refs for new commits and HEAD for branch switches."""
273
- git_dir = Path(watch_dir) / ".git"
274
- if not git_dir.exists():
297
+ """Poll shared refs for new commits and HEAD for branch switches."""
298
+ dirs = _resolve_git_dirs(watch_dir)
299
+ if dirs is None:
275
300
  return
301
+ git_dir, common_dir = dirs
276
302
  seen: set[str] = set()
277
- refs = git_dir / "refs" / "heads"
303
+ refs = common_dir / "refs" / "heads"
278
304
  head_file = git_dir / "HEAD"
279
305
  last_branch = _read_branch(head_file)
280
306
  while not stop.is_set():
@@ -284,7 +310,7 @@ def _poll_git(watch_dir: str, stop: threading.Event) -> None:
284
310
  if sha not in seen:
285
311
  seen.add(sha)
286
312
  if len(seen) > 1:
287
- _log_commit(watch_dir, sha)
313
+ _log_commit(watch_dir, sha, head_file)
288
314
  except OSError:
289
315
  pass
290
316
  current_branch = _read_branch(head_file)
@@ -296,7 +322,7 @@ def _poll_git(watch_dir: str, stop: threading.Event) -> None:
296
322
  stop.wait(2)
297
323
 
298
324
 
299
- def _log_commit(watch_dir: str, sha: str) -> None:
325
+ def _log_commit(watch_dir: str, sha: str, head_file: Path) -> None:
300
326
  try:
301
327
  fmt = subprocess.run(
302
328
  ["git", "-C", watch_dir, "log", "-1",
@@ -336,7 +362,7 @@ def _log_commit(watch_dir: str, sha: str) -> None:
336
362
  meta["body"] = body
337
363
  if file_statuses:
338
364
  meta["file_statuses"] = file_statuses
339
- branch = _read_branch(Path(watch_dir) / ".git" / "HEAD")
365
+ branch = _read_branch(head_file)
340
366
  if branch:
341
367
  meta["branch"] = branch
342
368
  include_lines = base.is_content_captured()
@@ -26,7 +26,8 @@ def enumerate_sub_repos(watch_dir: str, max_depth: int = 2) -> list[dict[str, st
26
26
  def visit(path: str, depth: int) -> None:
27
27
  if not os.path.isdir(path):
28
28
  return
29
- if os.path.isdir(os.path.join(path, ".git")):
29
+ # `.git` is a directory in a normal clone, a file in a worktree.
30
+ if os.path.exists(os.path.join(path, ".git")):
30
31
  url = _remote_url(path)
31
32
  if url and url not in seen_urls:
32
33
  rel = os.path.relpath(path, watch_dir)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.8.7"
3
+ version = "0.8.8"
4
4
  description = "See how you code. Capture and visualize your engineering process."
5
5
  requires-python = ">=3.12"
6
6
  dependencies = ["watchdog>=4.0", "websocket-client>=1.7", "cryptography>=46.0.7", "keyring>=25.0", "textual>=0.59", "rich>=13.7", "sqlcipher3>=0.6"]
@@ -12,6 +12,9 @@ from methodproof import repos
12
12
  def _git_init(path: Path, remote: str) -> None:
13
13
  path.mkdir(parents=True, exist_ok=True)
14
14
  subprocess.run(["git", "init", "-q"], cwd=path, check=True)
15
+ # CI runners have no global git identity; commits need a local one.
16
+ subprocess.run(["git", "config", "user.email", "test@methodproof.com"], cwd=path, check=True)
17
+ subprocess.run(["git", "config", "user.name", "test"], cwd=path, check=True)
15
18
  subprocess.run(["git", "remote", "add", "origin", remote], cwd=path, check=True)
16
19
 
17
20
 
@@ -71,3 +74,18 @@ def test_enumerate_sub_repos_skips_non_repo_dirs(tmp_path: Path) -> None:
71
74
  assert "node_modules" not in rels
72
75
  assert "not-a-repo" not in rels
73
76
  assert "real" in rels
77
+
78
+
79
+ def test_enumerate_sub_repos_detects_worktree(tmp_path: Path) -> None:
80
+ main = tmp_path / "main"
81
+ _git_init(main, "https://github.com/me/wt")
82
+ subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"],
83
+ cwd=main, check=True)
84
+ wt = tmp_path / "wt-feature"
85
+ subprocess.run(["git", "worktree", "add", "-q", str(wt), "-b", "feature"],
86
+ cwd=main, check=True)
87
+
88
+ # `.git` is a file in the worktree, not a directory.
89
+ assert (wt / ".git").is_file()
90
+ found = repos.enumerate_sub_repos(str(wt), max_depth=1)
91
+ assert found == [{"remote_url": "https://github.com/me/wt", "rel_path": ""}]
@@ -0,0 +1,41 @@
1
+ """Tests for git dir resolution — normal clones vs worktrees."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from methodproof.agents import watcher
7
+
8
+
9
+ def _git_init(path: Path) -> None:
10
+ path.mkdir(parents=True, exist_ok=True)
11
+ subprocess.run(["git", "init", "-q"], cwd=path, check=True)
12
+ # CI runners have no global git identity; commits need a local one.
13
+ subprocess.run(["git", "config", "user.email", "test@methodproof.com"], cwd=path, check=True)
14
+ subprocess.run(["git", "config", "user.name", "test"], cwd=path, check=True)
15
+ subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"],
16
+ cwd=path, check=True)
17
+
18
+
19
+ def test_resolve_git_dirs_normal_repo(tmp_path: Path) -> None:
20
+ _git_init(tmp_path)
21
+ git_dir, common_dir = watcher._resolve_git_dirs(str(tmp_path))
22
+ assert git_dir == common_dir
23
+ assert (git_dir / "HEAD").is_file()
24
+
25
+
26
+ def test_resolve_git_dirs_worktree(tmp_path: Path) -> None:
27
+ main = tmp_path / "main"
28
+ _git_init(main)
29
+ wt = tmp_path / "wt-feature"
30
+ subprocess.run(["git", "worktree", "add", "-q", str(wt), "-b", "feature"],
31
+ cwd=main, check=True)
32
+
33
+ git_dir, common_dir = watcher._resolve_git_dirs(str(wt))
34
+ # Worktree HEAD is per-worktree; shared refs live in the common dir.
35
+ assert git_dir != common_dir
36
+ assert watcher._read_branch(git_dir / "HEAD") == "feature"
37
+ assert (common_dir / "refs" / "heads" / "feature").is_file()
38
+
39
+
40
+ def test_resolve_git_dirs_non_repo(tmp_path: Path) -> None:
41
+ assert watcher._resolve_git_dirs(str(tmp_path)) is None
File without changes
File without changes
File without changes
File without changes