@@ -21,6 +21,7 @@ import type { TicketEvent } from "../adapters/messaging/types.js";
2121import type { ActiveRunOwner } from "../lib/active-run-owner.js" ;
2222import type { DownloadedAttachment } from "../sandbox/attachments.js" ;
2323import type { SelectedRepository } from "../adapters/vcs/repository-directory.js" ;
24+ import type { SelectedRepositoryPromptContext } from "../sandbox/context.js" ;
2425import {
2526 buildRuntimeGraph ,
2627 createWorkflowExecutionErrorState ,
@@ -2088,6 +2089,34 @@ export function buildResolutionEvidenceComment(research: ResearchResult): string
20882089 return sections . join ( "\n\n" ) ;
20892090}
20902091
2092+ /**
2093+ * Decide what to do with research's already-resolved declaration. A review
2094+ * comment on the ticket's own PR means a person explicitly asked for changes,
2095+ * so the no_change_needed exit must not be taken: the first declaration earns
2096+ * one corrective research retry, a repeat fails the block. Uses the same
2097+ * prComments condition as renderRepositoryContexts' remediation section, so
2098+ * the prompt and the engine agree on what counts as pending feedback. Pure so
2099+ * the decision table stays unit-testable.
2100+ */
2101+ export function resolveNoChangeAction (
2102+ research : ResearchResult ,
2103+ repositoryContexts : ReadonlyArray <
2104+ Pick < SelectedRepositoryPromptContext , "prComments" >
2105+ > ,
2106+ retryUsed : boolean ,
2107+ ) : "proceed" | "no_change" | "retry" | "fail" {
2108+ const noChangeSignal =
2109+ research . noChangeNeeded === true &&
2110+ ( research . resolutionEvidence ?? [ ] ) . length > 0 &&
2111+ ( research . writeRepositories ?? [ ] ) . length === 0 ;
2112+ if ( ! noChangeSignal ) return "proceed" ;
2113+ const hasPrFeedback = repositoryContexts . some (
2114+ ( context ) => context . prComments . length > 0 ,
2115+ ) ;
2116+ if ( ! hasPrFeedback ) return "no_change" ;
2117+ return retryUsed ? "fail" : "retry" ;
2118+ }
2119+
20912120export function v2TerminalBlockResult ( input : {
20922121 terminalStatus : TerminalStatus ;
20932122 postComment ?: string ;
@@ -4487,6 +4516,7 @@ async function agentWorkflowBody(
44874516 }
44884517
44894518 case "planning_agent" : {
4519+ let noChangeRetryUsed = false ;
44904520 for ( ; ; ) {
44914521 // AIW-147 IM-11: a human answer to the expansion-limit clarification
44924522 // attaches the repositories it named beyond the model round limit
@@ -4540,15 +4570,22 @@ async function agentWorkflowBody(
45404570 continue ;
45414571 }
45424572 const expansionRound = ctx . repositoryExpansion . rounds ;
4573+ // The retry re-runs the research phase, so both the label and the
4574+ // artifact phase must stay distinct from the first pass (same
4575+ // freshness trick as the -expansion-N suffix).
4576+ const noChangeRetrySuffix = noChangeRetryUsed ? " no-change retry" : "" ;
45434577 const researchLabel =
45444578 ctx . schemaVersion === 2
4545- ? `Research ${ node . id } ${ expansionRound > 0 ? ` expansion ${ expansionRound } ` : "" } `
4546- : `Research${ expansionRound > 0 ? ` expansion ${ expansionRound } ` : "" } ` ;
4579+ ? `Research ${ node . id } ${ expansionRound > 0 ? ` expansion ${ expansionRound } ` : "" } ${ noChangeRetrySuffix } `
4580+ : `Research${ expansionRound > 0 ? ` expansion ${ expansionRound } ` : "" } ${ noChangeRetrySuffix } ` ;
45474581 const baseResearchArtifactPhase = agentArtifactPhase ( "research" , execution ) ;
4548- const researchArtifactPhase =
4582+ const expandedResearchArtifactPhase =
45494583 expansionRound > 0
45504584 ? `${ baseResearchArtifactPhase } -expansion-${ expansionRound } `
45514585 : baseResearchArtifactPhase ;
4586+ const researchArtifactPhase = noChangeRetryUsed
4587+ ? `${ expandedResearchArtifactPhase } -no-change-retry`
4588+ : expandedResearchArtifactPhase ;
45524589 const researchPhase = phaseKey ( researchLabel , invocationAttempt ) ;
45534590 const { kind, model, runtime } = resolveAgentForNode ( node ) ;
45544591 const workspace = await ensureCodeWorkspace ( execution ) ;
@@ -4600,25 +4637,33 @@ async function agentWorkflowBody(
46004637 RESEARCH_SCHEMA ,
46014638 runtime ,
46024639 ) ;
4640+ const researchAdditions = [ ...ctx . preSandboxAdditions . research ] ;
4641+ if ( ctx . repositoryExpansion . priorRequests . length > 0 ) {
4642+ researchAdditions . push ( {
4643+ target : [ "research" as const ] ,
4644+ title : "Repository expansion history" ,
4645+ content : [
4646+ "The following repositories were requested and are now attached." ,
4647+ "Continue the same research; do not restart from assumptions." ,
4648+ JSON . stringify ( ctx . repositoryExpansion . priorRequests ) ,
4649+ ] . join ( "\n" ) ,
4650+ } ) ;
4651+ }
4652+ if ( noChangeRetryUsed ) {
4653+ researchAdditions . push ( {
4654+ target : [ "research" as const ] ,
4655+ title : "Do not declare this ticket already resolved" ,
4656+ content : [
4657+ "A human requested changes in the PR review feedback above, and the previous research pass wrongly concluded no change was needed." ,
4658+ "Treat addressing every point of that review feedback as the task: produce an implementation plan for it, declare the writeRepositories it touches, and do not set noChangeNeeded." ,
4659+ ] . join ( "\n" ) ,
4660+ } ) ;
4661+ }
46034662 const researchContext = {
46044663 ticket : resolveAgentTicketInput ( resolvedInputs , ticketData , ctx . clarifications ) ,
46054664 branchName,
46064665 attachments : downloadedAttachments ,
4607- preSandboxAdditions :
4608- ctx . repositoryExpansion . priorRequests . length > 0
4609- ? [
4610- ...ctx . preSandboxAdditions . research ,
4611- {
4612- target : [ "research" as const ] ,
4613- title : "Repository expansion history" ,
4614- content : [
4615- "The following repositories were requested and are now attached." ,
4616- "Continue the same research; do not restart from assumptions." ,
4617- JSON . stringify ( ctx . repositoryExpansion . priorRequests ) ,
4618- ] . join ( "\n" ) ,
4619- } ,
4620- ]
4621- : ctx . preSandboxAdditions . research ,
4666+ preSandboxAdditions : researchAdditions ,
46224667 repositoryContexts : ctx . repositoryContexts ,
46234668 workspaceManifest : ctx . workspaceManifest ?? undefined ,
46244669 } ;
@@ -4748,14 +4793,31 @@ async function agentWorkflowBody(
47484793
47494794 // An already resolved ticket (fix landed in an earlier commit, PR,
47504795 // or ticket comment) ends the run here as a successful no-op: there
4751- // is nothing for any downstream block to write. All three signals
4752- // must agree, so a half-filled signal keeps the normal plan path and
4753- // its researchDeclaredNoWritesGuard verdict untouched.
4754- const noChangeSignal =
4755- research . noChangeNeeded === true &&
4756- ( research . resolutionEvidence ?? [ ] ) . length > 0 &&
4757- ( research . writeRepositories ?? [ ] ) . length === 0 ;
4758- if ( noChangeSignal ) {
4796+ // is nothing for any downstream block to write. A half-filled
4797+ // signal keeps the normal plan path and its
4798+ // researchDeclaredNoWritesGuard verdict untouched. When the
4799+ // ticket's own PR carries human review feedback, that request is
4800+ // the task, so the exit is refused: one corrective research retry,
4801+ // then a hard fail instead of a false success.
4802+ const noChangeAction = resolveNoChangeAction (
4803+ research ,
4804+ ctx . repositoryContexts ,
4805+ noChangeRetryUsed ,
4806+ ) ;
4807+ if ( noChangeAction === "retry" ) {
4808+ console . warn (
4809+ "[agent] research declared no_change_needed despite pending PR review feedback; retrying research once with a corrective note" ,
4810+ ) ;
4811+ noChangeRetryUsed = true ;
4812+ continue ;
4813+ }
4814+ if ( noChangeAction === "fail" ) {
4815+ return executionError (
4816+ "research declared no change needed but the ticket's PR has unresolved human review feedback; refusing the no_change_needed exit" ,
4817+ { category : "engine" , phase : "research" } ,
4818+ ) ;
4819+ }
4820+ if ( noChangeAction === "no_change" ) {
47594821 // Ticket-bound side effects only, exactly like the terminate
47604822 // dispatch: an uncorrelated entry has no ticket to comment on,
47614823 // move, or notify about.
0 commit comments