Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,17 @@ work. Read
[Phased decomposition versus split materialization](docs/local_agent_loop.md#phased-decomposition-versus-split-materialization)
before filing child issues.

### Approved follow-up dedupe

When approved future follow-ups are summarized or filed, semantic reuse is
enabled by default after deterministic narrowing. Use
`--no-semantic-followup-dedupe` for deterministic-only operation. The provider
and its bounds are configurable with `--semantic-followup-backend`,
`--semantic-followup-model`, `--semantic-followup-timeout-seconds`,
`--semantic-followup-max-calls`, `--semantic-followup-max-candidates`, and
`--semantic-followup-prompt-char-limit`. Only high-confidence matches suppress
or merge work; uncertain matches are filed with a possible-duplicate note.

## Discuss Mode

Discuss mode asks agents to evaluate an issue without modifying the repository.
Expand Down
35 changes: 35 additions & 0 deletions docs/local_agent_loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,41 @@ be fixed before merge.
- `fix-and-summarize`: send same-PR follow-ups to the coder for another review round, then summarize future follow-ups after final approval.
- `fix-and-issue`: send same-PR follow-ups to the coder for another review round, then create issues for future follow-ups after final approval and comment with the created issue links.

Issue-filing modes use a conservative two-stage reconciliation. Deterministic
normalization, headings, identifiers, paths, and topic overlap run first. The
publisher then searches open follow-up trackers in the configured repository
with at most five focused queries, twenty results per query, and fifty unique
candidates per publication. Parent issue, approved-plan hash, PR, and related
links are preferred before a bounded repository-wide topic query. Search
results are repository/identity checked and the selected tracker is
revalidated as open immediately before reuse; closed or cross-repository
results never suppress a new issue. Search indexing is eventually consistent,
and there is no atomic repository-wide lock, so a create-then-interruption
window is recovered by bounded rediscovery on a later invocation.

For ambiguous in-batch groups and narrowed existing trackers, an optional cheap
semantic classifier receives only bounded excerpts and must return strict JSON
with `duplicate_of`, `confidence`, and a non-empty `reason`. Only `high`
confidence equivalence of the actual deliverable suppresses or merges work.
Medium/low confidence files normally with a sanitized possible-duplicate note;
provider failures, invalid output, timeouts, and local budget exhaustion fall
back to deterministic behavior. A quota-reset exhaustion is different: it is
propagated so the orchestration run can stop without filing or publishing a
success audit record. Configure the classifier with
`--semantic-followup-backend`, `--semantic-followup-model`,
`--semantic-followup-timeout-seconds`, `--semantic-followup-max-calls`,
`--semantic-followup-max-candidates`, and
`--semantic-followup-prompt-char-limit`; use
`--no-semantic-followup-dedupe` for deterministic-only operation.

Publication summaries distinguish created issues, reused trackers, uncertain
matches, and cap-skipped work. The three-new-issue cap is applied after all
groups have been checked for reuse, so reusing an existing tracker does not
consume a creation slot. Candidate titles, excerpts, retained reviewer/planning
context, and model reasons are sanitized before entering a GitHub body or
comment, while the expected publish-once audit record remains the only
authorized protocol record in those bodies.

For plan-first runs that continue into implementation, issue-filing modes apply
twice at different lifecycle points: planning-stage future follow-ups are filed
before implementation begins, while PR-stage approved-review future follow-ups
Expand Down
31 changes: 21 additions & 10 deletions helpers/skill_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
)
from coding_review_agent_loop.errors import AgentLoopError, UnknownPriorItemDispositionError
from coding_review_agent_loop.followups import (
FollowupSourceContext,
_approved_followup_from_unresolved_item,
_publish_approved_followups,
)
Expand Down Expand Up @@ -883,6 +884,7 @@ def _publish_pr_followups(
pr_comments: list,
*,
dry_run: bool,
parent_issue_number: int | None = None,
) -> dict:
"""Publish approved future follow-ups for a PR via the library logic (#300).

Expand All @@ -908,14 +910,22 @@ def _publish_pr_followups(
# (the coder dir). The Codex+Gemini skill flow never creates a Claude checkout,
# so ensure that directory exists or gh raises FileNotFoundError (#300).
Path(active_workdir(config)).mkdir(parents=True, exist_ok=True)
published = _publish_approved_followups(
Runner(dry_run=False),
config=config,
pr_number=pr,
head_sha=head_sha,
pr_comments=pr_comments,
followups=approved,
)
publish_kwargs = {
"config": config,
"pr_number": pr,
"head_sha": head_sha,
"pr_comments": pr_comments,
"followups": approved,
"source_context": FollowupSourceContext(
repo=repo,
source_kind="pr",
source_number=pr,
source_identity=head_sha,
parent_issue_numbers=(parent_issue_number,) if parent_issue_number is not None else (),
related_pr_numbers=(pr,),
),
}
published = _publish_approved_followups(Runner(dry_run=False), **publish_kwargs)
return {"mode": mode, "published": bool(published), "count": len(approved)}


Expand Down Expand Up @@ -2523,8 +2533,9 @@ def cmd_run_pr_round(args: argparse.Namespace) -> None:
else:
round_approved_reviewers.append(str(record.get("reviewer_name", "")))

pr_diff = _fetch_pr_diff(repo, pr)
issue_dict = _fetch_pr_json(repo, pr)
parent_issue_number = _linked_issue_number_from_pr(issue_dict)
pr_diff = _fetch_pr_diff(repo, pr)

# Repo-scoped agent memory for reviewer orientation (#306), prepared once.
memory = None
Expand Down Expand Up @@ -2699,7 +2710,7 @@ def cmd_run_pr_round(args: argparse.Namespace) -> None:
]
result_json["approved_followups"] = _publish_pr_followups(
repo, pr, head_sha, followups_mode, round_future_items, pr_comments,
dry_run=dry_run,
dry_run=dry_run, parent_issue_number=parent_issue_number,
)
usage = _aggregate_reviewer_usage(round_reviewer_records)
if usage is not None:
Expand Down
55 changes: 55 additions & 0 deletions src/coding_review_agent_loop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
DEFAULT_REPAIR_MODELS,
DEFAULT_FLAT_CHILD_LIMIT,
DEFAULT_ANTIGRAVITY_QUOTA_SIGNATURES,
DEFAULT_SEMANTIC_FOLLOWUP_BACKEND,
DEFAULT_SEMANTIC_FOLLOWUP_MAX_CALLS,
DEFAULT_SEMANTIC_FOLLOWUP_MAX_CANDIDATES,
DEFAULT_SEMANTIC_FOLLOWUP_PROMPT_CHAR_LIMIT,
DEFAULT_SEMANTIC_FOLLOWUP_TIMEOUT_SECONDS,
AgentLoopConfig,
config_from_args,
ensure_agent_workdirs,
Expand Down Expand Up @@ -562,6 +567,56 @@ def add_common(subparser: argparse.ArgumentParser) -> None:
"'fix-and-issue'; default: ignore)."
),
)
semantic_group = subparser.add_mutually_exclusive_group()
semantic_group.add_argument(
"--semantic-followup-dedupe",
"--followup-semantic-dedupe",
dest="semantic_followup_dedupe",
action="store_true",
default=True,
help="Use bounded semantic matching when deterministic follow-up dedupe is ambiguous (default).",
)
semantic_group.add_argument(
"--no-semantic-followup-dedupe",
dest="semantic_followup_dedupe",
action="store_false",
help="Disable semantic follow-up matching; retain deterministic and conservative fallback behavior.",
)
subparser.add_argument(
"--semantic-followup-backend",
choices=("claude", "codex", "gemini", "antigravity"),
default=DEFAULT_SEMANTIC_FOLLOWUP_BACKEND,
help="Cheap isolated provider used for semantic follow-up matching.",
)
subparser.add_argument(
"--semantic-followup-model",
default="",
help="Optional model override for the semantic follow-up provider.",
)
subparser.add_argument(
"--semantic-followup-timeout-seconds",
type=int,
default=DEFAULT_SEMANTIC_FOLLOWUP_TIMEOUT_SECONDS,
help="Per-call semantic follow-up provider timeout (default: 30).",
)
subparser.add_argument(
"--semantic-followup-max-calls",
type=int,
default=DEFAULT_SEMANTIC_FOLLOWUP_MAX_CALLS,
help="Maximum semantic provider calls per publication (default: 5).",
)
subparser.add_argument(
"--semantic-followup-max-candidates",
type=int,
default=DEFAULT_SEMANTIC_FOLLOWUP_MAX_CANDIDATES,
help="Maximum existing/batch candidates presented to one semantic call (default: 50).",
)
subparser.add_argument(
"--semantic-followup-prompt-char-limit",
type=int,
default=DEFAULT_SEMANTIC_FOLLOWUP_PROMPT_CHAR_LIMIT,
help="Maximum prompt size for one semantic follow-up call (default: 12000).",
)
subparser.add_argument(
"--planning-context-mode",
choices=("full", "compact"),
Expand Down
41 changes: 41 additions & 0 deletions src/coding_review_agent_loop/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
DEFAULT_ANTIGRAVITY_PRINT_TIMEOUT_SECONDS = 10 * 60
DEFAULT_REPAIR_MODELS: tuple[str, ...] = ("Gemini 3.7 Flash (Medium)",)
DEFAULT_REASONING_EFFORT = "medium"
DEFAULT_SEMANTIC_FOLLOWUP_BACKEND: AgentName = "gemini"
DEFAULT_SEMANTIC_FOLLOWUP_TIMEOUT_SECONDS = 30
DEFAULT_SEMANTIC_FOLLOWUP_MAX_CALLS = 5
DEFAULT_SEMANTIC_FOLLOWUP_MAX_CANDIDATES = 50
DEFAULT_SEMANTIC_FOLLOWUP_PROMPT_CHAR_LIMIT = 12_000
CODEX_REASONING_EFFORTS: frozenset[str] = frozenset(
{"minimal", "low", "medium", "high", "xhigh"}
)
Expand Down Expand Up @@ -119,6 +124,15 @@ class AgentLoopConfig:
agent_memory_dir: Path
refresh_test_profile: bool
approved_followups: str = "ignore"
# Approved-follow-up semantic reuse is deliberately bounded and can be
# disabled for offline/reproducibility-sensitive invocations.
semantic_followup_dedupe: bool = True
semantic_followup_backend: AgentName = DEFAULT_SEMANTIC_FOLLOWUP_BACKEND
semantic_followup_model: str = ""
semantic_followup_timeout_seconds: int = DEFAULT_SEMANTIC_FOLLOWUP_TIMEOUT_SECONDS
semantic_followup_max_calls: int = DEFAULT_SEMANTIC_FOLLOWUP_MAX_CALLS
semantic_followup_max_candidates: int = DEFAULT_SEMANTIC_FOLLOWUP_MAX_CANDIDATES
semantic_followup_prompt_char_limit: int = DEFAULT_SEMANTIC_FOLLOWUP_PROMPT_CHAR_LIMIT
plan_execution_mode: str = "plan-only"
planning_context_mode: str = "compact"
pr_review_context_mode: str = "full"
Expand Down Expand Up @@ -317,6 +331,16 @@ def __post_init__(self) -> None:
raise AgentLoopError("antigravity_models chain cannot be empty or contain blank entries.")
if self.antigravity_print_timeout_seconds <= 0:
raise AgentLoopError("--antigravity-print-timeout-seconds must be greater than zero.")
if self.semantic_followup_backend not in {"claude", "codex", "gemini", "antigravity"}:
raise AgentLoopError("--semantic-followup-backend must name a supported agent.")
for option_name, value in (
("--semantic-followup-timeout-seconds", self.semantic_followup_timeout_seconds),
("--semantic-followup-max-calls", self.semantic_followup_max_calls),
("--semantic-followup-max-candidates", self.semantic_followup_max_candidates),
("--semantic-followup-prompt-char-limit", self.semantic_followup_prompt_char_limit),
):
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise AgentLoopError(f"{option_name} must be a positive integer.")
timeout = self.coder_test_command_timeout_seconds
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
raise AgentLoopError(
Expand Down Expand Up @@ -1293,6 +1317,23 @@ def config_from_args(
),
refresh_test_profile=args.refresh_test_profile,
approved_followups=args.approved_followups,
semantic_followup_dedupe=getattr(args, "semantic_followup_dedupe", True),
semantic_followup_backend=getattr(
args, "semantic_followup_backend", DEFAULT_SEMANTIC_FOLLOWUP_BACKEND
),
semantic_followup_model=getattr(args, "semantic_followup_model", ""),
semantic_followup_timeout_seconds=getattr(
args, "semantic_followup_timeout_seconds", DEFAULT_SEMANTIC_FOLLOWUP_TIMEOUT_SECONDS
),
semantic_followup_max_calls=getattr(
args, "semantic_followup_max_calls", DEFAULT_SEMANTIC_FOLLOWUP_MAX_CALLS
),
semantic_followup_max_candidates=getattr(
args, "semantic_followup_max_candidates", DEFAULT_SEMANTIC_FOLLOWUP_MAX_CANDIDATES
),
semantic_followup_prompt_char_limit=getattr(
args, "semantic_followup_prompt_char_limit", DEFAULT_SEMANTIC_FOLLOWUP_PROMPT_CHAR_LIMIT
),
plan_execution_mode=getattr(args, "plan_execution_mode", None) or "plan-only",
planning_context_mode=getattr(args, "planning_context_mode", None) or "compact",
pr_review_context_mode=getattr(args, "pr_review_context_mode", None) or "full",
Expand Down
Loading
Loading