Skip to content

t1304: Soft TTSR — wire rules into OpenCode plugin hooks#2139

Merged
marcusquinn merged 3 commits intomainfrom
feature/t1304
Feb 22, 2026
Merged

t1304: Soft TTSR — wire rules into OpenCode plugin hooks#2139
marcusquinn merged 3 commits intomainfrom
feature/t1304

Conversation

@marcusquinn
Copy link
Owner

@marcusquinn marcusquinn commented Feb 22, 2026

Summary

Wire three previously-unused OpenCode experimental plugin hooks for Soft TTSR (preventative rule enforcement without stream-level interception):

  1. 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)
  2. experimental.chat.messages.transform — Scans the last 3 assistant messages for rule violations and injects a synthetic correction message into the message history (corrective)
  3. 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:

  • File discovery (use git ls-files not Glob)
  • File reading (use Read tool not cat/head/tail)
  • Read-before-Edit enforcement
  • Credential exposure prevention
  • Pre-edit check enforcement
  • Shell explicit returns (S7682)
  • Shell local params (S7679)

Users can extend/override rules via ~/.aidevops/agents/configs/ttsr-rules.json.

Testing

  • All hooks register correctly (9 total including existing hooks)
  • System prompt injection: 7 rules injected
  • Message transform: correctly detects violations, no false positives
  • Text complete: correctly flags violations, no false positives
  • Edge cases: empty messages, null parts, user-only messages all handled
  • Performance: 8ms for 100 iterations on 25KB text
  • Consistent IDs in synthetic correction messages

Ref #2128

Summary by CodeRabbit

  • New Features
    • Introduced Text-to-Speech Rules (TTSR) system for content compliance and enforcement.
    • Rules apply across system prompts, chat messages, and text outputs with violation detection and corrective feedback.
    • Support for custom user-defined rules with built-in defaults available.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

Walkthrough

Phase 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

Cohort / File(s) Summary
Phase 5 TTSR Implementation
.agents/plugins/opencode-aidevops/index.mjs
Introduces TTSR rule infrastructure (TTSR_RULES_PATH, BUILTIN_TTSR_RULES, loadTtsrRules) and three plugin hooks: systemTransformHook injects rules into system prompt, messagesTransformHook scans assistant messages for violations and injects corrective feedback, textCompleteHook performs post-hoc violation detection and logging. Includes helper functions for text extraction and violation scanning. Exposes hooks via plugin export.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Poem

🎤 Phase Five arrives with rules so refined,
Three hooks transform what's in mind,
Soft TTSR catches every word with care,
Violations detected in the air,
From system prompt to completion's end,
Speech rules now seamlessly blend! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: wiring TTSR rules into OpenCode plugin hooks, which matches the core objective of implementing Soft TTSR functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/t1304

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 38 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Sun Feb 22 05:07:56 UTC 2026: Code review monitoring started
Sun Feb 22 05:07:57 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 38

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 38
  • VULNERABILITIES: 0

Generated on: Sun Feb 22 05:07:59 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

@marcusquinn marcusquinn marked this pull request as ready for review February 22, 2026 05:09
@gemini-code-assist
Copy link

Summary of Changes

Hello @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

  • Introduced Soft TTSR (Text-to-Speech Rules): Implemented a new system for preventative, corrective, and observational enforcement of coding standards and best practices within the AI agent's interactions. This system leverages three distinct plugin hooks to guide the AI's behavior and provide feedback.
  • Defined Built-in TTSR Rules: Added a comprehensive set of default rules covering common pitfalls such as using Glob for file discovery, cat for file reading, editing files without prior reading, exposing credentials, modifying files on main/master without pre-edit-check.sh, and shell scripting best practices (explicit returns, local parameters).
  • Configurable and Extensible Rules: Enabled loading of user-defined TTSR rules from a ttsr-rules.json file, allowing users to override or extend built-in rules based on their specific needs. Rules are defined with an ID, description, regex pattern, correction message, severity, and a system prompt instruction.
  • Preventative Rule Enforcement: Integrated a system.transform hook that injects active TTSR rules into the system prompt before each LLM call, proactively guiding the model to adhere to standards.
  • Corrective Feedback Mechanism: Implemented a messages.transform hook that scans recent assistant outputs for rule violations. If violations are found, a synthetic user message containing correction context is injected into the message history, allowing the model to learn and self-correct in subsequent turns.
  • Observational Violation Detection: Added a text.complete hook that detects rule violations post-hoc in completed text parts. This hook logs violations for observability, appends violation markers as comments to the output, and records patterns for tracking, without altering the already generated text.
  • Plugin Integration: Wired the new TTSR hooks (experimental.chat.system.transform, experimental.chat.messages.transform, experimental.text.complete) into the AidevopsPlugin export, making them an active part of the agent's operational flow.
Changelog
  • .agents/plugins/opencode-aidevops/index.mjs
    • Added the core logic for Soft TTSR, including rule definitions, loading mechanisms, and violation scanning functions.
    • Implemented systemTransformHook to inject rules into the system prompt.
    • Implemented messagesTransformHook to provide corrective feedback by injecting synthetic user messages.
    • Implemented textCompleteHook for post-hoc violation detection, logging, and pattern tracking.
    • Updated the plugin description to include Soft TTSR as a new phase.
    • Registered the three new TTSR hooks within the AidevopsPlugin export.
Activity
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 38 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Sun Feb 22 05:10:09 UTC 2026: Code review monitoring started
Sun Feb 22 05:10:09 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 38

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 38
  • VULNERABILITIES: 0

Generated on: Sun Feb 22 05:10:11 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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
  1. The repository style guide (line 50) states that 2>/dev/null is 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])",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. The repository style guide (line 11) mandates using the local var="$1" pattern in functions. The current regex does not correctly enforce this. (link)

Comment on lines +1716 to +1718
} catch {
console.error("[aidevops] Failed to parse TTSR rules file — using built-in rules only");
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
} 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);
}

Comment on lines +1740 to +1742
} catch {
return { matched: false, matches: [] };
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.
@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 38 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Sun Feb 22 07:52:36 UTC 2026: Code review monitoring started
Sun Feb 22 07:52:37 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 38

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 38
  • VULNERABILITIES: 0

Generated on: Sun Feb 22 07:52:39 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

@sonarqubecloud
Copy link

coderabbitai[bot]
coderabbitai bot previously requested changes Feb 22, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
.agents/plugins/opencode-aidevops/index.mjs (2)

1407-1422: Bound the {50,} quantifier in shell-explicit-returns to 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-params has two .* inside its negative lookahead, though the m flag 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: messagesTransformHook has 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.

Comment on lines +1470 to +1483
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: [] };
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
fd -p "index.mjs" | head -5

Repository: marcusquinn/aidevops

Length of output: 107


🏁 Script executed:

# Read the checkRule function and surrounding context
sed -n '1450,1500p' .agents/plugins/opencode-aidevops/index.mjs

Repository: 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 2

Repository: 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 5

Repository: marcusquinn/aidevops

Length of output: 255


🏁 Script executed:

# Check for Codacy configuration files
fd -p "codacy|eslint|\.codacy" -type f

Repository: 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 3

Repository: 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 -100

Repository: marcusquinn/aidevops

Length of output: 2746


🏁 Script executed:

# Check for Codacy or ESLint configuration
fd -p "\.codacyrc|\.eslintrc|codacy.yaml|\.eslint" -type f

Repository: 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 -60

Repository: 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 -20

Repository: 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.mjs

Repository: 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 -40

Repository: marcusquinn/aidevops

Length of output: 974


🏁 Script executed:

# Look at what extractTextFromParts returns
rg "function extractTextFromParts" .agents/plugins/opencode-aidevops/index.mjs -A 15

Repository: 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 1

Repository: 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 2

Repository: 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 -20

Repository: 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 -10

Repository: 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/null

Repository: 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 -50

Repository: 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.mjs

Repository: 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 -100

Repository: 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.

Suggested change
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.

Comment on lines +1604 to +1614
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: "",
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +1634 to +1665
* 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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

@marcusquinn
Copy link
Owner Author

Resolves #2142.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant