diff --git a/.agents/plugins/opencode-aidevops/index.mjs b/.agents/plugins/opencode-aidevops/index.mjs index 5667e989f9..386686602f 100644 --- a/.agents/plugins/opencode-aidevops/index.mjs +++ b/.agents/plugins/opencode-aidevops/index.mjs @@ -1502,7 +1502,10 @@ const BUILTIN_TTSR_RULES = [ { id: "shell-local-params", description: "Use local var=\"$1\" pattern in shell functions", - pattern: "\\$[1-9](?!.*local\\s+\\w+=.*\\$[1-9])", + // Only match bare $N at the start of a line or after whitespace in what looks + // like a shell assignment/command context — avoids matching $1 inside prose, + // documentation, quoted examples, or tool output from file reads. + pattern: "^\\s+(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)", correction: "Use `local var=\"$1\"` pattern — never use positional parameters directly (SonarCloud S7679).", severity: "warn", systemPrompt: "Shell scripts: use `local var=\"$1\"` — never use $1 directly in function bodies.", @@ -1639,16 +1642,39 @@ async function systemTransformHook(_input, output) { * Extract text content from a Part array. * Only extracts text from TextPart objects (type === "text"). * @param {Array} parts - Array of Part objects + * @param {object} [options] - Extraction options + * @param {boolean} [options.excludeToolOutput=false] - Skip tool-result/tool-invocation parts * @returns {string} Concatenated text content */ -function extractTextFromParts(parts) { +function extractTextFromParts(parts, options = {}) { if (!Array.isArray(parts)) return ""; return parts - .filter((p) => p && p.type === "text" && typeof p.text === "string") + .filter((p) => { + if (!p || typeof p.text !== "string") return false; + if (p.type !== "text") return false; + // When excludeToolOutput is set, skip parts that contain tool output. + // Tool results are embedded in assistant messages as text parts whose + // content is the serialized tool response. We detect these by checking + // for the tool-invocation/tool-result type or by the presence of + // toolCallId/toolInvocationId fields that OpenCode attaches. + if (options.excludeToolOutput) { + if (p.toolCallId || p.toolInvocationId) return false; + } + return true; + }) .map((p) => p.text) .join("\n"); } +/** + * Cross-turn TTSR dedup state. + * Tracks which rules have already fired and on which message IDs, + * preventing the same rule from firing repeatedly on the same content + * across multiple LLM turns (which caused an infinite correction loop). + * @type {Map>} ruleId -> Set of messageIDs already corrected + */ +const _ttsrFiredState = new Map(); + /** * Hook: experimental.chat.messages.transform (t1304) * @@ -1660,6 +1686,13 @@ function extractTextFromParts(parts) { * If violations are found, append a synthetic correction message to the * message history so the model sees the feedback before generating. * + * Bug fix: Three changes to prevent infinite correction loops: + * 1. Only scan assistant-authored text, excluding tool output (Read/Bash + * results contain code the assistant *read*, not code it *wrote*). + * 2. Cross-turn dedup — track which rules fired on which messages to + * prevent the same rule re-firing on the same content every turn. + * 3. Skip messages that are themselves synthetic TTSR corrections. + * * @param {object} _input - {} * @param {object} output - { messages: { info: Message, parts: Part[] }[] } (mutable) */ @@ -1669,7 +1702,12 @@ async function messagesTransformHook(_input, output) { // Scan the last 3 assistant messages for violations const scanWindow = 3; const assistantMessages = output.messages - .filter((m) => m.info && m.info.role === "assistant") + .filter((m) => { + if (!m.info || m.info.role !== "assistant") return false; + // Skip synthetic TTSR correction messages that were injected previously + if (m.info.id && m.info.id.startsWith("ttsr-correction-")) return false; + return true; + }) .slice(-scanWindow); if (assistantMessages.length === 0) return; @@ -1677,20 +1715,39 @@ async function messagesTransformHook(_input, output) { const allViolations = []; for (const msg of assistantMessages) { - const text = extractTextFromParts(msg.parts); + const msgId = msg.info?.id || ""; + + // Extract only assistant-authored text, excluding tool output. + // Tool results (Read, Bash, etc.) contain code the assistant *read*, + // not code it *wrote* — scanning those causes false positives. + const text = extractTextFromParts(msg.parts, { excludeToolOutput: true }); if (!text) continue; const violations = scanForViolations(text); for (const v of violations) { - // Deduplicate by rule id - if (!allViolations.some((av) => av.rule.id === v.rule.id)) { - allViolations.push(v); + const ruleId = v.rule.id; + + // Cross-turn dedup: skip if this rule already fired on this message + const firedOn = _ttsrFiredState.get(ruleId); + if (firedOn && firedOn.has(msgId)) continue; + + // Deduplicate by rule id within this scan + if (!allViolations.some((av) => av.rule.id === ruleId)) { + allViolations.push({ ...v, msgId }); } } } if (allViolations.length === 0) return; + // Record that these rules fired on these messages (cross-turn dedup) + for (const v of allViolations) { + if (!_ttsrFiredState.has(v.rule.id)) { + _ttsrFiredState.set(v.rule.id, new Set()); + } + _ttsrFiredState.get(v.rule.id).add(v.msgId); + } + // Build correction context const corrections = allViolations.map((v) => { const severity = v.rule.severity === "error" ? "ERROR" : "WARNING"; diff --git a/MODELS-PERFORMANCE.md b/MODELS-PERFORMANCE.md index a92adabf1f..7a4e91fbff 100644 --- a/MODELS-PERFORMANCE.md +++ b/MODELS-PERFORMANCE.md @@ -5,7 +5,7 @@ Auto-generated by `generate-models-md.sh --mode performance` — do not edit man Global model catalog is in `MODELS.md`. Filtered by repo: `aidevops` -**Last updated**: 2026-02-24T01:54:29Z +**Last updated**: 2026-02-24T02:58:49Z - **Pattern data points**: 200 - **Scored responses**: 18 diff --git a/MODELS.md b/MODELS.md index 944f1df870..1b196d30a8 100644 --- a/MODELS.md +++ b/MODELS.md @@ -4,7 +4,7 @@ Global model catalog, routing tiers, and pricing. Auto-generated by `generate-models-md.sh --mode global` — do not edit manually. Per-repo performance data is in `MODELS-PERFORMANCE.md`. -**Last updated**: 2026-02-24T01:54:28Z +**Last updated**: 2026-02-24T02:58:49Z ## Model Catalog