methodproof 0.5.5__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.5.5 → methodproof-0.7.0}/CHANGELOG.md +11 -0
  2. {methodproof-0.5.5 → methodproof-0.7.0}/PKG-INFO +89 -6
  3. {methodproof-0.5.5 → methodproof-0.7.0}/README.md +88 -5
  4. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/__init__.py +1 -1
  5. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/_daemon.py +18 -3
  6. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/agents/base.py +62 -10
  7. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/agents/watcher.py +29 -1
  8. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/bridge.py +13 -5
  9. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/cli.py +81 -20
  10. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/claude_code.py +47 -1
  11. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/claude_code.sh +52 -2
  12. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/install.py +14 -0
  13. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/sync.py +15 -5
  14. {methodproof-0.5.5 → methodproof-0.7.0}/pyproject.toml +1 -1
  15. {methodproof-0.5.5 → methodproof-0.7.0}/uv.lock +2 -2
  16. {methodproof-0.5.5 → methodproof-0.7.0}/.github/workflows/ci.yml +0 -0
  17. {methodproof-0.5.5 → methodproof-0.7.0}/.gitignore +0 -0
  18. {methodproof-0.5.5 → methodproof-0.7.0}/LICENSE +0 -0
  19. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/__main__.py +0 -0
  20. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/agents/__init__.py +0 -0
  21. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/agents/music.py +0 -0
  22. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/agents/terminal.py +0 -0
  23. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/analysis.py +0 -0
  24. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/binding.py +0 -0
  25. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/bip39.py +0 -0
  26. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/config.py +0 -0
  27. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/crypto.py +0 -0
  28. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/graph.py +0 -0
  29. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hook.py +0 -0
  30. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/__init__.py +0 -0
  31. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/cline_hook.sh +0 -0
  32. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/codex_hook.sh +0 -0
  33. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/gemini_hook.sh +0 -0
  34. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/kiro_hook.sh +0 -0
  35. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/mcp_register.py +0 -0
  36. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/openclaw/HOOK.md +0 -0
  37. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/openclaw/handler.ts +0 -0
  38. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/openclaw_install.py +0 -0
  39. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/opencode_plugin.js +0 -0
  40. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/hooks/wrappers.py +0 -0
  41. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/integrity.py +0 -0
  42. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/kdf.py +0 -0
  43. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/keychain.py +0 -0
  44. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/live.py +0 -0
  45. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/lock.py +0 -0
  46. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/mcp.py +0 -0
  47. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/migrate_db.py +0 -0
  48. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/proxy.py +0 -0
  49. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/proxy_daemon.py +0 -0
  50. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/repos.py +0 -0
  51. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/skills/methodproof/SKILL.md +0 -0
  52. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/store.py +0 -0
  53. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/viewer.py +0 -0
  54. {methodproof-0.5.5 → methodproof-0.7.0}/methodproof/wordlist.py +0 -0
  55. {methodproof-0.5.5 → methodproof-0.7.0}/test_windows_compat.py +0 -0
  56. {methodproof-0.5.5 → methodproof-0.7.0}/tests/__init__.py +0 -0
  57. {methodproof-0.5.5 → methodproof-0.7.0}/tests/test_analysis.py +0 -0
  58. {methodproof-0.5.5 → methodproof-0.7.0}/tests/test_graph.py +0 -0
  59. {methodproof-0.5.5 → methodproof-0.7.0}/tests/test_hooks.py +0 -0
  60. {methodproof-0.5.5 → methodproof-0.7.0}/tests/test_live.py +0 -0
  61. {methodproof-0.5.5 → methodproof-0.7.0}/tests/test_openclaw_hooks.py +0 -0
  62. {methodproof-0.5.5 → methodproof-0.7.0}/tests/test_security.py +0 -0
  63. {methodproof-0.5.5 → methodproof-0.7.0}/tests/test_store.py +0 -0
  64. {methodproof-0.5.5 → 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.5.5
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
@@ -67,6 +67,87 @@ methodproof view # explore your session in the browser
67
67
  - **Auto-detection** — hooks for shell, Claude Code, OpenClaw, codex, gemini, aider installed automatically
68
68
  - **Platform sync** — `methodproof push` uploads sessions. `methodproof publish` makes them public and shareable
69
69
 
70
+ ## Security Architecture
71
+
72
+ Every event passes through consent gating, encryption, and hash chaining before touching disk. The platform adds a server co-signature on push, creating two-party evidence.
73
+
74
+ ```mermaid
75
+ flowchart TB
76
+ subgraph LOCAL["LOCAL MACHINE"]
77
+ direction TB
78
+ subgraph AGENTS["Capture Agents"]
79
+ direction LR
80
+ W["File Watcher"] & T["Terminal Hook"] & B["Browser Bridge"] & H["AI Tool Hooks"]
81
+ end
82
+ W & T & B & H -->|"raw event"| EMIT["emit(type, metadata)"]
83
+
84
+ subgraph CONSENT["Consent Layer"]
85
+ direction TB
86
+ EMIT --> EG{"Event Gate"}
87
+ EG -->|"category OFF"| DROP["Dropped"]
88
+ EG -->|"category ON"| FG["Field Gate: strip opted-out fields"]
89
+ FG --> JG{"Journal Mode?"}
90
+ JG -->|"OFF: structural only"| STRIP["Strip content fields"]
91
+ JG -->|"ON: full content"| KEEP["Keep all fields"]
92
+ end
93
+
94
+ subgraph CRYPTO["Encryption + Integrity"]
95
+ direction TB
96
+ STRIP & KEEP --> AES["AES-256-GCM: encrypt sensitive fields"]
97
+ AES --> LOCK["Thread Lock: atomic hash + buffer"]
98
+ LOCK --> CHAIN["SHA-256 Hash Chain"]
99
+ CHAIN --> BUF["Batched Flush"]
100
+ end
101
+
102
+ BUF --> DB[("SQLite WAL")]
103
+ BUF -.->|"--live"| WS["WebSocket Stream"]
104
+
105
+ subgraph KEYS["Key Vault"]
106
+ direction LR
107
+ ENT["Entropy"] --> KDF["Key Derivation"]
108
+ KDF --> DBK["Encryption Key"]
109
+ KDF --> BINDK["Binding Key"]
110
+ KDF --> ED25["Signing Key"]
111
+ ENT -.-> REC["Recovery Phrase"]
112
+ KDF -.-> KC["OS Keychain"]
113
+ end
114
+ end
115
+
116
+ subgraph PUSH["PUSH PROTOCOL"]
117
+ direction TB
118
+ DB --> BATCH["Batched Upload"]
119
+ BATCH --> BIND["Session Binding: HMAC over session metadata"]
120
+ BIND --> SIGN["Ed25519 Sign: session summary"]
121
+ end
122
+
123
+ subgraph PLATFORM["PLATFORM TRUST BOUNDARY"]
124
+ direction TB
125
+ SIGN --> VERIFY["Verify Ed25519 against registered public key"]
126
+ VERIFY --> COSIG["Server Co-Sign: HMAC over attestation receipt"]
127
+ COSIG --> NEO[("Graph Store")]
128
+ NEO --> SCORE["Integrity Score: chain + attestation + cosig + git xref + anomaly"]
129
+ end
130
+
131
+ style DROP fill:#d93326,stroke:#d93326,color:#fff
132
+ style AES fill:#803794,stroke:#803794,color:#fff
133
+ style CHAIN fill:#803794,stroke:#803794,color:#fff
134
+ style LOCK fill:#803794,stroke:#803794,color:#fff
135
+ style EG fill:#109446,stroke:#109446,color:#fff
136
+ style JG fill:#109446,stroke:#109446,color:#fff
137
+ style STRIP fill:#109446,stroke:#109446,color:#fff
138
+ style VERIFY fill:#c9a84c,stroke:#c9a84c,color:#fff
139
+ style COSIG fill:#c9a84c,stroke:#c9a84c,color:#fff
140
+ style SCORE fill:#c9a84c,stroke:#c9a84c,color:#fff
141
+ style SIGN fill:#192a56,stroke:#192a56,color:#fff
142
+ style BIND fill:#192a56,stroke:#192a56,color:#fff
143
+ style DB fill:#192a56,stroke:#192a56,color:#fff
144
+ style NEO fill:#192a56,stroke:#192a56,color:#fff
145
+ style KC fill:#803794,stroke:#803794,color:#fff
146
+ style REC fill:#803794,stroke:#803794,color:#fff
147
+ ```
148
+
149
+ <sup>Green = consent gates · Purple = cryptographic operations · Navy = storage + binding · Gold = integrity verification</sup>
150
+
70
151
  ## Commands
71
152
 
72
153
  | Command | What it does |
@@ -167,13 +248,15 @@ At session end, outcome metrics are computed: first-shot apply rate, follow-up s
167
248
  <details>
168
249
  <summary>Integrity verification</summary>
169
250
 
170
- Three layers ensure session data hasn't been tampered with:
251
+ Four layers ensure session data hasn't been tampered with (see [architecture diagram](#security-architecture) above):
252
+
253
+ **Hash-chained events** — every event includes a SHA-256 hash linking to the previous event. Hash computation is thread-safe (atomic with buffer append). Any modification breaks the chain, detectable via `GET /sessions/{id}/chain/verify`.
171
254
 
172
- **Hash-chained events** — every event includes a SHA-256 hash linking to the previous event. Any modification breaks the chain, detectable via `GET /sessions/{id}/chain/verify`.
255
+ **Ed25519 attestation** — on `methodproof push`, the CLI signs a session summary with your private key. Key generated during `methodproof init`, stored in `~/.methodproof/`.
173
256
 
174
- **Ed25519 attestation** — on `methodproof push`, the CLI signs a session summary with your private key. Install with `pip install methodproof[signing]`. Key generated during `methodproof init`, stored in `~/.methodproof/config.json`.
257
+ **Server co-signature** — the platform independently signs the attestation receipt with its own key and timestamp, creating two-party evidence. Sessions pushed without server co-signature receive a lower integrity score.
175
258
 
176
- **Binary hash self-reporting** — the CLI reports its own binary hash on push. The platform compares against known release hashes to detect modified builds.
259
+ **Binary hash self-reporting** — the CLI reports its own source hash on push. The platform compares against known release hashes to detect modified builds.
177
260
 
178
261
  </details>
179
262
 
@@ -222,7 +305,7 @@ methodproof start
222
305
 
223
306
  **Excluded patterns:** `__pycache__`, `.pyc`, `.git/`, `node_modules`, `.DS_Store`, `.swp`, temp files ending in `~`
224
307
 
225
- **Git commits** are detected by polling `.git/refs/heads/` every 2 seconds — only commits in a git repo rooted at (or above) the watch directory are captured.
308
+ **Git commits** are detected automatically — only commits in a git repo rooted at (or above) the watch directory are captured.
226
309
 
227
310
  ## Data Directory
228
311
 
@@ -52,6 +52,87 @@ methodproof view # explore your session in the browser
52
52
  - **Auto-detection** — hooks for shell, Claude Code, OpenClaw, codex, gemini, aider installed automatically
53
53
  - **Platform sync** — `methodproof push` uploads sessions. `methodproof publish` makes them public and shareable
54
54
 
55
+ ## Security Architecture
56
+
57
+ Every event passes through consent gating, encryption, and hash chaining before touching disk. The platform adds a server co-signature on push, creating two-party evidence.
58
+
59
+ ```mermaid
60
+ flowchart TB
61
+ subgraph LOCAL["LOCAL MACHINE"]
62
+ direction TB
63
+ subgraph AGENTS["Capture Agents"]
64
+ direction LR
65
+ W["File Watcher"] & T["Terminal Hook"] & B["Browser Bridge"] & H["AI Tool Hooks"]
66
+ end
67
+ W & T & B & H -->|"raw event"| EMIT["emit(type, metadata)"]
68
+
69
+ subgraph CONSENT["Consent Layer"]
70
+ direction TB
71
+ EMIT --> EG{"Event Gate"}
72
+ EG -->|"category OFF"| DROP["Dropped"]
73
+ EG -->|"category ON"| FG["Field Gate: strip opted-out fields"]
74
+ FG --> JG{"Journal Mode?"}
75
+ JG -->|"OFF: structural only"| STRIP["Strip content fields"]
76
+ JG -->|"ON: full content"| KEEP["Keep all fields"]
77
+ end
78
+
79
+ subgraph CRYPTO["Encryption + Integrity"]
80
+ direction TB
81
+ STRIP & KEEP --> AES["AES-256-GCM: encrypt sensitive fields"]
82
+ AES --> LOCK["Thread Lock: atomic hash + buffer"]
83
+ LOCK --> CHAIN["SHA-256 Hash Chain"]
84
+ CHAIN --> BUF["Batched Flush"]
85
+ end
86
+
87
+ BUF --> DB[("SQLite WAL")]
88
+ BUF -.->|"--live"| WS["WebSocket Stream"]
89
+
90
+ subgraph KEYS["Key Vault"]
91
+ direction LR
92
+ ENT["Entropy"] --> KDF["Key Derivation"]
93
+ KDF --> DBK["Encryption Key"]
94
+ KDF --> BINDK["Binding Key"]
95
+ KDF --> ED25["Signing Key"]
96
+ ENT -.-> REC["Recovery Phrase"]
97
+ KDF -.-> KC["OS Keychain"]
98
+ end
99
+ end
100
+
101
+ subgraph PUSH["PUSH PROTOCOL"]
102
+ direction TB
103
+ DB --> BATCH["Batched Upload"]
104
+ BATCH --> BIND["Session Binding: HMAC over session metadata"]
105
+ BIND --> SIGN["Ed25519 Sign: session summary"]
106
+ end
107
+
108
+ subgraph PLATFORM["PLATFORM TRUST BOUNDARY"]
109
+ direction TB
110
+ SIGN --> VERIFY["Verify Ed25519 against registered public key"]
111
+ VERIFY --> COSIG["Server Co-Sign: HMAC over attestation receipt"]
112
+ COSIG --> NEO[("Graph Store")]
113
+ NEO --> SCORE["Integrity Score: chain + attestation + cosig + git xref + anomaly"]
114
+ end
115
+
116
+ style DROP fill:#d93326,stroke:#d93326,color:#fff
117
+ style AES fill:#803794,stroke:#803794,color:#fff
118
+ style CHAIN fill:#803794,stroke:#803794,color:#fff
119
+ style LOCK fill:#803794,stroke:#803794,color:#fff
120
+ style EG fill:#109446,stroke:#109446,color:#fff
121
+ style JG fill:#109446,stroke:#109446,color:#fff
122
+ style STRIP fill:#109446,stroke:#109446,color:#fff
123
+ style VERIFY fill:#c9a84c,stroke:#c9a84c,color:#fff
124
+ style COSIG fill:#c9a84c,stroke:#c9a84c,color:#fff
125
+ style SCORE fill:#c9a84c,stroke:#c9a84c,color:#fff
126
+ style SIGN fill:#192a56,stroke:#192a56,color:#fff
127
+ style BIND fill:#192a56,stroke:#192a56,color:#fff
128
+ style DB fill:#192a56,stroke:#192a56,color:#fff
129
+ style NEO fill:#192a56,stroke:#192a56,color:#fff
130
+ style KC fill:#803794,stroke:#803794,color:#fff
131
+ style REC fill:#803794,stroke:#803794,color:#fff
132
+ ```
133
+
134
+ <sup>Green = consent gates · Purple = cryptographic operations · Navy = storage + binding · Gold = integrity verification</sup>
135
+
55
136
  ## Commands
56
137
 
57
138
  | Command | What it does |
@@ -152,13 +233,15 @@ At session end, outcome metrics are computed: first-shot apply rate, follow-up s
152
233
  <details>
153
234
  <summary>Integrity verification</summary>
154
235
 
155
- Three layers ensure session data hasn't been tampered with:
236
+ Four layers ensure session data hasn't been tampered with (see [architecture diagram](#security-architecture) above):
237
+
238
+ **Hash-chained events** — every event includes a SHA-256 hash linking to the previous event. Hash computation is thread-safe (atomic with buffer append). Any modification breaks the chain, detectable via `GET /sessions/{id}/chain/verify`.
156
239
 
157
- **Hash-chained events** — every event includes a SHA-256 hash linking to the previous event. Any modification breaks the chain, detectable via `GET /sessions/{id}/chain/verify`.
240
+ **Ed25519 attestation** — on `methodproof push`, the CLI signs a session summary with your private key. Key generated during `methodproof init`, stored in `~/.methodproof/`.
158
241
 
159
- **Ed25519 attestation** — on `methodproof push`, the CLI signs a session summary with your private key. Install with `pip install methodproof[signing]`. Key generated during `methodproof init`, stored in `~/.methodproof/config.json`.
242
+ **Server co-signature** — the platform independently signs the attestation receipt with its own key and timestamp, creating two-party evidence. Sessions pushed without server co-signature receive a lower integrity score.
160
243
 
161
- **Binary hash self-reporting** — the CLI reports its own binary hash on push. The platform compares against known release hashes to detect modified builds.
244
+ **Binary hash self-reporting** — the CLI reports its own source hash on push. The platform compares against known release hashes to detect modified builds.
162
245
 
163
246
  </details>
164
247
 
@@ -207,7 +290,7 @@ methodproof start
207
290
 
208
291
  **Excluded patterns:** `__pycache__`, `.pyc`, `.git/`, `node_modules`, `.DS_Store`, `.swp`, temp files ending in `~`
209
292
 
210
- **Git commits** are detected by polling `.git/refs/heads/` every 2 seconds — only commits in a git repo rooted at (or above) the watch directory are captured.
293
+ **Git commits** are detected automatically — only commits in a git repo rooted at (or above) the watch directory are captured.
211
294
 
212
295
  ## Data Directory
213
296
 
@@ -1,3 +1,3 @@
1
1
  """MethodProof — see how you code."""
2
2
 
3
- __version__ = "0.5.5"
3
+ __version__ = "0.6.0"
@@ -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,27 +58,36 @@ 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
64
74
  threads.append(threading.Thread(
65
75
  target=bridge.start,
66
76
  args=(sid, stop_event, 9877,
67
- cfg.get("token", ""), cfg.get("api_url", ""), cfg.get("e2e_key", "")),
77
+ cfg.get("token", ""), cfg.get("api_url", ""), cfg.get("e2e_key", ""),
78
+ cfg.get("journal_mode", False)),
68
79
  daemon=True,
69
80
  ))
81
+ if verbose:
82
+ base.log("info", "daemon.agent.bridge", port=9877)
70
83
 
71
84
  if capture.get("music", True):
72
85
  from methodproof.agents import music
73
86
  threads.append(threading.Thread(
74
87
  target=music.start, args=(stop_event,), daemon=True,
75
88
  ))
89
+ if verbose:
90
+ base.log("info", "daemon.agent.music")
76
91
 
77
92
  def _shutdown(sig_num: int, frame: object) -> None:
78
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:
@@ -145,17 +153,21 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
145
153
  if _e2e_key:
146
154
  from methodproof.crypto import encrypt_metadata
147
155
  entry["metadata"] = encrypt_metadata(dict(entry["metadata"]), _e2e_key)
148
- global _prev_hash
149
- from methodproof.integrity import compute_event_hash
150
- entry["_chain_hash"] = compute_event_hash(entry, _prev_hash, _account_id)
151
- _prev_hash = entry["_chain_hash"]
152
- if _live_mode:
153
- from methodproof import live as live_mod
154
- live_mod.send(entry)
155
156
  with _lock:
157
+ global _prev_hash
158
+ from methodproof.integrity import compute_event_hash
159
+ entry["_chain_hash"] = compute_event_hash(entry, _prev_hash, _account_id)
160
+ _prev_hash = entry["_chain_hash"]
156
161
  _buffer.append(entry)
157
162
  if len(_buffer) >= _FLUSH_SIZE:
158
163
  _flush_locked()
164
+ if _verbose:
165
+ log("debug", "emit.buffered", type=event_type, buffer=len(_buffer))
166
+ if _streaming:
167
+ _stream_event(entry)
168
+ if _live_mode:
169
+ from methodproof import live as live_mod
170
+ live_mod.send(entry)
159
171
 
160
172
 
161
173
  def flush() -> None:
@@ -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
 
@@ -12,7 +12,8 @@ _session_id = ""
12
12
  _api_token = ""
13
13
  _api_base = ""
14
14
  _e2e_key = ""
15
- _pairing: dict[str, Any] = {} # {token: {session_id, api_token, api_base, e2e_key, paired}}
15
+ _journal = False
16
+ _pairing: dict[str, Any] = {} # {token: {session_id, api_token, api_base, e2e_key, journal, paired}}
16
17
  _extension_paired = threading.Event()
17
18
  MAX_BODY = 10 * 1024 * 1024 # 10 MB
18
19
 
@@ -47,6 +48,7 @@ PAIR_PAGE = """<!DOCTYPE html>
47
48
  data-token="{api_token}"
48
49
  data-api-base="{api_base}"
49
50
  data-e2e-key="{e2e_key}"
51
+ data-journal="{journal}"
50
52
  style="display:none"></div>
51
53
  </div>
52
54
  <script>
@@ -62,11 +64,12 @@ PAIR_PAGE = """<!DOCTYPE html>
62
64
  </html>"""
63
65
 
64
66
 
65
- def generate_pair_token(session_id: str, api_token: str, api_base: str, e2e_key: str = "") -> str:
67
+ def generate_pair_token(session_id: str, api_token: str, api_base: str,
68
+ e2e_key: str = "", journal: bool = False) -> str:
66
69
  token = secrets.token_urlsafe(16)
67
70
  _pairing[token] = {
68
71
  "session_id": session_id, "api_token": api_token,
69
- "api_base": api_base, "e2e_key": e2e_key, "paired": False,
72
+ "api_base": api_base, "e2e_key": e2e_key, "journal": journal, "paired": False,
70
73
  }
71
74
  return token
72
75
 
@@ -92,6 +95,7 @@ class _Handler(BaseHTTPRequestHandler):
92
95
  api_token=data["api_token"],
93
96
  api_base=data["api_base"],
94
97
  e2e_key=data["e2e_key"],
98
+ journal="true" if data.get("journal") else "false",
95
99
  pair_token=token,
96
100
  ).encode()
97
101
  self.send_response(200)
@@ -109,6 +113,7 @@ class _Handler(BaseHTTPRequestHandler):
109
113
  "token": _api_token,
110
114
  "api_base": _api_base,
111
115
  "e2e_key": _e2e_key,
116
+ "journal": _journal,
112
117
  })
113
118
  _extension_paired.set()
114
119
  base.log("info", "extension.auto_paired", session_id=_session_id)
@@ -149,6 +154,7 @@ class _Handler(BaseHTTPRequestHandler):
149
154
  "api_token": body.get("api_token", ""),
150
155
  "api_base": body.get("api_base", ""),
151
156
  "e2e_key": body.get("e2e_key", ""),
157
+ "journal": body.get("journal", False),
152
158
  "paired": False,
153
159
  }
154
160
  self._json({"ok": True, "token": token})
@@ -185,12 +191,14 @@ class _Handler(BaseHTTPRequestHandler):
185
191
 
186
192
 
187
193
  def start(session_id: str, stop: threading.Event, port: int = 9877,
188
- api_token: str = "", api_base: str = "", e2e_key: str = "") -> None:
189
- global _session_id, _api_token, _api_base, _e2e_key
194
+ api_token: str = "", api_base: str = "", e2e_key: str = "",
195
+ journal: bool = False) -> None:
196
+ global _session_id, _api_token, _api_base, _e2e_key, _journal
190
197
  _session_id = session_id
191
198
  _api_token = api_token
192
199
  _api_base = api_base
193
200
  _e2e_key = e2e_key
201
+ _journal = journal
194
202
  _extension_paired.clear()
195
203
  HTTPServer.allow_reuse_address = True
196
204
  server = HTTPServer(("127.0.0.1", port), _Handler)
@@ -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,18 +1070,23 @@ 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=(
1029
1081
  sid, stop_event, 9877,
1030
1082
  cfg.get("token", ""), cfg.get("api_url", ""), cfg.get("e2e_key", ""),
1083
+ cfg.get("journal_mode", False),
1031
1084
  ), daemon=True))
1085
+ _log_step("Agent: bridge")
1032
1086
  if capture.get("music", True):
1033
1087
  from methodproof.agents import music
1034
1088
  threads.append(threading.Thread(target=music.start, args=(stop_event,), daemon=True))
1089
+ _log_step("Agent: music")
1035
1090
 
1036
1091
  def _shutdown(sig: int, frame: object) -> None:
1037
1092
  stop_event.set()
@@ -1053,10 +1108,14 @@ def cmd_start(args: argparse.Namespace) -> None:
1053
1108
  PIDFILE.unlink(missing_ok=True)
1054
1109
  sys.exit(0)
1055
1110
 
1111
+ _log_step(f"Starting {len(threads)} agent(s)")
1056
1112
  for t in threads:
1057
1113
  t.start()
1058
1114
  signal.signal(signal.SIGINT, _shutdown)
1059
- 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.")
1060
1119
  stopfile = config.DIR / "methodproof.stop"
1061
1120
  while not stop_event.is_set():
1062
1121
  if stopfile.exists():
@@ -1582,6 +1641,8 @@ def main() -> None:
1582
1641
  s.add_argument("--live", action="store_true", help="Stream graph live to your private profile")
1583
1642
  s.add_argument("--live-public", action="store_true", help="Stream graph live — visible to anyone with the link")
1584
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")
1585
1646
  sub.add_parser("stop", help="Stop recording")
1586
1647
  v = sub.add_parser("view", help="Inspect captured session data")
1587
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 ---
@@ -102,9 +102,10 @@ def push(session_id: str, token: str, api_url: str) -> str:
102
102
  event_hashes = store.get_event_hashes(session_id)
103
103
  hash_lookup = {h["event_id"]: h["hash"] for h in event_hashes}
104
104
  total = len(events)
105
+ batch_size = 500
105
106
  try:
106
- for i in range(0, total, 100):
107
- batch = events[i:i + 100]
107
+ for i in range(0, total, batch_size):
108
+ batch = events[i:i + batch_size]
108
109
  payload = [{"id": e["id"], "type": e["type"],
109
110
  "timestamp": _iso(e["timestamp"]),
110
111
  "timestamp_raw": e["timestamp"],
@@ -112,9 +113,18 @@ def push(session_id: str, token: str, api_url: str) -> str:
112
113
  "metadata": json.loads(e["metadata"]),
113
114
  "hash": hash_lookup.get(e["id"], "")}
114
115
  for e in batch]
115
- _request("POST", f"/sessions/{remote_id}/events", api_url, token,
116
- {"events": payload})
117
- done = min(i + 100, total)
116
+ for attempt in range(5):
117
+ try:
118
+ _request("POST", f"/sessions/{remote_id}/events", api_url, token,
119
+ {"events": payload})
120
+ break
121
+ except SystemExit as exc:
122
+ if "429" in str(exc) and attempt < 4:
123
+ import time
124
+ time.sleep(10 * (attempt + 1))
125
+ else:
126
+ raise
127
+ done = min(i + batch_size, total)
118
128
  print(f"\r Uploading: {done}/{total} events", end="", flush=True)
119
129
  print()
120
130
  except SystemExit:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.5.5"
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"]
@@ -649,7 +649,7 @@ name = "importlib-metadata"
649
649
  version = "9.0.0"
650
650
  source = { registry = "https://pypi.org/simple" }
651
651
  dependencies = [
652
- { name = "zipp" },
652
+ { name = "zipp", marker = "python_full_version < '3.12'" },
653
653
  ]
654
654
  sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" }
655
655
  wheels = [
@@ -862,7 +862,7 @@ wheels = [
862
862
 
863
863
  [[package]]
864
864
  name = "methodproof"
865
- version = "0.4.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