Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- **Reviewer subagents now receive deterministic Git scopes.** Structured automatic,
uncommitted, base, and commit targets resolve to full commit anchors before dispatch, reject
invalid or empty scopes explicitly, and keep repository metadata isolated from instructions
across foreground and background runs.
- **Parallel streamed tool calls are now correlated safely.** Interleaved argument chunks stay attached to their indexed calls, malformed or truncated call streams stop before tool execution, and failed attempts are not retried after output has already been shown.
- **Provider compatibility and Z.AI routing are now explicit.** Immutable compatibility profiles keep request-format quirks behind the chat-provider boundary, while independent Z.AI Coding Plan and API login routes use separate credentials, endpoints, model identities, catalog refresh, logout, and usage/rate-limit state. Curated GLM requests now apply exact context/output limits, thinking controls, reasoning replay, and tool-stream support without activating for local or unknown models.
- **Tool execution is now supervised as a terminal batch.** A private execution engine preserves the Toolset registry and legacy per-call API while centralizing ordered results, deduplication, callbacks, and batch summaries; cancellation is bounded, late work stays owned, and new batches fail closed until timed-out cleanup drains.
Expand Down
4 changes: 4 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- **Reviewer subagents now receive deterministic Git scopes.** Structured automatic,
uncommitted, base, and commit targets resolve to full commit anchors before dispatch, reject
invalid or empty scopes explicitly, and keep repository metadata isolated from instructions
across foreground and background runs.
- **Parallel streamed tool calls are now correlated safely.** Interleaved argument chunks stay attached to their indexed calls, malformed or truncated call streams stop before tool execution, and failed attempts are not retried after output has already been shown.
- **Provider compatibility and Z.AI routing are now explicit.** Immutable compatibility profiles keep request-format quirks behind the chat-provider boundary, while independent Z.AI Coding Plan and API login routes use separate credentials, endpoints, model identities, catalog refresh, logout, and usage/rate-limit state. Curated GLM requests now apply exact context/output limits, thinking controls, reasoning replay, and tool-stream support without activating for local or unknown models.
- **Tool execution is now supervised as a terminal batch.** A private execution engine preserves the Toolset registry and legacy per-call API while centralizing ordered results, deduplication, callbacks, and batch summaries; cancellation is bounded, late work stays owned, and new batches fail closed until timed-out cleanup drains.
Expand Down
3,226 changes: 3,226 additions & 0 deletions docs/superpowers/plans/2026-07-15-deterministic-review-target-resolution.md

Large diffs are not rendered by default.

346 changes: 346 additions & 0 deletions docs/superpowers/specs/2026-07-15-review-target-resolution-design.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/pythinker_code/background/agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pythinker_code.subagents.builder import SubagentBuilder
from pythinker_code.subagents.core import SubagentRunSpec, prepare_soul
from pythinker_code.subagents.output import SubagentOutputWriter
from pythinker_code.subagents.review_target import ResolvedReviewTarget
from pythinker_code.subagents.runner import (
_SUMMARY_MIN_LENGTH_BY_TYPE,
_SUMMARY_MIN_LENGTH_DEFAULT,
Expand Down Expand Up @@ -68,6 +69,7 @@ def __init__(
timeout_s: int | None = None,
resumed: bool = False,
isolation: str | None = None,
resolved_review_target: ResolvedReviewTarget | None = None,
) -> None:
self._runtime = runtime
self._manager = manager
Expand All @@ -79,6 +81,7 @@ def __init__(
self._timeout_s = timeout_s
self._resumed = resumed
self._isolation = isolation
self._resolved_review_target = resolved_review_target
self._worktree_path: Path | None = None
self._builder = SubagentBuilder(runtime)
self._approval_update_tasks: set[asyncio.Task[None]] = set()
Expand Down Expand Up @@ -201,6 +204,7 @@ async def _run_core(self, output: SubagentOutputWriter) -> None:
prompt=self._prompt,
resumed=self._resumed,
work_dir_override=work_dir_override,
resolved_review_target=self._resolved_review_target,
)
soul, prompt = await prepare_soul(
spec,
Expand Down
8 changes: 8 additions & 0 deletions src/pythinker_code/background/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pythinker_code.config import BackgroundConfig
from pythinker_code.notifications import NotificationEvent, NotificationManager
from pythinker_code.session import Session
from pythinker_code.subagents.review_target import ResolvedReviewTarget
from pythinker_code.utils.logging import logger

if TYPE_CHECKING:
Expand Down Expand Up @@ -336,6 +337,7 @@ def create_agent_task(
dependencies: list[str] | None = None,
budget_seconds: int | None = None,
isolation: str | None = None,
resolved_review_target: ResolvedReviewTarget | None = None,
) -> TaskView:
from .agent_runner import BackgroundAgentRunner

Expand Down Expand Up @@ -378,6 +380,11 @@ def create_agent_task(
"dependencies": list(dependencies or ()),
"budget_seconds": budget_seconds,
"isolation": isolation,
"resolved_review_target": (
resolved_review_target.model_dump(mode="json")
if resolved_review_target is not None
else None
),
},
)
self._store.create_task(spec)
Expand All @@ -402,6 +409,7 @@ def mark_agent_starting(runtime: TaskRuntime) -> bool:
timeout_s=effective_timeout,
resumed=resumed,
isolation=isolation,
resolved_review_target=resolved_review_target,
).run()
)
self._live_agent_tasks[task_id] = task
Expand Down
44 changes: 36 additions & 8 deletions src/pythinker_code/subagents/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

import html
import re
from collections.abc import Callable, Sequence
from dataclasses import dataclass, replace
Expand All @@ -21,12 +22,18 @@
from pythinker_code.soul.message import is_system_reminder_message
from pythinker_code.soul.pythinkersoul import PythinkerSoul
from pythinker_code.subagents.builder import SubagentBuilder
from pythinker_code.subagents.git_context import collect_git_context
from pythinker_code.subagents.models import AgentLaunchSpec, AgentTypeDefinition
from pythinker_code.subagents.review_target import (
REVIEWER_AGENT_TYPES,
ResolvedReviewTarget,
revalidate_review_target_head,
)
from pythinker_code.subagents.store import SubagentStore

# NOTE: these must match the registered type names in agents/default/agent.yaml
# (dashed), which _SUBAGENT_PROFILES also keys on — not the yaml file stems.
GIT_CONTEXT_AGENT_TYPES = frozenset({"explore", "review", "code-reviewer", "security-reviewer"})
GIT_CONTEXT_AGENT_TYPES = frozenset({"explore"}) | REVIEWER_AGENT_TYPES
"""Read-oriented agent types whose first prompt gets a git-context prefix.

Exploration and review both orient on repo state (branch, dirty files,
Expand Down Expand Up @@ -63,6 +70,7 @@ class SubagentRunSpec:
# Operational work-dir override (e.g. an isolation worktree); flows into
# the child runtime via copy_for_subagent.
work_dir_override: HostPath | None = None
resolved_review_target: ResolvedReviewTarget | None = None


_CHECKPOINT_MARKER_RE = re.compile(r"^CHECKPOINT \d+$")
Expand Down Expand Up @@ -116,6 +124,12 @@ def _prepend_output_language_instruction(prompt: str) -> str:
return f"{SUBAGENT_OUTPUT_LANGUAGE_INSTRUCTION}\n\n{prompt}"


def _compose_review_prompt(caller_prompt: str, target: ResolvedReviewTarget) -> str:
"""Keep caller instructions subordinate to the authoritative resolved target."""
safe_caller_prompt = html.escape(caller_prompt, quote=False)
return f"<review-task>\n{safe_caller_prompt}\n</review-task>\n\n{target.prompt}"


async def prepare_soul(
spec: SubagentRunSpec,
runtime: Runtime,
Expand Down Expand Up @@ -154,16 +168,30 @@ async def prepare_soul(
if on_stage:
on_stage("context_ready")

# 4. For new (non-resumed) read-oriented agents, prepend git context to the prompt
# 4. Compose the authoritative prompt for this run.
prompt = spec.prompt
git_context_dir = spec.work_dir_override or runtime.builtin_args.PYTHINKER_WORK_DIR
is_reviewer = spec.type_def.name in REVIEWER_AGENT_TYPES
if spec.resumed and spec.resolved_review_target is not None:
raise RuntimeError("A resumed subagent cannot receive a new resolved review target.")
if not spec.resumed and is_reviewer and spec.resolved_review_target is None:
raise RuntimeError("A fresh reviewer requires a resolved review target.")
if not is_reviewer and spec.resolved_review_target is not None:
raise RuntimeError("A non-reviewer cannot receive a resolved review target.")

git_ctx = ""
if spec.type_def.name in GIT_CONTEXT_AGENT_TYPES and not spec.resumed:
from pythinker_code.subagents.git_context import collect_git_context

git_context_dir = spec.work_dir_override or runtime.builtin_args.PYTHINKER_WORK_DIR
git_ctx = await collect_git_context(git_context_dir)
if git_ctx:
prompt = f"{git_ctx}\n\n{prompt}"
git_ctx = await collect_git_context(
git_context_dir,
include_merge_base=not is_reviewer,
)
if spec.resolved_review_target is not None:
prompt = _compose_review_prompt(prompt, spec.resolved_review_target)
if git_ctx:
prompt = f"{git_ctx}\n\n{prompt}"
prompt = _prepend_output_language_instruction(prompt)
if spec.resolved_review_target is not None:
await revalidate_review_target_head(spec.resolved_review_target, git_context_dir)

# 5. Write prompt snapshot (debugging aid)
store.prompt_path(spec.agent_id).write_text(prompt, encoding="utf-8")
Expand Down
Loading
Loading