feat: initialize standalone agent-workflow

This commit is contained in:
liujing
2026-07-16 20:56:24 +08:00
commit 35e8fa3717
31 changed files with 6972 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
/**
* Convergence detector for the workflow loop.
*
* Detects two stop conditions:
* 1. The same finding (same path + normalized summary) persists for 2+ consecutive cycles.
* 2. Findings oscillate between cycles (a finding disappears then reappears).
*
* Both conditions produce needs_human.
*/
import type { FindingDecision } from "./workflow-types.js";
export interface FindingSignature {
path?: string;
normalizedSummary: string;
}
/** Normalize a finding summary for stable comparison. */
function normalizeSummary(summary: string): string {
return summary
.toLowerCase()
.replace(/[^\w\s]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
/** Build convergence signatures from the issues Codex accepted for fixing. */
export function decisionSignatures(findings: FindingDecision[]): FindingSignature[] {
return findings
.filter((finding) => finding.disposition === "accept")
.map((finding) => ({
...(finding.path ? { path: finding.path } : {}),
normalizedSummary: normalizeSummary(finding.summary || finding.rationale || finding.findingId),
}));
}
function sigKey(sig: FindingSignature): string {
return `${sig.path ?? ""}::${sig.normalizedSummary}`;
}
export interface ConvergenceCheckResult {
converging: boolean;
/** Set when the same finding persists for 2 consecutive cycles. */
persistentFindings?: string[];
/** Set when findings oscillate (disappeared then reappeared). */
oscillatingFindings?: string[];
}
/**
* Check the last few rounds' signatures for convergence.
*
* @param history Array of per-cycle finding signatures, oldest first.
*/
export function checkConvergence(history: FindingSignature[][]): ConvergenceCheckResult {
if (history.length < 2) return { converging: true };
const last = history[history.length - 1]!;
const prev = history[history.length - 2]!;
const lastKeys = new Set(last.map(sigKey));
const prevKeys = new Set(prev.map(sigKey));
// Persistent: same key in both consecutive cycles
const persistent = [...lastKeys].filter((k) => prevKeys.has(k));
if (persistent.length > 0) {
return { converging: false, persistentFindings: persistent };
}
// Oscillating: check 3+ cycles (a key was present, absent, then present again)
if (history.length >= 3) {
const earlier = history[history.length - 3]!;
const earlierKeys = new Set(earlier.map(sigKey));
const oscillating = [...lastKeys].filter((k) => earlierKeys.has(k) && !prevKeys.has(k));
if (oscillating.length > 0) {
return { converging: false, oscillatingFindings: oscillating };
}
}
return { converging: true };
}