transcripto 0.1.1__tar.gz → 0.1.2__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.
- {transcripto-0.1.1/transcripto.egg-info → transcripto-0.1.2}/PKG-INFO +119 -19
- {transcripto-0.1.1 → transcripto-0.1.2}/README.md +118 -18
- {transcripto-0.1.1 → transcripto-0.1.2}/pyproject.toml +1 -1
- {transcripto-0.1.1 → transcripto-0.1.2/transcripto.egg-info}/PKG-INFO +119 -19
- {transcripto-0.1.1 → transcripto-0.1.2}/transcripto.py +276 -25
- {transcripto-0.1.1 → transcripto-0.1.2}/LICENSE +0 -0
- {transcripto-0.1.1 → transcripto-0.1.2}/setup.cfg +0 -0
- {transcripto-0.1.1 → transcripto-0.1.2}/transcripto.egg-info/SOURCES.txt +0 -0
- {transcripto-0.1.1 → transcripto-0.1.2}/transcripto.egg-info/dependency_links.txt +0 -0
- {transcripto-0.1.1 → transcripto-0.1.2}/transcripto.egg-info/entry_points.txt +0 -0
- {transcripto-0.1.1 → transcripto-0.1.2}/transcripto.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transcripto
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Search everything your coding agents ever did, grade your own prompts, and price your decisions. Local, stdlib-only, your data never leaves the machine.
|
|
5
5
|
Author: Oscar Morke
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,56 @@ your disk and never opens a socket.
|
|
|
29
29
|
uvx transcripto coach
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
> **check which build you got.** `trace`, `--harness cursor`, and the "run `index`
|
|
33
|
+
> first" errors below all arrive in **0.1.2**. On anything older, `trace` is not a
|
|
34
|
+
> command, cursor is rejected, and the index-gated commands fail with a raw sqlite
|
|
35
|
+
> traceback instead of an instruction. One line settles which one you are holding:
|
|
36
|
+
>
|
|
37
|
+
> ```
|
|
38
|
+
> uvx transcripto --version # 0.1.2 or newer = this README is accurate
|
|
39
|
+
> ```
|
|
40
|
+
>
|
|
41
|
+
> *Read 2026-08-31: PyPI was serving 0.1.1 while this README described 0.1.2, so
|
|
42
|
+
> `uvx transcripto` gave the older build. If `--version` is not even a recognised
|
|
43
|
+
> flag, you have 0.1.1 — it was added in 0.1.2 precisely because there was no way
|
|
44
|
+
> to tell.* The repo always matches this README and needs nothing installed:
|
|
45
|
+
>
|
|
46
|
+
> ```
|
|
47
|
+
> git clone https://github.com/Morkeeth/transcripto && cd transcripto
|
|
48
|
+
> python3 transcripto.py coach
|
|
49
|
+
> ```
|
|
50
|
+
|
|
51
|
+
## three harnesses, one instrument
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
transcripto coach # Claude Code, ~/.claude/projects
|
|
55
|
+
transcripto coach --harness codex # Codex, ~/.codex
|
|
56
|
+
transcripto coach --harness cursor # Cursor, ~/.cursor/projects/*/agent-transcripts
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Authorship is not the same gate in all three, and the tool says so rather than pooling them.**
|
|
60
|
+
Claude Code stamps `promptSource: typed`, which is the measured-reliable signal: about 95% of raw
|
|
61
|
+
`type: user` records are not the operator at all. Cursor has no such field. Its one honest
|
|
62
|
+
equivalent is the `<user_query>` wrapper it puts around a submitted prompt, which injected and
|
|
63
|
+
tool-result records do not carry. That is a weaker signal and it is labelled weaker.
|
|
64
|
+
|
|
65
|
+
## `trace` — what actually happened after you asked
|
|
66
|
+
|
|
67
|
+
`ask` shows what you typed. `find` shows what a file went through. Neither answers the
|
|
68
|
+
question that matters after the fact: **you asked for X, did anything durable happen?**
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
transcripto trace "the gate"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It walks each of your matching prompts forward inside its own session and lists the writes
|
|
75
|
+
and edits that followed, stopping at your next prompt so one turn cannot claim the next
|
|
76
|
+
turn's work. Green dot = something durable landed. Red = nothing was touched.
|
|
77
|
+
|
|
78
|
+
**Honest limit:** a write following a prompt in the same session is CO-OCCURRENCE, not proof
|
|
79
|
+
the write was caused by that prompt or that it was correct. Same proxy `coach` uses, labelled
|
|
80
|
+
the same way.
|
|
81
|
+
|
|
32
82
|
## what you get back
|
|
33
83
|
|
|
34
84
|
this is a real run on one machine, pasted unedited, 2026-08-28:
|
|
@@ -73,9 +123,13 @@ them as a snapshot rather than a constant. yours will be different, which is the
|
|
|
73
123
|
whole point. the last two lines are the ones that sting: it hands you back your
|
|
74
124
|
own best and worst prompt, verbatim, with the receipt for why it scored each one.
|
|
75
125
|
|
|
76
|
-
on that machine, prompts that wrote down what done looks like
|
|
77
|
-
the time (66 of 104)**. prompts with no stated intent survived
|
|
78
|
-
1043)**. i had spent a year blaming the model.
|
|
126
|
+
on that machine, on that date, prompts that wrote down what done looks like
|
|
127
|
+
survived **63% of the time (66 of 104)**. prompts with no stated intent survived
|
|
128
|
+
**39% (411 of 1043)**. i had spent a year blaming the model.
|
|
129
|
+
|
|
130
|
+
one day later, 2026-08-29, the same command on the same machine read 63% (67 of
|
|
131
|
+
107) and 40% (424 of 1072) over 2,874 transcripts. the percentages held and the
|
|
132
|
+
denominators moved, which is what a snapshot is supposed to do.
|
|
79
133
|
|
|
80
134
|
## the proxy caveat, which travels with every number
|
|
81
135
|
|
|
@@ -127,8 +181,10 @@ read these before you quote a number at anyone.
|
|
|
127
181
|
- **one operator's corpus.** every figure in this README comes from one machine.
|
|
128
182
|
it is an existence proof that the measurement runs, not a finding about how
|
|
129
183
|
people prompt. run it on yours and you get yours.
|
|
130
|
-
- **
|
|
131
|
-
|
|
184
|
+
- **three harnesses today: Claude Code, Codex, Cursor.** nothing else is supported.
|
|
185
|
+
aider and the rest are not read. and the three are not equal: Claude Code has a
|
|
186
|
+
measured-reliable authorship field, Cursor has only the `<user_query>` wrapper,
|
|
187
|
+
which is weaker and is labelled weaker wherever it is used.
|
|
132
188
|
- **the habit labels are heuristics.** "states-a-check-or-done-condition" is a
|
|
133
189
|
pattern match over your text, not comprehension. it will misfile some prompts.
|
|
134
190
|
- **correlation, not instruction.** detailed prompts surviving more often does not
|
|
@@ -144,25 +200,50 @@ claim, so here is the grep that settles it against the single file it ships as:
|
|
|
144
200
|
$ grep -nE '^[[:space:]]*(import|from) ' transcripto.py
|
|
145
201
|
8:import sys, os, json, glob, re, sqlite3, argparse
|
|
146
202
|
9:from datetime import datetime, timezone
|
|
147
|
-
|
|
203
|
+
260: import time
|
|
204
|
+
1051: import datetime
|
|
205
|
+
1061: from the separator), so the result is checked on disk and dropped if it is
|
|
148
206
|
```
|
|
149
207
|
|
|
150
|
-
|
|
151
|
-
which is why the pattern allows for indentation
|
|
152
|
-
would miss
|
|
208
|
+
five lines, four of which are imports and all four are stdlib. `time` and
|
|
209
|
+
`datetime` sit inside functions, which is why the pattern allows for indentation —
|
|
210
|
+
anchor it at `^import` and you would miss two, so do not take my word for the
|
|
211
|
+
anchor either. line 1061 is the pattern catching a docstring that happens to begin
|
|
212
|
+
with the word `from`; it is prose, not an import, and it is left in rather than
|
|
213
|
+
tuned out, because a grep you tuned until it agreed with you proves nothing.
|
|
153
214
|
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
what the list does NOT contain is the actual claim: no `socket`, no `urllib`, no
|
|
216
|
+
`requests`, no `http.client`, no `subprocess`. that one is checkable too, and the
|
|
217
|
+
right answer is no output at all:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
$ grep -nE '\b(socket|urllib|requests|http\.client|subprocess)\b' transcripto.py
|
|
221
|
+
$
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
your transcripts stay in `~/.claude`, `~/.codex` and `~/.cursor`. the index it
|
|
225
|
+
builds stays in `~/.trace`.
|
|
156
226
|
|
|
157
227
|
## the rest of it
|
|
158
228
|
|
|
229
|
+
`coach` and `cost` read your transcript files directly and need nothing set up.
|
|
230
|
+
**the other six read a local index, so run this once first:**
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
transcripto index # a few minutes on a large corpus, incremental after that
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
on a 2,874-file corpus that was 164 seconds, measured 2026-08-29. if you skip it,
|
|
237
|
+
the six say so and exit 2.
|
|
238
|
+
|
|
159
239
|
```
|
|
160
240
|
transcripto index build / refresh (incremental)
|
|
161
241
|
transcripto watch live, new sessions get picked up as your agents work
|
|
162
242
|
transcripto ask YOUR OWN messages about a topic, newest first + a rollup
|
|
163
243
|
transcripto search full-text across everything (you + agents + tool logs)
|
|
164
244
|
transcripto find every session that wrote / edited / read a file
|
|
165
|
-
transcripto
|
|
245
|
+
transcripto trace what durably happened after each prompt you typed (0.1.2+)
|
|
246
|
+
transcripto sessions recent sessions + the first prompt YOU typed in each
|
|
166
247
|
transcripto stats what you actually work on
|
|
167
248
|
transcripto cost what ONE of your decisions costs
|
|
168
249
|
transcripto coach which of YOUR prompt habits survive (a proxy)
|
|
@@ -172,11 +253,17 @@ transcripto coach which of YOUR prompt habits survive (a proxy)
|
|
|
172
253
|
thinking about X across ALL my sessions", in your own words only.
|
|
173
254
|
|
|
174
255
|
```
|
|
175
|
-
$ transcripto find USER-JOURNEY.md
|
|
176
|
-
|
|
256
|
+
$ transcripto find USER-JOURNEY.md # run 2026-08-29
|
|
257
|
+
USER-JOURNEY.md 4 touches across sessions (3 were writes/edits)
|
|
258
|
+
|
|
259
|
+
2026-08-20 WROTE ~/CODE/mountain-of-helicon-main/USER-JOURNEY.md abd9e871
|
|
260
|
+
2026-08-21 WROTE ~/…/Obsidian LIFE/00 Dashboard/suite-user-journey.md 0f845ede
|
|
261
|
+
2026-08-27 read ~/CODE/hack-fleet-ata/docs/USER-JOURNEY.md cddfde29
|
|
262
|
+
2026-08-27 WROTE ~/CODE/hack-fleet-ata/docs/USER-JOURNEY.md cddfde29
|
|
177
263
|
```
|
|
178
264
|
|
|
179
|
-
the file you lost, found across every session you ever ran,
|
|
265
|
+
the file you lost, found across every session you ever ran, with the session id
|
|
266
|
+
that touched it. `find` needs `transcripto index` first.
|
|
180
267
|
|
|
181
268
|
## Codex
|
|
182
269
|
|
|
@@ -215,11 +302,15 @@ just gives the file a name on your PATH.
|
|
|
215
302
|
## tests
|
|
216
303
|
|
|
217
304
|
```
|
|
218
|
-
./test_coach.sh
|
|
219
|
-
./test_codex.sh
|
|
220
|
-
./test_cost.sh
|
|
305
|
+
./test_coach.sh 15 assertions
|
|
306
|
+
./test_codex.sh 14 assertions
|
|
307
|
+
./test_cost.sh 12 assertions
|
|
308
|
+
./test_small_n.sh 7 assertions
|
|
309
|
+
./test_label_bands.sh 13 assertions
|
|
221
310
|
```
|
|
222
311
|
|
|
312
|
+
61 assertions, all green, re-run 2026-08-31.
|
|
313
|
+
|
|
223
314
|
offline, no keys, on fixtures that inherit the real transcript shape including
|
|
224
315
|
all four ways a non-human record disguises itself as `type: user`.
|
|
225
316
|
|
|
@@ -227,6 +318,15 @@ the load-bearing one in `test_coach.sh` is `REVERTED IS NOT SURVIVED`: a commit
|
|
|
227
318
|
that got `reset --hard` in the same session left no durable record. flip that one
|
|
228
319
|
line and the suite goes red, which is the point. a generous proxy is a broken one.
|
|
229
320
|
|
|
321
|
+
`test_label_bands.sh` is the other one, and it exists because 0.1.1 shipped the
|
|
322
|
+
defect it pins. `SURVIVES MOST` took the top five habits and `SURVIVES LEAST` took
|
|
323
|
+
the bottom five, which overlap whenever you have fewer than ten rankable habits —
|
|
324
|
+
so a new user, who necessarily has few, read the same habit at the same percentage
|
|
325
|
+
under both "do more of these" and "these tend to loop". on a 3-habit corpus 0.1.1
|
|
326
|
+
reprinted all three, all at 65% (22/34). the suite is red on the published 0.1.1
|
|
327
|
+
file and green on this one, and its `wide` band asserts the fix leaves a large
|
|
328
|
+
corpus byte-identical.
|
|
329
|
+
|
|
230
330
|
## why though
|
|
231
331
|
|
|
232
332
|
your agent history is proof. every "yeah it's done" has a real trace sitting
|
|
@@ -11,6 +11,56 @@ your disk and never opens a socket.
|
|
|
11
11
|
uvx transcripto coach
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
> **check which build you got.** `trace`, `--harness cursor`, and the "run `index`
|
|
15
|
+
> first" errors below all arrive in **0.1.2**. On anything older, `trace` is not a
|
|
16
|
+
> command, cursor is rejected, and the index-gated commands fail with a raw sqlite
|
|
17
|
+
> traceback instead of an instruction. One line settles which one you are holding:
|
|
18
|
+
>
|
|
19
|
+
> ```
|
|
20
|
+
> uvx transcripto --version # 0.1.2 or newer = this README is accurate
|
|
21
|
+
> ```
|
|
22
|
+
>
|
|
23
|
+
> *Read 2026-08-31: PyPI was serving 0.1.1 while this README described 0.1.2, so
|
|
24
|
+
> `uvx transcripto` gave the older build. If `--version` is not even a recognised
|
|
25
|
+
> flag, you have 0.1.1 — it was added in 0.1.2 precisely because there was no way
|
|
26
|
+
> to tell.* The repo always matches this README and needs nothing installed:
|
|
27
|
+
>
|
|
28
|
+
> ```
|
|
29
|
+
> git clone https://github.com/Morkeeth/transcripto && cd transcripto
|
|
30
|
+
> python3 transcripto.py coach
|
|
31
|
+
> ```
|
|
32
|
+
|
|
33
|
+
## three harnesses, one instrument
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
transcripto coach # Claude Code, ~/.claude/projects
|
|
37
|
+
transcripto coach --harness codex # Codex, ~/.codex
|
|
38
|
+
transcripto coach --harness cursor # Cursor, ~/.cursor/projects/*/agent-transcripts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Authorship is not the same gate in all three, and the tool says so rather than pooling them.**
|
|
42
|
+
Claude Code stamps `promptSource: typed`, which is the measured-reliable signal: about 95% of raw
|
|
43
|
+
`type: user` records are not the operator at all. Cursor has no such field. Its one honest
|
|
44
|
+
equivalent is the `<user_query>` wrapper it puts around a submitted prompt, which injected and
|
|
45
|
+
tool-result records do not carry. That is a weaker signal and it is labelled weaker.
|
|
46
|
+
|
|
47
|
+
## `trace` — what actually happened after you asked
|
|
48
|
+
|
|
49
|
+
`ask` shows what you typed. `find` shows what a file went through. Neither answers the
|
|
50
|
+
question that matters after the fact: **you asked for X, did anything durable happen?**
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
transcripto trace "the gate"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
It walks each of your matching prompts forward inside its own session and lists the writes
|
|
57
|
+
and edits that followed, stopping at your next prompt so one turn cannot claim the next
|
|
58
|
+
turn's work. Green dot = something durable landed. Red = nothing was touched.
|
|
59
|
+
|
|
60
|
+
**Honest limit:** a write following a prompt in the same session is CO-OCCURRENCE, not proof
|
|
61
|
+
the write was caused by that prompt or that it was correct. Same proxy `coach` uses, labelled
|
|
62
|
+
the same way.
|
|
63
|
+
|
|
14
64
|
## what you get back
|
|
15
65
|
|
|
16
66
|
this is a real run on one machine, pasted unedited, 2026-08-28:
|
|
@@ -55,9 +105,13 @@ them as a snapshot rather than a constant. yours will be different, which is the
|
|
|
55
105
|
whole point. the last two lines are the ones that sting: it hands you back your
|
|
56
106
|
own best and worst prompt, verbatim, with the receipt for why it scored each one.
|
|
57
107
|
|
|
58
|
-
on that machine, prompts that wrote down what done looks like
|
|
59
|
-
the time (66 of 104)**. prompts with no stated intent survived
|
|
60
|
-
1043)**. i had spent a year blaming the model.
|
|
108
|
+
on that machine, on that date, prompts that wrote down what done looks like
|
|
109
|
+
survived **63% of the time (66 of 104)**. prompts with no stated intent survived
|
|
110
|
+
**39% (411 of 1043)**. i had spent a year blaming the model.
|
|
111
|
+
|
|
112
|
+
one day later, 2026-08-29, the same command on the same machine read 63% (67 of
|
|
113
|
+
107) and 40% (424 of 1072) over 2,874 transcripts. the percentages held and the
|
|
114
|
+
denominators moved, which is what a snapshot is supposed to do.
|
|
61
115
|
|
|
62
116
|
## the proxy caveat, which travels with every number
|
|
63
117
|
|
|
@@ -109,8 +163,10 @@ read these before you quote a number at anyone.
|
|
|
109
163
|
- **one operator's corpus.** every figure in this README comes from one machine.
|
|
110
164
|
it is an existence proof that the measurement runs, not a finding about how
|
|
111
165
|
people prompt. run it on yours and you get yours.
|
|
112
|
-
- **
|
|
113
|
-
|
|
166
|
+
- **three harnesses today: Claude Code, Codex, Cursor.** nothing else is supported.
|
|
167
|
+
aider and the rest are not read. and the three are not equal: Claude Code has a
|
|
168
|
+
measured-reliable authorship field, Cursor has only the `<user_query>` wrapper,
|
|
169
|
+
which is weaker and is labelled weaker wherever it is used.
|
|
114
170
|
- **the habit labels are heuristics.** "states-a-check-or-done-condition" is a
|
|
115
171
|
pattern match over your text, not comprehension. it will misfile some prompts.
|
|
116
172
|
- **correlation, not instruction.** detailed prompts surviving more often does not
|
|
@@ -126,25 +182,50 @@ claim, so here is the grep that settles it against the single file it ships as:
|
|
|
126
182
|
$ grep -nE '^[[:space:]]*(import|from) ' transcripto.py
|
|
127
183
|
8:import sys, os, json, glob, re, sqlite3, argparse
|
|
128
184
|
9:from datetime import datetime, timezone
|
|
129
|
-
|
|
185
|
+
260: import time
|
|
186
|
+
1051: import datetime
|
|
187
|
+
1061: from the separator), so the result is checked on disk and dropped if it is
|
|
130
188
|
```
|
|
131
189
|
|
|
132
|
-
|
|
133
|
-
which is why the pattern allows for indentation
|
|
134
|
-
would miss
|
|
190
|
+
five lines, four of which are imports and all four are stdlib. `time` and
|
|
191
|
+
`datetime` sit inside functions, which is why the pattern allows for indentation —
|
|
192
|
+
anchor it at `^import` and you would miss two, so do not take my word for the
|
|
193
|
+
anchor either. line 1061 is the pattern catching a docstring that happens to begin
|
|
194
|
+
with the word `from`; it is prose, not an import, and it is left in rather than
|
|
195
|
+
tuned out, because a grep you tuned until it agreed with you proves nothing.
|
|
135
196
|
|
|
136
|
-
|
|
137
|
-
|
|
197
|
+
what the list does NOT contain is the actual claim: no `socket`, no `urllib`, no
|
|
198
|
+
`requests`, no `http.client`, no `subprocess`. that one is checkable too, and the
|
|
199
|
+
right answer is no output at all:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
$ grep -nE '\b(socket|urllib|requests|http\.client|subprocess)\b' transcripto.py
|
|
203
|
+
$
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
your transcripts stay in `~/.claude`, `~/.codex` and `~/.cursor`. the index it
|
|
207
|
+
builds stays in `~/.trace`.
|
|
138
208
|
|
|
139
209
|
## the rest of it
|
|
140
210
|
|
|
211
|
+
`coach` and `cost` read your transcript files directly and need nothing set up.
|
|
212
|
+
**the other six read a local index, so run this once first:**
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
transcripto index # a few minutes on a large corpus, incremental after that
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
on a 2,874-file corpus that was 164 seconds, measured 2026-08-29. if you skip it,
|
|
219
|
+
the six say so and exit 2.
|
|
220
|
+
|
|
141
221
|
```
|
|
142
222
|
transcripto index build / refresh (incremental)
|
|
143
223
|
transcripto watch live, new sessions get picked up as your agents work
|
|
144
224
|
transcripto ask YOUR OWN messages about a topic, newest first + a rollup
|
|
145
225
|
transcripto search full-text across everything (you + agents + tool logs)
|
|
146
226
|
transcripto find every session that wrote / edited / read a file
|
|
147
|
-
transcripto
|
|
227
|
+
transcripto trace what durably happened after each prompt you typed (0.1.2+)
|
|
228
|
+
transcripto sessions recent sessions + the first prompt YOU typed in each
|
|
148
229
|
transcripto stats what you actually work on
|
|
149
230
|
transcripto cost what ONE of your decisions costs
|
|
150
231
|
transcripto coach which of YOUR prompt habits survive (a proxy)
|
|
@@ -154,11 +235,17 @@ transcripto coach which of YOUR prompt habits survive (a proxy)
|
|
|
154
235
|
thinking about X across ALL my sessions", in your own words only.
|
|
155
236
|
|
|
156
237
|
```
|
|
157
|
-
$ transcripto find USER-JOURNEY.md
|
|
158
|
-
|
|
238
|
+
$ transcripto find USER-JOURNEY.md # run 2026-08-29
|
|
239
|
+
USER-JOURNEY.md 4 touches across sessions (3 were writes/edits)
|
|
240
|
+
|
|
241
|
+
2026-08-20 WROTE ~/CODE/mountain-of-helicon-main/USER-JOURNEY.md abd9e871
|
|
242
|
+
2026-08-21 WROTE ~/…/Obsidian LIFE/00 Dashboard/suite-user-journey.md 0f845ede
|
|
243
|
+
2026-08-27 read ~/CODE/hack-fleet-ata/docs/USER-JOURNEY.md cddfde29
|
|
244
|
+
2026-08-27 WROTE ~/CODE/hack-fleet-ata/docs/USER-JOURNEY.md cddfde29
|
|
159
245
|
```
|
|
160
246
|
|
|
161
|
-
the file you lost, found across every session you ever ran,
|
|
247
|
+
the file you lost, found across every session you ever ran, with the session id
|
|
248
|
+
that touched it. `find` needs `transcripto index` first.
|
|
162
249
|
|
|
163
250
|
## Codex
|
|
164
251
|
|
|
@@ -197,11 +284,15 @@ just gives the file a name on your PATH.
|
|
|
197
284
|
## tests
|
|
198
285
|
|
|
199
286
|
```
|
|
200
|
-
./test_coach.sh
|
|
201
|
-
./test_codex.sh
|
|
202
|
-
./test_cost.sh
|
|
287
|
+
./test_coach.sh 15 assertions
|
|
288
|
+
./test_codex.sh 14 assertions
|
|
289
|
+
./test_cost.sh 12 assertions
|
|
290
|
+
./test_small_n.sh 7 assertions
|
|
291
|
+
./test_label_bands.sh 13 assertions
|
|
203
292
|
```
|
|
204
293
|
|
|
294
|
+
61 assertions, all green, re-run 2026-08-31.
|
|
295
|
+
|
|
205
296
|
offline, no keys, on fixtures that inherit the real transcript shape including
|
|
206
297
|
all four ways a non-human record disguises itself as `type: user`.
|
|
207
298
|
|
|
@@ -209,6 +300,15 @@ the load-bearing one in `test_coach.sh` is `REVERTED IS NOT SURVIVED`: a commit
|
|
|
209
300
|
that got `reset --hard` in the same session left no durable record. flip that one
|
|
210
301
|
line and the suite goes red, which is the point. a generous proxy is a broken one.
|
|
211
302
|
|
|
303
|
+
`test_label_bands.sh` is the other one, and it exists because 0.1.1 shipped the
|
|
304
|
+
defect it pins. `SURVIVES MOST` took the top five habits and `SURVIVES LEAST` took
|
|
305
|
+
the bottom five, which overlap whenever you have fewer than ten rankable habits —
|
|
306
|
+
so a new user, who necessarily has few, read the same habit at the same percentage
|
|
307
|
+
under both "do more of these" and "these tend to loop". on a 3-habit corpus 0.1.1
|
|
308
|
+
reprinted all three, all at 65% (22/34). the suite is red on the published 0.1.1
|
|
309
|
+
file and green on this one, and its `wide` band asserts the fix leaves a large
|
|
310
|
+
corpus byte-identical.
|
|
311
|
+
|
|
212
312
|
## why though
|
|
213
313
|
|
|
214
314
|
your agent history is proof. every "yeah it's done" has a real trace sitting
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "transcripto"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Search everything your coding agents ever did, grade your own prompts, and price your decisions. Local, stdlib-only, your data never leaves the machine."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transcripto
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Search everything your coding agents ever did, grade your own prompts, and price your decisions. Local, stdlib-only, your data never leaves the machine.
|
|
5
5
|
Author: Oscar Morke
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,56 @@ your disk and never opens a socket.
|
|
|
29
29
|
uvx transcripto coach
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
> **check which build you got.** `trace`, `--harness cursor`, and the "run `index`
|
|
33
|
+
> first" errors below all arrive in **0.1.2**. On anything older, `trace` is not a
|
|
34
|
+
> command, cursor is rejected, and the index-gated commands fail with a raw sqlite
|
|
35
|
+
> traceback instead of an instruction. One line settles which one you are holding:
|
|
36
|
+
>
|
|
37
|
+
> ```
|
|
38
|
+
> uvx transcripto --version # 0.1.2 or newer = this README is accurate
|
|
39
|
+
> ```
|
|
40
|
+
>
|
|
41
|
+
> *Read 2026-08-31: PyPI was serving 0.1.1 while this README described 0.1.2, so
|
|
42
|
+
> `uvx transcripto` gave the older build. If `--version` is not even a recognised
|
|
43
|
+
> flag, you have 0.1.1 — it was added in 0.1.2 precisely because there was no way
|
|
44
|
+
> to tell.* The repo always matches this README and needs nothing installed:
|
|
45
|
+
>
|
|
46
|
+
> ```
|
|
47
|
+
> git clone https://github.com/Morkeeth/transcripto && cd transcripto
|
|
48
|
+
> python3 transcripto.py coach
|
|
49
|
+
> ```
|
|
50
|
+
|
|
51
|
+
## three harnesses, one instrument
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
transcripto coach # Claude Code, ~/.claude/projects
|
|
55
|
+
transcripto coach --harness codex # Codex, ~/.codex
|
|
56
|
+
transcripto coach --harness cursor # Cursor, ~/.cursor/projects/*/agent-transcripts
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Authorship is not the same gate in all three, and the tool says so rather than pooling them.**
|
|
60
|
+
Claude Code stamps `promptSource: typed`, which is the measured-reliable signal: about 95% of raw
|
|
61
|
+
`type: user` records are not the operator at all. Cursor has no such field. Its one honest
|
|
62
|
+
equivalent is the `<user_query>` wrapper it puts around a submitted prompt, which injected and
|
|
63
|
+
tool-result records do not carry. That is a weaker signal and it is labelled weaker.
|
|
64
|
+
|
|
65
|
+
## `trace` — what actually happened after you asked
|
|
66
|
+
|
|
67
|
+
`ask` shows what you typed. `find` shows what a file went through. Neither answers the
|
|
68
|
+
question that matters after the fact: **you asked for X, did anything durable happen?**
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
transcripto trace "the gate"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It walks each of your matching prompts forward inside its own session and lists the writes
|
|
75
|
+
and edits that followed, stopping at your next prompt so one turn cannot claim the next
|
|
76
|
+
turn's work. Green dot = something durable landed. Red = nothing was touched.
|
|
77
|
+
|
|
78
|
+
**Honest limit:** a write following a prompt in the same session is CO-OCCURRENCE, not proof
|
|
79
|
+
the write was caused by that prompt or that it was correct. Same proxy `coach` uses, labelled
|
|
80
|
+
the same way.
|
|
81
|
+
|
|
32
82
|
## what you get back
|
|
33
83
|
|
|
34
84
|
this is a real run on one machine, pasted unedited, 2026-08-28:
|
|
@@ -73,9 +123,13 @@ them as a snapshot rather than a constant. yours will be different, which is the
|
|
|
73
123
|
whole point. the last two lines are the ones that sting: it hands you back your
|
|
74
124
|
own best and worst prompt, verbatim, with the receipt for why it scored each one.
|
|
75
125
|
|
|
76
|
-
on that machine, prompts that wrote down what done looks like
|
|
77
|
-
the time (66 of 104)**. prompts with no stated intent survived
|
|
78
|
-
1043)**. i had spent a year blaming the model.
|
|
126
|
+
on that machine, on that date, prompts that wrote down what done looks like
|
|
127
|
+
survived **63% of the time (66 of 104)**. prompts with no stated intent survived
|
|
128
|
+
**39% (411 of 1043)**. i had spent a year blaming the model.
|
|
129
|
+
|
|
130
|
+
one day later, 2026-08-29, the same command on the same machine read 63% (67 of
|
|
131
|
+
107) and 40% (424 of 1072) over 2,874 transcripts. the percentages held and the
|
|
132
|
+
denominators moved, which is what a snapshot is supposed to do.
|
|
79
133
|
|
|
80
134
|
## the proxy caveat, which travels with every number
|
|
81
135
|
|
|
@@ -127,8 +181,10 @@ read these before you quote a number at anyone.
|
|
|
127
181
|
- **one operator's corpus.** every figure in this README comes from one machine.
|
|
128
182
|
it is an existence proof that the measurement runs, not a finding about how
|
|
129
183
|
people prompt. run it on yours and you get yours.
|
|
130
|
-
- **
|
|
131
|
-
|
|
184
|
+
- **three harnesses today: Claude Code, Codex, Cursor.** nothing else is supported.
|
|
185
|
+
aider and the rest are not read. and the three are not equal: Claude Code has a
|
|
186
|
+
measured-reliable authorship field, Cursor has only the `<user_query>` wrapper,
|
|
187
|
+
which is weaker and is labelled weaker wherever it is used.
|
|
132
188
|
- **the habit labels are heuristics.** "states-a-check-or-done-condition" is a
|
|
133
189
|
pattern match over your text, not comprehension. it will misfile some prompts.
|
|
134
190
|
- **correlation, not instruction.** detailed prompts surviving more often does not
|
|
@@ -144,25 +200,50 @@ claim, so here is the grep that settles it against the single file it ships as:
|
|
|
144
200
|
$ grep -nE '^[[:space:]]*(import|from) ' transcripto.py
|
|
145
201
|
8:import sys, os, json, glob, re, sqlite3, argparse
|
|
146
202
|
9:from datetime import datetime, timezone
|
|
147
|
-
|
|
203
|
+
260: import time
|
|
204
|
+
1051: import datetime
|
|
205
|
+
1061: from the separator), so the result is checked on disk and dropped if it is
|
|
148
206
|
```
|
|
149
207
|
|
|
150
|
-
|
|
151
|
-
which is why the pattern allows for indentation
|
|
152
|
-
would miss
|
|
208
|
+
five lines, four of which are imports and all four are stdlib. `time` and
|
|
209
|
+
`datetime` sit inside functions, which is why the pattern allows for indentation —
|
|
210
|
+
anchor it at `^import` and you would miss two, so do not take my word for the
|
|
211
|
+
anchor either. line 1061 is the pattern catching a docstring that happens to begin
|
|
212
|
+
with the word `from`; it is prose, not an import, and it is left in rather than
|
|
213
|
+
tuned out, because a grep you tuned until it agreed with you proves nothing.
|
|
153
214
|
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
what the list does NOT contain is the actual claim: no `socket`, no `urllib`, no
|
|
216
|
+
`requests`, no `http.client`, no `subprocess`. that one is checkable too, and the
|
|
217
|
+
right answer is no output at all:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
$ grep -nE '\b(socket|urllib|requests|http\.client|subprocess)\b' transcripto.py
|
|
221
|
+
$
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
your transcripts stay in `~/.claude`, `~/.codex` and `~/.cursor`. the index it
|
|
225
|
+
builds stays in `~/.trace`.
|
|
156
226
|
|
|
157
227
|
## the rest of it
|
|
158
228
|
|
|
229
|
+
`coach` and `cost` read your transcript files directly and need nothing set up.
|
|
230
|
+
**the other six read a local index, so run this once first:**
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
transcripto index # a few minutes on a large corpus, incremental after that
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
on a 2,874-file corpus that was 164 seconds, measured 2026-08-29. if you skip it,
|
|
237
|
+
the six say so and exit 2.
|
|
238
|
+
|
|
159
239
|
```
|
|
160
240
|
transcripto index build / refresh (incremental)
|
|
161
241
|
transcripto watch live, new sessions get picked up as your agents work
|
|
162
242
|
transcripto ask YOUR OWN messages about a topic, newest first + a rollup
|
|
163
243
|
transcripto search full-text across everything (you + agents + tool logs)
|
|
164
244
|
transcripto find every session that wrote / edited / read a file
|
|
165
|
-
transcripto
|
|
245
|
+
transcripto trace what durably happened after each prompt you typed (0.1.2+)
|
|
246
|
+
transcripto sessions recent sessions + the first prompt YOU typed in each
|
|
166
247
|
transcripto stats what you actually work on
|
|
167
248
|
transcripto cost what ONE of your decisions costs
|
|
168
249
|
transcripto coach which of YOUR prompt habits survive (a proxy)
|
|
@@ -172,11 +253,17 @@ transcripto coach which of YOUR prompt habits survive (a proxy)
|
|
|
172
253
|
thinking about X across ALL my sessions", in your own words only.
|
|
173
254
|
|
|
174
255
|
```
|
|
175
|
-
$ transcripto find USER-JOURNEY.md
|
|
176
|
-
|
|
256
|
+
$ transcripto find USER-JOURNEY.md # run 2026-08-29
|
|
257
|
+
USER-JOURNEY.md 4 touches across sessions (3 were writes/edits)
|
|
258
|
+
|
|
259
|
+
2026-08-20 WROTE ~/CODE/mountain-of-helicon-main/USER-JOURNEY.md abd9e871
|
|
260
|
+
2026-08-21 WROTE ~/…/Obsidian LIFE/00 Dashboard/suite-user-journey.md 0f845ede
|
|
261
|
+
2026-08-27 read ~/CODE/hack-fleet-ata/docs/USER-JOURNEY.md cddfde29
|
|
262
|
+
2026-08-27 WROTE ~/CODE/hack-fleet-ata/docs/USER-JOURNEY.md cddfde29
|
|
177
263
|
```
|
|
178
264
|
|
|
179
|
-
the file you lost, found across every session you ever ran,
|
|
265
|
+
the file you lost, found across every session you ever ran, with the session id
|
|
266
|
+
that touched it. `find` needs `transcripto index` first.
|
|
180
267
|
|
|
181
268
|
## Codex
|
|
182
269
|
|
|
@@ -215,11 +302,15 @@ just gives the file a name on your PATH.
|
|
|
215
302
|
## tests
|
|
216
303
|
|
|
217
304
|
```
|
|
218
|
-
./test_coach.sh
|
|
219
|
-
./test_codex.sh
|
|
220
|
-
./test_cost.sh
|
|
305
|
+
./test_coach.sh 15 assertions
|
|
306
|
+
./test_codex.sh 14 assertions
|
|
307
|
+
./test_cost.sh 12 assertions
|
|
308
|
+
./test_small_n.sh 7 assertions
|
|
309
|
+
./test_label_bands.sh 13 assertions
|
|
221
310
|
```
|
|
222
311
|
|
|
312
|
+
61 assertions, all green, re-run 2026-08-31.
|
|
313
|
+
|
|
223
314
|
offline, no keys, on fixtures that inherit the real transcript shape including
|
|
224
315
|
all four ways a non-human record disguises itself as `type: user`.
|
|
225
316
|
|
|
@@ -227,6 +318,15 @@ the load-bearing one in `test_coach.sh` is `REVERTED IS NOT SURVIVED`: a commit
|
|
|
227
318
|
that got `reset --hard` in the same session left no durable record. flip that one
|
|
228
319
|
line and the suite goes red, which is the point. a generous proxy is a broken one.
|
|
229
320
|
|
|
321
|
+
`test_label_bands.sh` is the other one, and it exists because 0.1.1 shipped the
|
|
322
|
+
defect it pins. `SURVIVES MOST` took the top five habits and `SURVIVES LEAST` took
|
|
323
|
+
the bottom five, which overlap whenever you have fewer than ten rankable habits —
|
|
324
|
+
so a new user, who necessarily has few, read the same habit at the same percentage
|
|
325
|
+
under both "do more of these" and "these tend to loop". on a 3-habit corpus 0.1.1
|
|
326
|
+
reprinted all three, all at 65% (22/34). the suite is red on the published 0.1.1
|
|
327
|
+
file and green on this one, and its `wide` band asserts the fix leaves a large
|
|
328
|
+
corpus byte-identical.
|
|
329
|
+
|
|
230
330
|
## why though
|
|
231
331
|
|
|
232
332
|
your agent history is proof. every "yeah it's done" has a real trace sitting
|
|
@@ -21,6 +21,12 @@ def _prog():
|
|
|
21
21
|
|
|
22
22
|
PROG = _prog()
|
|
23
23
|
|
|
24
|
+
# The single source of truth for the version, so `--version` cannot drift from the
|
|
25
|
+
# packaging. A stranger who reads the README on GitHub and installs from PyPI can be
|
|
26
|
+
# holding a different build than the one the README describes, and until this flag
|
|
27
|
+
# existed there was no way for them to tell which.
|
|
28
|
+
VERSION = "0.1.2"
|
|
29
|
+
|
|
24
30
|
USAGE = """
|
|
25
31
|
%(p)s index build / refresh the index (incremental)
|
|
26
32
|
%(p)s ask "<topic>" YOUR OWN messages about a topic, newest first + a rollup
|
|
@@ -30,7 +36,7 @@ USAGE = """
|
|
|
30
36
|
%(p)s stats what you work on most: projects, files, volume
|
|
31
37
|
%(p)s cost what ONE decision of yours costs: spend / turns you typed
|
|
32
38
|
%(p)s coach which of YOUR prompt habits actually survive (a proxy)
|
|
33
|
-
%(p)s coach --harness codex grade your Codex (~/.codex) transcripts instead
|
|
39
|
+
%(p)s coach --harness codex grade your Codex (~/.codex) transcripts instead\n %(p)s coach --harness cursor grade your Cursor (~/.cursor) transcripts instead
|
|
34
40
|
%(p)s coach --verified-human subtract likely-PASTED turns (echoes of agent output)
|
|
35
41
|
""" % {"p": PROG}
|
|
36
42
|
|
|
@@ -42,10 +48,24 @@ FILE_TOOLS = {"Write": "write", "Edit": "edit", "Read": "read",
|
|
|
42
48
|
"NotebookEdit": "edit", "MultiEdit": "edit"}
|
|
43
49
|
|
|
44
50
|
|
|
45
|
-
def connect():
|
|
51
|
+
def connect(require_index=True):
|
|
52
|
+
"""Open the local index. Six commands (search/ask/find/trace/sessions/stats)
|
|
53
|
+
READ it and are meaningless without it; `index` and `watch` BUILD it and pass
|
|
54
|
+
require_index=False. Before this guard existed a cold start printed a raw
|
|
55
|
+
`sqlite3.OperationalError: no such table: messages_fts` — and three of the six
|
|
56
|
+
printed it and still exited 0."""
|
|
46
57
|
os.makedirs(os.path.dirname(DB), exist_ok=True)
|
|
47
58
|
con = sqlite3.connect(DB)
|
|
48
59
|
con.execute("PRAGMA journal_mode=WAL")
|
|
60
|
+
if require_index and not con.execute(
|
|
61
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='messages'"
|
|
62
|
+
).fetchone():
|
|
63
|
+
print("\n no index yet. (looked in %s)\n" % DB)
|
|
64
|
+
print(" this command reads a local index of your transcripts. build it once:\n")
|
|
65
|
+
print(" transcripto index\n")
|
|
66
|
+
print(" it takes a few minutes on a large corpus and is incremental after that.")
|
|
67
|
+
print(" `coach` and `cost` read your transcripts directly and need no index.\n")
|
|
68
|
+
sys.exit(2)
|
|
49
69
|
return con
|
|
50
70
|
|
|
51
71
|
|
|
@@ -166,13 +186,26 @@ def is_human_turn(d):
|
|
|
166
186
|
return True
|
|
167
187
|
|
|
168
188
|
|
|
169
|
-
def _index_once(con):
|
|
170
|
-
"""Incrementally index every changed/new transcript. Returns (new_sessions, new_msgs).
|
|
189
|
+
def _index_once(con, progress=False):
|
|
190
|
+
"""Incrementally index every changed/new transcript. Returns (new_sessions, new_msgs).
|
|
191
|
+
|
|
192
|
+
progress=True prints a heartbeat to stderr. A first index over a few thousand
|
|
193
|
+
transcripts takes minutes, and a silent terminal for that long reads as a hang,
|
|
194
|
+
which is the point at which a first-time user kills it.
|
|
195
|
+
"""
|
|
171
196
|
seen = []
|
|
172
197
|
for root in ROOTS:
|
|
173
198
|
seen += glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True)
|
|
199
|
+
if progress:
|
|
200
|
+
sys.stderr.write("scanning %d transcript file(s) in %s\n"
|
|
201
|
+
% (len(seen), ", ".join(r.replace(HOME, "~") for r in ROOTS)))
|
|
202
|
+
sys.stderr.flush()
|
|
174
203
|
new = msgs = 0
|
|
175
|
-
for f in sorted(seen):
|
|
204
|
+
for done, f in enumerate(sorted(seen), 1):
|
|
205
|
+
if progress and done % 250 == 0:
|
|
206
|
+
sys.stderr.write(" %d/%d files · %d changed · %d messages\r"
|
|
207
|
+
% (done, len(seen), new, msgs))
|
|
208
|
+
sys.stderr.flush()
|
|
176
209
|
mt = os.path.getmtime(f)
|
|
177
210
|
row = con.execute("SELECT mtime FROM indexed WHERE session_file=?", (f,)).fetchone()
|
|
178
211
|
if row and abs(row[0] - mt) < 1e-6:
|
|
@@ -214,8 +247,10 @@ def _index_once(con):
|
|
|
214
247
|
|
|
215
248
|
|
|
216
249
|
def cmd_index(args):
|
|
217
|
-
con = connect(); init_schema(con)
|
|
218
|
-
new, msgs = _index_once(con)
|
|
250
|
+
con = connect(require_index=False); init_schema(con)
|
|
251
|
+
new, msgs = _index_once(con, progress=sys.stderr.isatty())
|
|
252
|
+
if sys.stderr.isatty():
|
|
253
|
+
sys.stderr.write(" " * 60 + "\r"); sys.stderr.flush()
|
|
219
254
|
tot = con.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
|
|
220
255
|
print("indexed %d changed sessions · +%d messages · %d total searchable" % (new, msgs, tot))
|
|
221
256
|
|
|
@@ -223,7 +258,7 @@ def cmd_index(args):
|
|
|
223
258
|
def cmd_watch(args):
|
|
224
259
|
"""Live indexing: pick up new transcripts + trace lines automatically as they land."""
|
|
225
260
|
import time
|
|
226
|
-
con = connect(); init_schema(con)
|
|
261
|
+
con = connect(require_index=False); init_schema(con)
|
|
227
262
|
_index_once(con)
|
|
228
263
|
tot = con.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
|
|
229
264
|
print(PROG + " watch, live. %d messages indexed; polling %s every %ds. Ctrl-C to stop."
|
|
@@ -299,11 +334,11 @@ def cmd_ask(args):
|
|
|
299
334
|
any_hit = 0
|
|
300
335
|
if any_hit:
|
|
301
336
|
print("no messages YOU typed about '%s', but %d agent/tool turns mention it."
|
|
302
|
-
"\ntry
|
|
303
|
-
% (args.query, any_hit, args.query))
|
|
337
|
+
"\ntry `%s search \"%s\"` to see those, or `%s index` if it's new."
|
|
338
|
+
% (args.query, any_hit, PROG, args.query, PROG))
|
|
304
339
|
else:
|
|
305
|
-
print("nothing about '%s' yet. try
|
|
306
|
-
% args.query)
|
|
340
|
+
print("nothing about '%s' yet. try `%s index` first, or broader terms."
|
|
341
|
+
% (args.query, PROG))
|
|
307
342
|
return
|
|
308
343
|
|
|
309
344
|
# ---- rollup: the arc across ALL your matches, not just the shown page ----
|
|
@@ -357,15 +392,99 @@ def cmd_find(args):
|
|
|
357
392
|
print("%s %s %s \033[2m%s\033[0m" % (_day(ts), tag, path, sid[:8]))
|
|
358
393
|
|
|
359
394
|
|
|
395
|
+
def cmd_trace(args):
|
|
396
|
+
"""WHAT ACTUALLY HAPPENED after you asked. The join nothing else makes.
|
|
397
|
+
|
|
398
|
+
`ask` shows what you typed. `find` shows what a file went through. Neither
|
|
399
|
+
answers the only question that matters after the fact: you asked for X, did
|
|
400
|
+
anything durable happen? This walks each of your matching prompts forward
|
|
401
|
+
inside its own session and lists the writes and edits that followed it,
|
|
402
|
+
stopping at your next prompt so one turn cannot claim the next turn's work.
|
|
403
|
+
|
|
404
|
+
HONEST LIMIT, stated because the product is about claims: a write following
|
|
405
|
+
a prompt in the same session is CO-OCCURRENCE, not proof the write was caused
|
|
406
|
+
by that prompt or that it was correct. It is the same proxy `coach` uses and
|
|
407
|
+
it is labelled the same way.
|
|
408
|
+
"""
|
|
409
|
+
con = connect()
|
|
410
|
+
try:
|
|
411
|
+
rows = con.execute(
|
|
412
|
+
"SELECT m.id,m.ts,m.project,m.cwd,m.session_id,m.text,m.git_branch"
|
|
413
|
+
" FROM messages_fts JOIN messages m ON m.id=messages_fts.rowid"
|
|
414
|
+
" WHERE messages_fts MATCH ? AND m.is_human=1"
|
|
415
|
+
" ORDER BY m.ts DESC LIMIT ?",
|
|
416
|
+
(_match(args.query), args.limit)).fetchall()
|
|
417
|
+
except sqlite3.OperationalError as e:
|
|
418
|
+
print("trace error:", e); return
|
|
419
|
+
if not rows:
|
|
420
|
+
print("no prompts YOU typed matching '%s'. try `%s ask \"%s\"` or `%s index`."
|
|
421
|
+
% (args.query, PROG, args.query, PROG)); return
|
|
422
|
+
|
|
423
|
+
landed = stalled = 0
|
|
424
|
+
blocks = []
|
|
425
|
+
for _id, ts, proj, cwd, sid, text, branch in rows:
|
|
426
|
+
nxt = con.execute(
|
|
427
|
+
"SELECT MIN(ts) FROM messages WHERE session_id=? AND is_human=1 AND ts>?",
|
|
428
|
+
(sid, ts)).fetchone()[0]
|
|
429
|
+
if nxt:
|
|
430
|
+
files = con.execute(
|
|
431
|
+
"SELECT ts,action,path FROM files WHERE session_id=? AND ts>? AND ts<?"
|
|
432
|
+
" ORDER BY ts", (sid, ts, nxt)).fetchall()
|
|
433
|
+
else:
|
|
434
|
+
files = con.execute(
|
|
435
|
+
"SELECT ts,action,path FROM files WHERE session_id=? AND ts>?"
|
|
436
|
+
" ORDER BY ts", (sid, ts)).fetchall()
|
|
437
|
+
durable = [f for f in files if f[1] in ("write", "edit")]
|
|
438
|
+
if durable: landed += 1
|
|
439
|
+
else: stalled += 1
|
|
440
|
+
blocks.append((ts, proj, cwd, sid, text, branch, files, durable))
|
|
441
|
+
|
|
442
|
+
total = len(blocks)
|
|
443
|
+
pct = (100.0 * landed / total) if total else 0.0
|
|
444
|
+
print("\033[1m%s\033[0m %d prompt%s you typed · \033[32m%d landed\033[0m · "
|
|
445
|
+
"\033[31m%d produced nothing durable\033[0m · %.0f%%"
|
|
446
|
+
% (args.query, total, "" if total == 1 else "s", landed, stalled, pct))
|
|
447
|
+
print("\033[2mdurable = a Write or Edit in the same session before your next prompt. "
|
|
448
|
+
"CO-OCCURRENCE, not proof of cause or correctness.\033[0m\n")
|
|
449
|
+
|
|
450
|
+
for ts, proj, cwd, sid, text, branch, files, durable in blocks:
|
|
451
|
+
mark = "\033[32m●\033[0m" if durable else "\033[31m○\033[0m"
|
|
452
|
+
head = " ".join((text or "").split())[:110]
|
|
453
|
+
br = (" \033[2m%s\033[0m" % branch) if branch else ""
|
|
454
|
+
print("%s %s \033[36m%s\033[0m%s \033[2m%s\033[0m"
|
|
455
|
+
% (mark, _day(ts), _repo(cwd, proj)[:22], br, (sid or "")[:8]))
|
|
456
|
+
print(" \033[1m\"%s\"\033[0m" % head)
|
|
457
|
+
if not files:
|
|
458
|
+
print(" \033[31mnothing touched\033[0m")
|
|
459
|
+
else:
|
|
460
|
+
shown = files if args.all else files[:8]
|
|
461
|
+
for fts, action, path in shown:
|
|
462
|
+
tag = {"write": "\033[32mWROTE\033[0m", "edit": "\033[33mEDIT \033[0m",
|
|
463
|
+
"read": "\033[2mread \033[0m"}.get(action, action.upper())
|
|
464
|
+
print(" %s %s" % (tag, path))
|
|
465
|
+
if len(files) > len(shown):
|
|
466
|
+
print(" \033[2m… %d more, -a to show all\033[0m" % (len(files) - len(shown)))
|
|
467
|
+
print()
|
|
468
|
+
|
|
469
|
+
|
|
360
470
|
def cmd_sessions(args):
|
|
361
471
|
con = connect()
|
|
362
472
|
rows = con.execute(
|
|
363
473
|
"SELECT session_id,project,MAX(ts) mx,COUNT(*) FROM messages"
|
|
364
474
|
" GROUP BY session_id ORDER BY mx DESC LIMIT ?", (args.limit,)).fetchall()
|
|
365
475
|
for sid, proj, mx, cnt in rows:
|
|
476
|
+
# Same gate the rest of the tool sells: the opening ask is the first turn
|
|
477
|
+
# the OPERATOR typed (is_human=1), not the first `type: user` record. Without
|
|
478
|
+
# it the column filled up with `<command-name>/clear</command-name>` — a slash
|
|
479
|
+
# command the harness wrote, printed under a heading that says "opening ask".
|
|
480
|
+
# Slash commands clear is_human on most harnesses but not all, so the
|
|
481
|
+
# wrapper is excluded by name too, and a run of them is skipped rather than
|
|
482
|
+
# taken as the title.
|
|
366
483
|
t = con.execute("SELECT text FROM messages WHERE session_id=? AND role='user'"
|
|
367
|
-
" AND text!=''
|
|
368
|
-
|
|
484
|
+
" AND is_human=1 AND text!='' AND text NOT LIKE '<command-name>%'"
|
|
485
|
+
" AND text NOT LIKE '<local-command-%' ORDER BY ts LIMIT 1",
|
|
486
|
+
(sid,)).fetchone()
|
|
487
|
+
title = (t[0][:90].replace("\n", " ") if t else "\033[2m(no prompt you typed)\033[0m")
|
|
369
488
|
print("\033[2m%s\033[0m \033[36m%-22s\033[0m %4d msg %s"
|
|
370
489
|
% (_day(mx), proj[:22], cnt, title))
|
|
371
490
|
|
|
@@ -908,6 +1027,102 @@ def _codex_rows(path):
|
|
|
908
1027
|
return rows
|
|
909
1028
|
|
|
910
1029
|
|
|
1030
|
+
# --- Cursor CLI ------------------------------------------------------------
|
|
1031
|
+
# Cursor writes ~/.cursor/projects/<slug>/agent-transcripts/<uuid>/<uuid>.jsonl
|
|
1032
|
+
# with the SAME {role, message:{content:[...]}} shape Claude Code uses, so
|
|
1033
|
+
# extract() already reads it. Three things are missing and this connector adds
|
|
1034
|
+
# them: there is no timestamp field (it is embedded in the user text), no cwd
|
|
1035
|
+
# field (it is the directory slug), and the human turn is wrapped in
|
|
1036
|
+
# <user_query> tags that would otherwise be graded as part of the prompt.
|
|
1037
|
+
|
|
1038
|
+
_CUR_TS = re.compile(r"<timestamp>(.*?)</timestamp>", re.S)
|
|
1039
|
+
_CUR_Q = re.compile(r"<user_query>\s*(.*?)\s*</user_query>", re.S)
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _cursor_ts(text):
|
|
1043
|
+
"""'Thursday, Aug 13, 2026, 5:53 PM (UTC+2)' -> ISO-ish, or ''. Never guesses
|
|
1044
|
+
a date it cannot parse: an unparsed stamp yields '' rather than today."""
|
|
1045
|
+
m = _CUR_TS.search(text or "")
|
|
1046
|
+
if not m:
|
|
1047
|
+
return ""
|
|
1048
|
+
raw = m.group(1).strip()
|
|
1049
|
+
for fmt in ("%A, %b %d, %Y, %I:%M %p", "%A, %B %d, %Y, %I:%M %p"):
|
|
1050
|
+
try:
|
|
1051
|
+
import datetime
|
|
1052
|
+
return datetime.datetime.strptime(raw.split(" (")[0], fmt).isoformat()
|
|
1053
|
+
except Exception:
|
|
1054
|
+
pass
|
|
1055
|
+
return ""
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _cursor_cwd(path):
|
|
1059
|
+
"""~/.cursor/projects/Users-morkeeth-CODE-zup/... -> /Users/morkeeth/CODE/zup.
|
|
1060
|
+
The slug is lossy (a real hyphen in a directory name is indistinguishable
|
|
1061
|
+
from the separator), so the result is checked on disk and dropped if it is
|
|
1062
|
+
not a real directory. A wrong cwd is worse than no cwd."""
|
|
1063
|
+
parts = os.path.normpath(path).split(os.sep)
|
|
1064
|
+
try:
|
|
1065
|
+
slug = parts[parts.index("projects") + 1]
|
|
1066
|
+
except (ValueError, IndexError):
|
|
1067
|
+
return ""
|
|
1068
|
+
cand = "/" + slug.replace("-", "/")
|
|
1069
|
+
if os.path.isdir(cand):
|
|
1070
|
+
return cand
|
|
1071
|
+
# try collapsing trailing segments back into hyphenated names
|
|
1072
|
+
segs = slug.split("-")
|
|
1073
|
+
for join_from in range(len(segs) - 1, 0, -1):
|
|
1074
|
+
cand = "/" + "/".join(segs[:join_from] + ["-".join(segs[join_from:])])
|
|
1075
|
+
if os.path.isdir(cand):
|
|
1076
|
+
return cand
|
|
1077
|
+
return ""
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _cursor_rows(path):
|
|
1081
|
+
"""Normalise Cursor records onto the Claude shape the indexer already reads.
|
|
1082
|
+
|
|
1083
|
+
AUTHORSHIP, stated because it is a DIFFERENT gate with different provenance.
|
|
1084
|
+
Claude Code stamps `promptSource: typed`, which is the measured-reliable signal
|
|
1085
|
+
(~95% of raw `type: user` records are not the operator). Cursor has no such
|
|
1086
|
+
field. Its one honest equivalent is the `<user_query>` wrapper: Cursor puts it
|
|
1087
|
+
around a prompt the operator submitted, and injected/tool-result user records
|
|
1088
|
+
do not carry it. So a Cursor record counts as typed IFF it carried that
|
|
1089
|
+
wrapper. This is a weaker signal than Claude's and it is labelled as such
|
|
1090
|
+
rather than silently pooled with it.
|
|
1091
|
+
"""
|
|
1092
|
+
cwd = _cursor_cwd(path)
|
|
1093
|
+
sid = os.path.splitext(os.path.basename(path))[0]
|
|
1094
|
+
last_ts = ""
|
|
1095
|
+
out = []
|
|
1096
|
+
for d in _iter_json(path):
|
|
1097
|
+
if not isinstance(d, dict) or "message" not in d:
|
|
1098
|
+
continue
|
|
1099
|
+
msg = d.get("message") or {}
|
|
1100
|
+
c = msg.get("content")
|
|
1101
|
+
wrapped = False
|
|
1102
|
+
if isinstance(c, list):
|
|
1103
|
+
for b in c:
|
|
1104
|
+
if isinstance(b, dict) and b.get("type") == "text":
|
|
1105
|
+
t = b.get("text") or ""
|
|
1106
|
+
ts = _cursor_ts(t)
|
|
1107
|
+
if ts:
|
|
1108
|
+
last_ts = ts
|
|
1109
|
+
q = _CUR_Q.search(t)
|
|
1110
|
+
if q:
|
|
1111
|
+
b["text"] = q.group(1)
|
|
1112
|
+
wrapped = True
|
|
1113
|
+
elif _CUR_TS.search(t):
|
|
1114
|
+
b["text"] = _CUR_TS.sub("", t).strip()
|
|
1115
|
+
role = d.get("role") or msg.get("role")
|
|
1116
|
+
d["type"] = "user" if role == "user" else "assistant"
|
|
1117
|
+
if role == "user" and wrapped:
|
|
1118
|
+
d["promptSource"] = "typed"
|
|
1119
|
+
d["timestamp"] = last_ts
|
|
1120
|
+
d["cwd"] = cwd
|
|
1121
|
+
d["sessionId"] = sid
|
|
1122
|
+
out.append(d)
|
|
1123
|
+
return out
|
|
1124
|
+
|
|
1125
|
+
|
|
911
1126
|
def _sniff(path):
|
|
912
1127
|
"""'codex' (rollout), 'codex-history' (history.jsonl), or 'claude', from the
|
|
913
1128
|
first parseable record. Lets `--root ~/.codex` and mixed dirs just work."""
|
|
@@ -916,6 +1131,9 @@ def _sniff(path):
|
|
|
916
1131
|
return "codex"
|
|
917
1132
|
if set(d.keys()) == {"session_id", "ts", "text"}:
|
|
918
1133
|
return "codex-history"
|
|
1134
|
+
# Cursor: role + message only, and none of Claude's envelope fields.
|
|
1135
|
+
if set(d.keys()) <= {"role", "message"} and "message" in d:
|
|
1136
|
+
return "cursor"
|
|
919
1137
|
return "claude"
|
|
920
1138
|
return "claude"
|
|
921
1139
|
|
|
@@ -928,6 +1146,8 @@ def _rows_for_file(path):
|
|
|
928
1146
|
return _codex_rows(path), "codex"
|
|
929
1147
|
if h == "codex-history":
|
|
930
1148
|
return [], "codex-history"
|
|
1149
|
+
if h == "cursor":
|
|
1150
|
+
return _cursor_rows(path), "cursor"
|
|
931
1151
|
return list(_iter_json(path)), "claude"
|
|
932
1152
|
|
|
933
1153
|
|
|
@@ -942,6 +1162,10 @@ def _coach_files(roots, harness):
|
|
|
942
1162
|
paths.append(r)
|
|
943
1163
|
continue
|
|
944
1164
|
base = os.path.basename(r.rstrip("/"))
|
|
1165
|
+
if harness == "cursor" or base == ".cursor":
|
|
1166
|
+
paths += sorted(glob.glob(
|
|
1167
|
+
os.path.join(r, "projects", "*", "agent-transcripts", "*", "*.jsonl")))
|
|
1168
|
+
continue
|
|
945
1169
|
if harness == "codex" or base == ".codex":
|
|
946
1170
|
got = sorted(glob.glob(os.path.join(r, "archived_sessions", "*.jsonl")))
|
|
947
1171
|
got += sorted(glob.glob(os.path.join(r, "sessions", "**", "*.jsonl"),
|
|
@@ -1194,7 +1418,11 @@ def _coach_roots(root=None, harness=None):
|
|
|
1194
1418
|
Single source of truth so the empty-result message names the real path."""
|
|
1195
1419
|
if root:
|
|
1196
1420
|
return [root]
|
|
1197
|
-
|
|
1421
|
+
if harness == "codex":
|
|
1422
|
+
return [os.path.expanduser("~/.codex")]
|
|
1423
|
+
if harness == "cursor":
|
|
1424
|
+
return [os.path.expanduser("~/.cursor")]
|
|
1425
|
+
return list(ROOTS)
|
|
1198
1426
|
|
|
1199
1427
|
|
|
1200
1428
|
def coach(roots=None, harness=None, verified_human=False):
|
|
@@ -1247,8 +1475,15 @@ def coach(roots=None, harness=None, verified_human=False):
|
|
|
1247
1475
|
"durable_rate": round(durable / len(episodes), 3) if episodes else 0.0,
|
|
1248
1476
|
"tiers": tiers,
|
|
1249
1477
|
"top_patterns": rankable[:5],
|
|
1250
|
-
|
|
1251
|
-
|
|
1478
|
+
# SURVIVES LEAST must draw only from habits SURVIVES MOST did not take.
|
|
1479
|
+
# The old guard was `len(rankable) > 5`, which only protected the <=5 case:
|
|
1480
|
+
# with 6-9 rankable habits, rankable[:5] and rankable[-5:] overlap, and the
|
|
1481
|
+
# tool printed the SAME row under "do more of these" and "these tend to
|
|
1482
|
+
# loop" with the same denominator. A first-time user has 6-9 habits, so the
|
|
1483
|
+
# first outside run was the one that saw it. Slicing from index 5 makes the
|
|
1484
|
+
# two lists disjoint by construction at every corpus size, and is
|
|
1485
|
+
# byte-identical to the old output once there are >=10 rankable habits.
|
|
1486
|
+
"bottom_patterns": rankable[5:][-5:][::-1],
|
|
1252
1487
|
"best_prompt": _pick(episodes, True,
|
|
1253
1488
|
lambda e: (e["score"], -e["corrective_turns"],
|
|
1254
1489
|
e["assistant_turns"])),
|
|
@@ -1280,8 +1515,12 @@ def cmd_coach(args):
|
|
|
1280
1515
|
print("\n this reads transcripts that already exist on your machine; it does not")
|
|
1281
1516
|
print(" create them. if that directory is empty, use the agent for a session first.\n")
|
|
1282
1517
|
print(" otherwise:")
|
|
1283
|
-
|
|
1284
|
-
|
|
1518
|
+
# All three supported harnesses are offered here. Listing only two while the
|
|
1519
|
+
# README sells three is how a stranger concludes cursor is unsupported.
|
|
1520
|
+
print(" %s coach --harness codex grade Codex (~/.codex) instead" % PROG)
|
|
1521
|
+
print(" %s coach --harness cursor grade Cursor "
|
|
1522
|
+
"(~/.cursor/projects/*/agent-transcripts)" % PROG)
|
|
1523
|
+
print(" %s coach --root <dir> point at a folder of .jsonl transcripts\n" % PROG)
|
|
1285
1524
|
return
|
|
1286
1525
|
B, D = "\033[1m", "\033[0m"
|
|
1287
1526
|
print("\n %sYOUR PROMPT HABITS, GRADED%s (offline, your machine only)\n" % (B, D))
|
|
@@ -1322,10 +1561,17 @@ def cmd_coach(args):
|
|
|
1322
1561
|
for p in r["top_patterns"]:
|
|
1323
1562
|
print(" %3d%% (%s/%s) %s" % (round(p["survival_rate"] * 100),
|
|
1324
1563
|
p["survived"], p["n"], p["pattern"]))
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1564
|
+
# With <=5 rankable habits SURVIVES MOST already showed all of them, so
|
|
1565
|
+
# there is no honest "least" left to print. An empty header under a
|
|
1566
|
+
# promise ("these tend to loop") is a claim about rows that do not exist.
|
|
1567
|
+
if r["bottom_patterns"]:
|
|
1568
|
+
print("\n %sSURVIVES LEAST%s these tend to loop:" % (B, D))
|
|
1569
|
+
for p in r["bottom_patterns"]:
|
|
1570
|
+
print(" %3d%% (%s/%s) %s" % (round(p["survival_rate"] * 100),
|
|
1571
|
+
p["survived"], p["n"], p["pattern"]))
|
|
1572
|
+
else:
|
|
1573
|
+
print("\n \033[2m(every rankable habit is listed above; too few to split "
|
|
1574
|
+
"into a most/least pair)\033[0m")
|
|
1329
1575
|
b = r["best_prompt"]
|
|
1330
1576
|
if b:
|
|
1331
1577
|
print("\n \033[32m+\033[0m your best landed prompt, with its witness:")
|
|
@@ -1344,12 +1590,16 @@ def cmd_coach(args):
|
|
|
1344
1590
|
def main():
|
|
1345
1591
|
p = argparse.ArgumentParser(prog=PROG, description=__doc__ + USAGE,
|
|
1346
1592
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
1593
|
+
p.add_argument("--version", action="version", version="%s %s" % (PROG, VERSION))
|
|
1347
1594
|
sub = p.add_subparsers(dest="cmd")
|
|
1348
1595
|
sub.add_parser("index").set_defaults(fn=cmd_index)
|
|
1349
1596
|
s = sub.add_parser("watch"); s.add_argument("--interval", type=int, default=5); s.set_defaults(fn=cmd_watch)
|
|
1350
1597
|
s = sub.add_parser("ask"); s.add_argument("query"); s.add_argument("-n", "--limit", type=int, default=25); s.set_defaults(fn=cmd_ask)
|
|
1351
1598
|
s = sub.add_parser("search"); s.add_argument("query"); s.add_argument("-n", "--limit", type=int, default=25); s.set_defaults(fn=cmd_search)
|
|
1352
1599
|
s = sub.add_parser("find"); s.add_argument("name"); s.set_defaults(fn=cmd_find)
|
|
1600
|
+
s = sub.add_parser("trace"); s.add_argument("query"); s.add_argument("-n", "--limit", type=int, default=10)
|
|
1601
|
+
s.add_argument("-a", "--all", action="store_true", help="show every file, not the first 8")
|
|
1602
|
+
s.set_defaults(fn=cmd_trace)
|
|
1353
1603
|
s = sub.add_parser("sessions"); s.add_argument("-n", "--limit", type=int, default=30); s.set_defaults(fn=cmd_sessions)
|
|
1354
1604
|
sub.add_parser("stats").set_defaults(fn=cmd_stats)
|
|
1355
1605
|
s = sub.add_parser("cost")
|
|
@@ -1359,9 +1609,10 @@ def main():
|
|
|
1359
1609
|
s.set_defaults(fn=cmd_cost)
|
|
1360
1610
|
s = sub.add_parser("coach")
|
|
1361
1611
|
s.add_argument("--root", help="grade this transcript dir instead of ~/.claude/projects")
|
|
1362
|
-
s.add_argument("--harness", choices=["claude", "codex"],
|
|
1612
|
+
s.add_argument("--harness", choices=["claude", "codex", "cursor"],
|
|
1363
1613
|
help="which agent's transcripts to grade. codex reads "
|
|
1364
|
-
"~/.codex (archived_sessions + sessions)
|
|
1614
|
+
"~/.codex (archived_sessions + sessions); cursor reads "
|
|
1615
|
+
"~/.cursor/projects/*/agent-transcripts. default: auto-detect")
|
|
1365
1616
|
s.add_argument("--verified-human", dest="verified_human", action="store_true",
|
|
1366
1617
|
help="subtract likely-PASTED turns: a typed turn whose text is a "
|
|
1367
1618
|
"verbatim/high n-gram echo of an earlier agent or tool message "
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|