t1303: Soft TTSR rule engine — rule loader and rule files#2136
t1303: Soft TTSR rule engine — rule loader and rule files#2136marcusquinn merged 3 commits intomainfrom
Conversation
Chose markdown-with-YAML-frontmatter format for rules — matches existing agent file conventions in .agents/. Rule loader is a standalone shell script that discovers, parses, and checks rules without external dependencies.
… handling (t1303)
Parser reads values literally — no YAML escape interpretation. Rule authors
write raw ERE regex without quotes or double-escaping. Fixed ${var^^} bashism
(requires bash 4+) and stdin "-" argument parsing.
WalkthroughThis PR establishes a comprehensive TTSR (Think-Then-Self-Reflect) rule system for AI agent output validation. It introduces a framework documentation, five guardrail rules covering security and operational best practices, and a Bash script to load, parse, and enforce rules against agent output with state tracking. Changes
Sequence DiagramsequenceDiagram
participant Agent as Agent Output
participant Loader as Rule Loader
participant Rules as Rules Directory
participant State as State File
participant Output as Correction Output
Agent->>Loader: check(output_text, turn_number)
Loader->>Rules: read rules/*.md
Loader->>Loader: parse YAML frontmatter & rule bodies
Loader->>State: load last-fired turn per rule
Loader->>Loader: for each enabled rule: evaluate ttsr_trigger regex
alt Rule matches & repeat policy satisfied
Loader->>State: record current turn as last-fired
State-->>Loader: state updated
Loader->>Output: emit correction (id, severity, body)
else No match or policy blocks
Loader->>Output: skip rule
end
Output-->>Agent: matched corrections in text/JSON
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Suggested Labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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 04:37:21 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 foundational soft 'Think-Then-Self-Reflect' (TTSR) rule engine. Its primary purpose is to enhance AI agent reliability and adherence to best practices by automatically identifying undesirable patterns in their output and injecting corrective guidance. This system provides a structured way to enforce operational and security policies without direct code changes to the AI agent itself. 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 04:38:17 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 rule engine (TTSR) for providing feedback on AI agent output, which is a great addition for improving agent behavior. The implementation includes a rule loader script and several example rules. The script is well-structured, but I've identified several critical and medium severity issues. The most significant concerns are around unsafe manual JSON generation which can lead to invalid output, suppression of important error messages that can mask configuration issues, and a design flaw in the state management that prevents state from persisting correctly across script invocations. These points are reinforced by established repository rules. I've provided detailed comments and suggestions to address these points, primarily recommending the use of jq for safe JSON handling and removing error suppression to improve robustness.
|
|
||
| # Default rules directory: relative to repo root (one level up from scripts/) | ||
| DEFAULT_RULES_DIR="${SCRIPT_DIR}/../rules" | ||
| DEFAULT_STATE_FILE="/tmp/ttsr-state-$$" |
There was a problem hiding this comment.
The default state file is named /tmp/ttsr-state-$$. This creates a new, unique state file for every invocation of the script, which prevents state (like for the once and after-gap repeat policies) from being preserved across multiple check calls within the same session. The state file name should be stable within a session, for example, based on the parent process ID ($PPID) or a session ID passed via an environment variable.
Additionally, if a file in /tmp is intended to be temporary, the style guide requires it to be cleaned up via a trap. The current implementation lacks this. Given the stateful nature of the rules, a more persistent and session-stable file path would be more appropriate than a PID-based temporary file.
References
- Temporary files must be cleaned up using a
trapon script exit. The default state file in/tmpis not cleaned up. (link) - For resource cleanup in shell scripts, use the established project pattern: use
_save_cleanup_scope,trap '_run_cleanups' RETURN, andpush_cleanupfor robust cleanup on any exit path, and also include explicit manual cleanup at the end of the normal execution path as a 'fast-path'.
| printf ' {"id":"%s","trigger":"%s","severity":"%s","repeat_policy":"%s","gap_turns":%s,"enabled":%s,"tags":"%s","file":"%s"}' \ | ||
| "$rule_id" "$rule_trigger" "$rule_severity" "$rule_repeat_policy" \ | ||
| "$rule_gap_turns" "$rule_enabled" "$rule_tags" "$rule_file" |
There was a problem hiding this comment.
Manually constructing JSON with printf is unsafe. If any of the variables contain special characters like quotes or backslashes, it will produce invalid JSON. It's much safer to use jq with --arg and --argjson to ensure all values are correctly escaped.
| printf ' {"id":"%s","trigger":"%s","severity":"%s","repeat_policy":"%s","gap_turns":%s,"enabled":%s,"tags":"%s","file":"%s"}' \ | |
| "$rule_id" "$rule_trigger" "$rule_severity" "$rule_repeat_policy" \ | |
| "$rule_gap_turns" "$rule_enabled" "$rule_tags" "$rule_file" | |
| printf ' %s' "$(jq -c -n \ | |
| --arg id "$rule_id" \ | |
| --arg trigger "$rule_trigger" \ | |
| --arg severity "$rule_severity" \ | |
| --arg repeat_policy "$rule_repeat_policy" \ | |
| --argjson gap_turns "$rule_gap_turns" \ | |
| --argjson enabled "$rule_enabled" \ | |
| --arg tags "$rule_tags" \ | |
| --arg file "$rule_file" \ | |
| '{id: $id, trigger: $trigger, severity: $severity, repeat_policy: $repeat_policy, gap_turns: $gap_turns, enabled: $enabled, tags: $tags, file: $file}')" |
References
- In shell scripts, use
jq --argfor strings and--argjsonfor other JSON types (like numbers) to safely pass variables into ajqfilter. This avoids syntax errors if the variables contain special characters.
| fi | ||
|
|
||
| # Check if trigger matches the output text | ||
| if printf '%s' "$output_text" | grep -qE "$rule_trigger" 2>/dev/null; then |
There was a problem hiding this comment.
Suppressing grep's standard error with 2>/dev/null can hide important errors, such as an invalid regular expression in a rule file. This would cause the rule to silently fail to match. Since the if statement correctly handles the "no match" case (exit code 1), the error suppression is not needed and should be removed to make debugging easier.
| if printf '%s' "$output_text" | grep -qE "$rule_trigger" 2>/dev/null; then | |
| if printf '%s' "$output_text" | grep -qE "$rule_trigger"; then |
References
- Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.
- In shell scripts with 'set -e' enabled, use '|| true' to prevent the script from exiting when a command like 'jq' fails on an optional lookup. Do not suppress stderr with '2>/dev/null' so that actual syntax or system errors remain visible for debugging.
| if [[ "$format" == "json" ]]; then | ||
| if [[ "$matched" -eq 1 ]]; then | ||
| corrections='[' | ||
| else | ||
| corrections="${corrections}," | ||
| fi | ||
| # Escape body for JSON (backslashes, quotes, newlines) | ||
| local escaped_body | ||
| escaped_body="$(printf '%s' "$rule_body" | awk ' | ||
| BEGIN { ORS="" } | ||
| { | ||
| gsub(/\\/, "\\\\") | ||
| gsub(/"/, "\\\"") | ||
| if (NR > 1) printf "\\n" | ||
| printf "%s", $0 | ||
| } | ||
| ')" | ||
| corrections="${corrections}{\"id\":\"${rule_id}\",\"severity\":\"${rule_severity}\",\"body\":\"${escaped_body}\"}" | ||
| else | ||
| local severity_upper | ||
| severity_upper="$(printf '%s' "$rule_severity" | tr '[:lower:]' '[:upper:]')" | ||
| corrections="${corrections}--- [${severity_upper}] Rule: ${rule_id} ---"$'\n'"${rule_body}"$'\n' | ||
| fi |
There was a problem hiding this comment.
The current implementation for creating JSON output is unsafe. It manually builds JSON strings and uses a custom awk script for escaping, which is incomplete (it doesn't handle all special characters like tabs or control characters) and can produce invalid JSON. This also applies to the id and severity fields, which are not escaped at all. A much more robust and secure approach is to use jq to construct the JSON objects, which correctly handles all necessary escaping.
| if [[ "$format" == "json" ]]; then | |
| if [[ "$matched" -eq 1 ]]; then | |
| corrections='[' | |
| else | |
| corrections="${corrections}," | |
| fi | |
| # Escape body for JSON (backslashes, quotes, newlines) | |
| local escaped_body | |
| escaped_body="$(printf '%s' "$rule_body" | awk ' | |
| BEGIN { ORS="" } | |
| { | |
| gsub(/\\/, "\\\\") | |
| gsub(/"/, "\\\"") | |
| if (NR > 1) printf "\\n" | |
| printf "%s", $0 | |
| } | |
| ')" | |
| corrections="${corrections}{\"id\":\"${rule_id}\",\"severity\":\"${rule_severity}\",\"body\":\"${escaped_body}\"}" | |
| else | |
| local severity_upper | |
| severity_upper="$(printf '%s' "$rule_severity" | tr '[:lower:]' '[:upper:]')" | |
| corrections="${corrections}--- [${severity_upper}] Rule: ${rule_id} ---"$'\n'"${rule_body}"$'\n' | |
| fi | |
| if [[ "$format" == "json" ]]; then | |
| local json_correction | |
| json_correction="$(jq -n \ | |
| --arg id "$rule_id" \ | |
| --arg severity "$rule_severity" \ | |
| --arg body "$rule_body" \ | |
| '{id: $id, severity: $severity, body: $body}')" | |
| if [[ "$matched" -eq 1 ]]; then | |
| corrections="[${json_correction}" | |
| else | |
| corrections="${corrections},${json_correction}" | |
| fi | |
| else | |
| local severity_upper | |
| severity_upper="$(printf '%s' "$rule_severity" | tr '[:lower:]' '[:upper:]')" | |
| corrections="${corrections}--- [${severity_upper}] Rule: ${rule_id} ---$\n'"${rule_body}"$\n'" | |
| fi |
References
- In shell scripts, use
jq --argfor strings and--argjsonfor other JSON types (like numbers) to safely pass variables into ajqfilter. This avoids syntax errors if the variables contain special characters.
| # ============================================================================= | ||
|
|
||
| log_error() { | ||
| local msg="$1" |
There was a problem hiding this comment.
The repository style guide requires declaring and assigning local variables from function arguments in separate steps to ensure exit code safety. This should be applied here and in other functions throughout the script that accept arguments.
| local msg="$1" | |
| local msg | |
| msg="$1" |
References
- The style guide specifies that local variables from function arguments should be declared and assigned separately (e.g.,
local var; var="$1") for exit code safety. (link) - When declaring and assigning a local variable in shell scripts, separate the 'local' declaration from the assignment if command substitution is involved, to ensure exit code safety.
- When declaring and assigning local variables in shell scripts, separate the
localdeclaration from the assignment if command substitution is involved, to ensure exit code safety. For consistency, this pattern should be maintained even for simple assignments.
| fi | ||
|
|
||
| local result | ||
| result="$(grep "^${rule_id}:" "$state_file" 2>/dev/null | tail -1 | cut -d: -f2)" || true |
There was a problem hiding this comment.
The use of 2>/dev/null with grep suppresses all error messages, which can hide issues like a malformed state file or permission errors. Since || true is already used to handle the case where grep finds no matches, the 2>/dev/null is not necessary for that purpose and can be safely removed to improve error visibility.
| result="$(grep "^${rule_id}:" "$state_file" 2>/dev/null | tail -1 | cut -d: -f2)" || true | |
| result="$(grep "^${rule_id}:" "$state_file" | tail -1 | cut -d: -f2)" || true |
References
- Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.
- In shell scripts with 'set -e' enabled, use '|| true' to prevent the script from exiting when a command like 'jq' fails on an optional lookup. Do not suppress stderr with '2>/dev/null' so that actual syntax or system errors remain visible for debugging.
- Avoid using
2>/dev/nullto suppress errors on file operations if the file's existence has already been verified by a preceding check (e.g.,[[ -f "$file" ]]or an early return). This practice is redundant for 'file not found' errors and can mask other important issues like permissions problems.
|
|
||
| # Remove old entry for this rule, append new | ||
| if [[ -f "$state_file" ]]; then | ||
| grep -v "^${rule_id}:" "$state_file" >"${state_file}.tmp" 2>/dev/null || true |
There was a problem hiding this comment.
Using 2>/dev/null here hides potential errors from grep, such as problems reading the state file. The || true guard is sufficient to prevent the script from exiting if no lines are filtered. Removing the redirection will make the script more robust by revealing underlying file system or permission issues.
| grep -v "^${rule_id}:" "$state_file" >"${state_file}.tmp" 2>/dev/null || true | |
| grep -v "^${rule_id}:" "$state_file" >"${state_file}.tmp" || true |
References
- Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.
- In shell scripts with 'set -e' enabled, use '|| true' to prevent the script from exiting when a command like 'jq' fails on an optional lookup. Do not suppress stderr with '2>/dev/null' so that actual syntax or system errors remain visible for debugging.
- Avoid using
2>/dev/nullto suppress errors on file operations if the file's existence has already been verified by a preceding check (e.g.,[[ -f "$file" ]]or an early return). This practice is redundant for 'file not found' errors and can mask other important issues like permissions problems.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
.agents/rules/README.md (1)
46-46: Testing commands should specify the script path.Users following this guide may not have
ttsr-rule-loader.shon their PATH. Consider showing the relative path from the repo root.📝 Suggested clarification
-4. Test with: `ttsr-rule-loader.sh list` and `ttsr-rule-loader.sh check <file>` +4. Test with: `.agents/scripts/ttsr-rule-loader.sh list` and `echo "test text" | .agents/scripts/ttsr-rule-loader.sh check -`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.agents/rules/README.md at line 46, Update the README's testing commands to show the script path so users not on PATH can run them; replace "ttsr-rule-loader.sh list" and "ttsr-rule-loader.sh check <file>" with the relative-path forms (for example "./ttsr-rule-loader.sh list" and "./ttsr-rule-loader.sh check <file>") to make the invocation explicit from the repo root..agents/scripts/ttsr-rule-loader.sh (1)
265-278: State lookup uses unanchored regex withrule_id— considergrep -Ffor robustness.
grep "^${rule_id}:"treatsrule_idas a BRE pattern. Current rule IDs (alphanumeric + hyphens) are safe, but if a future rule ID ever contains.or other regex metacharacters, it could match unintended state entries. Since you already have the:delimiter, a fixed-string match with post-filter would be more defensive.♻️ Safer fixed-string alternative
- result="$(grep "^${rule_id}:" "$state_file" 2>/dev/null | tail -1 | cut -d: -f2)" || true + result="$(grep -F "${rule_id}:" "$state_file" 2>/dev/null | grep "^${rule_id}:" | tail -1 | cut -d: -f2)" || trueOr use
awkfor exact field matching:- result="$(grep "^${rule_id}:" "$state_file" 2>/dev/null | tail -1 | cut -d: -f2)" || true + result="$(awk -F: -v id="$rule_id" '$1 == id { val=$2 } END { if (val) print val }' "$state_file" 2>/dev/null)" || true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.agents/scripts/ttsr-rule-loader.sh around lines 265 - 278, The get_last_fired function uses grep "^${rule_id}:" which treats rule_id as a regex; change the lookup to a fixed-string/field-aware match to avoid accidental regex metacharacter matches. Replace the grep pipeline that sets result ("result="$(grep "^${rule_id}:" "$state_file" ... | tail -1 | cut -d: -f2)") with an awk-based lookup that uses -F: and matches the first field exactly (use -v id="$rule_id" '$1==id {print $2}' on "$state_file" and then tail -1) so rule_id and state_file are handled safely and robustly.
🤖 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/rules/no-hardcoded-secrets.md:
- Line 3: Update the ttsr_trigger regex to use POSIX character class [[:space:]]
instead of \s for portability: locate the ttsr_trigger definition (the pattern
string
"(api[_-]?key|password|secret|token)\s*[:=]\s*['\"][A-Za-z0-9+/=_-]{16,}") and
replace each \s with [[:space:]] so the pattern becomes portable in ERE on
macOS/BSD while preserving the same optional-space qualifiers around the
separator and quotes.
In @.agents/rules/no-todo-edit-by-worker.md:
- Line 3: The ttsr_trigger regex currently lets the second and third alternates
match without the Edit|Write guard; update the ttsr_trigger so the file path
alternatives are grouped together after the initial Edit|Write.* portion (e.g.,
wrap the paths in a single parenthesized group or non-capturing group) so that
Edit|Write applies to all three branches (references: ttsr_trigger).
In @.agents/scripts/ttsr-rule-loader.sh:
- Around line 546-552: The --turn case currently assigns current_turn without
validation; add a positive-integer check for the provided value (the argument
currently read into "$2") before setting current_turn so subsequent arithmetic
(e.g., in cmd_check where current_turn is used in $((current_turn -
last_fired))) cannot fail. Implement a guard using a numeric regex (e.g.,
^[0-9]+$) and ensure the value is >0; if the check fails call log_error with a
clear message and invoke usage/exit, otherwise set current_turn and shift 2 as
before. Make sure to reference the same --turn case handling and the
current_turn variable so reviewers can locate the change.
- Around line 367-371: The JSON output is invalid because rule_trigger (and
potentially rule_tags/rule_file) are interpolated raw; extract the existing awk
JSON escaper used in cmd_check (lines ~436–444) into a reusable shell function
(e.g., json_escape()) and call it in cmd_list to escape rule_trigger (and
rule_tags, rule_file) before printing; then replace direct interpolations
"$rule_trigger" "$rule_tags" "$rule_file" in the printf with the escaped
variables so the JSON strings contain properly backslash-escaped backslashes and
quotes.
---
Nitpick comments:
In @.agents/rules/README.md:
- Line 46: Update the README's testing commands to show the script path so users
not on PATH can run them; replace "ttsr-rule-loader.sh list" and
"ttsr-rule-loader.sh check <file>" with the relative-path forms (for example
"./ttsr-rule-loader.sh list" and "./ttsr-rule-loader.sh check <file>") to make
the invocation explicit from the repo root.
In @.agents/scripts/ttsr-rule-loader.sh:
- Around line 265-278: The get_last_fired function uses grep "^${rule_id}:"
which treats rule_id as a regex; change the lookup to a fixed-string/field-aware
match to avoid accidental regex metacharacter matches. Replace the grep pipeline
that sets result ("result="$(grep "^${rule_id}:" "$state_file" ... | tail -1 |
cut -d: -f2)") with an awk-based lookup that uses -F: and matches the first
field exactly (use -v id="$rule_id" '$1==id {print $2}' on "$state_file" and
then tail -1) so rule_id and state_file are handled safely and robustly.
| @@ -0,0 +1,15 @@ | |||
| --- | |||
| id: no-hardcoded-secrets | |||
| ttsr_trigger: (api[_-]?key|password|secret|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{16,} | |||
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
macOS BSD grep -E support for \s escape sequence ERE
💡 Result:
\s is not POSIX ERE. In POSIX, most backslash escapes other than a small set are undefined, so you can’t rely on grep -E '\s' being whitespace across platforms. (gnu.org)
On macOS’s /usr/bin/grep (BSD grep) specifically:
- macOS’s underlying regex engine documents an “enhanced” mode that adds shorthand classes such as
\d,\w,\s, etc. (seere_format(7)“ENHANCED FEATURES”). (unix.com) - Whether those enhanced escapes work depends on whether the tool was built to enable that enhanced mode; it’s not guaranteed just because you used
-E(extended). (stackoverflow.com)
Portable recommendation (works on macOS BSD grep and everywhere POSIX):
- Use the POSIX character class instead of
\s:- whitespace:
[[:space:]] - non-whitespace:
[^[:space:]]
- whitespace:
BSD-vs-GNU difference summary: treating \s as whitespace is commonly a GNU grep extension, and BSD grep may not treat it that way—so prefer [[:space:]] for portability. (unix.stackexchange.com)
Citations:
- 1: https://www.gnu.org/s/emacs/manual/html_node/elisp/POSIX-Regexps.html?utm_source=openai
- 2: https://www.unix.com/man_page/osx/7/re_format/?utm_source=openai
- 3: https://stackoverflow.com/questions/12178924/os-x-sed-e-doesnt-accept-extended-regular-expressions?utm_source=openai
- 4: https://unix.stackexchange.com/questions/617010/why-is-egrep-ignoring-the-negative-whitespace?utm_source=openai
🌐 Web query:
does grep -E support \s on macOS older versions
💡 Result:
No—on older (and current) macOS, the default /usr/bin/grep -E uses POSIX ERE (BSD grep), and \s is not a POSIX ERE escape (a backslash before an “ordinary” character like s is unspecified/undefined in POSIX ERE), so it typically won’t mean “whitespace.” [1]
Use POSIX character classes instead:
grep -E '[[:space:]]+' file
grep -E '^[[:space:]]*$' fileIf you specifically need \s, you generally need PCRE, e.g. GNU grep’s PCRE mode:
ggrep -P '\s+'…but Apple’s BSD grep does not provide -P by default; you’d install GNU grep (e.g., via Homebrew) to get it. [2]
Sources:
[1] POSIX ERE escapes: backslash before ordinary char is undefined/unspecified (POSIX ERE syntax summary) https://www.classes.cs.uchicago.edu/archive/2013/fall/51025-1/boost_1_50_0/libs/regex/doc/html/boost_regex/syntax/basic_extended.html
[2] macOS ships BSD tools by default; install GNU grep (Homebrew) to get GNU features like ggrep / -P workflows https://stackoverflow.com/questions/16658333/grep-p-no-longer-works-how-can-i-rewrite-my-searches and https://johnk.dev/articles/mac-os-tools.html
🏁 Script executed:
# Check what grep is available and test \s support
echo "test string" | grep -E '\s' 2>&1 || echo "Failed"
echo "---"
# Also check if [[:space:]] works as fallback
echo "test string" | grep -E '[[:space:]]' && echo "POSIX char class works"Repository: marcusquinn/aidevops
Length of output: 114
Replace \s with POSIX character class for macOS compatibility.
The trigger uses \s in ERE mode, which is not POSIX standard and not guaranteed on macOS BSD grep. While some systems have enhanced regex features, this creates a portability risk where the security rule may fail silently on stricter POSIX ERE implementations, particularly on older macOS versions.
Use [[:space:]] instead to ensure consistent behavior across all systems:
Portable ERE fix
-ttsr_trigger: (api[_-]?key|password|secret|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{16,}
+ttsr_trigger: (api[_-]?key|password|secret|token)[[:space:]]*[:=][[:space:]]*['"][A-Za-z0-9+/=_-]{16,}📝 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.
| ttsr_trigger: (api[_-]?key|password|secret|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{16,} | |
| ttsr_trigger: (api[_-]?key|password|secret|token)[[:space:]]*[:=][[:space:]]*['"][A-Za-z0-9+/=_-]{16,} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/rules/no-hardcoded-secrets.md at line 3, Update the ttsr_trigger
regex to use POSIX character class [[:space:]] instead of \s for portability:
locate the ttsr_trigger definition (the pattern string
"(api[_-]?key|password|secret|token)\s*[:=]\s*['\"][A-Za-z0-9+/=_-]{16,}") and
replace each \s with [[:space:]] so the pattern becomes portable in ERE on
macOS/BSD while preserving the same optional-space qualifiers around the
separator and quotes.
| @@ -0,0 +1,14 @@ | |||
| --- | |||
| id: no-todo-edit-by-worker | |||
| ttsr_trigger: (Edit|Write).*TODO\.md|todo/PLANS\.md|todo/tasks/ | |||
There was a problem hiding this comment.
Regex alternation precedence bug — second and third branches lack the Edit|Write guard.
ERE alternation | has lower precedence than concatenation, so this trigger:
(Edit|Write).*TODO\.md|todo/PLANS\.md|todo/tasks/
is parsed as three independent alternatives:
(Edit|Write).*TODO\.md✅todo/PLANS\.md— matches any mention, including readstodo/tasks/— matches any mention, including reads
Since this is severity: error + repeat_policy: always, every AI output that merely references todo/PLANS.md or todo/tasks/ will fire a STOP directive — even for read-only operations.
🐛 Fix: Group the file paths inside the Edit/Write guard
-ttsr_trigger: (Edit|Write).*TODO\.md|todo/PLANS\.md|todo/tasks/
+ttsr_trigger: (Edit|Write).*(TODO\.md|todo/PLANS\.md|todo/tasks/)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/rules/no-todo-edit-by-worker.md at line 3, The ttsr_trigger regex
currently lets the second and third alternates match without the Edit|Write
guard; update the ttsr_trigger so the file path alternatives are grouped
together after the initial Edit|Write.* portion (e.g., wrap the paths in a
single parenthesized group or non-capturing group) so that Edit|Write applies to
all three branches (references: ttsr_trigger).
| if [[ "$format" == "json" ]]; then | ||
| [[ "$first" -eq 1 ]] && first=0 || printf ',\n' | ||
| printf ' {"id":"%s","trigger":"%s","severity":"%s","repeat_policy":"%s","gap_turns":%s,"enabled":%s,"tags":"%s","file":"%s"}' \ | ||
| "$rule_id" "$rule_trigger" "$rule_severity" "$rule_repeat_policy" \ | ||
| "$rule_gap_turns" "$rule_enabled" "$rule_tags" "$rule_file" |
There was a problem hiding this comment.
JSON output produces invalid JSON — rule_trigger is not escaped.
Rule triggers contain ERE regex with backslashes (\b, \., \s) that are interpolated raw into JSON strings. This produces:
- Semantically wrong values:
\bis a valid JSON escape for backspace, not a literal\b - Invalid JSON:
\.and\sare not valid JSON escape sequences — strict parsers will reject
The rule_body gets proper awk-based escaping in cmd_check (line 436–444), but rule_trigger (and other fields) in cmd_list do not.
🐛 Proposed fix — apply the same awk escaping to trigger (and reuse it)
Extract the awk escaper into a reusable function and apply it to all fields that may contain backslashes or quotes:
+# Escape a string for safe JSON embedding
+json_escape() {
+ printf '%s' "$1" | awk '
+ BEGIN { ORS="" }
+ {
+ gsub(/\\/, "\\\\")
+ gsub(/"/, "\\\"")
+ if (NR > 1) printf "\\n"
+ printf "%s", $0
+ }
+ '
+}
+Then in cmd_list, escape the trigger before interpolation:
+ local escaped_trigger
+ escaped_trigger="$(json_escape "$rule_trigger")"
printf ' {"id":"%s","trigger":"%s","severity":"%s","repeat_policy":"%s","gap_turns":%s,"enabled":%s,"tags":"%s","file":"%s"}' \
- "$rule_id" "$rule_trigger" "$rule_severity" "$rule_repeat_policy" \
+ "$rule_id" "$escaped_trigger" "$rule_severity" "$rule_repeat_policy" \
"$rule_gap_turns" "$rule_enabled" "$rule_tags" "$rule_file"Also apply to rule_tags and rule_file if they could contain special characters.
📝 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.
| if [[ "$format" == "json" ]]; then | |
| [[ "$first" -eq 1 ]] && first=0 || printf ',\n' | |
| printf ' {"id":"%s","trigger":"%s","severity":"%s","repeat_policy":"%s","gap_turns":%s,"enabled":%s,"tags":"%s","file":"%s"}' \ | |
| "$rule_id" "$rule_trigger" "$rule_severity" "$rule_repeat_policy" \ | |
| "$rule_gap_turns" "$rule_enabled" "$rule_tags" "$rule_file" | |
| if [[ "$format" == "json" ]]; then | |
| [[ "$first" -eq 1 ]] && first=0 || printf ',\n' | |
| local escaped_trigger | |
| escaped_trigger="$(json_escape "$rule_trigger")" | |
| printf ' {"id":"%s","trigger":"%s","severity":"%s","repeat_policy":"%s","gap_turns":%s,"enabled":%s,"tags":"%s","file":"%s"}' \ | |
| "$rule_id" "$escaped_trigger" "$rule_severity" "$rule_repeat_policy" \ | |
| "$rule_gap_turns" "$rule_enabled" "$rule_tags" "$rule_file" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/scripts/ttsr-rule-loader.sh around lines 367 - 371, The JSON output
is invalid because rule_trigger (and potentially rule_tags/rule_file) are
interpolated raw; extract the existing awk JSON escaper used in cmd_check (lines
~436–444) into a reusable shell function (e.g., json_escape()) and call it in
cmd_list to escape rule_trigger (and rule_tags, rule_file) before printing; then
replace direct interpolations "$rule_trigger" "$rule_tags" "$rule_file" in the
printf with the escaped variables so the JSON strings contain properly
backslash-escaped backslashes and quotes.
| --turn) | ||
| [[ $# -lt 2 ]] && { | ||
| log_error "--turn requires a value" | ||
| usage | ||
| } | ||
| current_turn="$2" | ||
| shift 2 |
There was a problem hiding this comment.
--turn value is not validated as a positive integer.
If a non-numeric value is passed (e.g., --turn abc), the $((current_turn - last_fired)) arithmetic on line 322 will produce a Bash error. Under set -e, this could terminate the script unexpectedly during cmd_check.
🛡️ Add integer validation
--turn)
[[ $# -lt 2 ]] && {
log_error "--turn requires a value"
usage
}
+ if ! [[ "$2" =~ ^[0-9]+$ ]]; then
+ log_error "--turn must be a positive integer"
+ usage
+ fi
current_turn="$2"
shift 2
;;As per coding guidelines: .agents/scripts/*.sh — "Reliability and robustness" and "Error recovery mechanisms."
📝 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.
| --turn) | |
| [[ $# -lt 2 ]] && { | |
| log_error "--turn requires a value" | |
| usage | |
| } | |
| current_turn="$2" | |
| shift 2 | |
| --turn) | |
| [[ $# -lt 2 ]] && { | |
| log_error "--turn requires a value" | |
| usage | |
| } | |
| if ! [[ "$2" =~ ^[0-9]+$ ]]; then | |
| log_error "--turn must be a positive integer" | |
| usage | |
| fi | |
| current_turn="$2" | |
| shift 2 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/scripts/ttsr-rule-loader.sh around lines 546 - 552, The --turn case
currently assigns current_turn without validation; add a positive-integer check
for the provided value (the argument currently read into "$2") before setting
current_turn so subsequent arithmetic (e.g., in cmd_check where current_turn is
used in $((current_turn - last_fired))) cannot fail. Implement a guard using a
numeric regex (e.g., ^[0-9]+$) and ensure the value is >0; if the check fails
call log_error with a clear message and invoke usage/exit, otherwise set
current_turn and shift 2 as before. Make sure to reference the same --turn case
handling and the current_turn variable so reviewers can locate the change.
Auto-dismissed: bot review does not block autonomous pipeline



Summary
.agents/rules/directory with TTSR (Think-Then-Self-Reflect) soft rule enginettsr-rule-loader.sh— discovers, parses, and checks rule files against AI outputDesign
Rules are markdown files with YAML frontmatter (matching existing agent file conventions):
ttsr_trigger: ERE regex matched against AI output textrepeat_policy:once|after-gap|always— controls firing frequencyseverity:info|warn|error— categorizes correction urgencyCommands:
list,check,show,reset. Supports text and JSON output formats.This is Phase 1 (soft TTSR — rules checked after output). Phase 2 will add real-time stream hook integration when supported.
Verification
Ref #2127
Summary by CodeRabbit
Release Notes
New Features
Documentation