cc-pushback 0.1.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.
- cc_pushback-0.1.0/LICENSE +133 -0
- cc_pushback-0.1.0/PKG-INFO +135 -0
- cc_pushback-0.1.0/README.md +103 -0
- cc_pushback-0.1.0/cc_pushback/__init__.py +28 -0
- cc_pushback-0.1.0/cc_pushback/__main__.py +6 -0
- cc_pushback-0.1.0/cc_pushback/claude.py +46 -0
- cc_pushback-0.1.0/cc_pushback/cli.py +138 -0
- cc_pushback-0.1.0/cc_pushback/context.py +11 -0
- cc_pushback-0.1.0/cc_pushback/detectors.py +138 -0
- cc_pushback-0.1.0/cc_pushback/formats.py +98 -0
- cc_pushback-0.1.0/cc_pushback/markers.py +25 -0
- cc_pushback-0.1.0/cc_pushback/models.py +15 -0
- cc_pushback-0.1.0/cc_pushback/nav.py +31 -0
- cc_pushback-0.1.0/cc_pushback/py.typed +0 -0
- cc_pushback-0.1.0/cc_pushback/report.py +484 -0
- cc_pushback-0.1.0/cc_pushback/scan.py +58 -0
- cc_pushback-0.1.0/cc_pushback/serve.py +60 -0
- cc_pushback-0.1.0/cc_pushback/spec.py +37 -0
- cc_pushback-0.1.0/cc_pushback/store.py +34 -0
- cc_pushback-0.1.0/pyproject.toml +101 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Required Notice: Copyright Yasyf Mohamedali (https://github.com/yasyf/cc-pushback)
|
|
2
|
+
|
|
3
|
+
# PolyForm Noncommercial License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree
|
|
10
|
+
to them as both strict obligations and conditions to all
|
|
11
|
+
your licenses.
|
|
12
|
+
|
|
13
|
+
## Copyright License
|
|
14
|
+
|
|
15
|
+
The licensor grants you a copyright license for the
|
|
16
|
+
software to do everything you might do with the software
|
|
17
|
+
that would otherwise infringe the licensor's copyright
|
|
18
|
+
in it for any permitted purpose. However, you may
|
|
19
|
+
only distribute the software according to [Distribution
|
|
20
|
+
License](#distribution-license) and make changes or new works
|
|
21
|
+
based on the software according to [Changes and New Works
|
|
22
|
+
License](#changes-and-new-works-license).
|
|
23
|
+
|
|
24
|
+
## Distribution License
|
|
25
|
+
|
|
26
|
+
The licensor grants you an additional copyright license
|
|
27
|
+
to distribute copies of the software. Your license
|
|
28
|
+
to distribute covers distributing the software with
|
|
29
|
+
changes and new works permitted by [Changes and New Works
|
|
30
|
+
License](#changes-and-new-works-license).
|
|
31
|
+
|
|
32
|
+
## Notices
|
|
33
|
+
|
|
34
|
+
You must ensure that anyone who gets a copy of any part of
|
|
35
|
+
the software from you also gets a copy of these terms or the
|
|
36
|
+
URL for them above, as well as copies of any plain-text lines
|
|
37
|
+
beginning with `Required Notice:` that the licensor provided
|
|
38
|
+
with the software. For example:
|
|
39
|
+
|
|
40
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
41
|
+
|
|
42
|
+
## Changes and New Works License
|
|
43
|
+
|
|
44
|
+
The licensor grants you an additional copyright license to
|
|
45
|
+
make changes and new works based on the software for any
|
|
46
|
+
permitted purpose.
|
|
47
|
+
|
|
48
|
+
## Patent License
|
|
49
|
+
|
|
50
|
+
The licensor grants you a patent license for the software that
|
|
51
|
+
covers patent claims the licensor can license, or becomes able
|
|
52
|
+
to license, that you would infringe by using the software.
|
|
53
|
+
|
|
54
|
+
## Noncommercial Purposes
|
|
55
|
+
|
|
56
|
+
Any noncommercial purpose is a permitted purpose.
|
|
57
|
+
|
|
58
|
+
## Personal Uses
|
|
59
|
+
|
|
60
|
+
Personal use for research, experiment, and testing for
|
|
61
|
+
the benefit of public knowledge, personal study, private
|
|
62
|
+
entertainment, hobby projects, amateur pursuits, or religious
|
|
63
|
+
observance, without any anticipated commercial application,
|
|
64
|
+
is use for a permitted purpose.
|
|
65
|
+
|
|
66
|
+
## Noncommercial Organizations
|
|
67
|
+
|
|
68
|
+
Use by any charitable organization, educational institution,
|
|
69
|
+
public research organization, public safety or health
|
|
70
|
+
organization, environmental protection organization,
|
|
71
|
+
or government institution is use for a permitted purpose
|
|
72
|
+
regardless of the source of funding or obligations resulting
|
|
73
|
+
from the funding.
|
|
74
|
+
|
|
75
|
+
## Fair Use
|
|
76
|
+
|
|
77
|
+
You may have "fair use" rights for the software under the
|
|
78
|
+
law. These terms do not limit them.
|
|
79
|
+
|
|
80
|
+
## No Other Rights
|
|
81
|
+
|
|
82
|
+
These terms do not allow you to sublicense or transfer any of
|
|
83
|
+
your licenses to anyone else, or prevent the licensor from
|
|
84
|
+
granting licenses to anyone else. These terms do not imply
|
|
85
|
+
any other licenses.
|
|
86
|
+
|
|
87
|
+
## Patent Defense
|
|
88
|
+
|
|
89
|
+
If you make any written claim that the software infringes or
|
|
90
|
+
contributes to infringement of any patent, your patent license
|
|
91
|
+
for the software granted under these terms ends immediately. If
|
|
92
|
+
your company makes such a claim, your patent license ends
|
|
93
|
+
immediately for work on behalf of your company.
|
|
94
|
+
|
|
95
|
+
## Violations
|
|
96
|
+
|
|
97
|
+
The first time you are notified in writing that you have
|
|
98
|
+
violated any of these terms, or done anything with the software
|
|
99
|
+
not covered by your licenses, your licenses can nonetheless
|
|
100
|
+
continue if you come into full compliance with these terms,
|
|
101
|
+
and take practical steps to correct past violations, within
|
|
102
|
+
32 days of receiving notice. Otherwise, all your licenses
|
|
103
|
+
end immediately.
|
|
104
|
+
|
|
105
|
+
## No Liability
|
|
106
|
+
|
|
107
|
+
***As far as the law allows, the software comes as is, without
|
|
108
|
+
any warranty or condition, and the licensor will not be liable
|
|
109
|
+
to you for any damages arising out of these terms or the use
|
|
110
|
+
or nature of the software, under any kind of legal claim.***
|
|
111
|
+
|
|
112
|
+
## Definitions
|
|
113
|
+
|
|
114
|
+
The **licensor** is the individual or entity offering these
|
|
115
|
+
terms, and the **software** is the software the licensor makes
|
|
116
|
+
available under these terms.
|
|
117
|
+
|
|
118
|
+
**You** refers to the individual or entity agreeing to these
|
|
119
|
+
terms.
|
|
120
|
+
|
|
121
|
+
**Your company** is any legal entity, sole proprietorship,
|
|
122
|
+
or other kind of organization that you work for, plus all
|
|
123
|
+
organizations that have control over, are under the control of,
|
|
124
|
+
or are under common control with that organization. **Control**
|
|
125
|
+
means ownership of substantially all the assets of an entity,
|
|
126
|
+
or the power to direct its management and policies by vote,
|
|
127
|
+
contract, or otherwise. Control can be direct or indirect.
|
|
128
|
+
|
|
129
|
+
**Your licenses** are all the licenses granted to you for the
|
|
130
|
+
software under these terms.
|
|
131
|
+
|
|
132
|
+
**Use** means anything you do with the software requiring one
|
|
133
|
+
of your licenses.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cc-pushback
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Learn your pushback style from past Claude Code feedback and code reviews, and replicate it with a language model.
|
|
5
|
+
Keywords:
|
|
6
|
+
Author: Yasyf Mohamedali
|
|
7
|
+
Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
|
|
8
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: aiohttp>=3.10
|
|
17
|
+
Requires-Dist: anyio>=4.4
|
|
18
|
+
Requires-Dist: cc-transcript>=0.7,<0.8
|
|
19
|
+
Requires-Dist: click>=8
|
|
20
|
+
Requires-Dist: spawnllm>=0.1
|
|
21
|
+
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: ty>=0.0.44 ; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.8 ; extra == 'dev'
|
|
24
|
+
Requires-Python: >=3.13
|
|
25
|
+
Project-URL: Homepage, https://github.com/yasyf/cc-pushback
|
|
26
|
+
Project-URL: Documentation, https://yasyf.github.io/cc-pushback/
|
|
27
|
+
Project-URL: Repository, https://github.com/yasyf/cc-pushback
|
|
28
|
+
Project-URL: Issues, https://github.com/yasyf/cc-pushback/issues
|
|
29
|
+
Project-URL: Changelog, https://github.com/yasyf/cc-pushback/blob/main/CHANGELOG.md
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# cc-pushback
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/cc-pushback/)
|
|
36
|
+
[](https://pypi.org/project/cc-pushback/)
|
|
37
|
+
[](https://yasyf.github.io/cc-pushback/)
|
|
38
|
+
[](https://github.com/yasyf/cc-pushback/blob/main/LICENSE)
|
|
39
|
+
|
|
40
|
+
cc-pushback mines your Claude Code transcripts for the moments you pushed back — corrections, interrupts, rejected plans, code-review comments, "no, do it this way" — and collects them, with the surrounding conversational context, into a local database. That corpus is the raw material for learning your pushback style; this first release builds it.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uvx cc-pushback --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`uvx` fetches cc-pushback into a throwaway environment and runs it. To add it
|
|
51
|
+
to a project instead:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add cc-pushback
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
Scan your transcripts for the moments you pushed back and accumulate them into a
|
|
60
|
+
local feedback database:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uvx cc-pushback scan
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
scanned 412 files, 1473 new rows
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`scan` is incremental and idempotent. Each transcript is parsed only when it is
|
|
71
|
+
new or has changed since the last scan, and every candidate is keyed by a content
|
|
72
|
+
digest, so re-running over unchanged inputs adds nothing. Recording a file and
|
|
73
|
+
inserting its candidates commit in one transaction — interrupt a scan and the
|
|
74
|
+
database is never left half-written. A transcript that fails to parse (one Claude
|
|
75
|
+
Code is still writing, say) is skipped and retried next time, never aborting the run.
|
|
76
|
+
|
|
77
|
+
The database lives at `~/.cc-pushback/feedback.db` by default (override with
|
|
78
|
+
`--db`). Inspect what has been collected:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uvx cc-pushback stats # counts by source kind, and the scanned-file count
|
|
82
|
+
uvx cc-pushback list # recent feedback, newest first
|
|
83
|
+
uvx cc-pushback list --source plan_review --limit 50
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### What gets collected
|
|
87
|
+
|
|
88
|
+
`scan` runs four detectors over each transcript:
|
|
89
|
+
|
|
90
|
+
- **Transcript messages** (`transcript_message`) — the pushback you typed
|
|
91
|
+
mid-session, after trivial acknowledgements and structural noise are filtered out.
|
|
92
|
+
- **Plan reviews** (`plan_review`) — rejected `ExitPlanMode` plans (with the
|
|
93
|
+
feedback you gave) and plan-mode re-entries right after an edit cycle, i.e.
|
|
94
|
+
"let's rethink this."
|
|
95
|
+
- **Interrupts and rejections** (`interrupt_rejection`) — permission denials and
|
|
96
|
+
`[Request interrupted by user]` corrections, with the denied tool and your
|
|
97
|
+
follow-up captured.
|
|
98
|
+
- **Review comments** (`review_comment`) — structured code-review messages
|
|
99
|
+
exploded into one row per inline comment.
|
|
100
|
+
|
|
101
|
+
Each row carries the conversational window around the feedback — the assistant
|
|
102
|
+
action it responded to, plus a few turns either side — captured at collection
|
|
103
|
+
time, because transcripts are ephemeral.
|
|
104
|
+
|
|
105
|
+
Restrict to specific kinds with `--source` (repeatable), or force a full re-mine
|
|
106
|
+
of every transcript (after a detector change, say) with `--full`:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uvx cc-pushback list --source transcript_message --source plan_review
|
|
110
|
+
uvx cc-pushback scan --full
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Mining transcripts from another machine
|
|
114
|
+
|
|
115
|
+
Transcripts live under `~/.claude/projects`. To mine a remote machine's history,
|
|
116
|
+
mirror its projects directory locally with `rsync`, then scan that directory:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
rsync -az yasyf@yasyf:.claude/projects/ ~/.cc-pushback/mirrors/yasyf/
|
|
120
|
+
uvx cc-pushback scan --transcripts ~/.cc-pushback/mirrors/yasyf/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--transcripts` is repeatable, so you can fold several mirrors into one scan.
|
|
124
|
+
Because discovery is mtime-keyed, repeating the `rsync` and re-scanning only
|
|
125
|
+
ingests what changed.
|
|
126
|
+
|
|
127
|
+
## What problems does this solve?
|
|
128
|
+
|
|
129
|
+
- **Your corrections evaporate.** Every "don't do it that way" you've typed into Claude Code is sitting unused in transcript files. cc-pushback turns that history into a structured dataset.
|
|
130
|
+
- **CLAUDE.md only captures what you remember to write down.** Most of your taste is tacit — you only notice a rule when it's violated. Collecting real pushbacks recovers the rules you never articulated.
|
|
131
|
+
- **The signal is buried in noise.** Trivial acknowledgements, structural reminders, and tool chatter drown out the moments that matter; cc-pushback keeps the pushback and discards the rest.
|
|
132
|
+
|
|
133
|
+
## Docs
|
|
134
|
+
|
|
135
|
+
[Read the docs](https://yasyf.github.io/cc-pushback/) for the full guide and API reference.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# cc-pushback
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/cc-pushback/)
|
|
4
|
+
[](https://pypi.org/project/cc-pushback/)
|
|
5
|
+
[](https://yasyf.github.io/cc-pushback/)
|
|
6
|
+
[](https://github.com/yasyf/cc-pushback/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
cc-pushback mines your Claude Code transcripts for the moments you pushed back — corrections, interrupts, rejected plans, code-review comments, "no, do it this way" — and collects them, with the surrounding conversational context, into a local database. That corpus is the raw material for learning your pushback style; this first release builds it.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uvx cc-pushback --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`uvx` fetches cc-pushback into a throwaway environment and runs it. To add it
|
|
19
|
+
to a project instead:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv add cc-pushback
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
Scan your transcripts for the moments you pushed back and accumulate them into a
|
|
28
|
+
local feedback database:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uvx cc-pushback scan
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
scanned 412 files, 1473 new rows
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`scan` is incremental and idempotent. Each transcript is parsed only when it is
|
|
39
|
+
new or has changed since the last scan, and every candidate is keyed by a content
|
|
40
|
+
digest, so re-running over unchanged inputs adds nothing. Recording a file and
|
|
41
|
+
inserting its candidates commit in one transaction — interrupt a scan and the
|
|
42
|
+
database is never left half-written. A transcript that fails to parse (one Claude
|
|
43
|
+
Code is still writing, say) is skipped and retried next time, never aborting the run.
|
|
44
|
+
|
|
45
|
+
The database lives at `~/.cc-pushback/feedback.db` by default (override with
|
|
46
|
+
`--db`). Inspect what has been collected:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uvx cc-pushback stats # counts by source kind, and the scanned-file count
|
|
50
|
+
uvx cc-pushback list # recent feedback, newest first
|
|
51
|
+
uvx cc-pushback list --source plan_review --limit 50
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### What gets collected
|
|
55
|
+
|
|
56
|
+
`scan` runs four detectors over each transcript:
|
|
57
|
+
|
|
58
|
+
- **Transcript messages** (`transcript_message`) — the pushback you typed
|
|
59
|
+
mid-session, after trivial acknowledgements and structural noise are filtered out.
|
|
60
|
+
- **Plan reviews** (`plan_review`) — rejected `ExitPlanMode` plans (with the
|
|
61
|
+
feedback you gave) and plan-mode re-entries right after an edit cycle, i.e.
|
|
62
|
+
"let's rethink this."
|
|
63
|
+
- **Interrupts and rejections** (`interrupt_rejection`) — permission denials and
|
|
64
|
+
`[Request interrupted by user]` corrections, with the denied tool and your
|
|
65
|
+
follow-up captured.
|
|
66
|
+
- **Review comments** (`review_comment`) — structured code-review messages
|
|
67
|
+
exploded into one row per inline comment.
|
|
68
|
+
|
|
69
|
+
Each row carries the conversational window around the feedback — the assistant
|
|
70
|
+
action it responded to, plus a few turns either side — captured at collection
|
|
71
|
+
time, because transcripts are ephemeral.
|
|
72
|
+
|
|
73
|
+
Restrict to specific kinds with `--source` (repeatable), or force a full re-mine
|
|
74
|
+
of every transcript (after a detector change, say) with `--full`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uvx cc-pushback list --source transcript_message --source plan_review
|
|
78
|
+
uvx cc-pushback scan --full
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Mining transcripts from another machine
|
|
82
|
+
|
|
83
|
+
Transcripts live under `~/.claude/projects`. To mine a remote machine's history,
|
|
84
|
+
mirror its projects directory locally with `rsync`, then scan that directory:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
rsync -az yasyf@yasyf:.claude/projects/ ~/.cc-pushback/mirrors/yasyf/
|
|
88
|
+
uvx cc-pushback scan --transcripts ~/.cc-pushback/mirrors/yasyf/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`--transcripts` is repeatable, so you can fold several mirrors into one scan.
|
|
92
|
+
Because discovery is mtime-keyed, repeating the `rsync` and re-scanning only
|
|
93
|
+
ingests what changed.
|
|
94
|
+
|
|
95
|
+
## What problems does this solve?
|
|
96
|
+
|
|
97
|
+
- **Your corrections evaporate.** Every "don't do it that way" you've typed into Claude Code is sitting unused in transcript files. cc-pushback turns that history into a structured dataset.
|
|
98
|
+
- **CLAUDE.md only captures what you remember to write down.** Most of your taste is tacit — you only notice a rule when it's violated. Collecting real pushbacks recovers the rules you never articulated.
|
|
99
|
+
- **The signal is buried in noise.** Trivial acknowledgements, structural reminders, and tool chatter drown out the moments that matter; cc-pushback keeps the pushback and discards the rest.
|
|
100
|
+
|
|
101
|
+
## Docs
|
|
102
|
+
|
|
103
|
+
[Read the docs](https://yasyf.github.io/cc-pushback/) for the full guide and API reference.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Collect developer pushback signals from existing Claude Code transcripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cc_pushback.context import ContextSnapshot, ContextTurn, build_snapshot
|
|
6
|
+
from cc_pushback.detectors import Detector, detect
|
|
7
|
+
from cc_pushback.models import DedupKey, FeedbackCandidate, SourceKind, dedup_key
|
|
8
|
+
from cc_pushback.scan import ScanReport, scan
|
|
9
|
+
from cc_pushback.spec import PUSHBACK_SPEC
|
|
10
|
+
from cc_pushback.store import FeedbackStore
|
|
11
|
+
|
|
12
|
+
# Not the retired export-control convention: this exists only so great-docs' API
|
|
13
|
+
# reference skips the SourceKind (Literal) and DedupKey (NewType) aliases, which its
|
|
14
|
+
# dynamic walker cannot render ("Cannot handle auto for object kind: TYPE_ALIAS").
|
|
15
|
+
# great-docs documents __all__ when present; keep it in sync with the re-exports above.
|
|
16
|
+
__all__ = [
|
|
17
|
+
"PUSHBACK_SPEC",
|
|
18
|
+
"ContextSnapshot",
|
|
19
|
+
"ContextTurn",
|
|
20
|
+
"Detector",
|
|
21
|
+
"FeedbackCandidate",
|
|
22
|
+
"FeedbackStore",
|
|
23
|
+
"ScanReport",
|
|
24
|
+
"build_snapshot",
|
|
25
|
+
"dedup_key",
|
|
26
|
+
"detect",
|
|
27
|
+
"scan",
|
|
28
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""A thin shell-out to the ``claude`` CLI for a single headless completion.
|
|
2
|
+
|
|
3
|
+
Argv construction and envelope parsing come from the shared ``spawnllm`` library;
|
|
4
|
+
the spawn stays local (``anyio.run_process``). It uses the user's existing Claude
|
|
5
|
+
Code auth (no API key), so the package stays offline unless ``claude`` is
|
|
6
|
+
actually on the path.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
import anyio
|
|
15
|
+
from spawnllm import ClaudeCliBackend, parse_result_envelope
|
|
16
|
+
|
|
17
|
+
CLAUDE_TIMEOUT = 180
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def claude_available() -> bool:
|
|
21
|
+
"""Returns whether the ``claude`` CLI is on ``PATH``."""
|
|
22
|
+
return shutil.which("claude") is not None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def run_claude(prompt: str, *, system: str, model: str) -> str:
|
|
26
|
+
"""Runs one headless ``claude`` turn and returns its text result.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
prompt: The user message to send.
|
|
30
|
+
system: The system prompt.
|
|
31
|
+
model: The model to run, for example ``claude-sonnet-4-6``.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The assistant's text response — the ``result`` field of the JSON output.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
subprocess.SubprocessError: If ``claude`` exits non-zero, times out, or
|
|
38
|
+
reports an error in its JSON envelope.
|
|
39
|
+
"""
|
|
40
|
+
argv = ClaudeCliBackend.cc_sentiment(system_prompt=system).build_argv(prompt, model=model)
|
|
41
|
+
try:
|
|
42
|
+
with anyio.fail_after(CLAUDE_TIMEOUT):
|
|
43
|
+
result = await anyio.run_process(argv, check=True)
|
|
44
|
+
except TimeoutError as exc:
|
|
45
|
+
raise subprocess.TimeoutExpired(argv, CLAUDE_TIMEOUT) from exc
|
|
46
|
+
return parse_result_envelope(result.stdout, argv=argv, stderr=result.stderr)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""The ``cc-pushback`` command-line interface: scan, stats, list, and view-samples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import anyio
|
|
10
|
+
import click
|
|
11
|
+
from cc_transcript import CLAUDE_PROJECTS_DIR
|
|
12
|
+
|
|
13
|
+
from cc_pushback.models import PUSHBACK_SOURCE_KINDS, SourceKind
|
|
14
|
+
from cc_pushback.report import Sample, build_summary, render_html
|
|
15
|
+
from cc_pushback.scan import scan as run_scan
|
|
16
|
+
from cc_pushback.serve import serve
|
|
17
|
+
from cc_pushback.store import FeedbackStore
|
|
18
|
+
|
|
19
|
+
SOURCE_KINDS = [*PUSHBACK_SOURCE_KINDS]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def coro[**P, R](fn: Callable[P, Awaitable[R]]) -> Callable[P, R]:
|
|
23
|
+
"""Adapts an async command body into the sync callback Click expects."""
|
|
24
|
+
|
|
25
|
+
@functools.wraps(fn)
|
|
26
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
27
|
+
return anyio.run(functools.partial(fn, *args, **kwargs))
|
|
28
|
+
|
|
29
|
+
return wrapper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group()
|
|
33
|
+
@click.version_option(package_name="cc-pushback")
|
|
34
|
+
def main() -> None:
|
|
35
|
+
"""Collect developer pushback signals from existing Claude Code transcripts."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@main.command()
|
|
39
|
+
@click.option(
|
|
40
|
+
"--transcripts",
|
|
41
|
+
"transcripts",
|
|
42
|
+
multiple=True,
|
|
43
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
44
|
+
help="Transcript directories to scan. Defaults to ~/.claude/projects.",
|
|
45
|
+
)
|
|
46
|
+
@click.option("--full", is_flag=True, help="Re-scan every transcript, ignoring recorded mtimes.")
|
|
47
|
+
@click.option(
|
|
48
|
+
"--db",
|
|
49
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
50
|
+
default=None,
|
|
51
|
+
help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
|
|
52
|
+
)
|
|
53
|
+
@coro
|
|
54
|
+
async def scan(transcripts: tuple[Path, ...], full: bool, db: Path | None) -> None:
|
|
55
|
+
"""Scan transcripts for feedback, incrementally.
|
|
56
|
+
|
|
57
|
+
Each transcript is parsed only when new or modified since the last scan, and
|
|
58
|
+
every candidate is inserted with ``INSERT OR IGNORE`` keyed by a content
|
|
59
|
+
digest, so re-running ``scan`` over unchanged inputs is a no-op. Recording a
|
|
60
|
+
file and inserting its candidates commit in one transaction.
|
|
61
|
+
"""
|
|
62
|
+
roots = transcripts or (CLAUDE_PROJECTS_DIR,)
|
|
63
|
+
async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
|
|
64
|
+
report = await run_scan(store, roots, full=full)
|
|
65
|
+
click.echo(f"scanned {report.scanned} files, {report.inserted} new rows")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@main.command()
|
|
69
|
+
@click.option(
|
|
70
|
+
"--db",
|
|
71
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
72
|
+
default=None,
|
|
73
|
+
help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
|
|
74
|
+
)
|
|
75
|
+
@coro
|
|
76
|
+
async def stats(db: Path | None) -> None:
|
|
77
|
+
"""Print ingestion counts by source kind and the scanned-file count."""
|
|
78
|
+
async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
|
|
79
|
+
report = await store.stats()
|
|
80
|
+
click.echo(f"total: {report.total} files: {report.files}")
|
|
81
|
+
for kind, count in report.by_source.items():
|
|
82
|
+
click.echo(f" {kind}: {count}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@main.command(name="list")
|
|
86
|
+
@click.option(
|
|
87
|
+
"--source",
|
|
88
|
+
"source",
|
|
89
|
+
type=click.Choice(SOURCE_KINDS),
|
|
90
|
+
default=None,
|
|
91
|
+
help="Restrict to one source kind.",
|
|
92
|
+
)
|
|
93
|
+
@click.option("--limit", type=int, default=20, show_default=True, help="Maximum events to show.")
|
|
94
|
+
@click.option(
|
|
95
|
+
"--db",
|
|
96
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
97
|
+
default=None,
|
|
98
|
+
help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
|
|
99
|
+
)
|
|
100
|
+
@coro
|
|
101
|
+
async def list_(source: SourceKind | None, limit: int, db: Path | None) -> None:
|
|
102
|
+
"""List recent feedback events, newest first."""
|
|
103
|
+
async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
|
|
104
|
+
rows = await store.recent(source_kind=source, limit=limit)
|
|
105
|
+
for row in rows:
|
|
106
|
+
click.echo(f"[{row['source_kind']}] {row['occurred_at']} {str(row['text'])[:200]}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@main.command(name="view-samples")
|
|
110
|
+
@click.option(
|
|
111
|
+
"--db",
|
|
112
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
113
|
+
default=None,
|
|
114
|
+
help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
|
|
115
|
+
)
|
|
116
|
+
@click.option(
|
|
117
|
+
"--llm/--no-llm",
|
|
118
|
+
default=True,
|
|
119
|
+
show_default=True,
|
|
120
|
+
help="Summarize with the claude CLI when it is on PATH, else use heuristics.",
|
|
121
|
+
)
|
|
122
|
+
@click.option("--model", default="claude-sonnet-4-6", show_default=True, help="Model for the claude CLI summary.")
|
|
123
|
+
@click.option("--port", type=int, default=0, show_default=True, help="Port to serve on; 0 picks a free one.")
|
|
124
|
+
@click.option("--open", "open_", is_flag=True, help="Open the page in a browser once serving.")
|
|
125
|
+
@coro
|
|
126
|
+
async def view_samples(db: Path | None, llm: bool, model: str, port: int, open_: bool) -> None:
|
|
127
|
+
"""Render every collected sample into one HTML page and serve it locally.
|
|
128
|
+
|
|
129
|
+
The page leads with a corpus summary and highlights, then lists every sample
|
|
130
|
+
with a kind filter, a free-text search, and an expandable context window. It is
|
|
131
|
+
built in memory and served over a transient HTTP server whose URL is printed;
|
|
132
|
+
press Ctrl-C to stop. The summary is written by the ``claude`` CLI when ``--llm``
|
|
133
|
+
is set and ``claude`` is installed, falling back to deterministic heuristics.
|
|
134
|
+
"""
|
|
135
|
+
async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
|
|
136
|
+
samples = [Sample.from_row(row) for row in await store.events()]
|
|
137
|
+
summary = await build_summary(samples, use_llm=llm, model=model)
|
|
138
|
+
await serve(render_html(samples, summary).encode("utf-8"), port=port, open_browser=open_)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Re-exports the conversational-window primitive from the mining domain.
|
|
2
|
+
|
|
3
|
+
Deprecated: import these names from :mod:`cc_transcript.domains.mining`. This shim
|
|
4
|
+
keeps cc-pushback's historical import paths working for at least one release.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from cc_transcript.domains.mining import ContextSnapshot, ContextTurn, build_snapshot, trigger_for, turn_for
|
|
10
|
+
|
|
11
|
+
__all__ = ["ContextSnapshot", "ContextTurn", "build_snapshot", "trigger_for", "turn_for"]
|