git-paoding 0.1.0__py3-none-any.whl
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.
- git_paoding/__init__.py +32 -0
- git_paoding/_agent_plugins/__init__.py +1 -0
- git_paoding/_agent_plugins/git-paoding/.claude-plugin/plugin.json +12 -0
- git_paoding/_agent_plugins/git-paoding/.codex-plugin/plugin.json +22 -0
- git_paoding/_agent_plugins/git-paoding/skills/git-paoding/SKILL.md +206 -0
- git_paoding/agent_install.py +113 -0
- git_paoding/api.py +383 -0
- git_paoding/cli/__init__.py +1 -0
- git_paoding/cli/facade.py +133 -0
- git_paoding/cli/main.py +277 -0
- git_paoding/cli/render.py +205 -0
- git_paoding/core/__init__.py +1 -0
- git_paoding/core/diffatoms.py +208 -0
- git_paoding/core/model.py +292 -0
- git_paoding/core/projection.py +376 -0
- git_paoding/core/publish.py +644 -0
- git_paoding/core/reconcile.py +220 -0
- git_paoding/core/selectors.py +279 -0
- git_paoding/github/__init__.py +1 -0
- git_paoding/github/backend.py +53 -0
- git_paoding/github/gh_cli.py +339 -0
- git_paoding/github/lifecycle.py +123 -0
- git_paoding/github/prbody.py +281 -0
- git_paoding/gitio/__init__.py +49 -0
- git_paoding/gitio/diffparse.py +273 -0
- git_paoding/gitio/plumbing.py +170 -0
- git_paoding/gitio/refs.py +145 -0
- git_paoding/gitio/runner.py +133 -0
- git_paoding/py.typed +1 -0
- git_paoding/store/__init__.py +6 -0
- git_paoding/store/jsonstore.py +142 -0
- git_paoding/store/lock.py +165 -0
- git_paoding-0.1.0.dist-info/METADATA +351 -0
- git_paoding-0.1.0.dist-info/RECORD +36 -0
- git_paoding-0.1.0.dist-info/WHEEL +4 -0
- git_paoding-0.1.0.dist-info/entry_points.txt +3 -0
git_paoding/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Public package interface for git-paoding."""
|
|
2
|
+
|
|
3
|
+
from git_paoding.api import (
|
|
4
|
+
add_slice,
|
|
5
|
+
archive,
|
|
6
|
+
assign,
|
|
7
|
+
assign_batch,
|
|
8
|
+
get_full_status,
|
|
9
|
+
get_status,
|
|
10
|
+
init_session,
|
|
11
|
+
publish,
|
|
12
|
+
remove_slice,
|
|
13
|
+
rename_slice,
|
|
14
|
+
set_focus,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"__version__",
|
|
21
|
+
"add_slice",
|
|
22
|
+
"archive",
|
|
23
|
+
"assign",
|
|
24
|
+
"assign_batch",
|
|
25
|
+
"get_full_status",
|
|
26
|
+
"get_status",
|
|
27
|
+
"init_session",
|
|
28
|
+
"publish",
|
|
29
|
+
"remove_slice",
|
|
30
|
+
"rename_slice",
|
|
31
|
+
"set_focus",
|
|
32
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Packaged agent integrations for git-paoding."""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-paoding",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Teach Claude Code to partition a canonical change into semantic Draft PR review projections.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "NagisaVon"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/NagisaVon/git-paoding",
|
|
9
|
+
"repository": "https://github.com/NagisaVon/git-paoding",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": ["git", "github", "code-review", "pull-requests"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-paoding",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Teach Codex to partition a canonical change into semantic Draft PR review projections.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "NagisaVon"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/NagisaVon/git-paoding",
|
|
9
|
+
"repository": "https://github.com/NagisaVon/git-paoding",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": ["git", "github", "code-review", "pull-requests"],
|
|
12
|
+
"skills": "./skills/",
|
|
13
|
+
"interface": {
|
|
14
|
+
"displayName": "git-paoding",
|
|
15
|
+
"shortDescription": "Create semantic review projections from one canonical change.",
|
|
16
|
+
"longDescription": "Guide Codex through git-paoding's inspect, assign, publish, refresh, and archive workflow while keeping one authoritative integration branch.",
|
|
17
|
+
"developerName": "NagisaVon",
|
|
18
|
+
"category": "Productivity",
|
|
19
|
+
"capabilities": [],
|
|
20
|
+
"defaultPrompt": "Use git-paoding to prepare semantic Draft PR review projections for the committed change on my current branch. Inspect first and do not publish until I authorize remote changes."
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-paoding
|
|
3
|
+
description:
|
|
4
|
+
Prepare and publish semantic review slices for a large committed change while
|
|
5
|
+
keeping one canonical integration branch. Use when an author wants focused
|
|
6
|
+
Draft GitHub PRs for comprehension without creating a development branch
|
|
7
|
+
stack.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<!-- cspell:words paoding buildability PAODING -->
|
|
11
|
+
|
|
12
|
+
# git-paoding
|
|
13
|
+
|
|
14
|
+
Use `git-paoding` to partition the current `Base -> Final` diff into human-sized
|
|
15
|
+
semantic review slices. Keep one canonical integrated implementation state
|
|
16
|
+
throughout.
|
|
17
|
+
|
|
18
|
+
## Preserve the mental model
|
|
19
|
+
|
|
20
|
+
- Edit, test, lint, and commit only on the canonical integration branch.
|
|
21
|
+
- Treat a slice as a review unit, never as a development phase, historical
|
|
22
|
+
commit range, buildable unit, or merge target.
|
|
23
|
+
- Never check out, edit, rebase, or restack generated `paoding/...` branches.
|
|
24
|
+
They are disposable projections derived from the canonical state and local
|
|
25
|
+
slice metadata.
|
|
26
|
+
- Let `publish` manage generated refs and stable PR identities. Do not manually
|
|
27
|
+
force-push projections or close and recreate a slice PR merely to refresh it.
|
|
28
|
+
- Never merge a slice PR. It is a Draft PR marked **DO NOT MERGE — review
|
|
29
|
+
projection only**.
|
|
30
|
+
- Keep authoritative CI, approval, branch protection, and merge on the
|
|
31
|
+
integration PR.
|
|
32
|
+
- Do not require slice-level CI or independent buildability. Cross-slice
|
|
33
|
+
dependencies are valid.
|
|
34
|
+
- New PR bodies contain no narrative template. Preserve human-written PR
|
|
35
|
+
narrative outside machine-managed regions byte-for-byte.
|
|
36
|
+
|
|
37
|
+
The operating rule is: **run `git-paoding publish` and do what it says**.
|
|
38
|
+
Publication is idempotent and self-checking; there is no separate refresh
|
|
39
|
+
ritual.
|
|
40
|
+
|
|
41
|
+
## Plan for review without bookkeeping every edit
|
|
42
|
+
|
|
43
|
+
When review concerns are already apparent, use them as provisional,
|
|
44
|
+
low-frequency structure guidance:
|
|
45
|
+
|
|
46
|
+
- Prefer one primary review concern per new module or test file when that is a
|
|
47
|
+
natural boundary.
|
|
48
|
+
- Separate meaningfully different test concerns when ordinary readable
|
|
49
|
+
organization permits.
|
|
50
|
+
- Avoid one large contiguous insertion that mixes unrelated concerns when a
|
|
51
|
+
natural separation already exists.
|
|
52
|
+
|
|
53
|
+
This is optional guidance, not a correctness precondition. Do not create
|
|
54
|
+
artificial files, blank lines, awkward abstractions, or inferior architecture
|
|
55
|
+
merely to manufacture atom boundaries. Do not require active slices or
|
|
56
|
+
attribution calls during the normal edit-test-lint-fix loop. Provisional
|
|
57
|
+
concerns may change.
|
|
58
|
+
|
|
59
|
+
If nobody planned slice boundaries early, continue normally. Attribution is lazy
|
|
60
|
+
and recoverable: `status` will report unassigned or ambiguous atoms at the
|
|
61
|
+
review checkpoint, and those atoms can be classified then. Forgotten bookkeeping
|
|
62
|
+
never makes publication irrecoverable.
|
|
63
|
+
|
|
64
|
+
## Prepare the session
|
|
65
|
+
|
|
66
|
+
Work from the branch containing the complete committed change. Confirm the CLI
|
|
67
|
+
and `gh` 2.45.0 or newer are available, `gh` is authenticated, and the canonical
|
|
68
|
+
branch is available on the Git remote. Do not push a branch without the change
|
|
69
|
+
owner's authorization.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git-paoding --version
|
|
73
|
+
gh --version
|
|
74
|
+
gh auth status
|
|
75
|
+
git-paoding init --base origin/main --slice-prefix ABC-123
|
|
76
|
+
git-paoding slice add storage --title "Storage boundary"
|
|
77
|
+
git-paoding slice add tests --title "Storage behavior tests"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Omit `--slice-prefix` to use `slice`. The prefix changes only slice PR titles;
|
|
81
|
+
it does not change stable slice IDs or generated refs. The integration PR title
|
|
82
|
+
is exactly the canonical branch name.
|
|
83
|
+
|
|
84
|
+
The base is pinned to a commit at initialization. Choose the real integration
|
|
85
|
+
target; do not silently change it later.
|
|
86
|
+
|
|
87
|
+
## Classify and publish
|
|
88
|
+
|
|
89
|
+
### 1. Inspect
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git-paoding status --json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Interpret exit code `2` as action needed, not an operational failure. Read
|
|
96
|
+
the versioned JSON atom list. Use atom IDs for precise selection; paths,
|
|
97
|
+
directories, globs, and Final-coordinate ranges select broader atom sets.
|
|
98
|
+
|
|
99
|
+
Line ranges use Final coordinates: `path:20-45` means lines 20 through 45 in the
|
|
100
|
+
file at the canonical branch tip. A partial range selects the whole atom; ranges
|
|
101
|
+
do not split atoms.
|
|
102
|
+
|
|
103
|
+
Previews are short. Run `git-paoding status --full` for complete changed-hunk
|
|
104
|
+
previews. If surrounding context is still needed, read the current file at
|
|
105
|
+
`final_start` through `final_start + final_len - 1` before assigning; do not
|
|
106
|
+
guess from the preview. For a deletion or other atom with no Final lines,
|
|
107
|
+
inspect the relevant Base-side change instead.
|
|
108
|
+
|
|
109
|
+
### 2. Assign
|
|
110
|
+
|
|
111
|
+
Prepare `paoding-assignments.json` using the frozen batch contract:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"contract_version": 0,
|
|
116
|
+
"assignments": {
|
|
117
|
+
"storage": ["src/storage.py"],
|
|
118
|
+
"tests": ["tests/test_storage.py"]
|
|
119
|
+
},
|
|
120
|
+
"force": false
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then run:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git-paoding assign --batch paoding-assignments.json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The batch plan is ordinary local input, not git-paoding session metadata. Keep
|
|
131
|
+
it untracked, place it outside the working tree, or manage it according to the
|
|
132
|
+
repository's own policy; do not commit it accidentally.
|
|
133
|
+
|
|
134
|
+
For an interactive assignment, pass the slice followed by one or more
|
|
135
|
+
selectors:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git-paoding assign storage src/storage.py
|
|
139
|
+
git-paoding assign tests tests/test_storage.py
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Review the assignment echo: it must list every moved or skipped atom with a
|
|
143
|
+
preview. Path and range selectors should claim only unassigned atoms by default.
|
|
144
|
+
Reassignment through a broad selector requires the explicit `--force` option;
|
|
145
|
+
an exact atom ID may take its atom directly. For batch repartitioning, set the
|
|
146
|
+
JSON `force` field instead of combining `--force` with `--batch`. If a selector
|
|
147
|
+
matches nothing or an atom ID became stale after a new commit, rerun
|
|
148
|
+
`status --json` rather than guessing.
|
|
149
|
+
|
|
150
|
+
### 3. Publish
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git-paoding publish
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Respond to the result by exit code:
|
|
157
|
+
|
|
158
|
+
- `0`: clean success; report the integration PR and slice PRs.
|
|
159
|
+
- `2`: attribution needs attention; no remote effects occurred. Return to
|
|
160
|
+
inspect and assign.
|
|
161
|
+
- `1`: operational error, such as missing session, unavailable or
|
|
162
|
+
unauthenticated `gh`, or a push/PR failure. Fix the stated cause before
|
|
163
|
+
retrying.
|
|
164
|
+
|
|
165
|
+
Do not loop indefinitely. Retry after a concrete local correction; ask the
|
|
166
|
+
author when the fix needs new credentials, a branch push, a changed base, or
|
|
167
|
+
another external mutation.
|
|
168
|
+
|
|
169
|
+
## Focus review feedback
|
|
170
|
+
|
|
171
|
+
For a task targeted at an existing slice, focus may act as a prior for genuinely
|
|
172
|
+
new atoms:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
git-paoding focus storage
|
|
176
|
+
git-paoding status --json
|
|
177
|
+
git-paoding focus --clear
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Focus must not overwrite confidently matched existing ownership. Clear it when
|
|
181
|
+
the targeted task ends so unrelated later work does not default to that slice.
|
|
182
|
+
|
|
183
|
+
## Recovery
|
|
184
|
+
|
|
185
|
+
- New, unassigned, ambiguous, or updated atoms after canonical edits are
|
|
186
|
+
expected. Rerun `status`, inspect only what needs attention, assign it, and
|
|
187
|
+
publish again.
|
|
188
|
+
- Renames and heavy rewrites may return as unassigned or ambiguous. That is safe
|
|
189
|
+
degradation, not corruption.
|
|
190
|
+
- If local session metadata is lost, do not edit generated refs or PR bodies to
|
|
191
|
+
reconstruct it. Reinitialize with the original pinned base and recreate the
|
|
192
|
+
same stable slice IDs.
|
|
193
|
+
- Expect all atom attribution to return as unassigned after reinitialization. A
|
|
194
|
+
clean publish can adopt existing open slice PRs through their machine markers.
|
|
195
|
+
- If a slice becomes empty, publication may leave its existing Draft PR open and
|
|
196
|
+
mark it empty; do not invent changes merely to keep the projection nonempty.
|
|
197
|
+
|
|
198
|
+
After GitHub reports the integration PR as merged, run:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git-paoding archive
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`archive` closes slice PRs, retains their URLs and discussion history, and
|
|
205
|
+
cleans generated refs; it never merges them. It refuses to run while the
|
|
206
|
+
integration PR is still open or merely closed.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Install the packaged git-paoding skill for supported coding agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
AgentTarget = Literal["codex", "claude"]
|
|
11
|
+
InstallScope = Literal["user", "project"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentInstallError(ValueError):
|
|
15
|
+
"""Raised when an agent skill installation is invalid or unsafe."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AgentInstallResult:
|
|
20
|
+
"""Result of installing one packaged agent skill."""
|
|
21
|
+
|
|
22
|
+
target: AgentTarget
|
|
23
|
+
scope: InstallScope
|
|
24
|
+
destination: Path
|
|
25
|
+
changed: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _packaged_skill() -> Any:
|
|
29
|
+
root = files("git_paoding._agent_plugins")
|
|
30
|
+
skill = root.joinpath("git-paoding").joinpath("skills").joinpath("git-paoding")
|
|
31
|
+
if not skill.is_dir():
|
|
32
|
+
raise AgentInstallError("the installed package does not contain the git-paoding skill")
|
|
33
|
+
return skill
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resource_snapshot(resource: Any, prefix: PurePosixPath = PurePosixPath()) -> dict[str, bytes]:
|
|
37
|
+
snapshot: dict[str, bytes] = {}
|
|
38
|
+
for child in resource.iterdir():
|
|
39
|
+
relative = prefix / child.name
|
|
40
|
+
if child.is_dir():
|
|
41
|
+
snapshot.update(_resource_snapshot(child, relative))
|
|
42
|
+
elif child.is_file():
|
|
43
|
+
snapshot[relative.as_posix()] = child.read_bytes()
|
|
44
|
+
return snapshot
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _directory_snapshot(directory: Path) -> dict[str, bytes]:
|
|
48
|
+
return {
|
|
49
|
+
path.relative_to(directory).as_posix(): path.read_bytes()
|
|
50
|
+
for path in directory.rglob("*")
|
|
51
|
+
if path.is_file()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _destination(
|
|
56
|
+
target: AgentTarget,
|
|
57
|
+
scope: InstallScope,
|
|
58
|
+
*,
|
|
59
|
+
project_root: Path,
|
|
60
|
+
home: Path,
|
|
61
|
+
) -> Path:
|
|
62
|
+
if target == "codex":
|
|
63
|
+
base = (
|
|
64
|
+
home / ".agents" / "skills" if scope == "user" else project_root / ".agents" / "skills"
|
|
65
|
+
)
|
|
66
|
+
elif target == "claude":
|
|
67
|
+
base = (
|
|
68
|
+
home / ".claude" / "skills" if scope == "user" else project_root / ".claude" / "skills"
|
|
69
|
+
)
|
|
70
|
+
else: # pragma: no cover - protected by public type and Click validation
|
|
71
|
+
raise AgentInstallError(f"unsupported agent target: {target}")
|
|
72
|
+
return base / "git-paoding"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def install_agent_skill(
|
|
76
|
+
target: AgentTarget,
|
|
77
|
+
scope: InstallScope,
|
|
78
|
+
*,
|
|
79
|
+
project_root: Path | None = None,
|
|
80
|
+
home: Path | None = None,
|
|
81
|
+
force: bool = False,
|
|
82
|
+
) -> AgentInstallResult:
|
|
83
|
+
"""Install the packaged skill into one agent's user or project skill root."""
|
|
84
|
+
|
|
85
|
+
resolved_project = (project_root or Path.cwd()).resolve()
|
|
86
|
+
resolved_home = (home or Path.home()).resolve()
|
|
87
|
+
destination = _destination(
|
|
88
|
+
target,
|
|
89
|
+
scope,
|
|
90
|
+
project_root=resolved_project,
|
|
91
|
+
home=resolved_home,
|
|
92
|
+
)
|
|
93
|
+
source_snapshot = _resource_snapshot(_packaged_skill())
|
|
94
|
+
if not source_snapshot or "SKILL.md" not in source_snapshot:
|
|
95
|
+
raise AgentInstallError("the packaged git-paoding skill is incomplete")
|
|
96
|
+
|
|
97
|
+
if destination.exists():
|
|
98
|
+
if not destination.is_dir():
|
|
99
|
+
raise AgentInstallError(f"skill destination is not a directory: {destination}")
|
|
100
|
+
if _directory_snapshot(destination) == source_snapshot:
|
|
101
|
+
return AgentInstallResult(target, scope, destination, changed=False)
|
|
102
|
+
if not force:
|
|
103
|
+
raise AgentInstallError(
|
|
104
|
+
f"skill destination already exists with different contents: {destination}; "
|
|
105
|
+
"pass --force to overwrite packaged files"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
for relative, content in source_snapshot.items():
|
|
109
|
+
output = destination / relative
|
|
110
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
output.write_bytes(content)
|
|
112
|
+
|
|
113
|
+
return AgentInstallResult(target, scope, destination, changed=True)
|