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
50 changes: 49 additions & 1 deletion src/advance/classic/fix/fix-tool-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { extractJsonText } from '../utils/json-extraction.js';
import type { ValidationStrategy, ValidationResult } from './validation-strategy.js';
import { ErrorDeltaValidationStrategy } from './validation-strategy.js';
import { defaultPromptLoader, type PromptLoader } from '../../llm/prompts/loader.js';
import type { MaintainerLocalJudge } from './maintainer-local-judge.js';

export interface FixToolLoopOptions {
llmClient: LlmClient;
Expand Down Expand Up @@ -64,6 +65,8 @@ export interface FixToolLoopOptions {
reason: string;
evidence?: string;
}>;
/** 可选的轻量判别辅助,用于无进展时的卡点校正建议 */
localJudge?: MaintainerLocalJudge;
}

/** 判断 stopReason 是否表示输出被长度截断 */
Expand Down Expand Up @@ -99,6 +102,7 @@ export class FixToolLoop {
private readonly validationStrategy: ValidationStrategy;
private readonly promptLoader: PromptLoader;
private readonly recheckAlreadyFixed?: FixToolLoopOptions['recheckAlreadyFixed'];
private readonly localJudge?: MaintainerLocalJudge;
private readonly messages: LlmMessage[] = [];

private appliedFiles = new Set<string>();
Expand Down Expand Up @@ -140,6 +144,7 @@ export class FixToolLoop {
this.finalActingSteps = Math.max(1, options.finalActingSteps ?? 3);
this.promptLoader = options.promptLoader ?? defaultPromptLoader;
this.recheckAlreadyFixed = options.recheckAlreadyFixed;
this.localJudge = options.localJudge;
this.registry = new ToolRegistry(FIX_TOOLS);
this.executor = new ToolExecutor({
worktreeManager: options.worktreeManager,
Expand Down Expand Up @@ -296,9 +301,14 @@ export class FixToolLoop {
});

if (this.stepsWithoutProgress === this.staleReminderStep) {
let reminder = this.promptLoader.load('fix-tool-loop-stale-reminder');
const stuckAdvice = await this.tryGetStuckAdvice();
if (stuckAdvice) {
reminder += `\n\n${stuckAdvice}`;
}
this.messages.push({
role: 'user',
content: this.promptLoader.load('fix-tool-loop-stale-reminder'),
content: reminder,
});
}

Expand Down Expand Up @@ -400,6 +410,44 @@ export class FixToolLoop {
}
}

/**
* 可选的卡点校正辅助:无进展时向 localJudge 请求转向建议。
* 不可靠/不可用时返回 null,不影响既有静态提醒流程。
*/
private async tryGetStuckAdvice(): Promise<string | null> {
if (!this.localJudge) return null;
try {
const progressSummary = `已执行 ${this.stepsWithoutProgress} 步无实质进展(未修改/删除文件、未读取新文件窗口)。已修改文件: ${Array.from(this.appliedFiles).join(', ') || '无'},已读取文件: ${this.readFilesThisRun.size} 个窗口。`;
const verdict = await this.localJudge.adviseOnStuckProgress(
this.finding.message,
progressSummary,
);
// StuckCorrectionResult 无 kind 字段;LocalJudgeVerdict(unreliable) 有 kind
if (!('suggestion' in verdict)) return null;
return this.formatStuckAdvice(verdict);
} catch {
return null;
}
}

/** 格式化卡点校正建议为提示文本 */
private formatStuckAdvice(result: {
suggestion: string;
suggestStop: boolean;
reason: string;
}): string | null {
if (result.suggestStop) {
return `⚠️ 辅助判别建议:当前方向可能无效,建议考虑收拢或改变策略。理由:${result.reason}`;
}
if (result.suggestion === 'refocus') {
return `💡 辅助判别建议:尝试缩小范围,聚焦到更具体的修改目标。理由:${result.reason}`;
}
if (result.suggestion === 'broaden') {
return `💡 辅助判别建议:当前范围可能太窄,考虑读取更多相关文件或上下文。理由:${result.reason}`;
}
return null;
}

getAppliedFiles(): string[] {
return Array.from(this.appliedFiles);
}
Expand Down
8 changes: 8 additions & 0 deletions src/advance/classic/fix/maintainer-actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
buildDefaultDeleteMessage,
} from './commit-pipeline.js';
import { isSelfAnswerableQuestion } from './ask-gate.js';
import type { MaintainerLocalJudge } from './maintainer-local-judge.js';
import { compactDiscussionReason } from '../runners/shared/reply-safety.js';

// 兼容既有引用(含测试):从本模块再导出,实现统一收敛到 commit-pipeline
Expand All @@ -64,6 +65,8 @@ export interface MaintainerActorOptions {
checkpoint?: () => void;
/** 可选的 M 系列过程指标计数器(M1/M2/M3/M5/M6 由本类自增) */
metrics?: MrLifecycleMetrics;
/** 可选的轻量判别辅助,用于 FixToolLoop 卡点校正建议 */
localJudge?: MaintainerLocalJudge;
}

export interface MaintainerActionResult {
Expand Down Expand Up @@ -862,6 +865,7 @@ export class MaintainerActor {
.filter(Boolean)
.join('\n\n'),
recheckAlreadyFixed: () => this.options.brain.recheckAlreadyFixed(finding),
localJudge: this.options.localJudge,
});
const reflowResult = await reflowLoop.run();
this.trackFinalActingRound(reflowLoop);
Expand Down Expand Up @@ -973,6 +977,7 @@ export class MaintainerActor {
.filter(Boolean)
.join('\n\n'),
recheckAlreadyFixed: () => this.options.brain.recheckAlreadyFixed(finding),
localJudge: this.options.localJudge,
});
const result = await loop.run();
this.trackFinalActingRound(loop);
Expand Down Expand Up @@ -1337,6 +1342,7 @@ export class MaintainerActor {
.filter(Boolean)
.join('\n\n'),
recheckAlreadyFixed: () => this.options.brain.recheckAlreadyFixed(finding),
localJudge: this.options.localJudge,
});
const result = await loop.run();
this.trackFinalActingRound(loop);
Expand Down Expand Up @@ -1742,6 +1748,7 @@ export class MaintainerActor {
.filter(Boolean)
.join('\n\n'),
recheckAlreadyFixed: () => this.options.brain.recheckAlreadyFixed(syntheticFinding),
localJudge: this.options.localJudge,
});
const result = await loop.run();
this.trackFinalActingRound(loop);
Expand Down Expand Up @@ -2098,6 +2105,7 @@ export class MaintainerActor {
.filter(Boolean)
.join('\n\n'),
recheckAlreadyFixed: () => this.options.brain.recheckAlreadyFixed(reflowFinding),
localJudge: this.options.localJudge,
});
const result = await loop.run();
this.trackFinalActingRound(loop);
Expand Down
109 changes: 106 additions & 3 deletions src/advance/classic/runners/maintainer-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ export class MaintainerRunner extends BaseRoleRunner {
recallPlanner,
checkpoint: () => saveState(project, state, 'maintainer'),
metrics: lifecycle.metrics,
localJudge: this.localJudge,
});

let currentHeadSha: string | undefined;
Expand Down Expand Up @@ -1937,6 +1938,37 @@ export class MaintainerRunner extends BaseRoleRunner {
return;
}

// 语义重识别:stale finding 无精确 key 匹配时(行号漂移),
// 尝试匹配同文件的历史 ignore 决策,避免完整 LLM 重新评估。
if (staleFinding && !existing) {
const semanticMatch = await this.trySemanticReidentification(
threadState,
finding,
key
);
if (semanticMatch) {
threadState.decisions[key] = semanticMatch;
threadState.lastHumanNoteAt = lastHumanNoteAt;
await actor.applyDecision(
mr,
discussion,
finding,
{
action: 'ignore',
alreadyFixed: semanticMatch.alreadyFixed,
reason: semanticMatch.reason,
replyBody: semanticMatch.replyBody,
},
state
);
console.log(
`[MaintainerRunner] stale finding ${key} 语义匹配历史 ignore 决策,跳过重评估`
);
recordProcessed();
return;
}
}

const fileContent = await readDiscussionFileContent(
worktreeManager,
projectRootPath,
Expand Down Expand Up @@ -2194,6 +2226,35 @@ export class MaintainerRunner extends BaseRoleRunner {
continue;
}

// 语义重识别:stale finding 无精确 key 匹配时,尝试匹配同文件的历史 ignore 决策
if (staleFinding && !existing) {
const semanticMatch = await this.trySemanticReidentification(
threadState,
finding,
key
);
if (semanticMatch) {
threadState.decisions[key] = semanticMatch;
this.applyStoredDecision(
semanticMatch,
finding,
{
fixedItems,
failedItems,
askedItems,
ignoredItems,
alreadyFixedItems,
fixableItems,
},
suppressRepeatedAsk
);
console.log(
`[MaintainerRunner] stale finding ${key} 语义匹配历史 ignore 决策,跳过重评估`
);
continue;
}
}

const focusedContent = await readDiscussionFileContent(
worktreeManager,
projectRootPath,
Expand Down Expand Up @@ -2900,8 +2961,7 @@ export class MaintainerRunner extends BaseRoleRunner {
}

try {
const baseResult = await brain.recheckAlreadyFixed(finding);

// 先跑轻量 LLM 辅助判断,命中时跳过昂贵的 CognitiveEngine 全量分析
const assist = await this.localJudge.assistAlreadyFixedCheck(
`${finding.message}\n${finding.suggestion}`,
focusedContextToString(focusedContent)
Expand All @@ -2914,7 +2974,8 @@ export class MaintainerRunner extends BaseRoleRunner {
};
}

return baseResult;
// 辅助不可靠或不认为已修复 → 走原有重逻辑
return await brain.recheckAlreadyFixed(finding);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
Expand All @@ -2924,6 +2985,48 @@ export class MaintainerRunner extends BaseRoleRunner {
}
}

/**
* 语义重识别:stale finding 无精确 key 匹配时(行号漂移),
* 尝试匹配同文件的历史 ignore 决策。
*
* 只在同文件存在 ignore 决策时尝试,且仅匹配最近一条,
* 避免 N 次 LLM 调用。不可靠时静默返回 null,走原有评估流程。
*/
private async trySemanticReidentification(
threadState: MaintainerThreadState,
finding: ReviewFinding,
currentKey: string
): Promise<MaintainerThreadState['decisions'][string] | null> {
const sameFileDecisions = Object.entries(threadState.decisions)
.filter(([k]) => k.startsWith(`${finding.file}:`) && k !== currentKey)
.filter(([, d]) => d.action === 'ignore')
.sort(([, a], [, b]) => b.decidedAt - a.decidedAt);

if (sameFileDecisions.length === 0) return null;

const [, bestCandidate] = sameFileDecisions[0];
try {
const verdict = await this.localJudge.reassessSemanticIdentity(
finding.message,
`${bestCandidate.action}: ${bestCandidate.reason}`
);
// SemanticReidentificationResult 无 kind 字段;LocalJudgeVerdict(unreliable) 有 kind
if ('kind' in verdict) return null;
if (verdict.likelySame && verdict.confidence !== 'low') {
console.log(
`[MaintainerRunner] stale finding ${currentKey} 语义匹配历史决策(confidence=${verdict.confidence}): ${verdict.reason}`
);
return {
...bestCandidate,
decidedAt: Date.now(),
};
}
} catch {
// 语义匹配失败不影响主流程
}
return null;
}

/**
* 轻量预检:从原始正文中识别批量统计/聚合报告。
*
Expand Down
Loading
Loading