Skip to content

Commit 045950e

Browse files
committed
fix(review): address PR feedback
1 parent 72090c4 commit 045950e

10 files changed

Lines changed: 378 additions & 133 deletions

File tree

src/pythinker_code/subagents/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def _prepend_output_language_instruction(prompt: str) -> str:
124124

125125

126126
def _compose_review_prompt(caller_prompt: str, target: ResolvedReviewTarget) -> str:
127+
"""Keep caller instructions subordinate to the authoritative resolved target."""
127128
return f"<review-task>\n{caller_prompt}\n</review-task>\n\n{target.prompt}"
128129

129130

src/pythinker_code/subagents/git_context.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ class GitCommandResult:
3737

3838
class GitCommandError(RuntimeError):
3939
def __init__(self, category: Literal["spawn", "timeout"], command: str) -> None:
40+
"""Create a safe Git failure that omits arguments and raw process output."""
4041
self.category = category
4142
self.command = command
4243
super().__init__(f"git {command} {category} failure")
4344

4445

4546
async def _read_bounded(stream: AsyncReadable, limit: int) -> tuple[bytes, bool]:
47+
"""Drain a stream to EOF while retaining at most ``limit`` bytes."""
4648
kept = bytearray()
4749
truncated = False
4850
while chunk := await stream.read(65536):
@@ -55,6 +57,7 @@ async def _read_bounded(stream: AsyncReadable, limit: int) -> tuple[bytes, bool]
5557
async def _collect_process(
5658
proc: HostProcess, limit: int
5759
) -> tuple[int, tuple[bytes, bool], tuple[bytes, bool]]:
60+
"""Wait for a process while draining both bounded output streams concurrently."""
5861
async with asyncio.TaskGroup() as tasks:
5962
wait_task = tasks.create_task(proc.wait())
6063
stdout_task = tasks.create_task(_read_bounded(proc.stdout, limit))
@@ -63,9 +66,10 @@ async def _collect_process(
6366

6467

6568
async def _await_cleanup_step(awaitable: Awaitable[object]) -> bool:
69+
"""Run one bounded cleanup step without replacing the primary failure."""
6670
try:
6771
await asyncio.wait_for(awaitable, timeout=_CLEANUP_STEP_TIMEOUT)
68-
except BaseException:
72+
except (Exception, asyncio.CancelledError):
6973
return False
7074
return True
7175

@@ -75,7 +79,7 @@ async def _cleanup_process(
7579
completion: asyncio.Task[tuple[int, tuple[bytes, bool], tuple[bytes, bool]]] | None,
7680
) -> None:
7781
"""Terminate, drain, and reap without replacing the primary failure."""
78-
with suppress(BaseException):
82+
with suppress(Exception, asyncio.CancelledError):
7983
if proc.returncode is None:
8084
await _await_cleanup_step(proc.kill())
8185

@@ -85,15 +89,15 @@ async def _cleanup_process(
8589
if completion_succeeded:
8690
return
8791

88-
with suppress(BaseException):
92+
with suppress(Exception, asyncio.CancelledError):
8993
await _await_cleanup_step(
9094
asyncio.gather(
9195
_read_bounded(proc.stdout, 0),
9296
_read_bounded(proc.stderr, 0),
9397
return_exceptions=True,
9498
)
9599
)
96-
with suppress(BaseException):
100+
with suppress(Exception, asyncio.CancelledError):
97101
await _await_cleanup_step(proc.wait())
98102

99103

@@ -104,6 +108,7 @@ async def run_git(
104108
timeout: float = _TIMEOUT,
105109
max_output_bytes: int = _MAX_GIT_OUTPUT_BYTES,
106110
) -> GitCommandResult:
111+
"""Run Git with bounded output and typed spawn or timeout failures."""
107112
if max_output_bytes < 1:
108113
raise ValueError("max_output_bytes must be positive")
109114
proc: HostProcess | None = None
@@ -171,9 +176,9 @@ async def collect_git_context(work_dir: HostPath, *, include_merge_base: bool =
171176
safe_url = _sanitize_remote_url(remote_url)
172177
if safe_url:
173178
sections.append(f"Remote: {escape_prompt_data(safe_url, max_chars=1024)}")
174-
project = _parse_project_name(remote_url)
175-
if project:
176-
sections.append(f"Project: {escape_prompt_data(project, max_chars=512)}")
179+
project = _parse_project_name(safe_url)
180+
if project:
181+
sections.append(f"Project: {escape_prompt_data(project, max_chars=512)}")
177182
if branch:
178183
sections.append(f"Branch: {escape_prompt_data(branch, max_chars=512)}")
179184
if include_merge_base:

src/pythinker_code/subagents/review_target.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class WorktreeChanges(BaseModel):
5656

5757
@property
5858
def any(self) -> bool:
59+
"""Return whether the index or worktree contains any reviewable change."""
5960
return self.staged or self.unstaged or self.untracked
6061

6162

@@ -95,12 +96,14 @@ class ReviewTargetErrorCode(StrEnum):
9596

9697
class ReviewTargetResolutionError(RuntimeError):
9798
def __init__(self, code: ReviewTargetErrorCode, brief: str, message: str) -> None:
99+
"""Create a categorized resolution failure with safe user-facing text."""
98100
self.code = code
99101
self.brief = brief
100102
super().__init__(message)
101103

102104

103105
def validate_review_target(target: ReviewTarget) -> None:
106+
"""Reject unsupported modes, fields, and unsafe or malformed refs."""
104107
if target.model_extra:
105108
raise ReviewTargetResolutionError(
106109
ReviewTargetErrorCode.invalid_target,
@@ -142,6 +145,7 @@ def validate_review_target(target: ReviewTarget) -> None:
142145

143146

144147
async def _run_resolver_git(args: list[str], cwd: str) -> GitCommandResult:
148+
"""Run Git and map host failures to review-target error categories."""
145149
try:
146150
return await run_git(args, cwd)
147151
except GitCommandError as exc:
@@ -159,6 +163,7 @@ async def _run_resolver_git(args: list[str], cwd: str) -> GitCommandResult:
159163

160164

161165
def _require_oid(result: GitCommandResult, *, message: str) -> str:
166+
"""Return a normalized full object ID or fail closed on malformed output."""
162167
value = result.stdout
163168
if value.endswith("\n"):
164169
value = value[:-1]
@@ -181,6 +186,7 @@ def _require_oid(result: GitCommandResult, *, message: str) -> str:
181186

182187

183188
def _quiet_verification_is_missing(result: GitCommandResult) -> bool:
189+
"""Recognize Git's exact quiet-verification response for a missing ref."""
184190
return (
185191
result.returncode == 1
186192
and not result.stdout
@@ -191,6 +197,7 @@ def _quiet_verification_is_missing(result: GitCommandResult) -> bool:
191197

192198

193199
def _raise_commit_verification_failed() -> None:
200+
"""Raise the sanitized failure shared by fatal commit verification paths."""
194201
raise ReviewTargetResolutionError(
195202
ReviewTargetErrorCode.git_failed,
196203
"Review target unavailable",
@@ -199,6 +206,7 @@ def _raise_commit_verification_failed() -> None:
199206

200207

201208
async def _try_resolve_commit(cwd: str, ref: str) -> str | None:
209+
"""Resolve a commit ref, returning ``None`` only for an exact quiet miss."""
202210
result = await _run_resolver_git(
203211
["rev-parse", "--verify", "--quiet", "--end-of-options", f"{ref}^{{commit}}"],
204212
cwd,
@@ -211,6 +219,7 @@ async def _try_resolve_commit(cwd: str, ref: str) -> str | None:
211219

212220

213221
async def _resolve_commit(cwd: str, ref: str) -> str:
222+
"""Resolve a required commit ref or raise a typed safe failure."""
214223
result = await _run_resolver_git(
215224
["rev-parse", "--verify", "--quiet", "--end-of-options", f"{ref}^{{commit}}"], cwd
216225
)
@@ -226,6 +235,7 @@ async def _resolve_commit(cwd: str, ref: str) -> str:
226235

227236

228237
async def _resolve_head(cwd: str) -> str:
238+
"""Validate the worktree and return its full HEAD commit ID."""
229239
repository = await _run_resolver_git(
230240
["rev-parse", "--is-inside-work-tree"],
231241
cwd,
@@ -253,6 +263,7 @@ async def _resolve_head(cwd: str) -> str:
253263

254264

255265
async def _quiet_diff_changed(cwd: str, args: list[str]) -> bool:
266+
"""Interpret Git's quiet-diff exit contract without accepting other failures."""
256267
result = await _run_resolver_git(args, cwd)
257268
if result.returncode in {0, 1}:
258269
return result.returncode == 1
@@ -264,6 +275,7 @@ async def _quiet_diff_changed(cwd: str, args: list[str]) -> bool:
264275

265276

266277
async def _worktree_changes(cwd: str) -> WorktreeChanges:
278+
"""Collect staged, unstaged, and untracked change presence."""
267279
staged = await _quiet_diff_changed(
268280
cwd,
269281
["diff", "--quiet", "--cached", "--no-ext-diff", "--no-textconv", "--exit-code", "--"],
@@ -289,6 +301,7 @@ async def _worktree_changes(cwd: str) -> WorktreeChanges:
289301

290302

291303
async def _merge_base(cwd: str, head_sha: str, base_sha: str) -> str | None:
304+
"""Resolve the merge base, distinguishing unrelated histories from Git failures."""
292305
result = await _run_resolver_git(["merge-base", head_sha, base_sha], cwd)
293306
if result.returncode == 1:
294307
return None
@@ -302,6 +315,7 @@ async def _merge_base(cwd: str, head_sha: str, base_sha: str) -> str | None:
302315

303316

304317
async def _base_has_tracked_changes(cwd: str, merge_base_sha: str) -> bool:
318+
"""Return whether tracked content differs from the selected merge base."""
305319
return await _quiet_diff_changed(
306320
cwd,
307321
[
@@ -317,6 +331,7 @@ async def _base_has_tracked_changes(cwd: str, merge_base_sha: str) -> bool:
317331

318332

319333
async def _commit_details(cwd: str, target_sha: str) -> tuple[tuple[str, ...], str]:
334+
"""Return validated parent IDs and an escaped title for a resolved commit."""
320335
parents_result = await _run_resolver_git(
321336
["rev-list", "--parents", "-n", "1", target_sha, "--"],
322337
cwd,
@@ -361,6 +376,7 @@ async def _commit_details(cwd: str, target_sha: str) -> tuple[tuple[str, ...], s
361376

362377

363378
def _review_target_block(lines: list[str]) -> str:
379+
"""Wrap resolved target facts in the authoritative prompt boundary."""
364380
body = "\n".join(lines)
365381
return (
366382
"<review-target>\n"
@@ -376,13 +392,15 @@ def _review_target_block(lines: list[str]) -> str:
376392

377393

378394
def _requested_lines(target: ReviewTarget) -> list[str]:
395+
"""Render the caller's validated requested mode and optional ref."""
379396
lines = [f"requested_mode: {target.kind}"]
380397
if target.ref is not None:
381398
lines.append(f"requested_ref: {escape_prompt_data(target.ref, max_chars=1024)}")
382399
return lines
383400

384401

385402
def _attempted_line(attempted: tuple[str, ...]) -> str | None:
403+
"""Render attempted default bases as escaped untrusted metadata."""
386404
if not attempted:
387405
return None
388406
rendered = ", ".join(escape_prompt_data(ref, max_chars=1024) for ref in attempted)
@@ -401,6 +419,7 @@ def _resolved_live_target(
401419
merge_base_sha: str | None = None,
402420
auto_note: str | None = None,
403421
) -> ResolvedReviewTarget:
422+
"""Build a live worktree or base target anchored to the current HEAD."""
404423
anchor = head_sha if kind == "uncommitted" else merge_base_sha
405424
assert anchor is not None
406425
lines = [
@@ -431,11 +450,11 @@ def _resolved_live_target(
431450
lines.extend(
432451
[
433452
"scope: inspect tracked changes from the anchor through the live index/worktree, "
434-
"then inspect every relevant untracked path reported by status.",
453+
+ "then inspect every relevant untracked path reported by status.",
435454
f"command: git diff --no-ext-diff --no-textconv {anchor} --",
436455
"command: git status --short --untracked-files=all --",
437456
"Live warning: concurrent index/worktree edits can change the inspected patch; this "
438-
"target does not claim a frozen snapshot.",
457+
+ "target does not claim a frozen snapshot.",
439458
]
440459
)
441460
safe_base = escape_prompt_data(base_ref, max_chars=1024) if base_ref is not None else None
@@ -471,6 +490,7 @@ async def _resolved_commit_target(
471490
attempted: tuple[str, ...] = (),
472491
auto_note: str | None = None,
473492
) -> ResolvedReviewTarget:
493+
"""Build an immutable commit target with validated parent metadata."""
474494
parent_shas, title = await _commit_details(cwd, target_sha)
475495
lines = [
476496
*_requested_lines(requested),
@@ -538,6 +558,7 @@ async def resolve_review_target(
538558
target: ReviewTarget,
539559
work_dir: HostPath,
540560
) -> ResolvedReviewTarget:
561+
"""Resolve one validated request to a deterministic, executable Git scope."""
541562
validate_review_target(target)
542563
cwd = str(work_dir)
543564
head_sha = await _resolve_head(cwd)
@@ -682,6 +703,7 @@ async def revalidate_review_target_head(
682703
target: ResolvedReviewTarget,
683704
work_dir: HostPath,
684705
) -> None:
706+
"""Reject a live target when HEAD moved after initial resolution."""
685707
if target.worktree_state != "live":
686708
return
687709
current_head = await _resolve_head(str(work_dir))

src/pythinker_code/tools/agent/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ def check_execution_policy(self, subagent_type: str) -> ToolError | None:
435435
async def _prepare_review_target(
436436
self, params: Params, requested_type: str
437437
) -> ResolvedReviewTarget | ToolError | None:
438+
"""Resolve fresh reviewer scope before allocating an agent instance."""
438439
if params.resume is not None:
439440
if params.review_target is not None:
440441
return ToolError(
@@ -604,6 +605,7 @@ async def _run_in_background(
604605
*,
605606
resolved_review_target: ResolvedReviewTarget | None,
606607
) -> ToolReturnValue:
608+
"""Launch a background agent while preserving its resolved review target."""
607609
assert self._runtime.subagent_store is not None
608610
try:
609611
tool_call = get_current_tool_call_or_none()

tasks/lessons.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ Format: trigger → rule.
5353

5454
## Review orchestration
5555

56+
- **When asked to apply all PR review feedback**, wait for the review bot's status on the current
57+
head to become terminal, fetch unresolved thread-level state, and verify each recommendation
58+
against runtime contracts before editing; after the push, re-check the new head rather than
59+
treating the prior bot success as transferable.
60+
5661
- **When running review/security subagents**, use the project-scoped agents in
5762
`.claude/agents/` (global `~/.claude/agents/security-reviewer.md` and
5863
`planner.md` describe the *other* Pythinker project — FastAPI/Vue/Mongo —

tasks/todo.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,38 @@
22

33
## Active
44

5+
### PR #208 review remediation (2026-07-15)
6+
7+
- [x] Fetch current CI, CodeRabbit, Code Quality, Codecov, and unresolved thread state.
8+
- [x] Classify all 11 inline findings and advisory pre-merge notices against repository standards.
9+
- [x] Add failing regressions for approved behavioral findings before production edits.
10+
- [x] Fix remote metadata authorization, cleanup exception handling, and review-target rendering.
11+
- [x] Rewrite over-mocked tests through public APIs and clear static test-quality findings.
12+
- [x] Run focused coverage, `make check-pythinker-code`, and `make test-pythinker-code`.
13+
- [x] Perform final diff review and prepare the verified commit without tool trailers.
14+
15+
Acceptance: unapproved remotes cannot leak project metadata; cleanup preserves cancellation semantics
16+
without swallowing process-control exceptions; tests assert supported public behavior; every inline
17+
finding has a verified disposition; and the pushed head has fresh local gate evidence.
18+
19+
#### Review: PR #208 review remediation
20+
21+
- **Outcome:** all 11 fetched inline findings were addressed: unapproved remote metadata no longer
22+
exposes a project identity; cleanup preserves cancellation while allowing process-control
23+
exceptions to propagate; review tests use supported public boundaries; and static style findings
24+
are cleared.
25+
- **TDD evidence:** the remote-metadata and process-control regressions initially failed together
26+
(`2 failed, 52 passed`) and passed after the production fixes (`54 passed`).
27+
- **Focused verification:** the full changed-feature set passed `304 passed, 1 warning`; focused
28+
coverage reported zero missing statements in `git_context.py` and `review_target.py`.
29+
- **Static verification:** `make check-pythinker-code` passed with Ruff clean, `1262 files already
30+
formatted`, Pyright `0 errors, 0 warnings, 0 informations`, and ty clean.
31+
- **Repository test gate:** `make test-pythinker-code` passed with `7147 passed, 9 skipped, 1
32+
xfailed, 5 warnings` plus `65 passed, 4 skipped, 1 warning` in `tests_e2e`.
33+
- **Review verdict:** independent final review found no Critical, Important, or Minor findings;
34+
`git diff --check` was silent. Ready to push to the existing PR branch.
35+
- **Blockers:** none.
36+
537
### Deterministic reviewer target resolution (2026-07-15)
638

739
- [x] Reconcile the adoption ledger with current code and Git history.

0 commit comments

Comments
 (0)