Skip to content
Merged
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
359 changes: 357 additions & 2 deletions .agents/plugins/opencode-aidevops/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,353 @@

// Tool definitions extracted to tools.mjs — imported at top of file

// ---------------------------------------------------------------------------
// Phase 5: Soft TTSR — Rule Enforcement via Plugin Hooks (t1304)
// ---------------------------------------------------------------------------
// "Soft TTSR" (Text-to-Speech Rules) provides preventative enforcement of
// coding standards without stream-level interception (which OpenCode doesn't
// expose). Three hooks work together:
//
// 1. system.transform — inject active rules into system prompt (preventative)
// 2. messages.transform — scan prior assistant outputs for violations, inject
// correction context into message history (corrective)
// 3. text.complete — detect violations post-hoc and flag them (observational)
//
// Rules are data-driven: each rule is an object with id, description, a regex
// pattern to detect violations, and a correction message. Rules can be loaded
// from a config file or use the built-in defaults.

/**
* Path to optional user-defined TTSR rules file.
* JSON array of rule objects: { id, description, pattern, correction, severity }
* @type {string}
*/
const TTSR_RULES_PATH = join(AGENTS_DIR, "configs", "ttsr-rules.json");

/**
* Built-in TTSR rules — enforced by default.
* Each rule has:
* - id: unique identifier
* - description: human-readable explanation
* - pattern: regex string to detect violations in assistant output
* - correction: message injected when violation is detected
* - severity: "error" | "warn" | "info"
* - systemPrompt: instruction injected into system prompt (preventative)
*
* @type {Array<{id: string, description: string, pattern: string, correction: string, severity: string, systemPrompt: string}>}
*/
const BUILTIN_TTSR_RULES = [
{
id: "no-glob-for-discovery",
description: "Use git ls-files or fd instead of Glob/find for file discovery",
pattern: "(?:mcp_glob|Glob tool|use.*\\bGlob\\b.*to find|I'll use Glob)",
correction: "Use `git ls-files` or `fd` for file discovery, not Glob. Glob is a last resort when Bash is unavailable.",
severity: "warn",
systemPrompt: "File discovery: use `git ls-files '<pattern>'` for git-tracked files, `fd` for untracked. NEVER use Glob/find as primary discovery.",
},
{
id: "no-cat-for-reading",
description: "Use Read tool instead of cat/head/tail for file reading",
pattern: "(?:^|\\s)cat\\s+['\"]?[/~\\w]|\\bhead\\s+-n|\\btail\\s+-n",
correction: "Use the Read tool for file reading, not cat/head/tail. These are Bash commands that waste context.",
severity: "info",
systemPrompt: "Use the Read tool for file reading. Avoid cat/head/tail in Bash — they waste context tokens.",
},
{
id: "read-before-edit",
description: "Always Read a file before Edit or Write operations",
pattern: "(?:I'll edit|Let me edit|I'll write to|Let me write)(?:(?!I'll read|let me read|I've read|already read).){0,200}$",
correction: "ALWAYS Read a file before Edit/Write. These tools fail without a prior Read in this conversation.",
severity: "error",
systemPrompt: "ALWAYS Read a file before Edit or Write. These tools FAIL without a prior Read in this conversation.",
},
{
id: "no-credentials-in-output",
description: "Never expose credentials, API keys, or secrets in output",
pattern: "(?:api[_-]?key|secret|password|token)\\s*[:=]\\s*['\"][A-Za-z0-9+/=_-]{16,}['\"]",
correction: "SECURITY: Never expose credentials in output. Use `aidevops secret set NAME` for secure storage.",
severity: "error",
systemPrompt: "NEVER expose credentials, API keys, or secrets in output or logs.",
},
{
id: "pre-edit-check",
description: "Run pre-edit-check.sh before modifying files",
pattern: "(?:I'll (?:create|modify|edit|write)|Let me (?:create|modify|edit|write)).*(?:on main|on master)\\b",
correction: "Run pre-edit-check.sh before modifying files. NEVER edit on main/master branch.",
severity: "error",
systemPrompt: "Before ANY file modification: run pre-edit-check.sh. NEVER edit on main/master.",
},
{
id: "shell-explicit-returns",
description: "Shell functions must have explicit return statements",
pattern: "(?:function\\s+\\w+|\\w+\\s*\\(\\)\\s*\\{)(?:(?!return\\s+[0-9]).){50,}\\}",
correction: "Shell functions must have explicit `return 0` or `return 1` statements (SonarCloud S7682).",
severity: "warn",
systemPrompt: "Shell scripts: every function must have an explicit `return 0` or `return 1`.",
},
{
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)

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.",
},
];

/**
* Cached loaded rules (built-in + user-defined).
* @type {Array<object> | null}
*/
let _ttsrRules = null;

/**
* Load TTSR rules: built-in defaults merged with optional user-defined rules.
* User rules can override built-in rules by matching id.
* @returns {Array<{id: string, description: string, pattern: string, correction: string, severity: string, systemPrompt: string}>}
*/
function loadTtsrRules() {
if (_ttsrRules !== null) return _ttsrRules;

_ttsrRules = [...BUILTIN_TTSR_RULES];

const userContent = readIfExists(TTSR_RULES_PATH);
if (userContent) {
try {
const userRules = JSON.parse(userContent);
if (Array.isArray(userRules)) {
for (const rule of userRules) {
if (!rule.id || !rule.pattern) continue;
const existingIdx = _ttsrRules.findIndex((r) => r.id === rule.id);
if (existingIdx >= 0) {
_ttsrRules[existingIdx] = { ..._ttsrRules[existingIdx], ...rule };
} else {
_ttsrRules.push(rule);
}
}
}
} catch {
console.error("[aidevops] Failed to parse TTSR rules file — using built-in rules only");
}
Comment on lines +1456 to +1458

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

}

return _ttsrRules;
}

/**
* Check text against a single TTSR rule.
* @param {string} text - Text to check
* @param {object} rule - TTSR rule object
* @returns {{ matched: boolean, matches: string[] }}
*/
function checkRule(text, rule) {
try {
const regex = new RegExp(rule.pattern, "gim");

Check warning on line 1472 in .agents/plugins/opencode-aidevops/index.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

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

Check failure on line 1472 in .agents/plugins/opencode-aidevops/index.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.agents/plugins/opencode-aidevops/index.mjs#L1472

The `RegExp` constructor was called with a non-literal value.
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: [] };
}
Comment on lines +1480 to +1482

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: [] };
  }

}
Comment on lines +1470 to +1483
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.


/**
* Scan text for all TTSR rule violations.
* @param {string} text - Text to scan
* @returns {Array<{rule: object, matches: string[]}>}
*/
function scanForViolations(text) {
const rules = loadTtsrRules();
const violations = [];

for (const rule of rules) {
const result = checkRule(text, rule);
if (result.matched) {
violations.push({ rule, matches: result.matches });
}
}

return violations;
}

/**
* Hook: experimental.chat.system.transform (t1304)
*
* Injects active TTSR rules into the system prompt as preventative guidance.
* This runs before every LLM call, ensuring the model is aware of rules
* before generating output.
*
* @param {object} _input - { sessionID?: string, model: Model }
* @param {object} output - { system: string[] } (mutable)
*/
async function systemTransformHook(_input, output) {
const rules = loadTtsrRules();
if (rules.length === 0) return;

const ruleLines = rules
.filter((r) => r.systemPrompt)
.map((r) => `- ${r.systemPrompt}`);

if (ruleLines.length === 0) return;

output.system.push(
[
"## aidevops Quality Rules (enforced)",
"The following rules are actively enforced. Violations will be flagged.",
...ruleLines,
].join("\n"),
);
}

/**
* Extract text content from a Part array.
* Only extracts text from TextPart objects (type === "text").
* @param {Array<object>} parts - Array of Part objects
* @returns {string} Concatenated text content
*/
function extractTextFromParts(parts) {
if (!Array.isArray(parts)) return "";
return parts
.filter((p) => p && p.type === "text" && typeof p.text === "string")
.map((p) => p.text)
.join("\n");
}

/**
* Hook: experimental.chat.messages.transform (t1304)
*
* Scans previous assistant outputs for rule violations and injects
* correction context into the message history. This provides corrective
* feedback so the model learns from its own violations within the session.
*
* Strategy: scan the last N assistant messages (not all — performance).
* If violations are found, append a synthetic correction message to the
* message history so the model sees the feedback before generating.
*
* @param {object} _input - {}
* @param {object} output - { messages: { info: Message, parts: Part[] }[] } (mutable)
*/
async function messagesTransformHook(_input, output) {

Check warning on line 1561 in .agents/plugins/opencode-aidevops/index.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.agents/plugins/opencode-aidevops/index.mjs#L1561

Method messagesTransformHook has a cyclomatic complexity of 9 (limit is 8)
if (!output.messages || output.messages.length === 0) return;

// Scan the last 3 assistant messages for violations
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) {
// Deduplicate by rule id
if (!allViolations.some((av) => av.rule.id === v.rule.id)) {
allViolations.push(v);
}
}
}

if (allViolations.length === 0) return;

// Build correction context
const corrections = allViolations.map((v) => {
const severity = v.rule.severity === "error" ? "ERROR" : "WARNING";
return `[${severity}] ${v.rule.id}: ${v.rule.correction}`;
});

const correctionText = [
"[aidevops TTSR] Rule violations detected in recent output:",
...corrections,
"",
"Apply these corrections in your next response.",
].join("\n");

// Inject as a synthetic user message at the end of the history
// so the model sees the correction before generating its next response
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: "",
},
Comment on lines +1604 to +1614
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.

parts: [
{
id: `${correctionId}-part`,
sessionID,
messageID: correctionId,
type: "text",
text: correctionText,
synthetic: true,
},
],
});

qualityLog(
"INFO",
`TTSR messages.transform: injected ${allViolations.length} correction(s): ${allViolations.map((v) => v.rule.id).join(", ")}`,
);
}

/**
* 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");
Comment on lines +1634 to +1665
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.


// Record pattern for tracking
const patternTracker = join(SCRIPTS_DIR, "pattern-tracker-helper.sh");
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)

5000,
);
}
}

// ---------------------------------------------------------------------------
// Main Plugin Export
// ---------------------------------------------------------------------------
Expand All @@ -1341,7 +1688,10 @@
* 3. Quality hooks — full pre-commit pipeline (ShellCheck, return statements,
* positional params, secrets scan, markdown lint) on Write/Edit operations
* 4. Shell environment — aidevops paths and variables
* 5. Compaction context — preserves operational state across context resets
* 5. Soft TTSR — preventative rule enforcement via system prompt injection,
* corrective feedback via message history scanning, and post-hoc violation
* detection via text completion hooks (t1304)
* 6. Compaction context — preserves operational state across context resets
*
* MCP registration (Phase 2, t008.2):
* - Registers all known MCP servers from a data-driven registry
Expand Down Expand Up @@ -1371,7 +1721,12 @@
// Phase 4: Shell environment
"shell.env": shellEnvHook,

// Compaction context
// Phase 5: Soft TTSR — rule enforcement (t1304)
"experimental.chat.system.transform": systemTransformHook,
"experimental.chat.messages.transform": messagesTransformHook,
"experimental.text.complete": textCompleteHook,

// Compaction context (includes OMOC state when detected)
"experimental.session.compacting": async (input, output) =>
compactingHook(input, output, directory),
};
Expand Down
Loading