methodproof 0.6.0__tar.gz → 0.7.0__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 (64) hide show
  1. {methodproof-0.6.0 → methodproof-0.7.0}/CHANGELOG.md +11 -0
  2. {methodproof-0.6.0 → methodproof-0.7.0}/PKG-INFO +1 -1
  3. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/_daemon.py +16 -2
  4. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/agents/base.py +55 -3
  5. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/agents/watcher.py +29 -1
  6. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/cli.py +80 -20
  7. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/claude_code.py +47 -1
  8. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/claude_code.sh +52 -2
  9. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/install.py +14 -0
  10. {methodproof-0.6.0 → methodproof-0.7.0}/pyproject.toml +1 -1
  11. {methodproof-0.6.0 → methodproof-0.7.0}/uv.lock +1 -1
  12. {methodproof-0.6.0 → methodproof-0.7.0}/.github/workflows/ci.yml +0 -0
  13. {methodproof-0.6.0 → methodproof-0.7.0}/.gitignore +0 -0
  14. {methodproof-0.6.0 → methodproof-0.7.0}/LICENSE +0 -0
  15. {methodproof-0.6.0 → methodproof-0.7.0}/README.md +0 -0
  16. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/__init__.py +0 -0
  17. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/__main__.py +0 -0
  18. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/agents/__init__.py +0 -0
  19. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/agents/music.py +0 -0
  20. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/agents/terminal.py +0 -0
  21. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/analysis.py +0 -0
  22. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/binding.py +0 -0
  23. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/bip39.py +0 -0
  24. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/bridge.py +0 -0
  25. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/config.py +0 -0
  26. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/crypto.py +0 -0
  27. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/graph.py +0 -0
  28. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hook.py +0 -0
  29. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/__init__.py +0 -0
  30. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/cline_hook.sh +0 -0
  31. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/codex_hook.sh +0 -0
  32. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/gemini_hook.sh +0 -0
  33. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/kiro_hook.sh +0 -0
  34. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/mcp_register.py +0 -0
  35. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/openclaw/HOOK.md +0 -0
  36. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/openclaw/handler.ts +0 -0
  37. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/openclaw_install.py +0 -0
  38. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/opencode_plugin.js +0 -0
  39. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/hooks/wrappers.py +0 -0
  40. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/integrity.py +0 -0
  41. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/kdf.py +0 -0
  42. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/keychain.py +0 -0
  43. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/live.py +0 -0
  44. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/lock.py +0 -0
  45. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/mcp.py +0 -0
  46. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/migrate_db.py +0 -0
  47. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/proxy.py +0 -0
  48. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/proxy_daemon.py +0 -0
  49. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/repos.py +0 -0
  50. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/skills/methodproof/SKILL.md +0 -0
  51. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/store.py +0 -0
  52. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/sync.py +0 -0
  53. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/viewer.py +0 -0
  54. {methodproof-0.6.0 → methodproof-0.7.0}/methodproof/wordlist.py +0 -0
  55. {methodproof-0.6.0 → methodproof-0.7.0}/test_windows_compat.py +0 -0
  56. {methodproof-0.6.0 → methodproof-0.7.0}/tests/__init__.py +0 -0
  57. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_analysis.py +0 -0
  58. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_graph.py +0 -0
  59. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_hooks.py +0 -0
  60. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_live.py +0 -0
  61. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_openclaw_hooks.py +0 -0
  62. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_security.py +0 -0
  63. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_store.py +0 -0
  64. {methodproof-0.6.0 → methodproof-0.7.0}/tests/test_wrappers.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0] — 2026-04-07
4
+
5
+ ### Fixed
6
+ - **Runaway event logging** — watcher ignored only 7 patterns, tracking 35K+ files including `.venv/` (20K), `dist/build/` (23K). Expanded to 30+ ignore patterns covering Python, JS/TS, Rust, Go, Java, Ruby, PHP, .NET, Swift, and build artifacts. **138K → 1,442 files (99% reduction)**
7
+ - **Claude Code hook errors** — hook script pointed to deleted pyenv site-packages path; added pidfile guard (`exit 0` when no session active) to prevent bridge connection failures
8
+
9
+ ### Added
10
+ - `mp start --streaming` — blocking foreground mode, streams every captured event to stdout in real-time with human-readable formatting
11
+ - `mp start --verbose` / `-v` — structured debug logging at each step (config, auth, session, anchor, daemon spawn); daemon log includes agent init and buffer/flush stats
12
+ - Step-by-step progress output on default `mp start` (`→ Loading config`, `→ Checking hooks`, etc.) so failures are immediately locatable
13
+
3
14
  ## [0.5.1] — 2026-04-06
4
15
 
5
16
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.6.0
3
+ Version: 0.7.0
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
@@ -18,6 +18,7 @@ PIDFILE = config.DIR / "methodproof.pid"
18
18
  def main() -> None:
19
19
  sid = sys.argv[1]
20
20
  watch_dir = sys.argv[2]
21
+ verbose = "--verbose" in sys.argv
21
22
 
22
23
  cfg = config.load()
23
24
  capture = cfg.get("capture", {})
@@ -28,10 +29,15 @@ def main() -> None:
28
29
  del cfg["_live_url"]
29
30
  config.save(cfg)
30
31
 
31
- base.init(sid, live=bool(live_url))
32
+ base.init(sid, live=bool(live_url), verbose=verbose)
33
+
34
+ if verbose:
35
+ active = [k for k, v in capture.items() if v]
36
+ base.log("info", "daemon.config", capture=active, live=bool(live_url),
37
+ journal=cfg.get("journal_mode", False))
32
38
 
33
- # Environment profile (was done pre-fork in old code, now done in daemon)
34
39
  if capture.get("environment_analysis", True):
40
+ base.log("info", "daemon.agent.environment_scan") if verbose else None
35
41
  try:
36
42
  from methodproof.analysis import scan_environment
37
43
  env_profile = scan_environment(watch_dir)
@@ -52,12 +58,16 @@ def main() -> None:
52
58
  threads.append(threading.Thread(
53
59
  target=watcher.start, args=(watch_dir, stop_event), daemon=True,
54
60
  ))
61
+ if verbose:
62
+ base.log("info", "daemon.agent.watcher", dir=watch_dir)
55
63
 
56
64
  if capture.get("terminal_commands", True) or capture.get("test_results", True):
57
65
  from methodproof.agents import terminal
58
66
  threads.append(threading.Thread(
59
67
  target=terminal.start, args=(stop_event,), daemon=True,
60
68
  ))
69
+ if verbose:
70
+ base.log("info", "daemon.agent.terminal", log=str(config.CMD_LOG))
61
71
 
62
72
  if capture.get("browser", True):
63
73
  from methodproof import bridge
@@ -68,12 +78,16 @@ def main() -> None:
68
78
  cfg.get("journal_mode", False)),
69
79
  daemon=True,
70
80
  ))
81
+ if verbose:
82
+ base.log("info", "daemon.agent.bridge", port=9877)
71
83
 
72
84
  if capture.get("music", True):
73
85
  from methodproof.agents import music
74
86
  threads.append(threading.Thread(
75
87
  target=music.start, args=(stop_event,), daemon=True,
76
88
  ))
89
+ if verbose:
90
+ base.log("info", "daemon.agent.music")
77
91
 
78
92
  def _shutdown(sig_num: int, frame: object) -> None:
79
93
  stop_event.set()
@@ -21,6 +21,8 @@ _MAX_RETRIES = 3
21
21
  _prev_hash = "genesis"
22
22
  _account_id = ""
23
23
  _journal_mode = False
24
+ _verbose = False
25
+ _streaming = False
24
26
 
25
27
  # Maps event types to the capture category that gates them
26
28
  _EVENT_GATES: dict[str, str] = {
@@ -91,11 +93,13 @@ def _load_encryption_key(cfg: dict) -> bytes | None:
91
93
  return bytes.fromhex(raw) if raw else None
92
94
 
93
95
 
94
- def init(session_id: str, live: bool = False) -> None:
95
- global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash, _journal_mode, _account_id
96
+ def init(session_id: str, live: bool = False, verbose: bool = False, streaming: bool = False) -> None:
97
+ global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash, _journal_mode, _account_id, _verbose, _streaming
96
98
  _session_id = session_id
97
99
  _initialized = True
98
100
  _live_mode = live
101
+ _verbose = verbose
102
+ _streaming = streaming
99
103
  _prev_hash = "genesis"
100
104
  from methodproof import config
101
105
  cfg = config.load()
@@ -103,6 +107,10 @@ def init(session_id: str, live: bool = False) -> None:
103
107
  _capture = cfg.get("capture", {})
104
108
  _journal_mode = cfg.get("journal_mode", False)
105
109
  _account_id = cfg.get("account_id", "")
110
+ if _verbose or _streaming:
111
+ active = [k for k, v in _capture.items() if v]
112
+ log("info", "base.init", encryption=bool(_e2e_key), journal=_journal_mode,
113
+ live=_live_mode, capture=active)
106
114
 
107
115
 
108
116
  def log(level: str, event: str, **kw: object) -> None:
@@ -153,6 +161,10 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
153
161
  _buffer.append(entry)
154
162
  if len(_buffer) >= _FLUSH_SIZE:
155
163
  _flush_locked()
164
+ if _verbose:
165
+ log("debug", "emit.buffered", type=event_type, buffer=len(_buffer))
166
+ if _streaming:
167
+ _stream_event(entry)
156
168
  if _live_mode:
157
169
  from methodproof import live as live_mod
158
170
  live_mod.send(entry)
@@ -168,15 +180,55 @@ def _flush_locked() -> None:
168
180
  return
169
181
  batch = list(_buffer)
170
182
  hashes = [(e["id"], e.pop("_chain_hash")) for e in batch if "_chain_hash" in e]
183
+ if _verbose or _streaming:
184
+ types = [e["type"] for e in batch]
185
+ log("info", "flush.start", count=len(batch), types=types)
171
186
  for attempt in range(_MAX_RETRIES):
172
187
  try:
173
188
  store.insert_events(_session_id, batch)
174
189
  if hashes:
175
190
  store.insert_event_hashes(hashes)
191
+ if _verbose or _streaming:
192
+ log("info", "flush.ok", count=len(batch))
176
193
  _buffer.clear()
177
194
  return
178
195
  except Exception as exc:
179
196
  log("warning", "flush.retry", attempt=attempt + 1, error=str(exc))
180
197
  time.sleep(0.1 * (attempt + 1))
181
- # Final attempt failed — keep events in buffer for next flush cycle
182
198
  log("error", "flush.failed", count=len(batch), retries=_MAX_RETRIES)
199
+
200
+
201
+ def _stream_event(entry: dict[str, Any]) -> None:
202
+ """Print a human-readable event line to stdout for --streaming mode."""
203
+ ts = time.strftime("%H:%M:%S", time.localtime(entry["timestamp"]))
204
+ etype = entry["type"]
205
+ meta = entry.get("metadata", {})
206
+ # Build a compact summary per event type
207
+ detail = ""
208
+ if etype == "file_edit":
209
+ detail = f'{meta.get("path", "?")} +{meta.get("lines_added", 0)}-{meta.get("lines_removed", 0)}'
210
+ elif etype == "file_create":
211
+ detail = f'{meta.get("path", "?")} ({meta.get("size", 0)}B)'
212
+ elif etype == "file_delete":
213
+ detail = meta.get("path", "?")
214
+ elif etype == "terminal_cmd":
215
+ detail = f'{meta.get("command", "?")[:60]} → exit {meta.get("exit_code", "?")}'
216
+ elif etype == "test_run":
217
+ detail = f'{meta.get("framework", "?")} {meta.get("passed", 0)}✓ {meta.get("failed", 0)}✗'
218
+ elif etype == "git_commit":
219
+ detail = f'{meta.get("hash", "?")} {meta.get("message", "")[:50]}'
220
+ elif etype in ("llm_prompt", "agent_prompt", "user_prompt"):
221
+ detail = f'len={meta.get("prompt_length", meta.get("message_length", "?"))}'
222
+ elif etype in ("llm_completion", "agent_completion"):
223
+ detail = f'len={meta.get("response_length", "?")}'
224
+ elif etype == "music_playing":
225
+ detail = f'{meta.get("artist", "?")} — {meta.get("track", "?")}'
226
+ elif etype.startswith("browser_"):
227
+ detail = meta.get("url", meta.get("query", ""))[:60]
228
+ elif etype == "environment_profile":
229
+ detail = f'{meta.get("tool_count", "?")} tools'
230
+ else:
231
+ keys = list(meta.keys())[:4]
232
+ detail = ", ".join(f"{k}={meta[k]}" for k in keys) if keys else ""
233
+ sys.stdout.write(f" [{ts}] {etype:30s} {detail}\n")
234
+ sys.stdout.flush()
@@ -13,7 +13,35 @@ from watchdog.observers import Observer
13
13
  from methodproof.agents import base
14
14
 
15
15
  IGNORE_PATTERNS = re.compile(
16
- r"(__pycache__|\.pyc|\.git/|node_modules|\.DS_Store|\.swp|~$)"
16
+ # Version control
17
+ r"(\.git/|\.hg/|\.svn/"
18
+ # OS / editor artifacts
19
+ r"|\.DS_Store|Thumbs\.db|\.swp|\.swo|~$|\.idea/|\.vscode/"
20
+ # Python
21
+ r"|__pycache__|\.pyc|\.pyo|\.egg-info/|\.eggs/|\.tox/"
22
+ r"|\.venv/|venv/|\.env/|env/|\.mypy_cache/|\.pytest_cache/|\.ruff_cache/"
23
+ r"|\.coverage|htmlcov/|\.nox/"
24
+ # JavaScript / TypeScript
25
+ r"|node_modules/|\.next/|\.nuxt/|\.expo/|\.turbo/|\.parcel-cache/"
26
+ r"|\.svelte-kit/|\.angular/|\.cache/"
27
+ # Rust
28
+ r"|/target/|\.cargo/"
29
+ # Go
30
+ r"|vendor/"
31
+ # Java / Kotlin / JVM
32
+ r"|\.gradle/|\.m2/|/out/"
33
+ # Ruby
34
+ r"|\.bundle/|\.gem/"
35
+ # PHP
36
+ r"|/vendor/|\.phpunit/"
37
+ # .NET / C#
38
+ r"|/bin/|/obj/|\.nuget/"
39
+ # Swift / Xcode
40
+ r"|\.build/|DerivedData/|Pods/"
41
+ # Build output / artifacts
42
+ r"|dist/|build/|\.output/"
43
+ # Logs and locks
44
+ r"|\.lock$|\.log$)"
17
45
  )
18
46
 
19
47
 
@@ -845,28 +845,54 @@ def _is_daemon_alive() -> bool:
845
845
  return False
846
846
 
847
847
 
848
+ def _log_step(msg: str, verbose: bool = False) -> None:
849
+ """Print a step-by-step progress line. Always shown."""
850
+ print(f" → {msg}")
851
+
852
+
853
+ def _log_debug(msg: str, **kw: object) -> None:
854
+ """Print structured debug line (--verbose only). Writes to stderr."""
855
+ import json as _json
856
+ entry = {"ts": time.time(), "level": "debug", "event": msg, **kw}
857
+ sys.stderr.write(_json.dumps(entry, default=str) + "\n")
858
+
859
+
848
860
  def cmd_start(args: argparse.Namespace) -> None:
861
+ verbose = getattr(args, "verbose", False)
862
+ streaming = getattr(args, "streaming", False)
863
+
864
+ _log_step("Loading config")
849
865
  cfg = config.load()
866
+ if verbose:
867
+ _log_debug("config.loaded", api_url=cfg.get("api_url"), account_id=cfg.get("account_id", "")[:8],
868
+ active_session=cfg.get("active_session"), journal=cfg.get("journal_mode"))
869
+
850
870
  if cfg.get("auto_update"):
871
+ _log_step("Checking for updates")
851
872
  _auto_update()
873
+
852
874
  if cfg.get("active_session"):
853
875
  if _is_daemon_alive():
854
876
  print(f"Session active: {cfg['active_session'][:8]}")
855
877
  print("Run `methodproof stop` first.")
856
878
  sys.exit(1)
857
- # Daemon is dead — clean up the stale session
858
879
  stale_sid = cfg["active_session"]
880
+ _log_step(f"Cleaning stale session {stale_sid[:8]}")
859
881
  store.complete_session(stale_sid)
860
882
  graph.build(stale_sid)
861
883
  PIDFILE.unlink(missing_ok=True)
862
884
  cfg["active_session"] = None
863
885
  config.save(cfg)
864
- print(f"Cleaned up stale session {stale_sid[:8]} (daemon was not running).")
886
+
887
+ _log_step("Checking hooks")
865
888
  if not hook.is_installed():
866
- print("Run `methodproof init` first.")
889
+ print("ERROR: Run `methodproof init` first.")
867
890
  sys.exit(1)
868
891
 
892
+ _log_step("Authenticating")
869
893
  account_id = _require_auth(cfg)
894
+ if verbose:
895
+ _log_debug("auth.ok", account_id=account_id[:8] if account_id else "none")
870
896
 
871
897
  # Check for new consent categories before recording
872
898
  capture = cfg.get("capture", {})
@@ -878,6 +904,7 @@ def cmd_start(args: argparse.Namespace) -> None:
878
904
  config.save(cfg)
879
905
  print()
880
906
 
907
+ _log_step("Creating session")
881
908
  sid = uuid.uuid4().hex
882
909
  watch_dir = os.path.abspath(args.dir or ".")
883
910
  repo_url = args.repo or repos.detect_repo(watch_dir)
@@ -885,7 +912,6 @@ def cmd_start(args: argparse.Namespace) -> None:
885
912
  visibility = "public" if args.public else "private"
886
913
  from methodproof.binding import compute_binding, compute_device_id
887
914
  device_id = compute_device_id()
888
- # Compute session binding if master key is available
889
915
  binding = ""
890
916
  if cfg.get("master_key_fingerprint") and account_id:
891
917
  from methodproof.keychain import load_secret
@@ -895,38 +921,45 @@ def cmd_start(args: argparse.Namespace) -> None:
895
921
  master = derive_master(entropy)
896
922
  bind_key = derive_bind_key(master, account_id)
897
923
  binding = compute_binding(bind_key, sid, account_id, device_id, time.time())
924
+ if verbose:
925
+ _log_debug("binding.computed", device_id=device_id[:8])
898
926
  store.create_session(sid, watch_dir, repo_url, json.dumps(tags), visibility,
899
927
  account_id, binding, device_id)
900
928
  cfg["active_session"] = sid
901
929
  config.save(cfg)
902
930
  PIDFILE.write_text(str(os.getpid()))
931
+ if verbose:
932
+ _log_debug("session.created", sid=sid[:8], watch_dir=watch_dir, visibility=visibility,
933
+ device_id=device_id[:8], bound=bool(binding))
903
934
 
904
- # Temporal anchor — server-signed timestamp (best-effort, skip if offline)
935
+ # Temporal anchor
905
936
  if cfg.get("token"):
937
+ _log_step("Requesting temporal anchor")
906
938
  try:
907
939
  from methodproof.sync import _request
908
940
  anchor = _request("POST", f"/sessions/{sid}/anchor", cfg["api_url"], cfg["token"])
909
941
  store.update_anchor(sid, anchor["anchor_ts"], anchor["signature"])
942
+ if verbose:
943
+ _log_debug("anchor.ok", anchor_ts=anchor["anchor_ts"])
910
944
  except Exception as exc:
911
- print(f"Anchor: skipped ({exc})")
945
+ _log_step(f"Anchor: skipped ({exc})")
912
946
 
913
947
  from methodproof.agents import base
914
- live_ok = False
915
948
  capture = cfg.get("capture", {})
916
949
 
950
+ # Live streaming
917
951
  live_url = ""
918
952
  want_live = args.live or getattr(args, "live_public", False)
919
953
  live_visibility = "public" if getattr(args, "live_public", False) else "private"
920
954
  if want_live:
955
+ _log_step("Connecting live stream")
921
956
  if not cfg.get("token"):
922
957
  print("Live mode requires login. Run `methodproof login` first.")
923
958
  sys.exit(1)
924
- # Create remote session first
925
959
  from methodproof.sync import _request
926
960
  result = _request("POST", "/personal/sessions", cfg["api_url"], cfg["token"])
927
961
  remote_id = result["session_id"]
928
962
  store.mark_synced(sid, remote_id)
929
- # Connect live WebSocket
930
963
  from methodproof import live as live_mod
931
964
  live_url = live_mod.start(cfg["api_url"], cfg["token"], remote_id, capture, live_visibility) or ""
932
965
  if not live_url:
@@ -939,7 +972,7 @@ def cmd_start(args: argparse.Namespace) -> None:
939
972
  print(f"Live (public): {live_url}")
940
973
  print(" Anyone with this link can watch your session build in real time.")
941
974
 
942
- # Journal mode — per-session override or persistent config
975
+ # Journal mode
943
976
  if getattr(args, "journal", False):
944
977
  cfg["journal_mode"] = True
945
978
  credits = cfg.get("journal_credits", 0)
@@ -966,25 +999,35 @@ def cmd_start(args: argparse.Namespace) -> None:
966
999
  print(f"Bridge: http://localhost:9877")
967
1000
  if live_url:
968
1001
  print(f"Live: {live_url}")
1002
+ if verbose:
1003
+ print(f"Mode: verbose (debug → daemon.log)")
1004
+ if streaming:
1005
+ print(f"Mode: streaming (blocking, events → stdout)")
1006
+
1007
+ # --streaming: blocking foreground mode with real-time event output
1008
+ if streaming:
1009
+ _run_foreground(sid, watch_dir, cfg, capture, live_url, verbose=True, streaming=True)
1010
+ return
969
1011
 
970
- # Daemonize: spawn a subprocess (safe on macOS — avoids CoreFoundation segfault from os.fork)
1012
+ # Daemonize (macOS/Linux)pass --verbose to daemon if set
971
1013
  if sys.platform != "win32":
972
1014
  import subprocess as _sp
973
1015
  daemon_log = config.DIR / "daemon.log"
974
1016
  log_fh = open(daemon_log, "a")
975
- proc = _sp.Popen(
976
- [sys.executable, "-m", "methodproof._daemon", sid, watch_dir],
977
- start_new_session=True,
978
- stdout=log_fh, stderr=log_fh, stdin=_sp.DEVNULL,
979
- )
1017
+ cmd = [sys.executable, "-m", "methodproof._daemon", sid, watch_dir]
1018
+ if verbose:
1019
+ cmd.append("--verbose")
1020
+ _log_step(f"Spawning daemon (log: {daemon_log})")
1021
+ proc = _sp.Popen(cmd, start_new_session=True, stdout=log_fh, stderr=log_fh, stdin=_sp.DEVNULL)
980
1022
  PIDFILE.write_text(str(proc.pid))
981
- # Verify daemon is alive after brief startup
1023
+ if verbose:
1024
+ _log_debug("daemon.spawned", pid=proc.pid, cmd=cmd)
982
1025
  time.sleep(1)
983
1026
  if proc.poll() is not None:
984
1027
  print(f"ERROR: Daemon exited immediately (code {proc.returncode}). Check {daemon_log}")
985
1028
  PIDFILE.unlink(missing_ok=True)
986
1029
  sys.exit(1)
987
- # Check extension auto-discovery (extension polls every ~6s)
1030
+ _log_step(f"Daemon alive (pid {proc.pid})")
988
1031
  if capture.get("browser", True):
989
1032
  import urllib.request as _ur
990
1033
  time.sleep(7)
@@ -1001,9 +1044,16 @@ def cmd_start(args: argparse.Namespace) -> None:
1001
1044
  return
1002
1045
 
1003
1046
  # Windows: foreground mode (no subprocess daemonization)
1047
+ _run_foreground(sid, watch_dir, cfg, capture, live_url, verbose=verbose, streaming=False)
1048
+
1049
+
1050
+ def _run_foreground(sid: str, watch_dir: str, cfg: dict, capture: dict,
1051
+ live_url: str, verbose: bool, streaming: bool) -> None:
1052
+ """Run capture agents in the foreground (blocking). Used by --streaming and Windows."""
1004
1053
  from methodproof.agents import base as _base
1005
- _base.init(sid, live=bool(live_url))
1054
+ _base.init(sid, live=bool(live_url), verbose=verbose, streaming=streaming)
1006
1055
  if capture.get("environment_analysis", True):
1056
+ _log_step("Scanning environment")
1007
1057
  try:
1008
1058
  from methodproof.analysis import scan_environment
1009
1059
  env_profile = scan_environment(watch_dir)
@@ -1020,9 +1070,11 @@ def cmd_start(args: argparse.Namespace) -> None:
1020
1070
  if files_enabled:
1021
1071
  from methodproof.agents import watcher
1022
1072
  threads.append(threading.Thread(target=watcher.start, args=(watch_dir, stop_event), daemon=True))
1073
+ _log_step("Agent: watcher")
1023
1074
  if capture.get("terminal_commands", True) or capture.get("test_results", True):
1024
1075
  from methodproof.agents import terminal
1025
1076
  threads.append(threading.Thread(target=terminal.start, args=(stop_event,), daemon=True))
1077
+ _log_step("Agent: terminal")
1026
1078
  if capture.get("browser", True):
1027
1079
  from methodproof import bridge
1028
1080
  threads.append(threading.Thread(target=bridge.start, args=(
@@ -1030,9 +1082,11 @@ def cmd_start(args: argparse.Namespace) -> None:
1030
1082
  cfg.get("token", ""), cfg.get("api_url", ""), cfg.get("e2e_key", ""),
1031
1083
  cfg.get("journal_mode", False),
1032
1084
  ), daemon=True))
1085
+ _log_step("Agent: bridge")
1033
1086
  if capture.get("music", True):
1034
1087
  from methodproof.agents import music
1035
1088
  threads.append(threading.Thread(target=music.start, args=(stop_event,), daemon=True))
1089
+ _log_step("Agent: music")
1036
1090
 
1037
1091
  def _shutdown(sig: int, frame: object) -> None:
1038
1092
  stop_event.set()
@@ -1054,10 +1108,14 @@ def cmd_start(args: argparse.Namespace) -> None:
1054
1108
  PIDFILE.unlink(missing_ok=True)
1055
1109
  sys.exit(0)
1056
1110
 
1111
+ _log_step(f"Starting {len(threads)} agent(s)")
1057
1112
  for t in threads:
1058
1113
  t.start()
1059
1114
  signal.signal(signal.SIGINT, _shutdown)
1060
- print("Press Ctrl+C or run `mp stop` to finish.")
1115
+ if streaming:
1116
+ print("\n Streaming events (Ctrl+C to stop):\n")
1117
+ else:
1118
+ print("Press Ctrl+C or run `mp stop` to finish.")
1061
1119
  stopfile = config.DIR / "methodproof.stop"
1062
1120
  while not stop_event.is_set():
1063
1121
  if stopfile.exists():
@@ -1583,6 +1641,8 @@ def main() -> None:
1583
1641
  s.add_argument("--live", action="store_true", help="Stream graph live to your private profile")
1584
1642
  s.add_argument("--live-public", action="store_true", help="Stream graph live — visible to anyone with the link")
1585
1643
  s.add_argument("--journal", action="store_true", help="Journal mode — full content capture (2 free credits, then Pro)")
1644
+ s.add_argument("--verbose", "-v", action="store_true", help="Debug logging at each step (still daemonizes)")
1645
+ s.add_argument("--streaming", action="store_true", help="Blocking foreground — stream every captured event to stdout")
1586
1646
  sub.add_parser("stop", help="Stop recording")
1587
1647
  v = sub.add_parser("view", help="Inspect captured session data")
1588
1648
  v.add_argument("session_id", nargs="?")
@@ -24,14 +24,36 @@ def _build_prompt_meta(text: str) -> dict:
24
24
 
25
25
 
26
26
  _TYPE_MAP = {
27
+ # Human-initiated
27
28
  "UserPromptSubmit": "user_prompt",
29
+ # Tool lifecycle
28
30
  "PreToolUse": "tool_call",
29
31
  "PostToolUse": "tool_result",
32
+ "PostToolUseFailure": "tool_failure",
33
+ # Agent lifecycle
30
34
  "SubagentStart": "agent_launch",
31
35
  "SubagentStop": "agent_complete",
36
+ # Task lifecycle
32
37
  "TaskCreated": "task_start",
33
38
  "TaskCompleted": "task_end",
39
+ # Session lifecycle
34
40
  "SessionStart": "claude_session_start",
41
+ "SessionEnd": "claude_session_end",
42
+ "Stop": "agent_turn_end",
43
+ "StopFailure": "agent_turn_error",
44
+ # Context
45
+ "CwdChanged": "cwd_changed",
46
+ "PreCompact": "context_compact_start",
47
+ "PostCompact": "context_compact_end",
48
+ # Permissions
49
+ "PermissionRequest": "permission_request",
50
+ "PermissionDenied": "permission_denied",
51
+ # MCP
52
+ "Elicitation": "mcp_elicitation",
53
+ "ElicitationResult": "mcp_elicitation_result",
54
+ # Worktree
55
+ "WorktreeCreate": "worktree_create",
56
+ "WorktreeRemove": "worktree_remove",
35
57
  }
36
58
 
37
59
  _TOOL = "claude_code"
@@ -43,11 +65,35 @@ _META_EXTRACTORS = {
43
65
  },
44
66
  "PreToolUse": lambda d: {"tool": _TOOL, "tool_name": d.get("tool_name", "unknown")},
45
67
  "PostToolUse": lambda d: {"tool": _TOOL, "tool_name": d.get("tool_name", "unknown"), "success": True},
68
+ "PostToolUseFailure": lambda d: {
69
+ "tool": _TOOL, "tool_name": d.get("tool_name", "unknown"),
70
+ "success": False, "is_interrupt": d.get("is_interrupt", False),
71
+ "error": str(d.get("error", ""))[:200],
72
+ },
46
73
  "SubagentStart": lambda d: {"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", "")},
47
- "SubagentStop": lambda d: {"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", "")},
74
+ "SubagentStop": lambda d: {
75
+ "tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", ""),
76
+ "last_message_preview": str(d.get("last_assistant_message", ""))[:200],
77
+ },
48
78
  "TaskCreated": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", ""), "subject": d.get("task_subject", "")},
49
79
  "TaskCompleted": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", "")},
50
80
  "SessionStart": lambda d: {"tool": _TOOL, "session_id": d.get("session_id", ""), "cwd": d.get("cwd", "")},
81
+ "SessionEnd": lambda d: {"tool": _TOOL, "session_id": d.get("session_id", "")},
82
+ "Stop": lambda d: {"tool": _TOOL},
83
+ "StopFailure": lambda d: {"tool": _TOOL, "error": str(d.get("error", ""))[:200]},
84
+ "CwdChanged": lambda d: {
85
+ "tool": _TOOL, "cwd": d.get("cwd", ""),
86
+ # NOTE: fires for both human `cd` and Claude tool use — caller is ambiguous
87
+ "source": "ambiguous",
88
+ },
89
+ "PreCompact": lambda d: {"tool": _TOOL},
90
+ "PostCompact": lambda d: {"tool": _TOOL},
91
+ "PermissionRequest": lambda d: {"tool": _TOOL, "tool_name": d.get("tool_name", "unknown")},
92
+ "PermissionDenied": lambda d: {"tool": _TOOL, "tool_name": d.get("tool_name", "unknown")},
93
+ "Elicitation": lambda d: {"tool": _TOOL},
94
+ "ElicitationResult": lambda d: {"tool": _TOOL},
95
+ "WorktreeCreate": lambda d: {"tool": _TOOL, "worktree_path": d.get("worktree_path", "")},
96
+ "WorktreeRemove": lambda d: {"tool": _TOOL, "worktree_path": d.get("worktree_path", "")},
51
97
  }
52
98
 
53
99
 
@@ -3,7 +3,9 @@
3
3
  # Receives JSON on stdin. Posts to local bridge. Fails silently.
4
4
  # Must complete in <1s to avoid blocking Claude Code.
5
5
 
6
- # Require jq without it, fall back to a minimal Python parser
6
+ # Skip if no session is running (no pidfile = no daemon = no bridge)
7
+ [ -f "${HOME}/.methodproof/methodproof.pid" ] || exit 0
8
+
7
9
  INPUT=$(cat)
8
10
 
9
11
  if command -v jq >/dev/null 2>&1; then
@@ -47,7 +49,7 @@ if command -v jq >/dev/null 2>&1; then
47
49
  ;;
48
50
  SubagentStop)
49
51
  TYPE="agent_complete"
50
- META=$(echo "$INPUT" | jq -c '{agent_type: (.agent_type // "unknown"), agent_id: (.agent_id // "")}' 2>/dev/null || echo '{}')
52
+ META=$(echo "$INPUT" | jq -c '{agent_type: (.agent_type // "unknown"), agent_id: (.agent_id // ""), last_message_preview: (.last_assistant_message // "" | .[0:200])}' 2>/dev/null || echo '{}')
51
53
  ;;
52
54
  TaskCreated)
53
55
  TYPE="task_created"
@@ -61,6 +63,54 @@ if command -v jq >/dev/null 2>&1; then
61
63
  TYPE="claude_session_start"
62
64
  META=$(echo "$INPUT" | jq -c '{claude_session_id: (.session_id // ""), cwd: (.cwd // "")}' 2>/dev/null || echo '{}')
63
65
  ;;
66
+ PostToolUseFailure)
67
+ TYPE="tool_failure"
68
+ META=$(echo "$INPUT" | jq -c '{tool_name: (.tool_name // "unknown"), is_interrupt: (.is_interrupt // false), error: (.error // "" | .[0:200])}' 2>/dev/null || echo '{}')
69
+ ;;
70
+ SessionEnd)
71
+ TYPE="claude_session_end"
72
+ META=$(echo "$INPUT" | jq -c '{claude_session_id: (.session_id // "")}' 2>/dev/null || echo '{}')
73
+ ;;
74
+ Stop)
75
+ TYPE="agent_turn_end"
76
+ META='{"tool":"claude_code"}'
77
+ ;;
78
+ StopFailure)
79
+ TYPE="agent_turn_error"
80
+ META=$(echo "$INPUT" | jq -c '{error: (.error // "" | .[0:200])}' 2>/dev/null || echo '{}')
81
+ ;;
82
+ CwdChanged)
83
+ TYPE="cwd_changed"
84
+ META=$(echo "$INPUT" | jq -c '{cwd: (.cwd // ""), source: "ambiguous"}' 2>/dev/null || echo '{}')
85
+ ;;
86
+ PreCompact)
87
+ TYPE="context_compact_start"
88
+ META='{"tool":"claude_code"}'
89
+ ;;
90
+ PostCompact)
91
+ TYPE="context_compact_end"
92
+ META='{"tool":"claude_code"}'
93
+ ;;
94
+ PermissionRequest)
95
+ TYPE="permission_request"
96
+ META=$(echo "$INPUT" | jq -c '{tool_name: (.tool_name // "unknown")}' 2>/dev/null || echo '{}')
97
+ ;;
98
+ PermissionDenied)
99
+ TYPE="permission_denied"
100
+ META=$(echo "$INPUT" | jq -c '{tool_name: (.tool_name // "unknown")}' 2>/dev/null || echo '{}')
101
+ ;;
102
+ WorktreeCreate)
103
+ TYPE="worktree_create"
104
+ META=$(echo "$INPUT" | jq -c '{worktree_path: (.worktree_path // "")}' 2>/dev/null || echo '{}')
105
+ ;;
106
+ WorktreeRemove)
107
+ TYPE="worktree_remove"
108
+ META=$(echo "$INPUT" | jq -c '{worktree_path: (.worktree_path // "")}' 2>/dev/null || echo '{}')
109
+ ;;
110
+ Elicitation|ElicitationResult)
111
+ TYPE=$(echo "$EVENT" | sed 's/Elicitation$/mcp_elicitation/;s/ElicitationResult/mcp_elicitation_result/')
112
+ META='{"tool":"claude_code"}'
113
+ ;;
64
114
  *)
65
115
  TYPE="claude_code_event"
66
116
  META="{\"event\":\"$EVENT\"}"
@@ -21,9 +21,23 @@ _HOOK_PY = _HOOKS_DIR / "claude_code.py"
21
21
  HOOK_SCRIPT = _HOOK_PY if sys.platform == "win32" else _HOOK_SH
22
22
 
23
23
  HOOK_EVENTS = [
24
+ # Core (already captured)
24
25
  "UserPromptSubmit", "PreToolUse", "PostToolUse",
25
26
  "SubagentStart", "SubagentStop",
26
27
  "TaskCreated", "TaskCompleted", "SessionStart",
28
+ # New: interruptions, lifecycle, context
29
+ "PostToolUseFailure", # tool failure + is_interrupt flag
30
+ "SessionEnd", # session terminated
31
+ "Stop", # Claude finished responding (normal or interrupted)
32
+ "StopFailure", # turn ended due to API error
33
+ "CwdChanged", # working directory changed (ambiguous: human or Claude)
34
+ "PreCompact", "PostCompact", # context window compaction
35
+ "PermissionRequest", # permission dialog shown
36
+ "PermissionDenied", # auto mode denied a tool
37
+ "Elicitation", # MCP requested user input
38
+ "ElicitationResult", # user responded to MCP
39
+ "WorktreeCreate", # git worktree created (parallel agent)
40
+ "WorktreeRemove", # git worktree removed
27
41
  ]
28
42
 
29
43
  # --- Codex CLI ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "See how you code. Capture and visualize your engineering process."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["watchdog>=4.0", "websocket-client>=1.7", "cryptography>=43.0", "keyring>=25.0"]
@@ -862,7 +862,7 @@ wheels = [
862
862
 
863
863
  [[package]]
864
864
  name = "methodproof"
865
- version = "0.5.5"
865
+ version = "0.6.0"
866
866
  source = { editable = "." }
867
867
  dependencies = [
868
868
  { name = "cryptography", version = "44.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
File without changes
File without changes
File without changes