t1304: Soft TTSR — wire rules into OpenCode plugin hooks#2139
t1304: Soft TTSR — wire rules into OpenCode plugin hooks#2139marcusquinn merged 3 commits intomainfrom
Conversation
WalkthroughPhase 5 Soft TTSR (Text-to-Speech Rules) is introduced via three plugin hooks that enforce speech rules across different stages: system prompt injection, message violation scanning, and post-completion text analysis. Built-in rules are merged with optional user-defined rules, enabling preventative and corrective rule enforcement through a data-driven rule engine. Changes
Sequence DiagramsequenceDiagram
participant PluginSys as Plugin System
participant RulesEngine as TTSR Rules<br/>Engine
participant SysPrompt as System Prompt<br/>Stage
participant ChatMsg as Messages<br/>Stage
participant TextComp as Text Complete<br/>Stage
PluginSys->>RulesEngine: loadTtsrRules()<br/>(builtin + user-defined)
RulesEngine-->>PluginSys: rules loaded
PluginSys->>SysPrompt: systemTransformHook()
SysPrompt->>RulesEngine: inject active TTSR rules
RulesEngine-->>SysPrompt: rules added to prompt
PluginSys->>ChatMsg: messagesTransformHook()
ChatMsg->>RulesEngine: scanForViolations()<br/>(last messages)
RulesEngine-->>ChatMsg: violations detected
ChatMsg->>ChatMsg: inject corrective<br/>feedback message
PluginSys->>TextComp: textCompleteHook()
TextComp->>RulesEngine: checkRule()<br/>(completed text)
RulesEngine-->>TextComp: violations + markers
TextComp->>TextComp: log violations<br/>& patterns
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔍 Code Quality Report�[0;35m[MONITOR]�[0m Code Review Monitoring Report �[0;34m[INFO]�[0m Latest Quality Status: �[0;34m[INFO]�[0m Recent monitoring activity: 📈 Current Quality Metrics
Generated on: Sun Feb 22 05:07:59 UTC 2026 Generated by AI DevOps Framework Code Review Monitoring |
Summary of ChangesHello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a robust 'Soft TTSR' (Text-to-Speech Rules) framework to enhance the quality and adherence to coding standards within the AI agent's generated content. By integrating a multi-pronged approach of preventative guidance, corrective feedback, and observational logging through various plugin hooks, the system aims to improve the AI's output by enforcing best practices and preventing common errors. This ensures a more consistent and secure development workflow, allowing for better control over the AI's behavior and output quality. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
🔍 Code Quality Report�[0;35m[MONITOR]�[0m Code Review Monitoring Report �[0;34m[INFO]�[0m Latest Quality Status: �[0;34m[INFO]�[0m Recent monitoring activity: 📈 Current Quality Metrics
Generated on: Sun Feb 22 05:10:11 UTC 2026 Generated by AI DevOps Framework Code Review Monitoring |
There was a problem hiding this comment.
Code Review
This pull request introduces a "Soft TTSR" rule enforcement system using three new OpenCode plugin hooks. The implementation is well-structured, with data-driven rules for preventative, corrective, and observational feedback. My review focuses on improving error handling, adherence to project conventions regarding error suppression, and the correctness of one of the rule's regular expressions. These changes will enhance the robustness and maintainability of the new feature.
| if (existsSync(patternTracker)) { | ||
| const ruleIds = violations.map((v) => v.rule.id).join(","); | ||
| run( | ||
| `bash "${patternTracker}" record "TTSR_VIOLATION" "rules: ${ruleIds}" --tag "ttsr" 2>/dev/null`, |
There was a problem hiding this comment.
The use of 2>/dev/null suppresses all stderr output from the pattern-tracker-helper.sh script. This can hide important errors related to script execution, permissions, or internal logic failures, making debugging difficult. This is contrary to the repository style guide, which discourages blanket error suppression. Please remove the redirection to allow potential errors to be visible.
`bash "${patternTracker}" record "TTSR_VIOLATION" "rules: ${ruleIds}" --tag "ttsr"`,References
- The repository style guide (line 50) states that
2>/dev/nullis acceptable only when redirecting to log files, not for blanket suppression of errors. (link)
| { | ||
| id: "shell-local-params", | ||
| description: "Use local var=\"$1\" pattern in shell functions", | ||
| pattern: "\\$[1-9](?!.*local\\s+\\w+=.*\\$[1-9])", |
There was a problem hiding this comment.
The regular expression for the shell-local-params rule appears to have a logical flaw. The negative lookahead (?!.*local\s+\w+=.*\$[1-9]) checks for the absence of a local assignment after the use of a positional parameter ($1, $2, etc.). To correctly enforce the local var="$1" pattern, as per the repository style guide (line 11), the check should verify that a local assignment has occurred before the parameter is used within its function scope. This regex is likely to produce false negatives (failing to flag incorrect usage) and may not be reliable. Consider refining the pattern to more accurately reflect the rule's intent.
References
- The repository style guide (line 11) mandates using the
local var="$1"pattern in functions. The current regex does not correctly enforce this. (link)
| } catch { | ||
| console.error("[aidevops] Failed to parse TTSR rules file — using built-in rules only"); | ||
| } |
There was a problem hiding this comment.
The catch block for JSON parsing doesn't log the specific error, which makes it harder to diagnose issues with a user's ttsr-rules.json file (e.g., syntax errors). Capturing and logging the error object would provide more context for debugging.
| } catch { | |
| console.error("[aidevops] Failed to parse TTSR rules file — using built-in rules only"); | |
| } | |
| } catch (e) { | |
| console.error("[aidevops] Failed to parse TTSR rules file — using built-in rules only:", e); | |
| } |
| } catch { | ||
| return { matched: false, matches: [] }; | ||
| } |
There was a problem hiding this comment.
The catch block is empty. If a rule contains an invalid regular expression, new RegExp() will throw an error, which is currently being silently ignored. This can make it very difficult to debug custom or built-in rules. The error should be logged to provide visibility into malformed rule patterns.
} catch (e) {
console.error(`[aidevops] Invalid regex in TTSR rule '${rule.id}':`, e);
return { matched: false, matches: [] };
}…(t1304) Wire three previously-unused experimental plugin hooks for preventative rule enforcement without stream-level interception: 1. experimental.chat.system.transform — injects active rules into system prompt so the model is aware of conventions before generating output 2. experimental.chat.messages.transform — scans last 3 assistant messages for rule violations and injects correction context into message history 3. experimental.text.complete — detects violations post-hoc, logs them for observability, and appends HTML comment markers for subsequent turns Rules are data-driven: 7 built-in rules covering file discovery, Read-before- Edit, credential exposure, pre-edit checks, and shell conventions. Users can extend/override via ~/.aidevops/agents/configs/ttsr-rules.json. Ref #2128
The TTSR no-credentials-in-output rule missed API keys containing hyphens (e.g., sk-xxx) because the character class only had [A-Za-z0-9+/=]. Added _- to catch keys like sk-*, ghp_*, etc.
Multiple Date.now() calls could produce different timestamps for the message ID and part messageID. Use a single correctionId variable.
f085a9a to
6812212
Compare
🔍 Code Quality Report�[0;35m[MONITOR]�[0m Code Review Monitoring Report �[0;34m[INFO]�[0m Latest Quality Status: �[0;34m[INFO]�[0m Recent monitoring activity: 📈 Current Quality Metrics
Generated on: Sun Feb 22 07:52:39 UTC 2026 Generated by AI DevOps Framework Code Review Monitoring |
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
.agents/plugins/opencode-aidevops/index.mjs (2)
1407-1422: Bound the{50,}quantifier inshell-explicit-returnsto avoid O(n²) worst-case scan.The
(?:(?!return\s+[0-9]).){50,}pattern is unbounded. For a large assistant output containing a shell function header followed by many characters but no}, the engine performs O(n²) character comparisons as it repeatedly tries and fails to anchor. Similarly,shell-local-paramshas two.*inside its negative lookahead, though themflag limits per-line scope there.♻️ Proposed fix — cap the `shell-explicit-returns` upper bound
- pattern: "(?:function\\s+\\w+|\\w+\\s*\\(\\)\\s*\\{)(?:(?!return\\s+[0-9]).){50,}\\}", + pattern: "(?:function\\s+\\w+|\\w+\\s*\\(\\)\\s*\\{)(?:(?!return\\s+[0-9]).){50,3000}\\}",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.agents/plugins/opencode-aidevops/index.mjs around lines 1407 - 1422, The two shell regexes are vulnerable to catastrophic backtracking: for "shell-explicit-returns" replace the open-ended "(?:(?!return\\s+[0-9]).){50,}" with a bounded repeat such as "(?:(?!return\\s+[0-9]).){50,2000}" to cap scanning, and for "shell-local-params" avoid ".*" inside the negative lookahead by replacing ".*local\\s+\\w+=.*\\$[1-9]" with "[^\\n]{0,200}local\\s+\\w+=[^\\n]{0,200}\\$[1-9]" (or similar bounded [^\\n]{0,N}) so the patterns in the rules with id "shell-explicit-returns" and "shell-local-params" use bounded repeats instead of unbounded ".*" to prevent O(n²) backtracking.
1561-1631:messagesTransformHookhas cyclomatic complexity 9 — one over the Codacy limit of 8.Extracting the per-message violation scanning and deduplication into a helper (e.g.,
collectUniqueViolations(messages)) brings the hook itself back under the limit and makes the two responsibilities testable in isolation.♻️ Proposed extraction
+/** + * Collect deduplicated violations from a slice of assistant messages. + * `@param` {{ info: object, parts: Array }[]} messages + * `@returns` {Array<{rule: object, matches: string[]}>} + */ +function collectUniqueViolations(messages) { + const seen = new Set(); + const violations = []; + for (const msg of messages) { + const text = extractTextFromParts(msg.parts); + if (!text) continue; + for (const v of scanForViolations(text)) { + if (!seen.has(v.rule.id)) { + seen.add(v.rule.id); + violations.push(v); + } + } + } + return violations; +} async function messagesTransformHook(_input, output) { if (!output.messages || output.messages.length === 0) return; const scanWindow = 3; const assistantMessages = output.messages .filter((m) => m.info && m.info.role === "assistant") .slice(-scanWindow); if (assistantMessages.length === 0) return; - const allViolations = []; - - for (const msg of assistantMessages) { - const text = extractTextFromParts(msg.parts); - if (!text) continue; - - const violations = scanForViolations(text); - for (const v of violations) { - if (!allViolations.some((av) => av.rule.id === v.rule.id)) { - allViolations.push(v); - } - } - } + const allViolations = collectUniqueViolations(assistantMessages); if (allViolations.length === 0) return;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.agents/plugins/opencode-aidevops/index.mjs around lines 1561 - 1631, messagesTransformHook currently exceeds the cyclomatic complexity limit; extract the per-message scanning and deduplication into a new helper (e.g., collectUniqueViolations(messages)) so the hook's logic becomes a single linear flow. Implement collectUniqueViolations to accept an array of assistant messages, call extractTextFromParts and scanForViolations for each message, deduplicate by v.rule.id, and return the consolidated allViolations array; then replace the current for-loop/deduplication block in messagesTransformHook with a call to collectUniqueViolations(assistantMessages). Keep references to extractTextFromParts and scanForViolations inside the helper and ensure messagesTransformHook still builds corrections/correctionText and injects the synthetic message as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.agents/plugins/opencode-aidevops/index.mjs:
- Around line 1604-1614: The synthetic correction message currently grabs
sessionID from the first history entry (output.messages[0]?.info?.sessionID)
which can be stale; change the lookup to use the last message in the array so
the synthetic message is anchored to the current turn (use
output.messages[output.messages.length - 1]?.info?.sessionID or equivalent) when
constructing correctionId/sessionID and before pushing the new message into
output.messages to ensure correct session context for correctionId, sessionID,
and the pushed message.
- Around line 1634-1665: Update the JSDoc on textCompleteHook to accurately
state that it may mutate output.text by appending observability markers; then
add a guard before constructing/appending markers so markers are only appended
for safe, natural-language/markdown prose and skipped for raw code/JSON/shell
outputs. Concretely: inside textCompleteHook, after computing violations and
before using output.text + markers, check output.text for code-like content
(e.g., starts with a fenced code block "```", is a bare JSON/object/array
(matches /^\s*[\{\[]/), or appears to be a single-line shell command or
non-markdown context) and bail out of the append if so; keep the qualityLog
behavior unchanged. Reference symbols: function textCompleteHook, output.text,
violations, markers, qualityLog.
- Around line 1470-1483: checkRule dynamically constructs a RegExp from
rule.pattern and runs regex.exec over unbounded text, which risks ReDoS; to fix
it, truncate or slice the incoming text at the start of checkRule (referencing
function checkRule and parameter text) to a safe maximum length (e.g., a few KB)
before creating new RegExp(rule.pattern, "gim") and running the exec loop,
keeping the existing matches length cap and try/catch around new RegExp; also
ensure callers like scanForViolations pass the original full text only when
needed but use the truncated text for matching to avoid blocking the event loop.
---
Nitpick comments:
In @.agents/plugins/opencode-aidevops/index.mjs:
- Around line 1407-1422: The two shell regexes are vulnerable to catastrophic
backtracking: for "shell-explicit-returns" replace the open-ended
"(?:(?!return\\s+[0-9]).){50,}" with a bounded repeat such as
"(?:(?!return\\s+[0-9]).){50,2000}" to cap scanning, and for
"shell-local-params" avoid ".*" inside the negative lookahead by replacing
".*local\\s+\\w+=.*\\$[1-9]" with
"[^\\n]{0,200}local\\s+\\w+=[^\\n]{0,200}\\$[1-9]" (or similar bounded
[^\\n]{0,N}) so the patterns in the rules with id "shell-explicit-returns" and
"shell-local-params" use bounded repeats instead of unbounded ".*" to prevent
O(n²) backtracking.
- Around line 1561-1631: messagesTransformHook currently exceeds the cyclomatic
complexity limit; extract the per-message scanning and deduplication into a new
helper (e.g., collectUniqueViolations(messages)) so the hook's logic becomes a
single linear flow. Implement collectUniqueViolations to accept an array of
assistant messages, call extractTextFromParts and scanForViolations for each
message, deduplicate by v.rule.id, and return the consolidated allViolations
array; then replace the current for-loop/deduplication block in
messagesTransformHook with a call to collectUniqueViolations(assistantMessages).
Keep references to extractTextFromParts and scanForViolations inside the helper
and ensure messagesTransformHook still builds corrections/correctionText and
injects the synthetic message as before.
| function checkRule(text, rule) { | ||
| try { | ||
| const regex = new RegExp(rule.pattern, "gim"); | ||
| const matches = []; | ||
| let match; | ||
| while ((match = regex.exec(text)) !== null) { | ||
| matches.push(match[0].substring(0, 120)); | ||
| if (matches.length >= 3) break; // Cap matches to avoid noise | ||
| } | ||
| return { matched: matches.length > 0, matches }; | ||
| } catch { | ||
| return { matched: false, matches: [] }; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the file
fd -p "index.mjs" | head -5Repository: marcusquinn/aidevops
Length of output: 107
🏁 Script executed:
# Read the checkRule function and surrounding context
sed -n '1450,1500p' .agents/plugins/opencode-aidevops/index.mjsRepository: marcusquinn/aidevops
Length of output: 1382
🏁 Script executed:
# Search for where checkRule is called to understand the text parameter
rg "checkRule\(" .agents/plugins/opencode-aidevops/index.mjs -B 2 -A 2Repository: marcusquinn/aidevops
Length of output: 373
🏁 Script executed:
# Search for rule validation logic
rg "ttsr-rules" .agents/plugins/opencode-aidevops/index.mjs -B 2 -A 5Repository: marcusquinn/aidevops
Length of output: 255
🏁 Script executed:
# Check for Codacy configuration files
fd -p "codacy|eslint|\.codacy" -type fRepository: marcusquinn/aidevops
Length of output: 234
🏁 Script executed:
# Search for scanForViolations calls to understand text source
rg "scanForViolations\(" .agents/plugins/opencode-aidevops/index.mjs -B 3 -A 3Repository: marcusquinn/aidevops
Length of output: 727
🏁 Script executed:
# Look at the full loadTtsrRules function to understand validation
sed -n '1400,1470p' .agents/plugins/opencode-aidevops/index.mjs | head -100Repository: marcusquinn/aidevops
Length of output: 2746
🏁 Script executed:
# Check for Codacy or ESLint configuration
fd -p "\.codacyrc|\.eslintrc|codacy.yaml|\.eslint" -type fRepository: marcusquinn/aidevops
Length of output: 234
🏁 Script executed:
# Look at the built-in rules to understand pattern types
rg "_ttsrRules\s*=" .agents/plugins/opencode-aidevops/index.mjs -A 30 | head -60Repository: marcusquinn/aidevops
Length of output: 1346
🏁 Script executed:
# Search for any lint configuration files more broadly
find . -name ".*rc" -o -name ".codacy*" -o -name "*.config.js" 2>/dev/null | grep -E "(eslint|codacy|lint)" | head -20Repository: marcusquinn/aidevops
Length of output: 87
🏁 Script executed:
# Check if there's a package.json with eslintConfig
fd "package.json" -x grep -l "eslintConfig\|codacy" {} \;Repository: marcusquinn/aidevops
Length of output: 46
🏁 Script executed:
# Examine the shell-explicit-returns pattern more carefully - it's in the built-in rules
sed -n '1300,1350p' .agents/plugins/opencode-aidevops/index.mjsRepository: marcusquinn/aidevops
Length of output: 2198
🏁 Script executed:
# Check the context around where scanForViolations is called - what's the typical text size?
rg "extractTextFromParts\(" .agents/plugins/opencode-aidevops/index.mjs -B 5 -A 10 | head -40Repository: marcusquinn/aidevops
Length of output: 974
🏁 Script executed:
# Look at what extractTextFromParts returns
rg "function extractTextFromParts" .agents/plugins/opencode-aidevops/index.mjs -A 15Repository: marcusquinn/aidevops
Length of output: 628
🏁 Script executed:
# Check the actual Codacy configuration
cat .codacy.yml 2>/dev/null || echo "File not readable"Repository: marcusquinn/aidevops
Length of output: 1231
🏁 Script executed:
# Look at the shell-explicit-returns pattern specifically - it looks complex
rg "shell-explicit-returns" .agents/plugins/opencode-aidevops/index.mjs -A 2 -B 1Repository: marcusquinn/aidevops
Length of output: 265
🏁 Script executed:
# Search for any test or validation of the checkRule function
rg "checkRule" . --type js -B 2 -A 2Repository: marcusquinn/aidevops
Length of output: 46
🏁 Script executed:
# Check if there's documentation about regex patterns or safety
rg "ReDoS|catastrophic|backtrack|timeout" . 2>/dev/null | head -20Repository: marcusquinn/aidevops
Length of output: 2286
🏁 Script executed:
# Look for any limits or caps already in place for text processing
rg "slice\(0," .agents/plugins/opencode-aidevops/index.mjs | head -10Repository: marcusquinn/aidevops
Length of output: 195
🏁 Script executed:
# Check if there's an ESLint configuration with specific rules about dynamic RegExp
find . -name ".eslintrc*" -o -name "eslint.config.*" 2>/dev/nullRepository: marcusquinn/aidevops
Length of output: 46
🏁 Script executed:
# Check package.json for any ESLint or security-related dependencies
fd "package.json" -x cat {} \; | grep -A 20 -B 5 "eslint\|security" | head -50Repository: marcusquinn/aidevops
Length of output: 46
🏁 Script executed:
# Verify the exact line numbers of the checkRule function
rg -n "function checkRule" .agents/plugins/opencode-aidevops/index.mjsRepository: marcusquinn/aidevops
Length of output: 101
🏁 Script executed:
# Check if there are any actual tests or CI logs that mention Codacy failures
fd "\.yml$|\.yaml$" -path "*/.github/*" -x cat {} \; 2>/dev/null | head -100Repository: marcusquinn/aidevops
Length of output: 46
ReDoS vulnerability: dynamic RegExp construction from user-supplied patterns can block the event loop.
new RegExp(rule.pattern, "gim") at line 1472 accepts arbitrary patterns from user-defined rules in ttsr-rules.json. The validation only checks for presence of id and pattern fields—a carefully crafted pattern like (?:a+)+ would cause catastrophic backtracking on large inputs. The try/catch guards against syntax errors but not performance degradation from valid, pathological patterns.
LLM outputs passed to scanForViolations() have no length cap, so a 25KB+ response could stall the event loop indefinitely while regex.exec() exhausts CPU on a malicious pattern.
Capping input length before the exec loop eliminates this risk without external dependencies:
🛡️ Proposed fix — cap input before the exec loop
function checkRule(text, rule) {
try {
+ // Cap input to guard against ReDoS from user-supplied or slow built-in patterns.
+ const safeText = text.length > 50_000 ? text.slice(0, 50_000) : text;
const regex = new RegExp(rule.pattern, "gim");
const matches = [];
let match;
- while ((match = regex.exec(text)) !== null) {
+ while ((match = regex.exec(safeText)) !== null) {
matches.push(match[0].substring(0, 120));
if (matches.length >= 3) break;
}
return { matched: matches.length > 0, matches };
} catch {
return { matched: false, matches: [] };
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function checkRule(text, rule) { | |
| try { | |
| const regex = new RegExp(rule.pattern, "gim"); | |
| const matches = []; | |
| let match; | |
| while ((match = regex.exec(text)) !== null) { | |
| matches.push(match[0].substring(0, 120)); | |
| if (matches.length >= 3) break; // Cap matches to avoid noise | |
| } | |
| return { matched: matches.length > 0, matches }; | |
| } catch { | |
| return { matched: false, matches: [] }; | |
| } | |
| } | |
| function checkRule(text, rule) { | |
| try { | |
| // Cap input to guard against ReDoS from user-supplied or slow built-in patterns. | |
| const safeText = text.length > 50_000 ? text.slice(0, 50_000) : text; | |
| const regex = new RegExp(rule.pattern, "gim"); | |
| const matches = []; | |
| let match; | |
| while ((match = regex.exec(safeText)) !== null) { | |
| matches.push(match[0].substring(0, 120)); | |
| if (matches.length >= 3) break; // Cap matches to avoid noise | |
| } | |
| return { matched: matches.length > 0, matches }; | |
| } catch { | |
| return { matched: false, matches: [] }; | |
| } | |
| } |
🧰 Tools
🪛 GitHub Check: Codacy Static Code Analysis
[warning] 1472-1472: .agents/plugins/opencode-aidevops/index.mjs#L1472
RegExp() called with a filepath function argument, this might allow an attacker to cause a Regular Expression Denial-of-Service (ReDoS) within your application as RegExP blocks the main thread.
[failure] 1472-1472: .agents/plugins/opencode-aidevops/index.mjs#L1472
The RegExp constructor was called with a non-literal value.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/plugins/opencode-aidevops/index.mjs around lines 1470 - 1483,
checkRule dynamically constructs a RegExp from rule.pattern and runs regex.exec
over unbounded text, which risks ReDoS; to fix it, truncate or slice the
incoming text at the start of checkRule (referencing function checkRule and
parameter text) to a safe maximum length (e.g., a few KB) before creating new
RegExp(rule.pattern, "gim") and running the exec loop, keeping the existing
matches length cap and try/catch around new RegExp; also ensure callers like
scanForViolations pass the original full text only when needed but use the
truncated text for matching to avoid blocking the event loop.
| const correctionId = `ttsr-correction-${Date.now()}`; | ||
| const sessionID = output.messages[0]?.info?.sessionID || ""; | ||
|
|
||
| output.messages.push({ | ||
| info: { | ||
| id: correctionId, | ||
| sessionID, | ||
| role: "user", | ||
| time: { created: Date.now() }, | ||
| parentID: "", | ||
| }, |
There was a problem hiding this comment.
Synthetic correction message borrows sessionID from the first history message rather than the current turn.
output.messages[0]?.info?.sessionID reads the oldest message's session ID. For long-running sessions or edge cases where the session context is refreshed, this can diverge from the current session. Use the last message's ID instead to ensure the synthetic message is anchored to the correct turn.
✏️ Proposed fix
- const sessionID = output.messages[0]?.info?.sessionID || "";
+ const sessionID = output.messages[output.messages.length - 1]?.info?.sessionID || "";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/plugins/opencode-aidevops/index.mjs around lines 1604 - 1614, The
synthetic correction message currently grabs sessionID from the first history
entry (output.messages[0]?.info?.sessionID) which can be stale; change the
lookup to use the last message in the array so the synthetic message is anchored
to the current turn (use output.messages[output.messages.length -
1]?.info?.sessionID or equivalent) when constructing correctionId/sessionID and
before pushing the new message into output.messages to ensure correct session
context for correctionId, sessionID, and the pushed message.
| * Hook: experimental.text.complete (t1304) | ||
| * | ||
| * Detects rule violations post-hoc in completed text parts and flags them. | ||
| * This is observational — it logs violations but does not modify the output | ||
| * (the text has already been shown to the user). The log data feeds into | ||
| * quality metrics and pattern tracking. | ||
| * | ||
| * @param {object} input - { sessionID: string, messageID: string, partID: string } | ||
| * @param {object} output - { text: string } (mutable) | ||
| */ | ||
| async function textCompleteHook(input, output) { | ||
| if (!output.text) return; | ||
|
|
||
| const violations = scanForViolations(output.text); | ||
| if (violations.length === 0) return; | ||
|
|
||
| // Log violations for observability | ||
| for (const v of violations) { | ||
| qualityLog( | ||
| v.rule.severity === "error" ? "ERROR" : "WARN", | ||
| `TTSR violation [${v.rule.id}]: ${v.rule.description} (session: ${input.sessionID}, message: ${input.messageID})`, | ||
| ); | ||
| } | ||
|
|
||
| // Append violation markers as comments at the end of the text | ||
| // so the model can see them in subsequent turns | ||
| const markers = violations.map((v) => { | ||
| const severity = v.rule.severity === "error" ? "ERROR" : "WARN"; | ||
| return `<!-- TTSR:${severity}:${v.rule.id} — ${v.rule.correction} -->`; | ||
| }); | ||
|
|
||
| output.text = output.text + "\n" + markers.join("\n"); |
There was a problem hiding this comment.
JSDoc contradicts the implementation, and HTML markers are unconditionally appended to all text types.
Line 1639 states "does not modify the output (the text has already been shown to the user)" — but lines 1660-1665 clearly mutate output.text by appending <!-- TTSR:... --> markers. The PR description itself confirms mutation is intentional ("appends HTML comment markers for subsequent turns"), so the JSDoc needs correcting.
Additionally, HTML comment markers are appended regardless of content type. If the LLM's text part contains raw shell, JSON, or a bare code block with no surrounding prose, those <!-- --> suffixes render as visible literal text to the user in non-markdown contexts.
✏️ Proposed JSDoc + guard fix
- * Detects rule violations post-hoc in completed text parts and flags them.
- * This is observational — it logs violations but does not modify the output
- * (the text has already been shown to the user). The log data feeds into
- * quality metrics and pattern tracking.
+ * Detects rule violations post-hoc in completed text parts, logs them, and
+ * appends HTML comment markers to the stored text for subsequent model turns.
+ * NOTE: output.text IS mutated; markers are invisible in markdown-rendered UIs
+ * but may appear as literal text in plain-text or code-only responses.- output.text = output.text + "\n" + markers.join("\n");
+ // Only append markers when the text contains markdown prose (not bare code/JSON).
+ const looksLikeMarkdown = /[#*_`[\]]/.test(output.text);
+ if (looksLikeMarkdown) {
+ output.text = output.text + "\n" + markers.join("\n");
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/plugins/opencode-aidevops/index.mjs around lines 1634 - 1665, Update
the JSDoc on textCompleteHook to accurately state that it may mutate output.text
by appending observability markers; then add a guard before
constructing/appending markers so markers are only appended for safe,
natural-language/markdown prose and skipped for raw code/JSON/shell outputs.
Concretely: inside textCompleteHook, after computing violations and before using
output.text + markers, check output.text for code-like content (e.g., starts
with a fenced code block "```", is a bare JSON/object/array (matches
/^\s*[\{\[]/), or appears to be a single-line shell command or non-markdown
context) and bail out of the append if so; keep the qualityLog behavior
unchanged. Reference symbols: function textCompleteHook, output.text,
violations, markers, qualityLog.
Auto-dismissed: bot review does not block autonomous pipeline
|
Resolves #2142. |



Summary
Wire three previously-unused OpenCode experimental plugin hooks for Soft TTSR (preventative rule enforcement without stream-level interception):
experimental.chat.system.transform— Injects active quality rules into the system prompt before every LLM call, so the model is aware of conventions before generating output (preventative)experimental.chat.messages.transform— Scans the last 3 assistant messages for rule violations and injects a synthetic correction message into the message history (corrective)experimental.text.complete— Detects violations post-hoc in completed text, logs them for observability, and appends HTML comment markers for subsequent turns (observational)Rules System
7 built-in data-driven rules covering:
git ls-filesnot Glob)Users can extend/override rules via
~/.aidevops/agents/configs/ttsr-rules.json.Testing
Ref #2128
Summary by CodeRabbit