Add review/personas/*.json: Built-in personas (embedded via go:embed)
Add review/formatter.go: FormatMarkdownWithDisplay for persona display names
Update cmd/review-bot/main.go: --persona and --persona-file flags
Update .gitea/actions/review/action.yml: persona and persona-file inputs
Update README.md: Comprehensive persona documentation
Add comprehensive tests for all new functionality
Usage
# Built-in persona- uses:rodin/review-bot/.gitea/actions/review@v1with:reviewer-name:securitypersona:security...# Custom persona from JSON file- uses:rodin/review-bot/.gitea/actions/review@v1with:reviewer-name:tradingpersona-file:.review/personas/trading.json...
Testing
All existing tests pass
New tests for persona loading, validation, prompt generation, and formatting
go vet passes
Backwards Compatibility
No breaking changes. When --persona and --persona-file are both omitted, behavior is unchanged (uses generic review prompt).
## Summary
Implement role-based review personas that provide specialized review focus, addressing issue #51.
## Problem
Current review-bot performs generic code review. Every reviewer uses the same base prompt and evaluates the same concerns. This leads to:
1. **Redundancy** — Two reviewers often flag identical issues
2. **Gaps** — Generic reviewers miss specialized concerns (security, domain logic, architecture)
3. **Noise** — NITs about style mixed with critical security findings
4. **No ownership** — Findings lack clear domain attribution
## Solution
Add role-based personas with:
- **Identity** — Who am I? What is my expertise?
- **Focus** — What do I look for?
- **Scope boundaries** — What do I explicitly NOT comment on?
- **Severity calibration** — What counts as MAJOR/MINOR/NIT for MY domain?
## Built-in Personas
| Name | Focus |
|------|-------|
| `security` | Vulnerabilities, auth, secrets, injection attacks |
| `architect` | Design patterns, code organization, API contracts |
| `docs` | Documentation quality, API clarity, error messages |
## Changes
- Add `review/persona.go`: Persona struct + JSON loading
- Add `review/persona_prompt.go`: BuildPersonaSystemPrompt
- Add `review/personas/*.json`: Built-in personas (embedded via go:embed)
- Add `review/formatter.go`: FormatMarkdownWithDisplay for persona display names
- Update `cmd/review-bot/main.go`: --persona and --persona-file flags
- Update `.gitea/actions/review/action.yml`: persona and persona-file inputs
- Update `README.md`: Comprehensive persona documentation
- Add comprehensive tests for all new functionality
## Usage
```yaml
# Built-in persona
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: security
persona: security
...
# Custom persona from JSON file
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: trading
persona-file: .review/personas/trading.json
...
```
## Testing
- All existing tests pass
- New tests for persona loading, validation, prompt generation, and formatting
- go vet passes
## Backwards Compatibility
No breaking changes. When --persona and --persona-file are both omitted, behavior is unchanged (uses generic review prompt).
Closes #51
security-review-bot
requested review from security-review-bot 2026-05-10 09:51:28 +00:00
The persona feature is implemented cleanly and integrates with the existing prompt/budget workflow without introducing major security risks. One minor hardening opportunity exists around loading persona files from disk.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
cmd/review-bot/main.go
109
The --persona-file path is read directly from the filesystem without workspace or symlink checks, allowing arbitrary file reads on the runner if the input were ever user-controlled. Mirror the path traversal protections used for --system-prompt-file (resolve to absolute path within the workspace, EvalSymlinks, and reject paths escaping the workspace) to prevent unintended access.
Recommendation
APPROVE — Overall, the changes are sound and CI passed. To harden against potential path traversal or unintended file access, apply the same workspace-bounding and symlink resolution checks to persona-file that you already use for system-prompt-file. Specifically, resolve the absolute path under the repository workspace, call filepath.EvalSymlinks, and verify the resolved path remains within the workspace before reading. This mitigates accidental or malicious reads of arbitrary files on the runner. With that adjustment, the feature is safe to merge.
# Security Review
## Summary
The persona feature is implemented cleanly and integrates with the existing prompt/budget workflow without introducing major security risks. One minor hardening opportunity exists around loading persona files from disk.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `cmd/review-bot/main.go` | 109 | The --persona-file path is read directly from the filesystem without workspace or symlink checks, allowing arbitrary file reads on the runner if the input were ever user-controlled. Mirror the path traversal protections used for --system-prompt-file (resolve to absolute path within the workspace, EvalSymlinks, and reject paths escaping the workspace) to prevent unintended access. |
## Recommendation
**APPROVE** — Overall, the changes are sound and CI passed. To harden against potential path traversal or unintended file access, apply the same workspace-bounding and symlink resolution checks to persona-file that you already use for system-prompt-file. Specifically, resolve the absolute path under the repository workspace, call filepath.EvalSymlinks, and verify the resolved path remains within the workspace before reading. This mitigates accidental or malicious reads of arbitrary files on the runner. With that adjustment, the feature is safe to merge.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against d7d5151a*
[MINOR] The --persona-file path is read directly from the filesystem without workspace or symlink checks, allowing arbitrary file reads on the runner if the input were ever user-controlled. Mirror the path traversal protections used for --system-prompt-file (resolve to absolute path within the workspace, EvalSymlinks, and reject paths escaping the workspace) to prevent unintended access.
**[MINOR]** The --persona-file path is read directly from the filesystem without workspace or symlink checks, allowing arbitrary file reads on the runner if the input were ever user-controlled. Mirror the path traversal protections used for --system-prompt-file (resolve to absolute path within the workspace, EvalSymlinks, and reject paths escaping the workspace) to prevent unintended access.
Good addition of role-based personas with clean Go implementation and tests. However, the composite action YAML has a malformed inputs section that can break action parsing and misconfigure inputs.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
.gitea/actions/review/action.yml
74
Malformed inputs block: properties for system-prompt-file are misplaced under persona-file due to insertion. The keys 'description', 'required', and 'default' are duplicated under persona-file, and system-prompt-file lacks its properties. This can cause YAML parsing issues and incorrect input definitions.
2
[MINOR]
cmd/review-bot/main.go
110
Persona file path is read directly from --persona-file without the workspace/symlink traversal safeguards applied to system-prompt-file. For consistency and security, restrict persona-file to the workspace and guard against symlink traversal.
3
[NIT]
review/formatter.go
18
Title capitalization uses byte-based slicing (strings.ToUpper(headerName[:1]) + headerName[1:]) which is not Unicode-safe. Consider using rune-aware capitalization with utf8.DecodeRuneInString and unicode.ToTitle for correctness with non-ASCII display names.
Recommendation
REQUEST_CHANGES — Fix the malformed composite action YAML. Under inputs, keep the system-prompt-file's description/required/default directly beneath the system-prompt-file key, and ensure persona and persona-file each have their own properly scoped properties without duplicated keys. After correcting the YAML, consider applying the same workspace and symlink traversal checks to persona-file that are already used for system-prompt-file to avoid reading paths outside the workspace. Optionally, make the header capitalization in FormatMarkdown and FormatMarkdownWithDisplay Unicode-safe. Once the action.yml is fixed, the persona feature and associated Go code/tests look solid and should be good to merge.
~~Original review~~
**Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/55#pullrequestreview-2033) for up-to-date findings.
<details><summary>Previous findings (commit d7d5151a)</summary>
# Gpt Review
## Summary
Good addition of role-based personas with clean Go implementation and tests. However, the composite action YAML has a malformed inputs section that can break action parsing and misconfigure inputs.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `.gitea/actions/review/action.yml` | 74 | Malformed inputs block: properties for system-prompt-file are misplaced under persona-file due to insertion. The keys 'description', 'required', and 'default' are duplicated under persona-file, and system-prompt-file lacks its properties. This can cause YAML parsing issues and incorrect input definitions. |
| 2 | [MINOR] | `cmd/review-bot/main.go` | 110 | Persona file path is read directly from --persona-file without the workspace/symlink traversal safeguards applied to system-prompt-file. For consistency and security, restrict persona-file to the workspace and guard against symlink traversal. |
| 3 | [NIT] | `review/formatter.go` | 18 | Title capitalization uses byte-based slicing (strings.ToUpper(headerName[:1]) + headerName[1:]) which is not Unicode-safe. Consider using rune-aware capitalization with utf8.DecodeRuneInString and unicode.ToTitle for correctness with non-ASCII display names. |
## Recommendation
**REQUEST_CHANGES** — Fix the malformed composite action YAML. Under inputs, keep the system-prompt-file's description/required/default directly beneath the system-prompt-file key, and ensure persona and persona-file each have their own properly scoped properties without duplicated keys. After correcting the YAML, consider applying the same workspace and symlink traversal checks to persona-file that are already used for system-prompt-file to avoid reading paths outside the workspace. Optionally, make the header capitalization in FormatMarkdown and FormatMarkdownWithDisplay Unicode-safe. Once the action.yml is fixed, the persona feature and associated Go code/tests look solid and should be good to merge.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against d7d5151a*
</details>
<!-- review-bot:gpt -->
[MAJOR] Malformed inputs block: properties for system-prompt-file are misplaced under persona-file due to insertion. The keys 'description', 'required', and 'default' are duplicated under persona-file, and system-prompt-file lacks its properties. This can cause YAML parsing issues and incorrect input definitions.
**[MAJOR]** Malformed inputs block: properties for system-prompt-file are misplaced under persona-file due to insertion. The keys 'description', 'required', and 'default' are duplicated under persona-file, and system-prompt-file lacks its properties. This can cause YAML parsing issues and incorrect input definitions.
[MINOR] Persona file path is read directly from --persona-file without the workspace/symlink traversal safeguards applied to system-prompt-file. For consistency and security, restrict persona-file to the workspace and guard against symlink traversal.
**[MINOR]** Persona file path is read directly from --persona-file without the workspace/symlink traversal safeguards applied to system-prompt-file. For consistency and security, restrict persona-file to the workspace and guard against symlink traversal.
security-review-bot
requested review from security-review-bot 2026-05-10 09:58:22 +00:00
The new persona feature is well-scoped and CI passes, but there is a security-sensitive gap in path handling for persona files. Unlike system-prompt-file, persona-file paths are not constrained to the workspace or protected against symlink traversal, creating a potential path traversal/data exfiltration vector in CI environments.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
review/persona.go
33
LoadPersona reads an arbitrary file path from the local filesystem without any path normalization, workspace boundary checks, or symlink resolution. In CI, a misconfigured workflow or malicious change could cause the bot to read files outside the repository workspace and include their contents (via persona fields) in the LLM system prompt, risking data exfiltration. Mirror the strict workspace + symlink checks used for system-prompt-file.
2
[MINOR]
cmd/review-bot/main.go
105
The persona-file code path accepts and loads a file path directly into review.LoadPersona without the workspace-bound and symlink checks applied to system-prompt-file. While LoadPersona performs JSON parsing (reducing accidental leakage), adding the same path/symlink validation here would provide consistent defense-in-depth and reduce the risk of reading unintended files.
Recommendation
REQUEST_CHANGES — Restrict persona-file to repository workspace paths and defend against symlink traversal, matching the approach used for system-prompt-file. Specifically: resolve the runner workspace (GITHUB_WORKSPACE or CWD), join and Clean the provided persona-file path, verify it resides within the workspace, EvalSymlinks, and re-verify it remains within the workspace before reading. Consider logging a clear error and exiting if validation fails. This will prevent potential path traversal and unintended file reads, and avoid exfiltrating sensitive local files into LLM prompts. No other security-relevant issues were found in the changes.
# Security Review
## Summary
The new persona feature is well-scoped and CI passes, but there is a security-sensitive gap in path handling for persona files. Unlike system-prompt-file, persona-file paths are not constrained to the workspace or protected against symlink traversal, creating a potential path traversal/data exfiltration vector in CI environments.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `review/persona.go` | 33 | LoadPersona reads an arbitrary file path from the local filesystem without any path normalization, workspace boundary checks, or symlink resolution. In CI, a misconfigured workflow or malicious change could cause the bot to read files outside the repository workspace and include their contents (via persona fields) in the LLM system prompt, risking data exfiltration. Mirror the strict workspace + symlink checks used for system-prompt-file. |
| 2 | [MINOR] | `cmd/review-bot/main.go` | 105 | The persona-file code path accepts and loads a file path directly into review.LoadPersona without the workspace-bound and symlink checks applied to system-prompt-file. While LoadPersona performs JSON parsing (reducing accidental leakage), adding the same path/symlink validation here would provide consistent defense-in-depth and reduce the risk of reading unintended files. |
## Recommendation
**REQUEST_CHANGES** — Restrict persona-file to repository workspace paths and defend against symlink traversal, matching the approach used for system-prompt-file. Specifically: resolve the runner workspace (GITHUB_WORKSPACE or CWD), join and Clean the provided persona-file path, verify it resides within the workspace, EvalSymlinks, and re-verify it remains within the workspace before reading. Consider logging a clear error and exiting if validation fails. This will prevent potential path traversal and unintended file reads, and avoid exfiltrating sensitive local files into LLM prompts. No other security-relevant issues were found in the changes.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against e4cde3ed*
[MINOR] The persona-file code path accepts and loads a file path directly into review.LoadPersona without the workspace-bound and symlink checks applied to system-prompt-file. While LoadPersona performs JSON parsing (reducing accidental leakage), adding the same path/symlink validation here would provide consistent defense-in-depth and reduce the risk of reading unintended files.
**[MINOR]** The persona-file code path accepts and loads a file path directly into review.LoadPersona without the workspace-bound and symlink checks applied to system-prompt-file. While LoadPersona performs JSON parsing (reducing accidental leakage), adding the same path/symlink validation here would provide consistent defense-in-depth and reduce the risk of reading unintended files.
[MAJOR] LoadPersona reads an arbitrary file path from the local filesystem without any path normalization, workspace boundary checks, or symlink resolution. In CI, a misconfigured workflow or malicious change could cause the bot to read files outside the repository workspace and include their contents (via persona fields) in the LLM system prompt, risking data exfiltration. Mirror the strict workspace + symlink checks used for system-prompt-file.
**[MAJOR]** LoadPersona reads an arbitrary file path from the local filesystem without any path normalization, workspace boundary checks, or symlink resolution. In CI, a misconfigured workflow or malicious change could cause the bot to read files outside the repository workspace and include their contents (via persona fields) in the LLM system prompt, risking data exfiltration. Mirror the strict workspace + symlink checks used for system-prompt-file.
The persona feature is well-implemented with solid tests and CI is passing, but the composite action YAML has a structural error in the inputs section that can break input parsing. Additionally, there are a couple of minor polish items.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
.gitea/actions/review/action.yml
75
The inputs section is malformed: system-prompt-file is declared without any properties, and the subsequent description/required/default fields appear under persona-file, resulting in duplicate keys for persona-file and leaving system-prompt-file with a null value. In YAML this can cause the last duplicate key to win and may lead to GitHub/Gitea Actions rejecting or mis-parsing these inputs.
2
[MINOR]
cmd/review-bot/main.go
114
Persona file loading (--persona-file) accepts an arbitrary path without the same workspace-bound path validation that is applied to --system-prompt-file. For consistency and safety in CI environments, consider applying similar workspace containment checks to avoid accidentally reading outside the repository workspace.
3
[NIT]
review/formatter.go
60
The new FormatMarkdownWithDisplay largely duplicates FormatMarkdown. Consider refactoring to reduce duplication (e.g., have one function take both display and sentinel names, or have one call the other with appropriate parameters).
Recommendation
REQUEST_CHANGES — Please fix the malformed inputs block in .gitea/actions/review/action.yml. Specifically, move the description, required, and default fields back under system-prompt-file and ensure persona-file has a single, correct set of fields without duplicates. This prevents ambiguous or rejected inputs by the actions runner. Additionally, consider adding workspace-bound path validation for --persona-file similar to what you implemented for --system-prompt-file, and optionally refactor the duplicated markdown formatting logic. Once the YAML is corrected, the implementation and tests for personas look good and should be ready to merge.
~~Original review~~
**Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/55#pullrequestreview-2039) for up-to-date findings.
<details><summary>Previous findings (commit e4cde3ed)</summary>
# Gpt Review
## Summary
The persona feature is well-implemented with solid tests and CI is passing, but the composite action YAML has a structural error in the inputs section that can break input parsing. Additionally, there are a couple of minor polish items.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `.gitea/actions/review/action.yml` | 75 | The `inputs` section is malformed: `system-prompt-file` is declared without any properties, and the subsequent `description/required/default` fields appear under `persona-file`, resulting in duplicate keys for `persona-file` and leaving `system-prompt-file` with a null value. In YAML this can cause the last duplicate key to win and may lead to GitHub/Gitea Actions rejecting or mis-parsing these inputs. |
| 2 | [MINOR] | `cmd/review-bot/main.go` | 114 | Persona file loading (`--persona-file`) accepts an arbitrary path without the same workspace-bound path validation that is applied to `--system-prompt-file`. For consistency and safety in CI environments, consider applying similar workspace containment checks to avoid accidentally reading outside the repository workspace. |
| 3 | [NIT] | `review/formatter.go` | 60 | The new `FormatMarkdownWithDisplay` largely duplicates `FormatMarkdown`. Consider refactoring to reduce duplication (e.g., have one function take both display and sentinel names, or have one call the other with appropriate parameters). |
## Recommendation
**REQUEST_CHANGES** — Please fix the malformed inputs block in .gitea/actions/review/action.yml. Specifically, move the `description`, `required`, and `default` fields back under `system-prompt-file` and ensure `persona-file` has a single, correct set of fields without duplicates. This prevents ambiguous or rejected inputs by the actions runner. Additionally, consider adding workspace-bound path validation for `--persona-file` similar to what you implemented for `--system-prompt-file`, and optionally refactor the duplicated markdown formatting logic. Once the YAML is corrected, the implementation and tests for personas look good and should be ready to merge.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against e4cde3ed*
</details>
<!-- review-bot:gpt -->
[MAJOR] The inputs section is malformed: system-prompt-file is declared without any properties, and the subsequent description/required/default fields appear under persona-file, resulting in duplicate keys for persona-file and leaving system-prompt-file with a null value. In YAML this can cause the last duplicate key to win and may lead to GitHub/Gitea Actions rejecting or mis-parsing these inputs.
**[MAJOR]** The `inputs` section is malformed: `system-prompt-file` is declared without any properties, and the subsequent `description/required/default` fields appear under `persona-file`, resulting in duplicate keys for `persona-file` and leaving `system-prompt-file` with a null value. In YAML this can cause the last duplicate key to win and may lead to GitHub/Gitea Actions rejecting or mis-parsing these inputs.
[MINOR] Persona file loading (--persona-file) accepts an arbitrary path without the same workspace-bound path validation that is applied to --system-prompt-file. For consistency and safety in CI environments, consider applying similar workspace containment checks to avoid accidentally reading outside the repository workspace.
**[MINOR]** Persona file loading (`--persona-file`) accepts an arbitrary path without the same workspace-bound path validation that is applied to `--system-prompt-file`. For consistency and safety in CI environments, consider applying similar workspace containment checks to avoid accidentally reading outside the repository workspace.
[NIT] The new FormatMarkdownWithDisplay largely duplicates FormatMarkdown. Consider refactoring to reduce duplication (e.g., have one function take both display and sentinel names, or have one call the other with appropriate parameters).
**[NIT]** The new `FormatMarkdownWithDisplay` largely duplicates `FormatMarkdown`. Consider refactoring to reduce duplication (e.g., have one function take both display and sentinel names, or have one call the other with appropriate parameters).
[completeness] MAJOR:.gitea/actions/review/action.yml has a structural error. When the persona and persona-file inputs were added, the system-prompt-file input definition was broken. Currently:
system-prompt-file: has no attributes (empty value) — it will be null/undefined as an input
persona-file: has duplicate description, required, and default keys (the original system-prompt-file attributes were left dangling)
In YAML, duplicate keys mean the last one wins, so persona-file will have the wrong description
Fix: Restore system-prompt-file attributes and ensure proper YAML structure:
system-prompt-file:description:'Local file with additional system prompt instructions (e.g. security review focus)'required:falsedefault:''persona:description:'Built-in persona name (security, architect, docs)'required:falsedefault:''persona-file:description:'Path to persona JSON file with custom review focus'required:falsedefault:''
[fit] NIT: Double blank line at cmd/review-bot/main.go:95-96 (minor style inconsistency with rest of codebase)
[completeness] NIT:BuildSystemPromptWithPersona in review/persona_prompt.go is defined and tested but never called from main.go. It's a reasonable public API for callers not using the budget system, but could be documented as such or removed if truly unused.
What's Good
Clean implementation following codebase patterns (error handling, struct design)
Comprehensive test coverage for all new functionality
Thoughtful design doc in docs/DESIGN-51-personas.md
README documentation is thorough with good examples
Backwards compatible — no flags = existing behavior
Built-in personas are well-designed with clear scope boundaries
CI passes
Recommendation
The action.yml issue is a blocker — it will break existing users of system-prompt-file and give persona-file the wrong description. Fix that, and the PR is ready.
## Self-review: NEEDS_WORK
### Findings
- [ ] [completeness] **MAJOR:** `.gitea/actions/review/action.yml` has a structural error. When the `persona` and `persona-file` inputs were added, the `system-prompt-file` input definition was broken. Currently:
- `system-prompt-file:` has no attributes (empty value) — it will be null/undefined as an input
- `persona-file:` has duplicate `description`, `required`, and `default` keys (the original `system-prompt-file` attributes were left dangling)
- In YAML, duplicate keys mean the last one wins, so `persona-file` will have the wrong description
**Fix:** Restore `system-prompt-file` attributes and ensure proper YAML structure:
```yaml
system-prompt-file:
description: 'Local file with additional system prompt instructions (e.g. security review focus)'
required: false
default: ''
persona:
description: 'Built-in persona name (security, architect, docs)'
required: false
default: ''
persona-file:
description: 'Path to persona JSON file with custom review focus'
required: false
default: ''
```
- [ ] [fit] **NIT:** Double blank line at `cmd/review-bot/main.go:95-96` (minor style inconsistency with rest of codebase)
- [ ] [completeness] **NIT:** `BuildSystemPromptWithPersona` in `review/persona_prompt.go` is defined and tested but never called from `main.go`. It's a reasonable public API for callers not using the budget system, but could be documented as such or removed if truly unused.
### What's Good
- Clean implementation following codebase patterns (error handling, struct design)
- Comprehensive test coverage for all new functionality
- Thoughtful design doc in `docs/DESIGN-51-personas.md`
- README documentation is thorough with good examples
- Backwards compatible — no flags = existing behavior
- Built-in personas are well-designed with clear scope boundaries
- CI passes
### Recommendation
The action.yml issue is a blocker — it will break existing users of `system-prompt-file` and give `persona-file` the wrong description. Fix that, and the PR is ready.
Solid persona feature implementation with clean embedding, prompt construction, and tests. However, loading a persona file lacks the same workspace-bound path validation and symlink checks applied to system-prompt files, creating a potential secret exfiltration risk in CI.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
cmd/review-bot/main.go
112
persona-file is read from the filesystem without validating it resides within the checkout workspace or guarding against symlink traversal. A malicious config could point to arbitrary files (e.g., /etc/*) and exfiltrate their contents to the LLM. Apply the same absolute path, prefix, and symlink-resolution checks used for system-prompt-file.
2
[MINOR]
review/formatter.go
62
FormatMarkdown and FormatMarkdownWithDisplay duplicate substantial formatting logic. Consider refactoring to a shared helper to reduce drift and maintenance overhead.
3
[NIT]
docs/DESIGN-51-personas.md
1
The design doc initially describes YAML personas before noting a switch to JSON at the end. This could confuse readers; consider moving the JSON revision note to the top or removing YAML snippets.
Recommendation
REQUEST_CHANGES — Address the security gap when loading persona files: restrict persona-file to the repository workspace and guard against symlink traversal just like system-prompt-file (resolve absolute path, clean it, ensure it’s within the workspace prefix, then EvalSymlinks and re-check). This prevents unintended disclosure of runner files to the LLM. After adding this validation, the implementation appears robust: embedded built-in personas, mutually exclusive flags, prompt assembly, and tests are well done. Optionally, refactor markdown formatting to reduce duplication and tidy the design doc to emphasize JSON personas up front.
~~Original review~~
**Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/55#pullrequestreview-2044) for up-to-date findings.
<details><summary>Previous findings (commit 5682d824)</summary>
# Gpt Review
## Summary
Solid persona feature implementation with clean embedding, prompt construction, and tests. However, loading a persona file lacks the same workspace-bound path validation and symlink checks applied to system-prompt files, creating a potential secret exfiltration risk in CI.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `cmd/review-bot/main.go` | 112 | persona-file is read from the filesystem without validating it resides within the checkout workspace or guarding against symlink traversal. A malicious config could point to arbitrary files (e.g., /etc/*) and exfiltrate their contents to the LLM. Apply the same absolute path, prefix, and symlink-resolution checks used for system-prompt-file. |
| 2 | [MINOR] | `review/formatter.go` | 62 | FormatMarkdown and FormatMarkdownWithDisplay duplicate substantial formatting logic. Consider refactoring to a shared helper to reduce drift and maintenance overhead. |
| 3 | [NIT] | `docs/DESIGN-51-personas.md` | 1 | The design doc initially describes YAML personas before noting a switch to JSON at the end. This could confuse readers; consider moving the JSON revision note to the top or removing YAML snippets. |
## Recommendation
**REQUEST_CHANGES** — Address the security gap when loading persona files: restrict persona-file to the repository workspace and guard against symlink traversal just like system-prompt-file (resolve absolute path, clean it, ensure it’s within the workspace prefix, then EvalSymlinks and re-check). This prevents unintended disclosure of runner files to the LLM. After adding this validation, the implementation appears robust: embedded built-in personas, mutually exclusive flags, prompt assembly, and tests are well done. Optionally, refactor markdown formatting to reduce duplication and tidy the design doc to emphasize JSON personas up front.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 5682d824*
</details>
<!-- review-bot:gpt -->
[MAJOR] persona-file is read from the filesystem without validating it resides within the checkout workspace or guarding against symlink traversal. A malicious config could point to arbitrary files (e.g., /etc/*) and exfiltrate their contents to the LLM. Apply the same absolute path, prefix, and symlink-resolution checks used for system-prompt-file.
**[MAJOR]** persona-file is read from the filesystem without validating it resides within the checkout workspace or guarding against symlink traversal. A malicious config could point to arbitrary files (e.g., /etc/*) and exfiltrate their contents to the LLM. Apply the same absolute path, prefix, and symlink-resolution checks used for system-prompt-file.
[NIT] The design doc initially describes YAML personas before noting a switch to JSON at the end. This could confuse readers; consider moving the JSON revision note to the top or removing YAML snippets.
**[NIT]** The design doc initially describes YAML personas before noting a switch to JSON at the end. This could confuse readers; consider moving the JSON revision note to the top or removing YAML snippets.
[MINOR] FormatMarkdown and FormatMarkdownWithDisplay duplicate substantial formatting logic. Consider refactoring to a shared helper to reduce drift and maintenance overhead.
**[MINOR]** FormatMarkdown and FormatMarkdownWithDisplay duplicate substantial formatting logic. Consider refactoring to a shared helper to reduce drift and maintenance overhead.
security-review-bot
requested review from security-review-bot 2026-05-10 10:27:14 +00:00
Good feature addition with clean integration and tests; however, loading a persona from an arbitrary filesystem path lacks the same path/symlink validation applied to system-prompt files. This enables arbitrary file reads on the CI runner and can lead to denial-of-service by reading device files, and potential sensitive data exfiltration to the LLM.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
cmd/review-bot/main.go
100
Unvalidated persona-file path is read without restricting to the workspace or validating symlinks. An attacker who can modify workflow inputs could point to arbitrary files (e.g., /dev/zero causing DoS, or JSON config files), risking resource exhaustion and potential data exfiltration to the LLM.
2
[MAJOR]
review/persona.go
41
LoadPersona uses os.ReadFile on the provided path without checks. Combined with the caller’s lack of path/symlink validation, this permits reading arbitrary paths on the runner (including device nodes or large files), enabling DoS and possible sensitive data leakage if the file parses as JSON.
Recommendation
REQUEST_CHANGES — Harden persona-file handling to match (and reuse) the protections already implemented for system-prompt-file:
Resolve the workspace path (GITHUB_WORKSPACE or current working directory), construct an absolute path for persona-file, and ensure it remains within the workspace (prefix check).
Resolve symlinks via filepath.EvalSymlinks and re-validate it remains within the workspace (to prevent symlink traversal).
Verify the target is a regular file (os.Stat and check FileMode) and enforce a reasonable maximum size before reading (e.g., a few hundred KB) to mitigate DoS from large or special files (avoid reading device nodes like /dev/zero).
Consider failing fast with a clear error if the path validation fails.
Optionally, sanitize or constrain persona.DisplayName if you intend to render it in Markdown in contexts where untrusted persona files may be used; though current usage is low risk, constraining to a reasonable character set would be consistent with reviewer-name validation.
Once these changes are in place, re-run tests to confirm no regressions and that persona loading behaves correctly under path validation.
# Security Review
## Summary
Good feature addition with clean integration and tests; however, loading a persona from an arbitrary filesystem path lacks the same path/symlink validation applied to system-prompt files. This enables arbitrary file reads on the CI runner and can lead to denial-of-service by reading device files, and potential sensitive data exfiltration to the LLM.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `cmd/review-bot/main.go` | 100 | Unvalidated persona-file path is read without restricting to the workspace or validating symlinks. An attacker who can modify workflow inputs could point to arbitrary files (e.g., /dev/zero causing DoS, or JSON config files), risking resource exhaustion and potential data exfiltration to the LLM. |
| 2 | [MAJOR] | `review/persona.go` | 41 | LoadPersona uses os.ReadFile on the provided path without checks. Combined with the caller’s lack of path/symlink validation, this permits reading arbitrary paths on the runner (including device nodes or large files), enabling DoS and possible sensitive data leakage if the file parses as JSON. |
## Recommendation
**REQUEST_CHANGES** — Harden persona-file handling to match (and reuse) the protections already implemented for system-prompt-file:
- Resolve the workspace path (GITHUB_WORKSPACE or current working directory), construct an absolute path for persona-file, and ensure it remains within the workspace (prefix check).
- Resolve symlinks via filepath.EvalSymlinks and re-validate it remains within the workspace (to prevent symlink traversal).
- Verify the target is a regular file (os.Stat and check FileMode) and enforce a reasonable maximum size before reading (e.g., a few hundred KB) to mitigate DoS from large or special files (avoid reading device nodes like /dev/zero).
- Consider failing fast with a clear error if the path validation fails.
Optionally, sanitize or constrain persona.DisplayName if you intend to render it in Markdown in contexts where untrusted persona files may be used; though current usage is low risk, constraining to a reasonable character set would be consistent with reviewer-name validation.
Once these changes are in place, re-run tests to confirm no regressions and that persona loading behaves correctly under path validation.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 5682d824*
[MAJOR] Unvalidated persona-file path is read without restricting to the workspace or validating symlinks. An attacker who can modify workflow inputs could point to arbitrary files (e.g., /dev/zero causing DoS, or JSON config files), risking resource exhaustion and potential data exfiltration to the LLM.
**[MAJOR]** Unvalidated persona-file path is read without restricting to the workspace or validating symlinks. An attacker who can modify workflow inputs could point to arbitrary files (e.g., /dev/zero causing DoS, or JSON config files), risking resource exhaustion and potential data exfiltration to the LLM.
[MAJOR] LoadPersona uses os.ReadFile on the provided path without checks. Combined with the caller’s lack of path/symlink validation, this permits reading arbitrary paths on the runner (including device nodes or large files), enabling DoS and possible sensitive data leakage if the file parses as JSON.
**[MAJOR]** LoadPersona uses os.ReadFile on the provided path without checks. Combined with the caller’s lack of path/symlink validation, this permits reading arbitrary paths on the runner (including device nodes or large files), enabling DoS and possible sensitive data leakage if the file parses as JSON.
[coherence] The change is focused on a single feature (role-based personas). The CI model changes in ci.yml (disabling unavailable models) are noted via a comment and don't disrupt the feature scope.
[fit] Uses encoding/json (standard library) instead of YAML dependencies, matching the project's zero-external-deps policy. Error handling follows existing patterns.
[fit] go:embed for built-in personas is idiomatic Go 1.16+ pattern.
[solution] Design is sound: mutual exclusivity validated, BuildPersonaSystemPrompt cleanly replaces BuildSystemBase, FormatMarkdownWithDisplay handles display/sentinel name separation correctly.
[completeness] Tests pass (92% coverage in review package), go vet clean, documentation is comprehensive.
[instinct] Code reads well. Minor opportunities: contains helper in persona_test.go reimplements strings.Contains (test-only, harmless); FormatMarkdown/FormatMarkdownWithDisplay share duplicated logic (refactoring opportunity, not a blocker).
No issues found — ready for human review.
## Self-review: PASS
- [x] [coherence] The change is focused on a single feature (role-based personas). The CI model changes in `ci.yml` (disabling unavailable models) are noted via a comment and don't disrupt the feature scope.
- [x] [fit] Uses `encoding/json` (standard library) instead of YAML dependencies, matching the project's zero-external-deps policy. Error handling follows existing patterns.
- [x] [fit] `go:embed` for built-in personas is idiomatic Go 1.16+ pattern.
- [x] [solution] Design is sound: mutual exclusivity validated, BuildPersonaSystemPrompt cleanly replaces BuildSystemBase, FormatMarkdownWithDisplay handles display/sentinel name separation correctly.
- [x] [completeness] Tests pass (92% coverage in review package), go vet clean, documentation is comprehensive.
- [x] [instinct] Code reads well. Minor opportunities: `contains` helper in `persona_test.go` reimplements `strings.Contains` (test-only, harmless); FormatMarkdown/FormatMarkdownWithDisplay share duplicated logic (refactoring opportunity, not a blocker).
No issues found — ready for human review.
Well-structured addition of role-based personas with clean CLI, action, and README integration. Persona loading via embedded JSON is dependency-free, path validation is secure, tests are comprehensive, and CI passed.
Recommendation
APPROVE — The implementation is solid and idiomatic: personas are embedded and validated, mutually exclusive flags are enforced, path handling prevents traversal, and the formatter cleanly separates display name from sentinel. Tests cover new functionality thoroughly and CI is green. Proceed to merge.
~~Original review~~
**Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/55#pullrequestreview-2064) for up-to-date findings.
<details><summary>Previous findings (commit 9b57a538)</summary>
# Gpt Review
## Summary
Well-structured addition of role-based personas with clean CLI, action, and README integration. Persona loading via embedded JSON is dependency-free, path validation is secure, tests are comprehensive, and CI passed.
## Recommendation
**APPROVE** — The implementation is solid and idiomatic: personas are embedded and validated, mutually exclusive flags are enforced, path handling prevents traversal, and the formatter cleanly separates display name from sentinel. Tests cover new functionality thoroughly and CI is green. Proceed to merge.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 9b57a538*
</details>
<!-- review-bot:gpt -->
security-review-bot
requested review from security-review-bot 2026-05-10 10:56:04 +00:00
Adds role-based review personas with safe file loading, proper path validation against workspace and symlink traversal, and clean integration into prompt assembly and formatting. CI passed and no exploitable vulnerabilities were found.
Findings
#
Severity
File
Line
Finding
1
[NIT]
review/formatter.go
1
Persona display name is inserted into the review header/footer without escaping. While Gitea typically sanitizes Markdown/HTML, consider defensively restricting or escaping display names to reduce theoretical XSS risk if platform sanitization changes.
Recommendation
APPROVE — Overall the changes are sound from a security perspective: persona files are validated to reside within the workspace and symlink escapes are blocked; reviewer-name is validated to safe characters; and built-in personas are embedded. No injection, auth, or secrets issues were identified. As a minor hardening step, consider sanitizing or restricting the persona display_name before rendering it in review comments to mitigate theoretical XSS if upstream sanitization were to weaken. Otherwise, proceed with merge.
# Security Review
## Summary
Adds role-based review personas with safe file loading, proper path validation against workspace and symlink traversal, and clean integration into prompt assembly and formatting. CI passed and no exploitable vulnerabilities were found.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [NIT] | `review/formatter.go` | 1 | Persona display name is inserted into the review header/footer without escaping. While Gitea typically sanitizes Markdown/HTML, consider defensively restricting or escaping display names to reduce theoretical XSS risk if platform sanitization changes. |
## Recommendation
**APPROVE** — Overall the changes are sound from a security perspective: persona files are validated to reside within the workspace and symlink escapes are blocked; reviewer-name is validated to safe characters; and built-in personas are embedded. No injection, auth, or secrets issues were identified. As a minor hardening step, consider sanitizing or restricting the persona display_name before rendering it in review comments to mitigate theoretical XSS if upstream sanitization were to weaken. Otherwise, proceed with merge.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 9b57a538*
aweiker
was assigned by rodin2026-05-10 10:59:03 +00:00
This PR cleanly implements role-based review personas with good separation of concerns: persona definition (JSON + embed), prompt construction, and formatter extension. The implementation is backwards-compatible, well-tested, and follows the repository's zero-dependency convention. A few minor issues worth noting but none are blockers.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/persona_test.go
191
The contains / containsHelper functions in persona_test.go are reinventing strings.Contains. The standard library already provides this. The custom implementation is error-prone (the contains wrapper has a subtle redundancy: it checks len(s) >= len(substr) and s == substr before calling containsHelper, which already handles the equality case in its loop). Just use strings.Contains directly.
2
[MINOR]
review/formatter.go
59
FormatMarkdownWithDisplay is almost a complete copy of FormatMarkdown. The two functions share ~90% of their logic (summary, findings table, recommendation, sentinel). This violates DRY and means future changes to the review format must be applied in two places. FormatMarkdown could simply delegate to FormatMarkdownWithDisplay(result, "", reviewerName) after capitalizing the name for display, or FormatMarkdownWithDisplay could be the single implementation that both call.
3
[MINOR]
review/formatter.go
65
The title capitalisation strings.ToUpper(headerName[:1]) + headerName[1:] is duplicated in both FormatMarkdown (line 12) and FormatMarkdownWithDisplay (line 65). This byte-slice indexing also breaks on multi-byte (non-ASCII) first characters. Use strings.ToUpper(string([]rune(headerName)[:1])) + string([]rune(headerName)[1:]) or unicode.ToUpper for correctness, and extract it into a helper.
4
[NIT]
review/persona_prompt.go
86
The output format JSON is constructed via a long sequence of individual sb.WriteString calls with manual newlines. This is verbose and fragile. A single multi-line string literal (raw string or const) would be cleaner and easier to maintain, following the pattern already used in BuildSystemBase.
5
[NIT]
docs/DESIGN-51-personas.md
1
The design doc still references YAML throughout (phase checklist, finding #2, struct tags use yaml: annotations) even after the revision section at the bottom switched to JSON. The checklist items reference security.yaml, architect.yaml, docs.yaml. These inconsistencies are confusing for future readers. The doc should be updated to consistently reflect the JSON decision.
6
[NIT]
cmd/review-bot/main_test.go
8
The path/filepath import is not in alphabetical order relative to the other imports in the block (os, os/exec, strings, path/filepath). goimports / gofmt would sort this. Minor but the project convention is canonical formatting.
Recommendation
APPROVE — APPROVE — CI passes, the implementation is correct, backwards-compatible, well-tested, and respects the zero-dependency convention. The MINOR findings (code duplication in formatter, reinvented strings.Contains in tests, non-ASCII-safe capitalisation) are worth addressing in a follow-up or in the same PR if the author prefers, but they don't block merging.
~~Original review~~
**Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/55#pullrequestreview-2083) for up-to-date findings.
<details><summary>Previous findings (commit ca1e17de)</summary>
# Sonnet Review
## Summary
This PR cleanly implements role-based review personas with good separation of concerns: persona definition (JSON + embed), prompt construction, and formatter extension. The implementation is backwards-compatible, well-tested, and follows the repository's zero-dependency convention. A few minor issues worth noting but none are blockers.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/persona_test.go` | 191 | The `contains` / `containsHelper` functions in persona_test.go are reinventing `strings.Contains`. The standard library already provides this. The custom implementation is error-prone (the `contains` wrapper has a subtle redundancy: it checks `len(s) >= len(substr)` and `s == substr` before calling `containsHelper`, which already handles the equality case in its loop). Just use `strings.Contains` directly. |
| 2 | [MINOR] | `review/formatter.go` | 59 | `FormatMarkdownWithDisplay` is almost a complete copy of `FormatMarkdown`. The two functions share ~90% of their logic (summary, findings table, recommendation, sentinel). This violates DRY and means future changes to the review format must be applied in two places. `FormatMarkdown` could simply delegate to `FormatMarkdownWithDisplay(result, "", reviewerName)` after capitalizing the name for display, or `FormatMarkdownWithDisplay` could be the single implementation that both call. |
| 3 | [MINOR] | `review/formatter.go` | 65 | The title capitalisation `strings.ToUpper(headerName[:1]) + headerName[1:]` is duplicated in both `FormatMarkdown` (line 12) and `FormatMarkdownWithDisplay` (line 65). This byte-slice indexing also breaks on multi-byte (non-ASCII) first characters. Use `strings.ToUpper(string([]rune(headerName)[:1])) + string([]rune(headerName)[1:])` or `unicode.ToUpper` for correctness, and extract it into a helper. |
| 4 | [NIT] | `review/persona_prompt.go` | 86 | The output format JSON is constructed via a long sequence of individual `sb.WriteString` calls with manual newlines. This is verbose and fragile. A single multi-line string literal (raw string or `const`) would be cleaner and easier to maintain, following the pattern already used in `BuildSystemBase`. |
| 5 | [NIT] | `docs/DESIGN-51-personas.md` | 1 | The design doc still references YAML throughout (phase checklist, finding #2, struct tags use `yaml:` annotations) even after the revision section at the bottom switched to JSON. The checklist items reference `security.yaml`, `architect.yaml`, `docs.yaml`. These inconsistencies are confusing for future readers. The doc should be updated to consistently reflect the JSON decision. |
| 6 | [NIT] | `cmd/review-bot/main_test.go` | 8 | The `path/filepath` import is not in alphabetical order relative to the other imports in the block (`os`, `os/exec`, `strings`, `path/filepath`). `goimports` / `gofmt` would sort this. Minor but the project convention is canonical formatting. |
## Recommendation
**APPROVE** — APPROVE — CI passes, the implementation is correct, backwards-compatible, well-tested, and respects the zero-dependency convention. The MINOR findings (code duplication in formatter, reinvented `strings.Contains` in tests, non-ASCII-safe capitalisation) are worth addressing in a follow-up or in the same PR if the author prefers, but they don't block merging.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against ca1e17de*
</details>
<!-- review-bot:sonnet -->
[NIT] The path/filepath import is not in alphabetical order relative to the other imports in the block (os, os/exec, strings, path/filepath). goimports / gofmt would sort this. Minor but the project convention is canonical formatting.
**[NIT]** The `path/filepath` import is not in alphabetical order relative to the other imports in the block (`os`, `os/exec`, `strings`, `path/filepath`). `goimports` / `gofmt` would sort this. Minor but the project convention is canonical formatting.
[NIT] The design doc still references YAML throughout (phase checklist, finding #2, struct tags use yaml: annotations) even after the revision section at the bottom switched to JSON. The checklist items reference security.yaml, architect.yaml, docs.yaml. These inconsistencies are confusing for future readers. The doc should be updated to consistently reflect the JSON decision.
**[NIT]** The design doc still references YAML throughout (phase checklist, finding #2, struct tags use `yaml:` annotations) even after the revision section at the bottom switched to JSON. The checklist items reference `security.yaml`, `architect.yaml`, `docs.yaml`. These inconsistencies are confusing for future readers. The doc should be updated to consistently reflect the JSON decision.
[MINOR]FormatMarkdownWithDisplay is almost a complete copy of FormatMarkdown. The two functions share ~90% of their logic (summary, findings table, recommendation, sentinel). This violates DRY and means future changes to the review format must be applied in two places. FormatMarkdown could simply delegate to FormatMarkdownWithDisplay(result, "", reviewerName) after capitalizing the name for display, or FormatMarkdownWithDisplay could be the single implementation that both call.
**[MINOR]** `FormatMarkdownWithDisplay` is almost a complete copy of `FormatMarkdown`. The two functions share ~90% of their logic (summary, findings table, recommendation, sentinel). This violates DRY and means future changes to the review format must be applied in two places. `FormatMarkdown` could simply delegate to `FormatMarkdownWithDisplay(result, "", reviewerName)` after capitalizing the name for display, or `FormatMarkdownWithDisplay` could be the single implementation that both call.
[MINOR] The title capitalisation strings.ToUpper(headerName[:1]) + headerName[1:] is duplicated in both FormatMarkdown (line 12) and FormatMarkdownWithDisplay (line 65). This byte-slice indexing also breaks on multi-byte (non-ASCII) first characters. Use strings.ToUpper(string([]rune(headerName)[:1])) + string([]rune(headerName)[1:]) or unicode.ToUpper for correctness, and extract it into a helper.
**[MINOR]** The title capitalisation `strings.ToUpper(headerName[:1]) + headerName[1:]` is duplicated in both `FormatMarkdown` (line 12) and `FormatMarkdownWithDisplay` (line 65). This byte-slice indexing also breaks on multi-byte (non-ASCII) first characters. Use `strings.ToUpper(string([]rune(headerName)[:1])) + string([]rune(headerName)[1:])` or `unicode.ToUpper` for correctness, and extract it into a helper.
[NIT] The output format JSON is constructed via a long sequence of individual sb.WriteString calls with manual newlines. This is verbose and fragile. A single multi-line string literal (raw string or const) would be cleaner and easier to maintain, following the pattern already used in BuildSystemBase.
**[NIT]** The output format JSON is constructed via a long sequence of individual `sb.WriteString` calls with manual newlines. This is verbose and fragile. A single multi-line string literal (raw string or `const`) would be cleaner and easier to maintain, following the pattern already used in `BuildSystemBase`.
[MINOR] The contains / containsHelper functions in persona_test.go are reinventing strings.Contains. The standard library already provides this. The custom implementation is error-prone (the contains wrapper has a subtle redundancy: it checks len(s) >= len(substr) and s == substr before calling containsHelper, which already handles the equality case in its loop). Just use strings.Contains directly.
**[MINOR]** The `contains` / `containsHelper` functions in persona_test.go are reinventing `strings.Contains`. The standard library already provides this. The custom implementation is error-prone (the `contains` wrapper has a subtle redundancy: it checks `len(s) >= len(substr)` and `s == substr` before calling `containsHelper`, which already handles the equality case in its loop). Just use `strings.Contains` directly.
security-review-bot
requested review from security-review-bot 2026-05-10 15:36:27 +00:00
The changes add role-based personas and integrate them safely into prompt construction and the CLI. Path handling for persona and system prompt files includes defenses against traversal and symlink attacks. One minor concern is untrusted display names from repo-controlled persona files being injected into review Markdown without escaping.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
cmd/review-bot/main.go
311
Untrusted persona display_name (from a repo-controlled persona-file) is passed directly to FormatMarkdownWithDisplay and rendered into review Markdown without escaping. If the hosting platform's sanitizer is bypassable, this could enable Markdown/HTML injection in the review header/footer. Consider sanitizing or escaping displayName (and restricting to a safe character set) before inclusion.
Recommendation
APPROVE — Overall, the implementation is sound and improves functionality without introducing major security risks. The newly added validateWorkspacePath correctly mitigates path traversal and symlink attacks when loading files from the workspace. To harden against potential content injection, ensure persona.DisplayName is sanitized or escaped before inserting into Markdown (or restrict it to alphanumerics, spaces, dashes/underscores). With that minor adjustment, the feature set looks secure and ready to merge.
# Security Review
## Summary
The changes add role-based personas and integrate them safely into prompt construction and the CLI. Path handling for persona and system prompt files includes defenses against traversal and symlink attacks. One minor concern is untrusted display names from repo-controlled persona files being injected into review Markdown without escaping.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `cmd/review-bot/main.go` | 311 | Untrusted persona display_name (from a repo-controlled persona-file) is passed directly to FormatMarkdownWithDisplay and rendered into review Markdown without escaping. If the hosting platform's sanitizer is bypassable, this could enable Markdown/HTML injection in the review header/footer. Consider sanitizing or escaping displayName (and restricting to a safe character set) before inclusion. |
## Recommendation
**APPROVE** — Overall, the implementation is sound and improves functionality without introducing major security risks. The newly added validateWorkspacePath correctly mitigates path traversal and symlink attacks when loading files from the workspace. To harden against potential content injection, ensure persona.DisplayName is sanitized or escaped before inserting into Markdown (or restrict it to alphanumerics, spaces, dashes/underscores). With that minor adjustment, the feature set looks secure and ready to merge.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against ca1e17de*
[MINOR] Untrusted persona display_name (from a repo-controlled persona-file) is passed directly to FormatMarkdownWithDisplay and rendered into review Markdown without escaping. If the hosting platform's sanitizer is bypassable, this could enable Markdown/HTML injection in the review header/footer. Consider sanitizing or escaping displayName (and restricting to a safe character set) before inclusion.
**[MINOR]** Untrusted persona display_name (from a repo-controlled persona-file) is passed directly to FormatMarkdownWithDisplay and rendered into review Markdown without escaping. If the hosting platform's sanitizer is bypassable, this could enable Markdown/HTML injection in the review header/footer. Consider sanitizing or escaping displayName (and restricting to a safe character set) before inclusion.
gpt-review-bot
approved these changes 2026-05-10 15:37:11 +00:00
Solid implementation of role-based personas with clean integration into flags, action inputs, prompt assembly, and formatting. Tests and docs are comprehensive, and CI is passing.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
cmd/review-bot/main.go
630
validateWorkspacePath uses a string HasPrefix check to ensure paths are within the workspace; this can be brittle across platforms and case-insensitive filesystems. Consider using filepath.Rel and verifying the resulting path does not start with ".." to robustly confirm containment.
2
[NIT]
review/formatter.go
60
Header title capitalization uses byte slicing (headerName[:1]) which can mis-handle non-ASCII names (multi-byte runes). Consider using unicode/utf8 to upper-case the first rune safely.
3
[MINOR]
cmd/review-bot/main_test.go
95
The TestValidateWorkspacePath case labeled "absolute path gets normalized to relative" expects an error substring "failed to resolve", but validateWorkspacePath returns an earlier "resolves outside workspace" error for absolute paths. The expectation and test comment are inconsistent with actual behavior and may cause confusion; align the expected error substring to the actual branch.
Recommendation
APPROVE — Overall the persona feature is well integrated and follows repository conventions. The path validation helper is a good security addition; consider switching to filepath.Rel for a more robust containment check. Adjust the header capitalization to handle Unicode reviewer or display names gracefully. Lastly, update the absolute-path test expectation and its comment in TestValidateWorkspacePath to match the function’s behavior. With these minor refinements, the changes are ready to merge.
~~Original review~~
**Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/55#pullrequestreview-2091) for up-to-date findings.
<details><summary>Previous findings (commit ca1e17de)</summary>
# Gpt Review
## Summary
Solid implementation of role-based personas with clean integration into flags, action inputs, prompt assembly, and formatting. Tests and docs are comprehensive, and CI is passing.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `cmd/review-bot/main.go` | 630 | validateWorkspacePath uses a string HasPrefix check to ensure paths are within the workspace; this can be brittle across platforms and case-insensitive filesystems. Consider using filepath.Rel and verifying the resulting path does not start with ".." to robustly confirm containment. |
| 2 | [NIT] | `review/formatter.go` | 60 | Header title capitalization uses byte slicing (headerName[:1]) which can mis-handle non-ASCII names (multi-byte runes). Consider using unicode/utf8 to upper-case the first rune safely. |
| 3 | [MINOR] | `cmd/review-bot/main_test.go` | 95 | The TestValidateWorkspacePath case labeled "absolute path gets normalized to relative" expects an error substring "failed to resolve", but validateWorkspacePath returns an earlier "resolves outside workspace" error for absolute paths. The expectation and test comment are inconsistent with actual behavior and may cause confusion; align the expected error substring to the actual branch. |
## Recommendation
**APPROVE** — Overall the persona feature is well integrated and follows repository conventions. The path validation helper is a good security addition; consider switching to filepath.Rel for a more robust containment check. Adjust the header capitalization to handle Unicode reviewer or display names gracefully. Lastly, update the absolute-path test expectation and its comment in TestValidateWorkspacePath to match the function’s behavior. With these minor refinements, the changes are ready to merge.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against ca1e17de*
</details>
<!-- review-bot:gpt -->
[MINOR] validateWorkspacePath uses a string HasPrefix check to ensure paths are within the workspace; this can be brittle across platforms and case-insensitive filesystems. Consider using filepath.Rel and verifying the resulting path does not start with ".." to robustly confirm containment.
**[MINOR]** validateWorkspacePath uses a string HasPrefix check to ensure paths are within the workspace; this can be brittle across platforms and case-insensitive filesystems. Consider using filepath.Rel and verifying the resulting path does not start with ".." to robustly confirm containment.
[MINOR] The TestValidateWorkspacePath case labeled "absolute path gets normalized to relative" expects an error substring "failed to resolve", but validateWorkspacePath returns an earlier "resolves outside workspace" error for absolute paths. The expectation and test comment are inconsistent with actual behavior and may cause confusion; align the expected error substring to the actual branch.
**[MINOR]** The TestValidateWorkspacePath case labeled "absolute path gets normalized to relative" expects an error substring "failed to resolve", but validateWorkspacePath returns an earlier "resolves outside workspace" error for absolute paths. The expectation and test comment are inconsistent with actual behavior and may cause confusion; align the expected error substring to the actual branch.
[NIT] Header title capitalization uses byte slicing (headerName[:1]) which can mis-handle non-ASCII names (multi-byte runes). Consider using unicode/utf8 to upper-case the first rune safely.
**[NIT]** Header title capitalization uses byte slicing (headerName[:1]) which can mis-handle non-ASCII names (multi-byte runes). Consider using unicode/utf8 to upper-case the first rune safely.
This PR adds a well-designed role-based review persona feature. The implementation is clean, follows Go stdlib patterns, maintains zero external dependencies, and includes comprehensive tests. CI passed. A few minor issues worth noting but none are blockers.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
.gitea/actions/review/action.yml
74
The YAML for the system-prompt-file input is malformed — the persona and persona-file inputs are inserted between the system-prompt-file key and its description/required/default fields. This means system-prompt-file loses its description/metadata in the action definition. The two new inputs should be placed after the complete system-prompt-file block, not in the middle of it.
2
[MINOR]
review/persona.go
7
path/filepath is imported and used in LoadBuiltinPersona to construct filepath.Join("personas", filename). On Windows this would produce personas\filename.json, but embed.FS always uses forward slashes. This should use "personas/" + filename directly (or path.Join from the path package, not filepath). This is a correctness bug on non-Unix platforms, though it won't affect Linux CI runners.
3
[MINOR]
review/persona_test.go
194
The contains / containsHelper helpers duplicate functionality from strings.Contains. The test file is in the review package (not review_test), so it could just use strings.Contains directly — or at minimum import strings. Adding custom substring matching helpers in tests is unnecessary complexity and a maintenance burden.
4
[MINOR]
cmd/review-bot/main.go
94
There is a blank line immediately after the os.Exit(1) block (the required-fields validation) and before the persona-flags validation comment. This is a minor style issue (double blank line) that gofmt would not flag but deviates from the existing code style where single blank lines separate logical sections.
5
[NIT]
review/formatter.go
56
FormatMarkdownWithDisplay is nearly identical to FormatMarkdown — the only difference is the two-name parameter split. Consider refactoring FormatMarkdown to delegate to FormatMarkdownWithDisplay(result, reviewerName, reviewerName) to avoid duplicating the findings table and recommendation rendering logic.
6
[NIT]
review/persona_prompt.go
85
BuildSystemPromptWithPersona exists as a helper but is never called from main.go — the main function manually assembles systemBase then appends additionalPrompt, patterns, and conventions separately via the budget system. This exported function is unused dead code in practice. Either wire it into the main flow or unexport/remove it.
7
[NIT]
docs/DESIGN-51-personas.md
1
The design doc still references YAML in several places (e.g. the Persona struct uses yaml: tags, the design says 'personas/*.yaml', the completion checklist asks 'Persona struct matches YAML schema exactly?') even though the design was revised to JSON. The doc is internally inconsistent. Since this is a docs file it doesn't affect correctness, but it will confuse future readers.
Recommendation
APPROVE — APPROVE — The implementation is correct, well-tested, follows established patterns (embed.FS, encoding/json, zero deps), and maintains backward compatibility. The YAML action.yml structural bug (Finding 1) and the filepath vs path bug (Finding 2) are the most actionable items. The filepath issue won't affect Linux CI but is technically wrong. All other findings are style/cleanup nits that can be addressed in a follow-up. The feature works as designed and CI passes.
# Sonnet Review
## Summary
This PR adds a well-designed role-based review persona feature. The implementation is clean, follows Go stdlib patterns, maintains zero external dependencies, and includes comprehensive tests. CI passed. A few minor issues worth noting but none are blockers.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `.gitea/actions/review/action.yml` | 74 | The YAML for the `system-prompt-file` input is malformed — the `persona` and `persona-file` inputs are inserted between the `system-prompt-file` key and its `description`/`required`/`default` fields. This means `system-prompt-file` loses its description/metadata in the action definition. The two new inputs should be placed after the complete `system-prompt-file` block, not in the middle of it. |
| 2 | [MINOR] | `review/persona.go` | 7 | `path/filepath` is imported and used in `LoadBuiltinPersona` to construct `filepath.Join("personas", filename)`. On Windows this would produce `personas\filename.json`, but `embed.FS` always uses forward slashes. This should use `"personas/" + filename` directly (or `path.Join` from the `path` package, not `filepath`). This is a correctness bug on non-Unix platforms, though it won't affect Linux CI runners. |
| 3 | [MINOR] | `review/persona_test.go` | 194 | The `contains` / `containsHelper` helpers duplicate functionality from `strings.Contains`. The test file is in the `review` package (not `review_test`), so it could just use `strings.Contains` directly — or at minimum import `strings`. Adding custom substring matching helpers in tests is unnecessary complexity and a maintenance burden. |
| 4 | [MINOR] | `cmd/review-bot/main.go` | 94 | There is a blank line immediately after the `os.Exit(1)` block (the required-fields validation) and before the persona-flags validation comment. This is a minor style issue (double blank line) that `gofmt` would not flag but deviates from the existing code style where single blank lines separate logical sections. |
| 5 | [NIT] | `review/formatter.go` | 56 | `FormatMarkdownWithDisplay` is nearly identical to `FormatMarkdown` — the only difference is the two-name parameter split. Consider refactoring `FormatMarkdown` to delegate to `FormatMarkdownWithDisplay(result, reviewerName, reviewerName)` to avoid duplicating the findings table and recommendation rendering logic. |
| 6 | [NIT] | `review/persona_prompt.go` | 85 | `BuildSystemPromptWithPersona` exists as a helper but is never called from `main.go` — the main function manually assembles `systemBase` then appends `additionalPrompt`, patterns, and conventions separately via the budget system. This exported function is unused dead code in practice. Either wire it into the main flow or unexport/remove it. |
| 7 | [NIT] | `docs/DESIGN-51-personas.md` | 1 | The design doc still references YAML in several places (e.g. the Persona struct uses `yaml:` tags, the design says 'personas/*.yaml', the completion checklist asks 'Persona struct matches YAML schema exactly?') even though the design was revised to JSON. The doc is internally inconsistent. Since this is a docs file it doesn't affect correctness, but it will confuse future readers. |
## Recommendation
**APPROVE** — APPROVE — The implementation is correct, well-tested, follows established patterns (embed.FS, encoding/json, zero deps), and maintains backward compatibility. The YAML action.yml structural bug (Finding 1) and the filepath vs path bug (Finding 2) are the most actionable items. The filepath issue won't affect Linux CI but is technically wrong. All other findings are style/cleanup nits that can be addressed in a follow-up. The feature works as designed and CI passes.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against d33a4532*
[MINOR] The YAML for the system-prompt-file input is malformed — the persona and persona-file inputs are inserted between the system-prompt-file key and its description/required/default fields. This means system-prompt-file loses its description/metadata in the action definition. The two new inputs should be placed after the complete system-prompt-file block, not in the middle of it.
**[MINOR]** The YAML for the `system-prompt-file` input is malformed — the `persona` and `persona-file` inputs are inserted between the `system-prompt-file` key and its `description`/`required`/`default` fields. This means `system-prompt-file` loses its description/metadata in the action definition. The two new inputs should be placed after the complete `system-prompt-file` block, not in the middle of it.
[MINOR] There is a blank line immediately after the os.Exit(1) block (the required-fields validation) and before the persona-flags validation comment. This is a minor style issue (double blank line) that gofmt would not flag but deviates from the existing code style where single blank lines separate logical sections.
**[MINOR]** There is a blank line immediately after the `os.Exit(1)` block (the required-fields validation) and before the persona-flags validation comment. This is a minor style issue (double blank line) that `gofmt` would not flag but deviates from the existing code style where single blank lines separate logical sections.
[NIT] The design doc still references YAML in several places (e.g. the Persona struct uses yaml: tags, the design says 'personas/*.yaml', the completion checklist asks 'Persona struct matches YAML schema exactly?') even though the design was revised to JSON. The doc is internally inconsistent. Since this is a docs file it doesn't affect correctness, but it will confuse future readers.
**[NIT]** The design doc still references YAML in several places (e.g. the Persona struct uses `yaml:` tags, the design says 'personas/*.yaml', the completion checklist asks 'Persona struct matches YAML schema exactly?') even though the design was revised to JSON. The doc is internally inconsistent. Since this is a docs file it doesn't affect correctness, but it will confuse future readers.
[NIT]FormatMarkdownWithDisplay is nearly identical to FormatMarkdown — the only difference is the two-name parameter split. Consider refactoring FormatMarkdown to delegate to FormatMarkdownWithDisplay(result, reviewerName, reviewerName) to avoid duplicating the findings table and recommendation rendering logic.
**[NIT]** `FormatMarkdownWithDisplay` is nearly identical to `FormatMarkdown` — the only difference is the two-name parameter split. Consider refactoring `FormatMarkdown` to delegate to `FormatMarkdownWithDisplay(result, reviewerName, reviewerName)` to avoid duplicating the findings table and recommendation rendering logic.
[MINOR]path/filepath is imported and used in LoadBuiltinPersona to construct filepath.Join("personas", filename). On Windows this would produce personas\filename.json, but embed.FS always uses forward slashes. This should use "personas/" + filename directly (or path.Join from the path package, not filepath). This is a correctness bug on non-Unix platforms, though it won't affect Linux CI runners.
**[MINOR]** `path/filepath` is imported and used in `LoadBuiltinPersona` to construct `filepath.Join("personas", filename)`. On Windows this would produce `personas\filename.json`, but `embed.FS` always uses forward slashes. This should use `"personas/" + filename` directly (or `path.Join` from the `path` package, not `filepath`). This is a correctness bug on non-Unix platforms, though it won't affect Linux CI runners.
[NIT]BuildSystemPromptWithPersona exists as a helper but is never called from main.go — the main function manually assembles systemBase then appends additionalPrompt, patterns, and conventions separately via the budget system. This exported function is unused dead code in practice. Either wire it into the main flow or unexport/remove it.
**[NIT]** `BuildSystemPromptWithPersona` exists as a helper but is never called from `main.go` — the main function manually assembles `systemBase` then appends `additionalPrompt`, patterns, and conventions separately via the budget system. This exported function is unused dead code in practice. Either wire it into the main flow or unexport/remove it.
[MINOR] The contains / containsHelper helpers duplicate functionality from strings.Contains. The test file is in the review package (not review_test), so it could just use strings.Contains directly — or at minimum import strings. Adding custom substring matching helpers in tests is unnecessary complexity and a maintenance burden.
**[MINOR]** The `contains` / `containsHelper` helpers duplicate functionality from `strings.Contains`. The test file is in the `review` package (not `review_test`), so it could just use `strings.Contains` directly — or at minimum import `strings`. Adding custom substring matching helpers in tests is unnecessary complexity and a maintenance burden.
This PR cleanly implements role-based review personas with good separation of concerns: JSON-based persona definitions, embedded built-ins, workspace path validation extracted into a reusable helper, and a separate FormatMarkdownWithDisplay function for persona display names. CI passes. The implementation is idiomatic Go, maintains zero external dependencies, and is fully backwards compatible.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/persona_test.go
197
The contains and containsHelper helpers are reinventing strings.Contains. The project already imports strings elsewhere and the stdlib strings.Contains is available in test files. Using strings.Contains directly would be clearer and eliminate ~10 lines of custom code.
2
[MINOR]
review/persona_prompt.go
85
BuildPersonaSystemPrompt builds the JSON output format spec using ~20 individual sb.WriteString calls on string literals. This duplicates logic that already exists in BuildSystemBase() (or the base prompt builder). A shared constant or helper for the JSON output spec would reduce the risk of the persona and generic prompts diverging silently.
3
[NIT]
review/formatter.go
56
FormatMarkdownWithDisplay duplicates nearly all of FormatMarkdown — the only differences are the header/footer name split. Consider refactoring FormatMarkdown to call FormatMarkdownWithDisplay(result, reviewerName, reviewerName) to keep the two in sync, or extract the shared table-building logic.
4
[NIT]
cmd/review-bot/main_test.go
7
The path/filepath import is not in alphabetical order relative to the other imports (os, os/exec, strings). goimports would place it before os. Minor style issue.
5
[NIT]
docs/DESIGN-51-personas.md
1
The design doc references YAML throughout (personas/*.yaml, gopkg.in/yaml.v3) before the revision section at the bottom switches to JSON. The doc would be cleaner if the YAML references in the early sections were updated to reflect the final JSON decision, rather than leaving the revision as an addendum. As a static doc this is low priority.
Recommendation
APPROVE — APPROVE — The implementation is correct, well-tested, and follows established project patterns (zero dependencies, table-driven tests, early returns, proper error wrapping). The minor findings are quality improvements but none block merging. The contains reimplementation in the test file is the most unnecessary piece of code, but it doesn't affect correctness.
# Sonnet Review
## Summary
This PR cleanly implements role-based review personas with good separation of concerns: JSON-based persona definitions, embedded built-ins, workspace path validation extracted into a reusable helper, and a separate `FormatMarkdownWithDisplay` function for persona display names. CI passes. The implementation is idiomatic Go, maintains zero external dependencies, and is fully backwards compatible.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/persona_test.go` | 197 | The `contains` and `containsHelper` helpers are reinventing `strings.Contains`. The project already imports `strings` elsewhere and the stdlib `strings.Contains` is available in test files. Using `strings.Contains` directly would be clearer and eliminate ~10 lines of custom code. |
| 2 | [MINOR] | `review/persona_prompt.go` | 85 | `BuildPersonaSystemPrompt` builds the JSON output format spec using ~20 individual `sb.WriteString` calls on string literals. This duplicates logic that already exists in `BuildSystemBase()` (or the base prompt builder). A shared constant or helper for the JSON output spec would reduce the risk of the persona and generic prompts diverging silently. |
| 3 | [NIT] | `review/formatter.go` | 56 | `FormatMarkdownWithDisplay` duplicates nearly all of `FormatMarkdown` — the only differences are the header/footer name split. Consider refactoring `FormatMarkdown` to call `FormatMarkdownWithDisplay(result, reviewerName, reviewerName)` to keep the two in sync, or extract the shared table-building logic. |
| 4 | [NIT] | `cmd/review-bot/main_test.go` | 7 | The `path/filepath` import is not in alphabetical order relative to the other imports (`os`, `os/exec`, `strings`). `goimports` would place it before `os`. Minor style issue. |
| 5 | [NIT] | `docs/DESIGN-51-personas.md` | 1 | The design doc references YAML throughout (personas/*.yaml, gopkg.in/yaml.v3) before the revision section at the bottom switches to JSON. The doc would be cleaner if the YAML references in the early sections were updated to reflect the final JSON decision, rather than leaving the revision as an addendum. As a static doc this is low priority. |
## Recommendation
**APPROVE** — APPROVE — The implementation is correct, well-tested, and follows established project patterns (zero dependencies, table-driven tests, early returns, proper error wrapping). The minor findings are quality improvements but none block merging. The `contains` reimplementation in the test file is the most unnecessary piece of code, but it doesn't affect correctness.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 4776b221*
[NIT] The path/filepath import is not in alphabetical order relative to the other imports (os, os/exec, strings). goimports would place it before os. Minor style issue.
**[NIT]** The `path/filepath` import is not in alphabetical order relative to the other imports (`os`, `os/exec`, `strings`). `goimports` would place it before `os`. Minor style issue.
[NIT] The design doc references YAML throughout (personas/*.yaml, gopkg.in/yaml.v3) before the revision section at the bottom switches to JSON. The doc would be cleaner if the YAML references in the early sections were updated to reflect the final JSON decision, rather than leaving the revision as an addendum. As a static doc this is low priority.
**[NIT]** The design doc references YAML throughout (personas/*.yaml, gopkg.in/yaml.v3) before the revision section at the bottom switches to JSON. The doc would be cleaner if the YAML references in the early sections were updated to reflect the final JSON decision, rather than leaving the revision as an addendum. As a static doc this is low priority.
[NIT]FormatMarkdownWithDisplay duplicates nearly all of FormatMarkdown — the only differences are the header/footer name split. Consider refactoring FormatMarkdown to call FormatMarkdownWithDisplay(result, reviewerName, reviewerName) to keep the two in sync, or extract the shared table-building logic.
**[NIT]** `FormatMarkdownWithDisplay` duplicates nearly all of `FormatMarkdown` — the only differences are the header/footer name split. Consider refactoring `FormatMarkdown` to call `FormatMarkdownWithDisplay(result, reviewerName, reviewerName)` to keep the two in sync, or extract the shared table-building logic.
[MINOR]BuildPersonaSystemPrompt builds the JSON output format spec using ~20 individual sb.WriteString calls on string literals. This duplicates logic that already exists in BuildSystemBase() (or the base prompt builder). A shared constant or helper for the JSON output spec would reduce the risk of the persona and generic prompts diverging silently.
**[MINOR]** `BuildPersonaSystemPrompt` builds the JSON output format spec using ~20 individual `sb.WriteString` calls on string literals. This duplicates logic that already exists in `BuildSystemBase()` (or the base prompt builder). A shared constant or helper for the JSON output spec would reduce the risk of the persona and generic prompts diverging silently.
[MINOR] The contains and containsHelper helpers are reinventing strings.Contains. The project already imports strings elsewhere and the stdlib strings.Contains is available in test files. Using strings.Contains directly would be clearer and eliminate ~10 lines of custom code.
**[MINOR]** The `contains` and `containsHelper` helpers are reinventing `strings.Contains`. The project already imports `strings` elsewhere and the stdlib `strings.Contains` is available in test files. Using `strings.Contains` directly would be clearer and eliminate ~10 lines of custom code.
This PR cleanly implements role-based review personas with good separation of concerns, comprehensive tests, and zero new dependencies (JSON over YAML is the right call). The implementation is well-structured, backwards-compatible, and follows the repository's patterns. A few minor issues worth noting but none are blockers.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/persona_test.go
196
The contains and containsHelper helpers re-implement strings.Contains from scratch. This is unnecessary — strings.Contains is already in the standard library and is used everywhere else in the test files. This also violates the principle of using standard library functions.
2
[MINOR]
review/formatter.go
56
FormatMarkdownWithDisplay and FormatMarkdown share almost identical logic (only the header/footer differ). This duplication means any change to the findings table, summary format, etc. must be made in two places. Consider having FormatMarkdown delegate to FormatMarkdownWithDisplay, or extracting the shared body-building logic into a private helper.
3
[MINOR]
review/persona.go
3
filepath is imported but used only for filepath.Join("personas", filename) in LoadBuiltinPersona. Since embed.FS uses forward-slash paths on all platforms (per Go spec), using "personas/" + filename (plain string concatenation) would be both simpler and more correct — filepath.Join uses the OS separator on Windows, which would break the embed.FS lookup.
4
[NIT]
cmd/review-bot/main_test.go
9
Import "path/filepath" is not in alphabetical order with the other imports — it appears after "strings" instead of between "os/exec" and "strings". goimports would fix this automatically.
5
[NIT]
review/persona_prompt.go
58
The BuildPersonaSystemPrompt function builds the JSON output format using many individual sb.WriteString calls for what is essentially a multi-line string constant. This is harder to read and maintain than a single const or backtick string. Consider using a const for the static instructions block.
6
[NIT]
docs/DESIGN-51-personas.md
1
The design doc still references YAML in several places (Persona struct with yaml: tags, .review/personas/trading.yaml, security.yaml phase items) after the JSON revision was made. These are internal doc inconsistencies but worth cleaning up for accuracy.
Recommendation
APPROVE — The implementation is solid and ready to merge. The filepath.Join issue with embed.FS (MINOR) is the most substantive concern — on Windows this would produce a backslash path that embed.FS cannot find, making built-in personas fail. All other findings are style/duplication issues. The core design decisions are sound: JSON over YAML keeps zero dependencies, the persona/system-prompt-file distinction is well-documented, backwards compatibility is preserved, and the workspace path validation was correctly refactored into a shared helper.
# Sonnet Review
## Summary
This PR cleanly implements role-based review personas with good separation of concerns, comprehensive tests, and zero new dependencies (JSON over YAML is the right call). The implementation is well-structured, backwards-compatible, and follows the repository's patterns. A few minor issues worth noting but none are blockers.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/persona_test.go` | 196 | The `contains` and `containsHelper` helpers re-implement `strings.Contains` from scratch. This is unnecessary — `strings.Contains` is already in the standard library and is used everywhere else in the test files. This also violates the principle of using standard library functions. |
| 2 | [MINOR] | `review/formatter.go` | 56 | `FormatMarkdownWithDisplay` and `FormatMarkdown` share almost identical logic (only the header/footer differ). This duplication means any change to the findings table, summary format, etc. must be made in two places. Consider having `FormatMarkdown` delegate to `FormatMarkdownWithDisplay`, or extracting the shared body-building logic into a private helper. |
| 3 | [MINOR] | `review/persona.go` | 3 | `filepath` is imported but used only for `filepath.Join("personas", filename)` in `LoadBuiltinPersona`. Since `embed.FS` uses forward-slash paths on all platforms (per Go spec), using `"personas/" + filename` (plain string concatenation) would be both simpler and more correct — `filepath.Join` uses the OS separator on Windows, which would break the embed.FS lookup. |
| 4 | [NIT] | `cmd/review-bot/main_test.go` | 9 | Import `"path/filepath"` is not in alphabetical order with the other imports — it appears after `"strings"` instead of between `"os/exec"` and `"strings"`. `goimports` would fix this automatically. |
| 5 | [NIT] | `review/persona_prompt.go` | 58 | The `BuildPersonaSystemPrompt` function builds the JSON output format using many individual `sb.WriteString` calls for what is essentially a multi-line string constant. This is harder to read and maintain than a single `const` or backtick string. Consider using a `const` for the static instructions block. |
| 6 | [NIT] | `docs/DESIGN-51-personas.md` | 1 | The design doc still references YAML in several places (Persona struct with `yaml:` tags, `.review/personas/trading.yaml`, `security.yaml` phase items) after the JSON revision was made. These are internal doc inconsistencies but worth cleaning up for accuracy. |
## Recommendation
**APPROVE** — The implementation is solid and ready to merge. The `filepath.Join` issue with `embed.FS` (MINOR) is the most substantive concern — on Windows this would produce a backslash path that `embed.FS` cannot find, making built-in personas fail. All other findings are style/duplication issues. The core design decisions are sound: JSON over YAML keeps zero dependencies, the persona/system-prompt-file distinction is well-documented, backwards compatibility is preserved, and the workspace path validation was correctly refactored into a shared helper.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 9c7538ca*
[NIT] Import "path/filepath" is not in alphabetical order with the other imports — it appears after "strings" instead of between "os/exec" and "strings". goimports would fix this automatically.
**[NIT]** Import `"path/filepath"` is not in alphabetical order with the other imports — it appears after `"strings"` instead of between `"os/exec"` and `"strings"`. `goimports` would fix this automatically.
[NIT] The design doc still references YAML in several places (Persona struct with yaml: tags, .review/personas/trading.yaml, security.yaml phase items) after the JSON revision was made. These are internal doc inconsistencies but worth cleaning up for accuracy.
**[NIT]** The design doc still references YAML in several places (Persona struct with `yaml:` tags, `.review/personas/trading.yaml`, `security.yaml` phase items) after the JSON revision was made. These are internal doc inconsistencies but worth cleaning up for accuracy.
[MINOR]FormatMarkdownWithDisplay and FormatMarkdown share almost identical logic (only the header/footer differ). This duplication means any change to the findings table, summary format, etc. must be made in two places. Consider having FormatMarkdown delegate to FormatMarkdownWithDisplay, or extracting the shared body-building logic into a private helper.
**[MINOR]** `FormatMarkdownWithDisplay` and `FormatMarkdown` share almost identical logic (only the header/footer differ). This duplication means any change to the findings table, summary format, etc. must be made in two places. Consider having `FormatMarkdown` delegate to `FormatMarkdownWithDisplay`, or extracting the shared body-building logic into a private helper.
[MINOR]filepath is imported but used only for filepath.Join("personas", filename) in LoadBuiltinPersona. Since embed.FS uses forward-slash paths on all platforms (per Go spec), using "personas/" + filename (plain string concatenation) would be both simpler and more correct — filepath.Join uses the OS separator on Windows, which would break the embed.FS lookup.
**[MINOR]** `filepath` is imported but used only for `filepath.Join("personas", filename)` in `LoadBuiltinPersona`. Since `embed.FS` uses forward-slash paths on all platforms (per Go spec), using `"personas/" + filename` (plain string concatenation) would be both simpler and more correct — `filepath.Join` uses the OS separator on Windows, which would break the embed.FS lookup.
[NIT] The BuildPersonaSystemPrompt function builds the JSON output format using many individual sb.WriteString calls for what is essentially a multi-line string constant. This is harder to read and maintain than a single const or backtick string. Consider using a const for the static instructions block.
**[NIT]** The `BuildPersonaSystemPrompt` function builds the JSON output format using many individual `sb.WriteString` calls for what is essentially a multi-line string constant. This is harder to read and maintain than a single `const` or backtick string. Consider using a `const` for the static instructions block.
[MINOR] The contains and containsHelper helpers re-implement strings.Contains from scratch. This is unnecessary — strings.Contains is already in the standard library and is used everywhere else in the test files. This also violates the principle of using standard library functions.
**[MINOR]** The `contains` and `containsHelper` helpers re-implement `strings.Contains` from scratch. This is unnecessary — `strings.Contains` is already in the standard library and is used everywhere else in the test files. This also violates the principle of using standard library functions.
security-review-bot
requested review from security-review-bot 2026-05-10 15:47:08 +00:00
Persona support is added with careful path validation for local files and embedded built-ins. No exploitable vulnerabilities are apparent; CI passed and the changes improve security posture by adding safe path handling. A couple of minor hardening opportunities exist around unescaped display strings and path checks.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/formatter.go
66
Persona display_name is used directly in the markdown header/footer without escaping. While the cleanup sentinel uses the validated reviewer-name, a custom persona file could inject markdown mentions or formatting into the posted review. Consider sanitizing or constraining display_name (e.g., limit characters or escape markdown) to reduce potential content/mention abuse.
2
[MINOR]
cmd/review-bot/main.go
610
validateWorkspacePath relies on string prefix checks for containment. Although it also resolves symlinks and uses a path separator guard, on case-insensitive filesystems and certain UNC/volume edge cases, prefix checks can be brittle. Consider using filepath.Rel to compute the relative path and ensuring it does not start with ".." and that the volume/drive matches, for stronger cross-platform guarantees.
Recommendation
APPROVE — Overall, the persona feature and related prompt assembly are implemented safely, with good defense against path traversal and symlink escapes when reading local files. Approve the PR. For defense-in-depth, consider sanitizing persona display_name before rendering it in review markdown to avoid potential mention/format injection, and strengthen validateWorkspacePath by using filepath.Rel-based checks (and drive/volume matching on Windows) instead of HasPrefix to make the containment check more robust across platforms.
# Security Review
## Summary
Persona support is added with careful path validation for local files and embedded built-ins. No exploitable vulnerabilities are apparent; CI passed and the changes improve security posture by adding safe path handling. A couple of minor hardening opportunities exist around unescaped display strings and path checks.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/formatter.go` | 66 | Persona display_name is used directly in the markdown header/footer without escaping. While the cleanup sentinel uses the validated reviewer-name, a custom persona file could inject markdown mentions or formatting into the posted review. Consider sanitizing or constraining display_name (e.g., limit characters or escape markdown) to reduce potential content/mention abuse. |
| 2 | [MINOR] | `cmd/review-bot/main.go` | 610 | validateWorkspacePath relies on string prefix checks for containment. Although it also resolves symlinks and uses a path separator guard, on case-insensitive filesystems and certain UNC/volume edge cases, prefix checks can be brittle. Consider using filepath.Rel to compute the relative path and ensuring it does not start with ".." and that the volume/drive matches, for stronger cross-platform guarantees. |
## Recommendation
**APPROVE** — Overall, the persona feature and related prompt assembly are implemented safely, with good defense against path traversal and symlink escapes when reading local files. Approve the PR. For defense-in-depth, consider sanitizing persona display_name before rendering it in review markdown to avoid potential mention/format injection, and strengthen validateWorkspacePath by using filepath.Rel-based checks (and drive/volume matching on Windows) instead of HasPrefix to make the containment check more robust across platforms.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 9c7538ca*
[MINOR] validateWorkspacePath relies on string prefix checks for containment. Although it also resolves symlinks and uses a path separator guard, on case-insensitive filesystems and certain UNC/volume edge cases, prefix checks can be brittle. Consider using filepath.Rel to compute the relative path and ensuring it does not start with ".." and that the volume/drive matches, for stronger cross-platform guarantees.
**[MINOR]** validateWorkspacePath relies on string prefix checks for containment. Although it also resolves symlinks and uses a path separator guard, on case-insensitive filesystems and certain UNC/volume edge cases, prefix checks can be brittle. Consider using filepath.Rel to compute the relative path and ensuring it does not start with ".." and that the volume/drive matches, for stronger cross-platform guarantees.
[MINOR] Persona display_name is used directly in the markdown header/footer without escaping. While the cleanup sentinel uses the validated reviewer-name, a custom persona file could inject markdown mentions or formatting into the posted review. Consider sanitizing or constraining display_name (e.g., limit characters or escape markdown) to reduce potential content/mention abuse.
**[MINOR]** Persona display_name is used directly in the markdown header/footer without escaping. While the cleanup sentinel uses the validated reviewer-name, a custom persona file could inject markdown mentions or formatting into the posted review. Consider sanitizing or constraining display_name (e.g., limit characters or escape markdown) to reduce potential content/mention abuse.
Overall the persona feature is well integrated, with clean flag handling, secure path validation, embedded built-in personas, and comprehensive tests. However, there's a blocking test expectation mismatch in TestValidateWorkspacePath that will fail on typical systems, and a minor Unicode-safety issue in header capitalization.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
cmd/review-bot/main_test.go
46
TestValidateWorkspacePath case "absolute path gets normalized to relative" expects an error containing "failed to resolve", but validateWorkspacePath returns an error "resolves outside workspace" for an absolute path like /etc/passwd. Update the test to match the actual error message (or adjust validateWorkspacePath to return a consistent error in this scenario).
2
[MINOR]
review/formatter.go
1
Header title capitalization uses headerName[:1] which is not Unicode-safe and can split multi-byte runes if display_name contains non-ASCII characters. Consider using utf8.DecodeRuneInString and unicode.ToUpper for the first rune.
3
[NIT]
review/formatter.go
1
FormatMarkdownWithDisplay largely duplicates FormatMarkdown. Consider refactoring common rendering logic to reduce duplication and maintenance overhead.
Recommendation
REQUEST_CHANGES — Address the failing test expectation in TestValidateWorkspacePath by aligning the expected error substring with the actual behavior of validateWorkspacePath for absolute paths (e.g., expect "resolves outside workspace"). This will prevent test failures on typical Unix environments where /etc/passwd exists. Additionally, consider making header capitalization Unicode-safe in FormatMarkdownWithDisplay and refactoring duplicated formatting code for maintainability. With these fixes, the PR should be ready to merge.
# Gpt Review
## Summary
Overall the persona feature is well integrated, with clean flag handling, secure path validation, embedded built-in personas, and comprehensive tests. However, there's a blocking test expectation mismatch in TestValidateWorkspacePath that will fail on typical systems, and a minor Unicode-safety issue in header capitalization.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `cmd/review-bot/main_test.go` | 46 | TestValidateWorkspacePath case "absolute path gets normalized to relative" expects an error containing "failed to resolve", but validateWorkspacePath returns an error "resolves outside workspace" for an absolute path like /etc/passwd. Update the test to match the actual error message (or adjust validateWorkspacePath to return a consistent error in this scenario). |
| 2 | [MINOR] | `review/formatter.go` | 1 | Header title capitalization uses headerName[:1] which is not Unicode-safe and can split multi-byte runes if display_name contains non-ASCII characters. Consider using utf8.DecodeRuneInString and unicode.ToUpper for the first rune. |
| 3 | [NIT] | `review/formatter.go` | 1 | FormatMarkdownWithDisplay largely duplicates FormatMarkdown. Consider refactoring common rendering logic to reduce duplication and maintenance overhead. |
## Recommendation
**REQUEST_CHANGES** — Address the failing test expectation in TestValidateWorkspacePath by aligning the expected error substring with the actual behavior of validateWorkspacePath for absolute paths (e.g., expect "resolves outside workspace"). This will prevent test failures on typical Unix environments where /etc/passwd exists. Additionally, consider making header capitalization Unicode-safe in FormatMarkdownWithDisplay and refactoring duplicated formatting code for maintainability. With these fixes, the PR should be ready to merge.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 9c7538ca*
[MAJOR] TestValidateWorkspacePath case "absolute path gets normalized to relative" expects an error containing "failed to resolve", but validateWorkspacePath returns an error "resolves outside workspace" for an absolute path like /etc/passwd. Update the test to match the actual error message (or adjust validateWorkspacePath to return a consistent error in this scenario).
**[MAJOR]** TestValidateWorkspacePath case "absolute path gets normalized to relative" expects an error containing "failed to resolve", but validateWorkspacePath returns an error "resolves outside workspace" for an absolute path like /etc/passwd. Update the test to match the actual error message (or adjust validateWorkspacePath to return a consistent error in this scenario).
A well-designed feature addition implementing role-based review personas. The code is idiomatic Go, follows the repository's zero-dependency convention, has comprehensive test coverage, and the refactoring of validateWorkspacePath into a reusable helper is a nice improvement. CI passes.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/persona_test.go
196
The contains and containsHelper helpers are reimplementing strings.Contains, which is already available in the standard library. The helpers add no value and the custom implementation has a subtle bug: containsHelper is only called when len(s) > 0, but the outer contains check len(s) >= len(substr) already handles the empty-substr case. Just use strings.Contains directly — it handles all edge cases correctly and is idiomatic Go.
2
[MINOR]
review/formatter.go
56
FormatMarkdownWithDisplay duplicates nearly all of the logic in FormatMarkdown. The only difference is the headerName/sentinelName split. FormatMarkdown could simply delegate to FormatMarkdownWithDisplay(result, reviewerName, reviewerName), eliminating ~35 lines of duplication. Duplicated formatting logic is a maintenance hazard — a future change (e.g., adding a new section) must be applied in two places.
3
[MINOR]
review/persona_prompt.go
70
The BuildPersonaSystemPrompt function has 30+ consecutive sb.WriteString(...) calls for the hardcoded JSON output format schema. This duplicates the output format specification that already lives in the base BuildSystemBase() function. If the JSON schema changes (e.g., adding a new field), both functions must be updated. Consider extracting the shared output format section into a package-level constant or helper function to keep the schema definition in one place.
4
[NIT]
review/persona.go
5
path/filepath is imported but only used for filepath.Join("personas", filename) in LoadBuiltinPersona. Since the path separator for embedded FS is always / (regardless of OS), embed.FS paths must use forward slashes per the io/fs spec. Using filepath.Join here works on Linux/macOS but would produce personas\security on Windows if that ever becomes a build target. Using "personas/" + filename directly would be safer and removes the need for the path/filepath import.
5
[NIT]
docs/DESIGN-51-personas.md
1
The design document still references YAML as the persona format in multiple places (e.g., the Persona struct Go code block uses yaml: struct tags, the persona file format sections show YAML examples). The design revision section at the bottom explains the switch to JSON, but the earlier sections weren't updated. This creates confusion for anyone reading the doc top-to-bottom. Consider updating or striking through the YAML sections, or noting "see Design Revision below" at the first YAML mention.
6
[NIT]
cmd/review-bot/main_test.go
7
The path/filepath import was added but placed out of order — the Go convention (and goimports) groups stdlib imports alphabetically. It should appear between "os/exec" and "strings", not after "strings". Minor but goimports would reorder this.
Recommendation
APPROVE — APPROVE — This is a clean, well-structured feature. The design decision to use JSON instead of YAML preserves the zero-dependency constraint correctly. The validateWorkspacePath refactoring is a genuine improvement. The minor findings (duplication in formatter, custom contains helper, filepath.Join on embed.FS paths) are worth addressing but don't block merging. The stale design doc is a documentation-only issue.
# Sonnet Review
## Summary
A well-designed feature addition implementing role-based review personas. The code is idiomatic Go, follows the repository's zero-dependency convention, has comprehensive test coverage, and the refactoring of `validateWorkspacePath` into a reusable helper is a nice improvement. CI passes.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/persona_test.go` | 196 | The `contains` and `containsHelper` helpers are reimplementing `strings.Contains`, which is already available in the standard library. The helpers add no value and the custom implementation has a subtle bug: `containsHelper` is only called when `len(s) > 0`, but the outer `contains` check `len(s) >= len(substr)` already handles the empty-substr case. Just use `strings.Contains` directly — it handles all edge cases correctly and is idiomatic Go. |
| 2 | [MINOR] | `review/formatter.go` | 56 | `FormatMarkdownWithDisplay` duplicates nearly all of the logic in `FormatMarkdown`. The only difference is the `headerName`/`sentinelName` split. `FormatMarkdown` could simply delegate to `FormatMarkdownWithDisplay(result, reviewerName, reviewerName)`, eliminating ~35 lines of duplication. Duplicated formatting logic is a maintenance hazard — a future change (e.g., adding a new section) must be applied in two places. |
| 3 | [MINOR] | `review/persona_prompt.go` | 70 | The `BuildPersonaSystemPrompt` function has 30+ consecutive `sb.WriteString(...)` calls for the hardcoded JSON output format schema. This duplicates the output format specification that already lives in the base `BuildSystemBase()` function. If the JSON schema changes (e.g., adding a new field), both functions must be updated. Consider extracting the shared output format section into a package-level constant or helper function to keep the schema definition in one place. |
| 4 | [NIT] | `review/persona.go` | 5 | `path/filepath` is imported but only used for `filepath.Join("personas", filename)` in `LoadBuiltinPersona`. Since the path separator for embedded FS is always `/` (regardless of OS), `embed.FS` paths must use forward slashes per the `io/fs` spec. Using `filepath.Join` here works on Linux/macOS but would produce `personas\security` on Windows if that ever becomes a build target. Using `"personas/" + filename` directly would be safer and removes the need for the `path/filepath` import. |
| 5 | [NIT] | `docs/DESIGN-51-personas.md` | 1 | The design document still references YAML as the persona format in multiple places (e.g., the `Persona struct` Go code block uses `yaml:` struct tags, the persona file format sections show YAML examples). The design revision section at the bottom explains the switch to JSON, but the earlier sections weren't updated. This creates confusion for anyone reading the doc top-to-bottom. Consider updating or striking through the YAML sections, or noting "see Design Revision below" at the first YAML mention. |
| 6 | [NIT] | `cmd/review-bot/main_test.go` | 7 | The `path/filepath` import was added but placed out of order — the Go convention (and `goimports`) groups stdlib imports alphabetically. It should appear between `"os/exec"` and `"strings"`, not after `"strings"`. Minor but `goimports` would reorder this. |
## Recommendation
**APPROVE** — APPROVE — This is a clean, well-structured feature. The design decision to use JSON instead of YAML preserves the zero-dependency constraint correctly. The `validateWorkspacePath` refactoring is a genuine improvement. The minor findings (duplication in formatter, custom `contains` helper, filepath.Join on embed.FS paths) are worth addressing but don't block merging. The stale design doc is a documentation-only issue.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 83441bfb*
[NIT] The path/filepath import was added but placed out of order — the Go convention (and goimports) groups stdlib imports alphabetically. It should appear between "os/exec" and "strings", not after "strings". Minor but goimports would reorder this.
**[NIT]** The `path/filepath` import was added but placed out of order — the Go convention (and `goimports`) groups stdlib imports alphabetically. It should appear between `"os/exec"` and `"strings"`, not after `"strings"`. Minor but `goimports` would reorder this.
[NIT] The design document still references YAML as the persona format in multiple places (e.g., the Persona struct Go code block uses yaml: struct tags, the persona file format sections show YAML examples). The design revision section at the bottom explains the switch to JSON, but the earlier sections weren't updated. This creates confusion for anyone reading the doc top-to-bottom. Consider updating or striking through the YAML sections, or noting "see Design Revision below" at the first YAML mention.
**[NIT]** The design document still references YAML as the persona format in multiple places (e.g., the `Persona struct` Go code block uses `yaml:` struct tags, the persona file format sections show YAML examples). The design revision section at the bottom explains the switch to JSON, but the earlier sections weren't updated. This creates confusion for anyone reading the doc top-to-bottom. Consider updating or striking through the YAML sections, or noting "see Design Revision below" at the first YAML mention.
[MINOR]FormatMarkdownWithDisplay duplicates nearly all of the logic in FormatMarkdown. The only difference is the headerName/sentinelName split. FormatMarkdown could simply delegate to FormatMarkdownWithDisplay(result, reviewerName, reviewerName), eliminating ~35 lines of duplication. Duplicated formatting logic is a maintenance hazard — a future change (e.g., adding a new section) must be applied in two places.
**[MINOR]** `FormatMarkdownWithDisplay` duplicates nearly all of the logic in `FormatMarkdown`. The only difference is the `headerName`/`sentinelName` split. `FormatMarkdown` could simply delegate to `FormatMarkdownWithDisplay(result, reviewerName, reviewerName)`, eliminating ~35 lines of duplication. Duplicated formatting logic is a maintenance hazard — a future change (e.g., adding a new section) must be applied in two places.
[NIT]path/filepath is imported but only used for filepath.Join("personas", filename) in LoadBuiltinPersona. Since the path separator for embedded FS is always / (regardless of OS), embed.FS paths must use forward slashes per the io/fs spec. Using filepath.Join here works on Linux/macOS but would produce personas\security on Windows if that ever becomes a build target. Using "personas/" + filename directly would be safer and removes the need for the path/filepath import.
**[NIT]** `path/filepath` is imported but only used for `filepath.Join("personas", filename)` in `LoadBuiltinPersona`. Since the path separator for embedded FS is always `/` (regardless of OS), `embed.FS` paths must use forward slashes per the `io/fs` spec. Using `filepath.Join` here works on Linux/macOS but would produce `personas\security` on Windows if that ever becomes a build target. Using `"personas/" + filename` directly would be safer and removes the need for the `path/filepath` import.
[MINOR] The BuildPersonaSystemPrompt function has 30+ consecutive sb.WriteString(...) calls for the hardcoded JSON output format schema. This duplicates the output format specification that already lives in the base BuildSystemBase() function. If the JSON schema changes (e.g., adding a new field), both functions must be updated. Consider extracting the shared output format section into a package-level constant or helper function to keep the schema definition in one place.
**[MINOR]** The `BuildPersonaSystemPrompt` function has 30+ consecutive `sb.WriteString(...)` calls for the hardcoded JSON output format schema. This duplicates the output format specification that already lives in the base `BuildSystemBase()` function. If the JSON schema changes (e.g., adding a new field), both functions must be updated. Consider extracting the shared output format section into a package-level constant or helper function to keep the schema definition in one place.
[MINOR] The contains and containsHelper helpers are reimplementing strings.Contains, which is already available in the standard library. The helpers add no value and the custom implementation has a subtle bug: containsHelper is only called when len(s) > 0, but the outer contains check len(s) >= len(substr) already handles the empty-substr case. Just use strings.Contains directly — it handles all edge cases correctly and is idiomatic Go.
**[MINOR]** The `contains` and `containsHelper` helpers are reimplementing `strings.Contains`, which is already available in the standard library. The helpers add no value and the custom implementation has a subtle bug: `containsHelper` is only called when `len(s) > 0`, but the outer `contains` check `len(s) >= len(substr)` already handles the empty-substr case. Just use `strings.Contains` directly — it handles all edge cases correctly and is idiomatic Go.
gpt-review-bot
approved these changes 2026-05-10 15:49:08 +00:00
Solid implementation of role-based personas with clean CLI/action wiring, secure file path validation, embedded built-ins, and comprehensive tests. The changes are cohesive, backward compatible, and CI passed.
Findings
#
Severity
File
Line
Finding
1
[NIT]
review/persona.go
19
Persona.ModelPref (model_preference) is defined but not used anywhere. Consider either wiring it into model selection or removing it to avoid confusion.
2
[NIT]
review/persona_prompt.go
1
BuildPersonaSystemPrompt re-specifies the output schema that likely mirrors BuildSystemBase; consider refactoring to share a common formatter for the JSON output specification to avoid future drift.
3
[NIT]
.gitea/actions/review/action.yml
1
Added persona inputs are fine, but consider documenting expected values inline (e.g., valid built-in names) or validating them early to provide faster feedback in CI logs.
Recommendation
APPROVE — Overall, this is a well-designed and well-tested feature addition. The persona system integrates cleanly with the existing prompt/budget pipeline, the action and CLI flags are mutually exclusive and validated, path handling is secure via workspace-bound resolution, and the embedded built-in personas are convenient and covered by tests. Consider addressing the small nits: either use Persona.ModelPref (e.g., log a warning or influence model choice) or remove it to reduce ambiguity, and consider centralizing the common JSON output schema between persona and default prompts to minimize duplication. Otherwise, this is good to merge.
# Gpt Review
## Summary
Solid implementation of role-based personas with clean CLI/action wiring, secure file path validation, embedded built-ins, and comprehensive tests. The changes are cohesive, backward compatible, and CI passed.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [NIT] | `review/persona.go` | 19 | Persona.ModelPref (model_preference) is defined but not used anywhere. Consider either wiring it into model selection or removing it to avoid confusion. |
| 2 | [NIT] | `review/persona_prompt.go` | 1 | BuildPersonaSystemPrompt re-specifies the output schema that likely mirrors BuildSystemBase; consider refactoring to share a common formatter for the JSON output specification to avoid future drift. |
| 3 | [NIT] | `.gitea/actions/review/action.yml` | 1 | Added persona inputs are fine, but consider documenting expected values inline (e.g., valid built-in names) or validating them early to provide faster feedback in CI logs. |
## Recommendation
**APPROVE** — Overall, this is a well-designed and well-tested feature addition. The persona system integrates cleanly with the existing prompt/budget pipeline, the action and CLI flags are mutually exclusive and validated, path handling is secure via workspace-bound resolution, and the embedded built-in personas are convenient and covered by tests. Consider addressing the small nits: either use Persona.ModelPref (e.g., log a warning or influence model choice) or remove it to reduce ambiguity, and consider centralizing the common JSON output schema between persona and default prompts to minimize duplication. Otherwise, this is good to merge.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 83441bfb*
[NIT] Persona.ModelPref (model_preference) is defined but not used anywhere. Consider either wiring it into model selection or removing it to avoid confusion.
**[NIT]** Persona.ModelPref (model_preference) is defined but not used anywhere. Consider either wiring it into model selection or removing it to avoid confusion.
[NIT] BuildPersonaSystemPrompt re-specifies the output schema that likely mirrors BuildSystemBase; consider refactoring to share a common formatter for the JSON output specification to avoid future drift.
**[NIT]** BuildPersonaSystemPrompt re-specifies the output schema that likely mirrors BuildSystemBase; consider refactoring to share a common formatter for the JSON output specification to avoid future drift.
security-review-bot
requested review from security-review-bot 2026-05-10 15:49:50 +00:00
The persona feature is implemented cleanly with sensible security boundaries. Paths for persona and prompt files are validated against the workspace with symlink checks, and built-in personas are embedded to avoid remote fetches. No exploitable vulnerabilities were found.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
cmd/review-bot/main.go
628
Workspace boundary check relies on strings.HasPrefix for path containment. While mitigated by filepath.Clean and EvalSymlinks, using filepath.Rel (and verifying it does not start with "..") would be a more robust, cross-platform defense-in-depth against subtle path normalization edge cases.
2
[NIT]
review/formatter.go
19
Persona display name is inserted into the Markdown header and footer without sanitization. Although Gitea typically sanitizes HTML and this is repository-controlled input, consider documenting that display_name should avoid raw HTML or adding minimal sanitization to reduce the risk of rendering surprises.
Recommendation
APPROVE — Approve this change. The new persona system uses embedded JSON for built-in roles and validates local file inputs with a clear workspace-bound and symlink-safe check. No injection, auth, or secret-handling issues were identified. As defense-in-depth, consider switching the path containment check to use filepath.Rel to avoid relying on prefix comparisons, and optionally document or sanitize persona display names to prevent unexpected Markdown/HTML rendering. Otherwise, the implementation is sound and CI has passed.
# Security Review
## Summary
The persona feature is implemented cleanly with sensible security boundaries. Paths for persona and prompt files are validated against the workspace with symlink checks, and built-in personas are embedded to avoid remote fetches. No exploitable vulnerabilities were found.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `cmd/review-bot/main.go` | 628 | Workspace boundary check relies on strings.HasPrefix for path containment. While mitigated by filepath.Clean and EvalSymlinks, using filepath.Rel (and verifying it does not start with "..") would be a more robust, cross-platform defense-in-depth against subtle path normalization edge cases. |
| 2 | [NIT] | `review/formatter.go` | 19 | Persona display name is inserted into the Markdown header and footer without sanitization. Although Gitea typically sanitizes HTML and this is repository-controlled input, consider documenting that display_name should avoid raw HTML or adding minimal sanitization to reduce the risk of rendering surprises. |
## Recommendation
**APPROVE** — Approve this change. The new persona system uses embedded JSON for built-in roles and validates local file inputs with a clear workspace-bound and symlink-safe check. No injection, auth, or secret-handling issues were identified. As defense-in-depth, consider switching the path containment check to use filepath.Rel to avoid relying on prefix comparisons, and optionally document or sanitize persona display names to prevent unexpected Markdown/HTML rendering. Otherwise, the implementation is sound and CI has passed.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 83441bfb*
[MINOR] Workspace boundary check relies on strings.HasPrefix for path containment. While mitigated by filepath.Clean and EvalSymlinks, using filepath.Rel (and verifying it does not start with "..") would be a more robust, cross-platform defense-in-depth against subtle path normalization edge cases.
**[MINOR]** Workspace boundary check relies on strings.HasPrefix for path containment. While mitigated by filepath.Clean and EvalSymlinks, using filepath.Rel (and verifying it does not start with "..") would be a more robust, cross-platform defense-in-depth against subtle path normalization edge cases.
The PR adds a useful persona feature but critically violates the project's stated convention of 'Go standard library only — no external dependencies' by introducing gopkg.in/yaml.v3. The design document even acknowledges this conflict and resolves to use JSON, but the implementation proceeds to use YAML anyway. This is the primary blocker; all other issues are secondary.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
go.mod
5
Introduces gopkg.in/yaml.v3 as a dependency. The repository conventions explicitly state 'Go standard library only — no external dependencies.' The design document (DESIGN-51-personas.md) even documents this conflict under 'Design Revision: JSON Instead of YAML' and resolves to use JSON to maintain zero dependencies — but the implementation ignores that resolution and uses YAML anyway. The persona loading code must be changed to use only JSON, or the YAML dependency must be removed.
2
[MAJOR]
review/persona.go
12
Imports gopkg.in/yaml.v3 in violation of the zero-external-dependencies convention. The parsePersona function should use only encoding/json. Built-in personas stored as .yaml files should either be converted to .json or parsed with a minimal hand-rolled YAML subset — though the simplest fix consistent with the design document's own resolution is to store built-in personas as .json and remove the yaml.v3 import entirely.
3
[MINOR]
review/persona.go
96
parsePersona uses filepath.Ext(source) to detect JSON vs YAML, but for built-in personas the source is 'builtin:security' (no file extension), so it falls through to the YAML path. This works today only because yaml.v3 is a superset of JSON, but it's fragile: if YAML is replaced with JSON-only, built-in persona sources would need to be renamed (e.g., 'builtin:security.yaml') or the detection logic changed. The dispatch logic is unclear about its contract.
4
[MINOR]
review/persona.go
78
ListBuiltinPersonas silently returns nil when ReadDir fails. The caller (LoadBuiltinPersona error message) would then say 'available: ' with no names, which is confusing. Consider logging or returning an error, or at minimum ensuring the error path doesn't produce a misleading 'available: ' suffix in the error message.
5
[MINOR]
review/persona_prompt.go
94
BuildSystemPromptWithPersona has the conventions and patterns arguments in the wrong order relative to BuildSystemBase's documented usage in the budget assembly (where patterns come before conventions in the prompt). More importantly, the argument order is (persona, conventions, patterns) but the caller in main.go assembles the prompt manually rather than using this function — making BuildSystemPromptWithPersona a dead export that duplicates logic without being used. Either use it in main.go or remove it.
6
[MINOR]
cmd/review-bot/main.go
75
The --persona-file flag description says 'Path to persona JSON file' but the implementation also accepts YAML. The description should say 'Path to persona JSON or YAML file' (or after the dependency fix, just 'JSON file').
7
[NIT]
review/persona.go
89
Trailing whitespace on line 89 (blank line with spaces inside parsePersona before the ext detection block). Minor style issue.
8
[NIT]
review/persona_prompt.go
68
The JSON output format is written as a series of sb.WriteString calls with manually formatted JSON. This is the same pattern used in BuildSystemBase and is consistent, but the inline JSON template is duplicated between BuildPersonaSystemPrompt and BuildSystemBase. A shared constant or helper would reduce drift risk if the output schema ever changes.
9
[NIT]
cmd/review-bot/main_test.go
157
Double blank line after the closing brace of TestValidateWorkspacePath before the makeReview helper. Minor style inconsistency.
10
[NIT]
README.md
407
The README custom persona example shows YAML format and says 'JSON format is also supported for backwards compatibility' — but this is a new feature with no prior format, so there's no backwards compatibility concern. After removing the yaml.v3 dependency and going JSON-only per the design document's own resolution, this sentence should be removed or changed to accurately describe which format is canonical.
Recommendation
REQUEST_CHANGES — REQUEST_CHANGES. The blocking issue is the introduction of gopkg.in/yaml.v3 in direct violation of the project's 'Go standard library only — no external dependencies' convention. Ironically, the design document that ships with this PR explicitly documents this conflict and resolves to use JSON instead of YAML — but the implementation ignores that resolution. The fix is straightforward: convert the built-in persona files from .yaml to .json, update the embed glob to *.json, remove the yaml.v3 import, and use only encoding/json. Once the dependency is removed, also clean up: (1) the --persona-file description to accurately state the supported format, (2) the README note about 'backwards compatibility' which doesn't apply, and (3) consider whether BuildSystemPromptWithPersona should be used in main.go or removed. The overall feature design is sound and the code quality is good — this is a well-structured addition that just needs the dependency violation corrected.
# Sonnet Review
## Summary
The PR adds a useful persona feature but critically violates the project's stated convention of 'Go standard library only — no external dependencies' by introducing gopkg.in/yaml.v3. The design document even acknowledges this conflict and resolves to use JSON, but the implementation proceeds to use YAML anyway. This is the primary blocker; all other issues are secondary.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `go.mod` | 5 | Introduces gopkg.in/yaml.v3 as a dependency. The repository conventions explicitly state 'Go standard library only — no external dependencies.' The design document (DESIGN-51-personas.md) even documents this conflict under 'Design Revision: JSON Instead of YAML' and resolves to use JSON to maintain zero dependencies — but the implementation ignores that resolution and uses YAML anyway. The persona loading code must be changed to use only JSON, or the YAML dependency must be removed. |
| 2 | [MAJOR] | `review/persona.go` | 12 | Imports gopkg.in/yaml.v3 in violation of the zero-external-dependencies convention. The parsePersona function should use only encoding/json. Built-in personas stored as .yaml files should either be converted to .json or parsed with a minimal hand-rolled YAML subset — though the simplest fix consistent with the design document's own resolution is to store built-in personas as .json and remove the yaml.v3 import entirely. |
| 3 | [MINOR] | `review/persona.go` | 96 | parsePersona uses filepath.Ext(source) to detect JSON vs YAML, but for built-in personas the source is 'builtin:security' (no file extension), so it falls through to the YAML path. This works today only because yaml.v3 is a superset of JSON, but it's fragile: if YAML is replaced with JSON-only, built-in persona sources would need to be renamed (e.g., 'builtin:security.yaml') or the detection logic changed. The dispatch logic is unclear about its contract. |
| 4 | [MINOR] | `review/persona.go` | 78 | ListBuiltinPersonas silently returns nil when ReadDir fails. The caller (LoadBuiltinPersona error message) would then say 'available: ' with no names, which is confusing. Consider logging or returning an error, or at minimum ensuring the error path doesn't produce a misleading 'available: ' suffix in the error message. |
| 5 | [MINOR] | `review/persona_prompt.go` | 94 | BuildSystemPromptWithPersona has the conventions and patterns arguments in the wrong order relative to BuildSystemBase's documented usage in the budget assembly (where patterns come before conventions in the prompt). More importantly, the argument order is (persona, conventions, patterns) but the caller in main.go assembles the prompt manually rather than using this function — making BuildSystemPromptWithPersona a dead export that duplicates logic without being used. Either use it in main.go or remove it. |
| 6 | [MINOR] | `cmd/review-bot/main.go` | 75 | The --persona-file flag description says 'Path to persona JSON file' but the implementation also accepts YAML. The description should say 'Path to persona JSON or YAML file' (or after the dependency fix, just 'JSON file'). |
| 7 | [NIT] | `review/persona.go` | 89 | Trailing whitespace on line 89 (blank line with spaces inside parsePersona before the ext detection block). Minor style issue. |
| 8 | [NIT] | `review/persona_prompt.go` | 68 | The JSON output format is written as a series of sb.WriteString calls with manually formatted JSON. This is the same pattern used in BuildSystemBase and is consistent, but the inline JSON template is duplicated between BuildPersonaSystemPrompt and BuildSystemBase. A shared constant or helper would reduce drift risk if the output schema ever changes. |
| 9 | [NIT] | `cmd/review-bot/main_test.go` | 157 | Double blank line after the closing brace of TestValidateWorkspacePath before the makeReview helper. Minor style inconsistency. |
| 10 | [NIT] | `README.md` | 407 | The README custom persona example shows YAML format and says 'JSON format is also supported for backwards compatibility' — but this is a new feature with no prior format, so there's no backwards compatibility concern. After removing the yaml.v3 dependency and going JSON-only per the design document's own resolution, this sentence should be removed or changed to accurately describe which format is canonical. |
## Recommendation
**REQUEST_CHANGES** — REQUEST_CHANGES. The blocking issue is the introduction of gopkg.in/yaml.v3 in direct violation of the project's 'Go standard library only — no external dependencies' convention. Ironically, the design document that ships with this PR explicitly documents this conflict and resolves to use JSON instead of YAML — but the implementation ignores that resolution. The fix is straightforward: convert the built-in persona files from .yaml to .json, update the embed glob to *.json, remove the yaml.v3 import, and use only encoding/json. Once the dependency is removed, also clean up: (1) the --persona-file description to accurately state the supported format, (2) the README note about 'backwards compatibility' which doesn't apply, and (3) consider whether BuildSystemPromptWithPersona should be used in main.go or removed. The overall feature design is sound and the code quality is good — this is a well-structured addition that just needs the dependency violation corrected.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 3d507073*
[NIT] The README custom persona example shows YAML format and says 'JSON format is also supported for backwards compatibility' — but this is a new feature with no prior format, so there's no backwards compatibility concern. After removing the yaml.v3 dependency and going JSON-only per the design document's own resolution, this sentence should be removed or changed to accurately describe which format is canonical.
**[NIT]** The README custom persona example shows YAML format and says 'JSON format is also supported for backwards compatibility' — but this is a new feature with no prior format, so there's no backwards compatibility concern. After removing the yaml.v3 dependency and going JSON-only per the design document's own resolution, this sentence should be removed or changed to accurately describe which format is canonical.
[MINOR] The --persona-file flag description says 'Path to persona JSON file' but the implementation also accepts YAML. The description should say 'Path to persona JSON or YAML file' (or after the dependency fix, just 'JSON file').
**[MINOR]** The --persona-file flag description says 'Path to persona JSON file' but the implementation also accepts YAML. The description should say 'Path to persona JSON or YAML file' (or after the dependency fix, just 'JSON file').
[MAJOR] Introduces gopkg.in/yaml.v3 as a dependency. The repository conventions explicitly state 'Go standard library only — no external dependencies.' The design document (DESIGN-51-personas.md) even documents this conflict under 'Design Revision: JSON Instead of YAML' and resolves to use JSON to maintain zero dependencies — but the implementation ignores that resolution and uses YAML anyway. The persona loading code must be changed to use only JSON, or the YAML dependency must be removed.
**[MAJOR]** Introduces gopkg.in/yaml.v3 as a dependency. The repository conventions explicitly state 'Go standard library only — no external dependencies.' The design document (DESIGN-51-personas.md) even documents this conflict under 'Design Revision: JSON Instead of YAML' and resolves to use JSON to maintain zero dependencies — but the implementation ignores that resolution and uses YAML anyway. The persona loading code must be changed to use only JSON, or the YAML dependency must be removed.
[MAJOR] Imports gopkg.in/yaml.v3 in violation of the zero-external-dependencies convention. The parsePersona function should use only encoding/json. Built-in personas stored as .yaml files should either be converted to .json or parsed with a minimal hand-rolled YAML subset — though the simplest fix consistent with the design document's own resolution is to store built-in personas as .json and remove the yaml.v3 import entirely.
**[MAJOR]** Imports gopkg.in/yaml.v3 in violation of the zero-external-dependencies convention. The parsePersona function should use only encoding/json. Built-in personas stored as .yaml files should either be converted to .json or parsed with a minimal hand-rolled YAML subset — though the simplest fix consistent with the design document's own resolution is to store built-in personas as .json and remove the yaml.v3 import entirely.
[MINOR] ListBuiltinPersonas silently returns nil when ReadDir fails. The caller (LoadBuiltinPersona error message) would then say 'available: ' with no names, which is confusing. Consider logging or returning an error, or at minimum ensuring the error path doesn't produce a misleading 'available: ' suffix in the error message.
**[MINOR]** ListBuiltinPersonas silently returns nil when ReadDir fails. The caller (LoadBuiltinPersona error message) would then say 'available: ' with no names, which is confusing. Consider logging or returning an error, or at minimum ensuring the error path doesn't produce a misleading 'available: ' suffix in the error message.
[MINOR] parsePersona uses filepath.Ext(source) to detect JSON vs YAML, but for built-in personas the source is 'builtin:security' (no file extension), so it falls through to the YAML path. This works today only because yaml.v3 is a superset of JSON, but it's fragile: if YAML is replaced with JSON-only, built-in persona sources would need to be renamed (e.g., 'builtin:security.yaml') or the detection logic changed. The dispatch logic is unclear about its contract.
**[MINOR]** parsePersona uses filepath.Ext(source) to detect JSON vs YAML, but for built-in personas the source is 'builtin:security' (no file extension), so it falls through to the YAML path. This works today only because yaml.v3 is a superset of JSON, but it's fragile: if YAML is replaced with JSON-only, built-in persona sources would need to be renamed (e.g., 'builtin:security.yaml') or the detection logic changed. The dispatch logic is unclear about its contract.
[NIT] The JSON output format is written as a series of sb.WriteString calls with manually formatted JSON. This is the same pattern used in BuildSystemBase and is consistent, but the inline JSON template is duplicated between BuildPersonaSystemPrompt and BuildSystemBase. A shared constant or helper would reduce drift risk if the output schema ever changes.
**[NIT]** The JSON output format is written as a series of sb.WriteString calls with manually formatted JSON. This is the same pattern used in BuildSystemBase and is consistent, but the inline JSON template is duplicated between BuildPersonaSystemPrompt and BuildSystemBase. A shared constant or helper would reduce drift risk if the output schema ever changes.
[MINOR] BuildSystemPromptWithPersona has the conventions and patterns arguments in the wrong order relative to BuildSystemBase's documented usage in the budget assembly (where patterns come before conventions in the prompt). More importantly, the argument order is (persona, conventions, patterns) but the caller in main.go assembles the prompt manually rather than using this function — making BuildSystemPromptWithPersona a dead export that duplicates logic without being used. Either use it in main.go or remove it.
**[MINOR]** BuildSystemPromptWithPersona has the conventions and patterns arguments in the wrong order relative to BuildSystemBase's documented usage in the budget assembly (where patterns come before conventions in the prompt). More importantly, the argument order is (persona, conventions, patterns) but the caller in main.go assembles the prompt manually rather than using this function — making BuildSystemPromptWithPersona a dead export that duplicates logic without being used. Either use it in main.go or remove it.
The persona feature is well integrated (flags, prompt selection, formatting, tests) and CI passes, but the change introduces an external YAML dependency which violates the repository’s zero-dependency convention. There are also minor documentation inconsistencies and a small portability concern in workspace path validation.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
go.mod
5
Introduces external dependency gopkg.in/yaml.v3, which violates the repository convention of using Go standard library only (zero dependencies).
2
[MAJOR]
review/persona.go
17
Imports gopkg.in/yaml.v3 and parses YAML personas, conflicting with the project’s zero-dependency requirement. Either remove YAML support or replace with JSON-only parsing.
3
[MINOR]
.gitea/actions/review/action.yml
86
The persona-file input description states "Path to persona JSON file" but the implementation supports both YAML and JSON. Update the description to reflect YAML/JSON support for consistency.
4
[MINOR]
docs/DESIGN-51-personas.md
300
The design document states a "Design Revision: JSON Instead of YAML" to preserve zero dependencies, but the implementation retains YAML parsing and adds a YAML dependency. Align docs and code (prefer JSON-only) to match project conventions.
5
[MINOR]
cmd/review-bot/main.go
610
validateWorkspacePath uses strings.HasPrefix for path containment checks, which can be brittle on case-insensitive filesystems. Prefer using filepath.Rel and ensuring the result does not start with ".." for more robust containment checks.
Recommendation
REQUEST_CHANGES — The personas feature is thoughtfully implemented and well tested, but it conflicts with the repository’s "Go standard library only — no external dependencies" rule. To proceed:
Remove the YAML dependency:
Convert built-in personas to embedded JSON files and update //go:embed accordingly.
Drop gopkg.in/yaml.v3 from go.mod and remove YAML parsing paths.
Restrict LoadPersona to JSON files only (or support YAML only if you can implement a zero-dep parser, which is not recommended here). Update README/action descriptions to reflect the final supported format consistently.
Documentation consistency:
Update .gitea/actions/review/action.yml persona-file input description to mention the correct supported format(s) (after (1), likely JSON-only).
Ensure docs/DESIGN-51-personas.md and README consistently state the supported persona format.
Path validation hardening (optional but recommended):
Replace the strings.HasPrefix containment check in validateWorkspacePath with filepath.Rel and ensure the relative path does not start with "..". This is more robust across platforms and case sensitivity.
Once YAML is removed and docs are aligned, the feature will comply with project conventions and can be approved.
# Gpt Review
## Summary
The persona feature is well integrated (flags, prompt selection, formatting, tests) and CI passes, but the change introduces an external YAML dependency which violates the repository’s zero-dependency convention. There are also minor documentation inconsistencies and a small portability concern in workspace path validation.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `go.mod` | 5 | Introduces external dependency gopkg.in/yaml.v3, which violates the repository convention of using Go standard library only (zero dependencies). |
| 2 | [MAJOR] | `review/persona.go` | 17 | Imports gopkg.in/yaml.v3 and parses YAML personas, conflicting with the project’s zero-dependency requirement. Either remove YAML support or replace with JSON-only parsing. |
| 3 | [MINOR] | `.gitea/actions/review/action.yml` | 86 | The persona-file input description states "Path to persona JSON file" but the implementation supports both YAML and JSON. Update the description to reflect YAML/JSON support for consistency. |
| 4 | [MINOR] | `docs/DESIGN-51-personas.md` | 300 | The design document states a "Design Revision: JSON Instead of YAML" to preserve zero dependencies, but the implementation retains YAML parsing and adds a YAML dependency. Align docs and code (prefer JSON-only) to match project conventions. |
| 5 | [MINOR] | `cmd/review-bot/main.go` | 610 | validateWorkspacePath uses strings.HasPrefix for path containment checks, which can be brittle on case-insensitive filesystems. Prefer using filepath.Rel and ensuring the result does not start with ".." for more robust containment checks. |
## Recommendation
**REQUEST_CHANGES** — The personas feature is thoughtfully implemented and well tested, but it conflicts with the repository’s "Go standard library only — no external dependencies" rule. To proceed:
1) Remove the YAML dependency:
- Convert built-in personas to embedded JSON files and update //go:embed accordingly.
- Drop gopkg.in/yaml.v3 from go.mod and remove YAML parsing paths.
- Restrict LoadPersona to JSON files only (or support YAML only if you can implement a zero-dep parser, which is not recommended here). Update README/action descriptions to reflect the final supported format consistently.
2) Documentation consistency:
- Update .gitea/actions/review/action.yml persona-file input description to mention the correct supported format(s) (after (1), likely JSON-only).
- Ensure docs/DESIGN-51-personas.md and README consistently state the supported persona format.
3) Path validation hardening (optional but recommended):
- Replace the strings.HasPrefix containment check in validateWorkspacePath with filepath.Rel and ensure the relative path does not start with "..". This is more robust across platforms and case sensitivity.
Once YAML is removed and docs are aligned, the feature will comply with project conventions and can be approved.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 3d507073*
[MINOR] The persona-file input description states "Path to persona JSON file" but the implementation supports both YAML and JSON. Update the description to reflect YAML/JSON support for consistency.
**[MINOR]** The persona-file input description states "Path to persona JSON file" but the implementation supports both YAML and JSON. Update the description to reflect YAML/JSON support for consistency.
[MINOR] validateWorkspacePath uses strings.HasPrefix for path containment checks, which can be brittle on case-insensitive filesystems. Prefer using filepath.Rel and ensuring the result does not start with ".." for more robust containment checks.
**[MINOR]** validateWorkspacePath uses strings.HasPrefix for path containment checks, which can be brittle on case-insensitive filesystems. Prefer using filepath.Rel and ensuring the result does not start with ".." for more robust containment checks.
[MINOR] The design document states a "Design Revision: JSON Instead of YAML" to preserve zero dependencies, but the implementation retains YAML parsing and adds a YAML dependency. Align docs and code (prefer JSON-only) to match project conventions.
**[MINOR]** The design document states a "Design Revision: JSON Instead of YAML" to preserve zero dependencies, but the implementation retains YAML parsing and adds a YAML dependency. Align docs and code (prefer JSON-only) to match project conventions.
[MAJOR] Introduces external dependency gopkg.in/yaml.v3, which violates the repository convention of using Go standard library only (zero dependencies).
**[MAJOR]** Introduces external dependency gopkg.in/yaml.v3, which violates the repository convention of using Go standard library only (zero dependencies).
[MAJOR] Imports gopkg.in/yaml.v3 and parses YAML personas, conflicting with the project’s zero-dependency requirement. Either remove YAML support or replace with JSON-only parsing.
**[MAJOR]** Imports gopkg.in/yaml.v3 and parses YAML personas, conflicting with the project’s zero-dependency requirement. Either remove YAML support or replace with JSON-only parsing.
security-review-bot
requested review from security-review-bot 2026-05-10 16:10:46 +00:00
The changes add role-based review personas with proper path validation and clean integration into the CLI and action. No exploitable vulnerabilities are evident, and CI has passed.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/persona.go
33
Persona files from the repository are read and unmarshaled without an explicit size limit. A very large YAML/JSON persona file could cause elevated memory/CPU usage during os.ReadFile and yaml/json unmarshaling, enabling a potential CI resource exhaustion vector. Consider enforcing a maximum file size before reading/unmarshaling and rejecting oversized files.
2
[MINOR]
go.mod
5
An external dependency (gopkg.in/yaml.v3 v3.0.1) is introduced for YAML support. While common and currently patched against known alias-expansion issues, any new dependency increases supply-chain risk. Ensure this version is regularly audited and pinned, and consider preferring JSON where possible to minimize the attack surface.
3
[NIT]
review/formatter.go
61
Persona DisplayName is inserted into the review header and footer without escaping. Although Gitea typically sanitizes markdown, an attacker-controlled DisplayName from a repo persona file could inject undesirable markdown. Consider sanitizing or constraining DisplayName to a safe character set to reduce reliance on downstream sanitization.
Recommendation
APPROVE — Overall, the persona feature is implemented securely, with good path traversal and symlink defenses via validateWorkspacePath. Approve the changes. As hardening steps, enforce a reasonable maximum size on persona and system prompt files before unmarshaling to mitigate potential CI resource exhaustion, maintain vigilance on the new YAML dependency (audit and pin versions), and consider sanitizing persona DisplayName outputs to guard against potential markdown injection should downstream sanitization be bypassed.
# Security Review
## Summary
The changes add role-based review personas with proper path validation and clean integration into the CLI and action. No exploitable vulnerabilities are evident, and CI has passed.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/persona.go` | 33 | Persona files from the repository are read and unmarshaled without an explicit size limit. A very large YAML/JSON persona file could cause elevated memory/CPU usage during os.ReadFile and yaml/json unmarshaling, enabling a potential CI resource exhaustion vector. Consider enforcing a maximum file size before reading/unmarshaling and rejecting oversized files. |
| 2 | [MINOR] | `go.mod` | 5 | An external dependency (gopkg.in/yaml.v3 v3.0.1) is introduced for YAML support. While common and currently patched against known alias-expansion issues, any new dependency increases supply-chain risk. Ensure this version is regularly audited and pinned, and consider preferring JSON where possible to minimize the attack surface. |
| 3 | [NIT] | `review/formatter.go` | 61 | Persona DisplayName is inserted into the review header and footer without escaping. Although Gitea typically sanitizes markdown, an attacker-controlled DisplayName from a repo persona file could inject undesirable markdown. Consider sanitizing or constraining DisplayName to a safe character set to reduce reliance on downstream sanitization. |
## Recommendation
**APPROVE** — Overall, the persona feature is implemented securely, with good path traversal and symlink defenses via validateWorkspacePath. Approve the changes. As hardening steps, enforce a reasonable maximum size on persona and system prompt files before unmarshaling to mitigate potential CI resource exhaustion, maintain vigilance on the new YAML dependency (audit and pin versions), and consider sanitizing persona DisplayName outputs to guard against potential markdown injection should downstream sanitization be bypassed.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 3d507073*
[MINOR] An external dependency (gopkg.in/yaml.v3 v3.0.1) is introduced for YAML support. While common and currently patched against known alias-expansion issues, any new dependency increases supply-chain risk. Ensure this version is regularly audited and pinned, and consider preferring JSON where possible to minimize the attack surface.
**[MINOR]** An external dependency (gopkg.in/yaml.v3 v3.0.1) is introduced for YAML support. While common and currently patched against known alias-expansion issues, any new dependency increases supply-chain risk. Ensure this version is regularly audited and pinned, and consider preferring JSON where possible to minimize the attack surface.
[NIT] Persona DisplayName is inserted into the review header and footer without escaping. Although Gitea typically sanitizes markdown, an attacker-controlled DisplayName from a repo persona file could inject undesirable markdown. Consider sanitizing or constraining DisplayName to a safe character set to reduce reliance on downstream sanitization.
**[NIT]** Persona DisplayName is inserted into the review header and footer without escaping. Although Gitea typically sanitizes markdown, an attacker-controlled DisplayName from a repo persona file could inject undesirable markdown. Consider sanitizing or constraining DisplayName to a safe character set to reduce reliance on downstream sanitization.
[MINOR] Persona files from the repository are read and unmarshaled without an explicit size limit. A very large YAML/JSON persona file could cause elevated memory/CPU usage during os.ReadFile and yaml/json unmarshaling, enabling a potential CI resource exhaustion vector. Consider enforcing a maximum file size before reading/unmarshaling and rejecting oversized files.
**[MINOR]** Persona files from the repository are read and unmarshaled without an explicit size limit. A very large YAML/JSON persona file could cause elevated memory/CPU usage during os.ReadFile and yaml/json unmarshaling, enabling a potential CI resource exhaustion vector. Consider enforcing a maximum file size before reading/unmarshaling and rejecting oversized files.
Add persona system for specialized review roles. Each persona defines:
- A specific review focus (security, architecture, documentation)
- Custom system prompt additions
- Personality/tone adjustments
Built-in personas: security, architect, docs
Custom personas: load from JSON via persona-file flag
Includes workspace validation to prevent path traversal attacks.
Closes#51
The implementation is well-structured and feature-complete, but it introduces an external dependency (github.com/goccy/go-yaml) in a repository that explicitly requires Go standard library only. This is a direct violation of the repository's stated conventions. All other aspects of the code are high quality.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
go.mod
5
The repository conventions explicitly state 'Go standard library only — no external dependencies.' This PR adds github.com/goccy/go-yaml v1.19.2 as an external dependency. The design doc (docs/DESIGN-51-personas.md) even notes a 'Design Revision' acknowledging this decision, but the justification (multi-line strings, comments in YAML) does not override the stated project constraint. YAML parsing can be handled with the stdlib encoding/json package (JSON-only persona files) or by writing a minimal YAML subset parser, but bringing in an external library violates the project's zero-dependencies rule.
2
[MINOR]
review/persona.go
86
The parsePersona function uses filepath.Ext(source) to detect the format, but for built-in personas the source is "builtin:security" (no file extension). The comment says 'YAML (also handles .yaml, .yml, and builtin: prefix)' but this only works by coincidence — filepath.Ext("builtin:security") returns "" which falls into the else-YAML branch. This is fragile. A more explicit check like source == ".json" or extracting the logic to handle the builtin: prefix explicitly would be clearer and more robust.
3
[MINOR]
review/persona.go
60
LoadBuiltinPersona calls ListBuiltinPersonas() to build an error message when the persona isn't found. However, ListBuiltinPersonas reads the directory from the embedded FS — if that somehow fails, it returns nil and the error message becomes 'unknown built-in persona "foo" (available: )'. This is a minor usability issue, but it would be better to hardcode the list of known personas or at least handle the empty-list case.
4
[MINOR]
review/formatter.go
27
The comment on FormatMarkdownWithDisplay says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).' While the trust reasoning is sound for built-in personas, --persona-file allows the display name to come from any YAML file in the workspace. If a workspace file is somehow malicious (e.g., a compromised dependency's config), the unescaped display name could inject markdown. This is a low-risk but worth noting given the security context of this tool.
5
[NIT]
review/persona.go
82
There is a trailing whitespace after var p Persona before the blank line. Minor style issue.
6
[NIT]
cmd/review-bot/main_test.go
158
There is a double blank line between TestValidateWorkspacePath and makeReview. Consistent single blank lines between test functions would match the rest of the file.
7
[NIT]
review/persona_test.go
226
TestLoadPersonaInvalidYAML uses "not: valid: yaml: here" as invalid YAML, but this is actually valid YAML (it's a key not with value valid: yaml: here as a string). The test may pass only because the YAML parses to a struct with empty required fields, triggering the validation error rather than a parse error. The test description says 'invalid YAML' but it's really testing 'YAML missing required fields'. The test still provides coverage but the intent is misleading.
Recommendation
REQUEST_CHANGES — REQUEST_CHANGES — The external dependency on github.com/goccy/go-yaml must be removed before this can be merged. The project's conventions document is unambiguous: 'Go standard library only — no external dependencies.' The design document itself acknowledges this was a deliberate decision, but the decision contradicts the project's rules. The fix is straightforward: either (1) support only JSON format for persona files using encoding/json from stdlib, or (2) write a minimal YAML parser for the simple key-value/list structure used in persona files. Given that JSON is already supported and the built-in personas could be shipped as JSON, option 1 is the path of least resistance. The rest of the implementation is clean, well-tested, and correctly designed.
# Sonnet Review
## Summary
The implementation is well-structured and feature-complete, but it introduces an external dependency (`github.com/goccy/go-yaml`) in a repository that explicitly requires Go standard library only. This is a direct violation of the repository's stated conventions. All other aspects of the code are high quality.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `go.mod` | 5 | The repository conventions explicitly state 'Go standard library only — no external dependencies.' This PR adds `github.com/goccy/go-yaml v1.19.2` as an external dependency. The design doc (`docs/DESIGN-51-personas.md`) even notes a 'Design Revision' acknowledging this decision, but the justification (multi-line strings, comments in YAML) does not override the stated project constraint. YAML parsing can be handled with the stdlib `encoding/json` package (JSON-only persona files) or by writing a minimal YAML subset parser, but bringing in an external library violates the project's zero-dependencies rule. |
| 2 | [MINOR] | `review/persona.go` | 86 | The `parsePersona` function uses `filepath.Ext(source)` to detect the format, but for built-in personas the `source` is `"builtin:security"` (no file extension). The comment says 'YAML (also handles .yaml, .yml, and builtin: prefix)' but this only works by coincidence — `filepath.Ext("builtin:security")` returns `""` which falls into the else-YAML branch. This is fragile. A more explicit check like `source == ".json"` or extracting the logic to handle the `builtin:` prefix explicitly would be clearer and more robust. |
| 3 | [MINOR] | `review/persona.go` | 60 | `LoadBuiltinPersona` calls `ListBuiltinPersonas()` to build an error message when the persona isn't found. However, `ListBuiltinPersonas` reads the directory from the embedded FS — if that somehow fails, it returns `nil` and the error message becomes 'unknown built-in persona "foo" (available: )'. This is a minor usability issue, but it would be better to hardcode the list of known personas or at least handle the empty-list case. |
| 4 | [MINOR] | `review/formatter.go` | 27 | The comment on `FormatMarkdownWithDisplay` says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).' While the trust reasoning is sound for built-in personas, `--persona-file` allows the display name to come from any YAML file in the workspace. If a workspace file is somehow malicious (e.g., a compromised dependency's config), the unescaped display name could inject markdown. This is a low-risk but worth noting given the security context of this tool. |
| 5 | [NIT] | `review/persona.go` | 82 | There is a trailing whitespace after `var p Persona` before the blank line. Minor style issue. |
| 6 | [NIT] | `cmd/review-bot/main_test.go` | 158 | There is a double blank line between `TestValidateWorkspacePath` and `makeReview`. Consistent single blank lines between test functions would match the rest of the file. |
| 7 | [NIT] | `review/persona_test.go` | 226 | `TestLoadPersonaInvalidYAML` uses `"not: valid: yaml: here"` as invalid YAML, but this is actually valid YAML (it's a key `not` with value `valid: yaml: here` as a string). The test may pass only because the YAML parses to a struct with empty required fields, triggering the validation error rather than a parse error. The test description says 'invalid YAML' but it's really testing 'YAML missing required fields'. The test still provides coverage but the intent is misleading. |
## Recommendation
**REQUEST_CHANGES** — REQUEST_CHANGES — The external dependency on `github.com/goccy/go-yaml` must be removed before this can be merged. The project's `conventions` document is unambiguous: 'Go standard library only — no external dependencies.' The design document itself acknowledges this was a deliberate decision, but the decision contradicts the project's rules. The fix is straightforward: either (1) support only JSON format for persona files using `encoding/json` from stdlib, or (2) write a minimal YAML parser for the simple key-value/list structure used in persona files. Given that JSON is already supported and the built-in personas could be shipped as JSON, option 1 is the path of least resistance. The rest of the implementation is clean, well-tested, and correctly designed.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 57e62a34*
[NIT] There is a double blank line between TestValidateWorkspacePath and makeReview. Consistent single blank lines between test functions would match the rest of the file.
**[NIT]** There is a double blank line between `TestValidateWorkspacePath` and `makeReview`. Consistent single blank lines between test functions would match the rest of the file.
[MAJOR] The repository conventions explicitly state 'Go standard library only — no external dependencies.' This PR adds github.com/goccy/go-yaml v1.19.2 as an external dependency. The design doc (docs/DESIGN-51-personas.md) even notes a 'Design Revision' acknowledging this decision, but the justification (multi-line strings, comments in YAML) does not override the stated project constraint. YAML parsing can be handled with the stdlib encoding/json package (JSON-only persona files) or by writing a minimal YAML subset parser, but bringing in an external library violates the project's zero-dependencies rule.
**[MAJOR]** The repository conventions explicitly state 'Go standard library only — no external dependencies.' This PR adds `github.com/goccy/go-yaml v1.19.2` as an external dependency. The design doc (`docs/DESIGN-51-personas.md`) even notes a 'Design Revision' acknowledging this decision, but the justification (multi-line strings, comments in YAML) does not override the stated project constraint. YAML parsing can be handled with the stdlib `encoding/json` package (JSON-only persona files) or by writing a minimal YAML subset parser, but bringing in an external library violates the project's zero-dependencies rule.
[MINOR] The comment on FormatMarkdownWithDisplay says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).' While the trust reasoning is sound for built-in personas, --persona-file allows the display name to come from any YAML file in the workspace. If a workspace file is somehow malicious (e.g., a compromised dependency's config), the unescaped display name could inject markdown. This is a low-risk but worth noting given the security context of this tool.
**[MINOR]** The comment on `FormatMarkdownWithDisplay` says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).' While the trust reasoning is sound for built-in personas, `--persona-file` allows the display name to come from any YAML file in the workspace. If a workspace file is somehow malicious (e.g., a compromised dependency's config), the unescaped display name could inject markdown. This is a low-risk but worth noting given the security context of this tool.
[MINOR]LoadBuiltinPersona calls ListBuiltinPersonas() to build an error message when the persona isn't found. However, ListBuiltinPersonas reads the directory from the embedded FS — if that somehow fails, it returns nil and the error message becomes 'unknown built-in persona "foo" (available: )'. This is a minor usability issue, but it would be better to hardcode the list of known personas or at least handle the empty-list case.
**[MINOR]** `LoadBuiltinPersona` calls `ListBuiltinPersonas()` to build an error message when the persona isn't found. However, `ListBuiltinPersonas` reads the directory from the embedded FS — if that somehow fails, it returns `nil` and the error message becomes 'unknown built-in persona "foo" (available: )'. This is a minor usability issue, but it would be better to hardcode the list of known personas or at least handle the empty-list case.
[MINOR] The parsePersona function uses filepath.Ext(source) to detect the format, but for built-in personas the source is "builtin:security" (no file extension). The comment says 'YAML (also handles .yaml, .yml, and builtin: prefix)' but this only works by coincidence — filepath.Ext("builtin:security") returns "" which falls into the else-YAML branch. This is fragile. A more explicit check like source == ".json" or extracting the logic to handle the builtin: prefix explicitly would be clearer and more robust.
**[MINOR]** The `parsePersona` function uses `filepath.Ext(source)` to detect the format, but for built-in personas the `source` is `"builtin:security"` (no file extension). The comment says 'YAML (also handles .yaml, .yml, and builtin: prefix)' but this only works by coincidence — `filepath.Ext("builtin:security")` returns `""` which falls into the else-YAML branch. This is fragile. A more explicit check like `source == ".json"` or extracting the logic to handle the `builtin:` prefix explicitly would be clearer and more robust.
[NIT]TestLoadPersonaInvalidYAML uses "not: valid: yaml: here" as invalid YAML, but this is actually valid YAML (it's a key not with value valid: yaml: here as a string). The test may pass only because the YAML parses to a struct with empty required fields, triggering the validation error rather than a parse error. The test description says 'invalid YAML' but it's really testing 'YAML missing required fields'. The test still provides coverage but the intent is misleading.
**[NIT]** `TestLoadPersonaInvalidYAML` uses `"not: valid: yaml: here"` as invalid YAML, but this is actually valid YAML (it's a key `not` with value `valid: yaml: here` as a string). The test may pass only because the YAML parses to a struct with empty required fields, triggering the validation error rather than a parse error. The test description says 'invalid YAML' but it's really testing 'YAML missing required fields'. The test still provides coverage but the intent is misleading.
security-review-bot
requested review from security-review-bot 2026-05-10 16:28:23 +00:00
The persona feature is integrated cleanly with proper path validation, and CI has passed. Security-wise, there are no exploitable issues in the changes; only minor hardening opportunities exist.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/persona.go
100
Persona YAML is parsed without limits using a third‑party YAML library. If a workflow references a persona file from the PR branch, a malicious PR could supply a very large or adversarial YAML (e.g., anchor expansion) to cause excessive CPU/memory consumption during unmarshalling. Consider enforcing a maximum file size before reading/parsing and/or preferring JSON (stdlib) for untrusted inputs.
2
[NIT]
review/formatter.go
21
DisplayName is inserted into the markdown header/body without escaping. While Gitea typically sanitizes markdown, if this output is ever reused in a different renderer, unescaped content could cause formatting issues. Consider conservative sanitization or documenting the assumption that Gitea sanitization is always applied.
Recommendation
APPROVE — Approve. The additions for role-based personas are implemented with attention to safety: persona-file and system-prompt-file paths are validated against workspace roots and symlink traversal, reviewer-name is strictly validated for sentinel safety, and built-in personas are embedded. As a defense-in-depth improvement, enforce a size limit before parsing persona YAML and prefer JSON where possible to reduce parser complexity. Otherwise, no exploitable vulnerabilities were identified.
# Security Review
## Summary
The persona feature is integrated cleanly with proper path validation, and CI has passed. Security-wise, there are no exploitable issues in the changes; only minor hardening opportunities exist.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/persona.go` | 100 | Persona YAML is parsed without limits using a third‑party YAML library. If a workflow references a persona file from the PR branch, a malicious PR could supply a very large or adversarial YAML (e.g., anchor expansion) to cause excessive CPU/memory consumption during unmarshalling. Consider enforcing a maximum file size before reading/parsing and/or preferring JSON (stdlib) for untrusted inputs. |
| 2 | [NIT] | `review/formatter.go` | 21 | DisplayName is inserted into the markdown header/body without escaping. While Gitea typically sanitizes markdown, if this output is ever reused in a different renderer, unescaped content could cause formatting issues. Consider conservative sanitization or documenting the assumption that Gitea sanitization is always applied. |
## Recommendation
**APPROVE** — Approve. The additions for role-based personas are implemented with attention to safety: persona-file and system-prompt-file paths are validated against workspace roots and symlink traversal, reviewer-name is strictly validated for sentinel safety, and built-in personas are embedded. As a defense-in-depth improvement, enforce a size limit before parsing persona YAML and prefer JSON where possible to reduce parser complexity. Otherwise, no exploitable vulnerabilities were identified.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 57e62a34*
[NIT] DisplayName is inserted into the markdown header/body without escaping. While Gitea typically sanitizes markdown, if this output is ever reused in a different renderer, unescaped content could cause formatting issues. Consider conservative sanitization or documenting the assumption that Gitea sanitization is always applied.
**[NIT]** DisplayName is inserted into the markdown header/body without escaping. While Gitea typically sanitizes markdown, if this output is ever reused in a different renderer, unescaped content could cause formatting issues. Consider conservative sanitization or documenting the assumption that Gitea sanitization is always applied.
[MINOR] Persona YAML is parsed without limits using a third‑party YAML library. If a workflow references a persona file from the PR branch, a malicious PR could supply a very large or adversarial YAML (e.g., anchor expansion) to cause excessive CPU/memory consumption during unmarshalling. Consider enforcing a maximum file size before reading/parsing and/or preferring JSON (stdlib) for untrusted inputs.
**[MINOR]** Persona YAML is parsed without limits using a third‑party YAML library. If a workflow references a persona file from the PR branch, a malicious PR could supply a very large or adversarial YAML (e.g., anchor expansion) to cause excessive CPU/memory consumption during unmarshalling. Consider enforcing a maximum file size before reading/parsing and/or preferring JSON (stdlib) for untrusted inputs.
Solid feature addition with clear tests and documentation, but it introduces a new external dependency in violation of repository conventions that require Go standard library only. Minor inconsistencies and small edge-case issues should also be addressed.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
go.mod
5
Adds external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'. Either remove this dependency (e.g., support only JSON) or update the repo conventions accordingly.
2
[MINOR]
cmd/review-bot/main_test.go
80
TestValidateWorkspacePath expects the error substring 'failed to resolve' for an absolute path ("/etc/passwd"), but validateWorkspacePath returns 'resolves outside workspace' for absolute paths. This mismatch can cause brittle or failing tests; align the expected substring with the actual error or adjust the function to match the test intent.
3
[MINOR]
docs/DESIGN-51-personas.md
304
Design doc states 'Decision: Add gopkg.in/yaml.v3' but the implementation uses github.com/goccy/go-yaml. Update the design document to reflect the actual library choice or switch the implementation to match the doc.
4
[NIT]
review/formatter.go
28
Header capitalization uses byte indexing (strings.ToUpper(headerName[:1]) + headerName[1:]) which is not Unicode-safe for multi-byte runes. Consider using utf8.DecodeRuneInString to handle non-ASCII display names correctly.
5
[NIT]
.gitea/actions/review/action.yml
86
The 'persona-file' input description says 'Path to persona JSON file', but the implementation supports YAML and JSON. Update the description to avoid misleading users (e.g., 'YAML/JSON').
6
[NIT]
review/persona.go
48
ListBuiltinPersonas returns nil on error, which may require nil checks at call sites. Returning an empty slice is often more ergonomic and safer.
Recommendation
REQUEST_CHANGES — The role-based persona feature is well implemented with clear prompt assembly, tests, and documentation. However, to comply with repository conventions, remove the external YAML dependency or restrict personas to JSON parsing via the standard library, or update the repository policy to allow vetted dependencies. Align TestValidateWorkspacePath’s expected error message with validateWorkspacePath’s current behavior (or adjust the function if you prefer the alternative behavior). Update the design doc to reflect the actual YAML library used, fix the action input description for persona-file to mention YAML/JSON, and consider Unicode-safe capitalization in formatter. Once the dependency convention issue is resolved and minor inconsistencies are addressed, this change can be approved.
# Gpt Review
## Summary
Solid feature addition with clear tests and documentation, but it introduces a new external dependency in violation of repository conventions that require Go standard library only. Minor inconsistencies and small edge-case issues should also be addressed.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `go.mod` | 5 | Adds external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'. Either remove this dependency (e.g., support only JSON) or update the repo conventions accordingly. |
| 2 | [MINOR] | `cmd/review-bot/main_test.go` | 80 | TestValidateWorkspacePath expects the error substring 'failed to resolve' for an absolute path ("/etc/passwd"), but validateWorkspacePath returns 'resolves outside workspace' for absolute paths. This mismatch can cause brittle or failing tests; align the expected substring with the actual error or adjust the function to match the test intent. |
| 3 | [MINOR] | `docs/DESIGN-51-personas.md` | 304 | Design doc states 'Decision: Add gopkg.in/yaml.v3' but the implementation uses github.com/goccy/go-yaml. Update the design document to reflect the actual library choice or switch the implementation to match the doc. |
| 4 | [NIT] | `review/formatter.go` | 28 | Header capitalization uses byte indexing (strings.ToUpper(headerName[:1]) + headerName[1:]) which is not Unicode-safe for multi-byte runes. Consider using utf8.DecodeRuneInString to handle non-ASCII display names correctly. |
| 5 | [NIT] | `.gitea/actions/review/action.yml` | 86 | The 'persona-file' input description says 'Path to persona JSON file', but the implementation supports YAML and JSON. Update the description to avoid misleading users (e.g., 'YAML/JSON'). |
| 6 | [NIT] | `review/persona.go` | 48 | ListBuiltinPersonas returns nil on error, which may require nil checks at call sites. Returning an empty slice is often more ergonomic and safer. |
## Recommendation
**REQUEST_CHANGES** — The role-based persona feature is well implemented with clear prompt assembly, tests, and documentation. However, to comply with repository conventions, remove the external YAML dependency or restrict personas to JSON parsing via the standard library, or update the repository policy to allow vetted dependencies. Align TestValidateWorkspacePath’s expected error message with validateWorkspacePath’s current behavior (or adjust the function if you prefer the alternative behavior). Update the design doc to reflect the actual YAML library used, fix the action input description for persona-file to mention YAML/JSON, and consider Unicode-safe capitalization in formatter. Once the dependency convention issue is resolved and minor inconsistencies are addressed, this change can be approved.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 57e62a34*
[NIT] The 'persona-file' input description says 'Path to persona JSON file', but the implementation supports YAML and JSON. Update the description to avoid misleading users (e.g., 'YAML/JSON').
**[NIT]** The 'persona-file' input description says 'Path to persona JSON file', but the implementation supports YAML and JSON. Update the description to avoid misleading users (e.g., 'YAML/JSON').
[MINOR] TestValidateWorkspacePath expects the error substring 'failed to resolve' for an absolute path ("/etc/passwd"), but validateWorkspacePath returns 'resolves outside workspace' for absolute paths. This mismatch can cause brittle or failing tests; align the expected substring with the actual error or adjust the function to match the test intent.
**[MINOR]** TestValidateWorkspacePath expects the error substring 'failed to resolve' for an absolute path ("/etc/passwd"), but validateWorkspacePath returns 'resolves outside workspace' for absolute paths. This mismatch can cause brittle or failing tests; align the expected substring with the actual error or adjust the function to match the test intent.
The persona YAML allows `critical` severity, but the LLM output parser (`review/parser.go`) only accepts MAJOR/MINOR/NIT.
**Resolution:** Keep standard output format. Persona severity section is ONLY for calibrating the LLM's judgment (prompt guidance). Output must still use MAJOR/MINOR/NIT. Document this clearly in persona format docs.
[MINOR] Design doc states 'Decision: Add gopkg.in/yaml.v3' but the implementation uses github.com/goccy/go-yaml. Update the design document to reflect the actual library choice or switch the implementation to match the doc.
**[MINOR]** Design doc states 'Decision: Add gopkg.in/yaml.v3' but the implementation uses github.com/goccy/go-yaml. Update the design document to reflect the actual library choice or switch the implementation to match the doc.
[MAJOR] Adds external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'. Either remove this dependency (e.g., support only JSON) or update the repo conventions accordingly.
**[MAJOR]** Adds external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'. Either remove this dependency (e.g., support only JSON) or update the repo conventions accordingly.
[NIT] Header capitalization uses byte indexing (strings.ToUpper(headerName[:1]) + headerName[1:]) which is not Unicode-safe for multi-byte runes. Consider using utf8.DecodeRuneInString to handle non-ASCII display names correctly.
**[NIT]** Header capitalization uses byte indexing (strings.ToUpper(headerName[:1]) + headerName[1:]) which is not Unicode-safe for multi-byte runes. Consider using utf8.DecodeRuneInString to handle non-ASCII display names correctly.
[NIT] ListBuiltinPersonas returns nil on error, which may require nil checks at call sites. Returning an empty slice is often more ergonomic and safer.
**[NIT]** ListBuiltinPersonas returns nil on error, which may require nil checks at call sites. Returning an empty slice is often more ergonomic and safer.
The PR adds a well-structured persona feature with good test coverage, but introduces an external dependency (github.com/goccy/go-yaml) that violates the project's explicit 'Go standard library only — no external dependencies' convention. This is a blocking issue. There are also a few minor design issues worth addressing.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
go.mod
5
The project's stated convention is 'Go standard library only — no external dependencies,' but this PR adds github.com/goccy/go-yaml v1.19.2. The design doc mentions gopkg.in/yaml.v3 but the implementation uses a different third-party library. YAML parsing can be done with the stdlib by requiring JSON format for all persona files, or by writing a minimal YAML-subset parser. Alternatively, the project convention must be explicitly updated before adding dependencies.
2
[MINOR]
review/persona.go
84
The parsePersona function uses filepath.Ext(source) to decide the format, but for built-in personas the source is "builtin:security" which has no file extension — it correctly falls through to the YAML path. However, for user-provided files without a .json extension (e.g., .yml), it also falls through to YAML, which is correct behavior but is undocumented. More importantly, the comment says 'YAML is a superset of JSON' as justification for the fallback, but this is only approximately true and the actual behavior depends on the library. The logic would be clearer as: if JSON extension → JSON unmarshal, else → YAML unmarshal (which currently is what happens, but the comment is misleading).
3
[MINOR]
review/persona.go
55
LoadBuiltinPersona calls ListBuiltinPersonas() to build an error message only on failure. This means if embeddedPersonas.ReadFile fails AND embeddedPersonas.ReadDir (inside ListBuiltinPersonas) also fails, the error message will be unknown built-in persona "x" (available: ) with an empty list — which is confusing. Consider hardcoding the known persona names in the error message or handling the ReadDir error in ListBuiltinPersonas more gracefully.
4
[MINOR]
review/formatter.go
36
The comment says displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input). While display names are set by repo owners (trusted), they come from YAML files that could be committed by contributors. If a malicious contributor adds a display_name containing --> or similar, it could break the sentinel comment or inject content into the review body. The sentinel is protected (uses sentinelName from reviewerName which is validated), but the header # <displayName> Review is unescaped. This is low severity given Gitea sanitizes Markdown, but the trust model statement in the comment is slightly overstated.
5
[NIT]
review/persona.go
79
Trailing whitespace after the var p Persona declaration (blank line with spaces before the comment). Run gofmt.
6
[NIT]
review/persona.go
94
Trailing whitespace after the closing brace of the else block. Run gofmt.
7
[NIT]
docs/DESIGN-51-personas.md
1
The design document mentions gopkg.in/yaml.v3 but the implementation uses github.com/goccy/go-yaml. The design doc's 'Design Revision' section should be updated to reflect the actual library chosen, or the implementation should use gopkg.in/yaml.v3 as designed.
8
[NIT]
review/persona_prompt.go
56
BuildSystemPromptWithPersona is exported and documented as a convenience wrapper, but it's not used anywhere in the codebase (main.go assembles the prompt manually). This creates a parallel code path that could diverge. Either use it in main.go or make it unexported/remove it to avoid maintenance burden.
Recommendation
REQUEST_CHANGES — REQUEST_CHANGES — The external dependency on github.com/goccy/go-yaml directly violates the project's explicit 'Go standard library only' convention. This needs to be resolved before merging:
Option A (recommended): Drop YAML support entirely and support only JSON for persona files. The built-in personas can be written as JSON. JSON is part of the stdlib (encoding/json). This is the simplest fix and aligns with project conventions. The YAML support in the README can be documented as 'convert to JSON before use.'
Option B: Write a minimal YAML parser sufficient for persona files (only needs string scalars, lists, and maps — no anchors, etc.). This is non-trivial but keeps the dependency-free guarantee.
Option C: Explicitly update the project conventions to permit this specific dependency with justification, get buy-in, then re-merge.
Apart from the dependency issue, the feature design is sound: the persona loading/validation logic is clean, tests are thorough, the path traversal protection via validateWorkspacePath is a good refactor, and the display-name vs sentinel-name separation is well-reasoned. Address the MAJOR finding and the code is close to mergeable.
# Sonnet Review
## Summary
The PR adds a well-structured persona feature with good test coverage, but introduces an external dependency (github.com/goccy/go-yaml) that violates the project's explicit 'Go standard library only — no external dependencies' convention. This is a blocking issue. There are also a few minor design issues worth addressing.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `go.mod` | 5 | The project's stated convention is 'Go standard library only — no external dependencies,' but this PR adds `github.com/goccy/go-yaml v1.19.2`. The design doc mentions gopkg.in/yaml.v3 but the implementation uses a different third-party library. YAML parsing can be done with the stdlib by requiring JSON format for all persona files, or by writing a minimal YAML-subset parser. Alternatively, the project convention must be explicitly updated before adding dependencies. |
| 2 | [MINOR] | `review/persona.go` | 84 | The `parsePersona` function uses `filepath.Ext(source)` to decide the format, but for built-in personas the source is `"builtin:security"` which has no file extension — it correctly falls through to the YAML path. However, for user-provided files without a `.json` extension (e.g., `.yml`), it also falls through to YAML, which is correct behavior but is undocumented. More importantly, the comment says 'YAML is a superset of JSON' as justification for the fallback, but this is only approximately true and the actual behavior depends on the library. The logic would be clearer as: if JSON extension → JSON unmarshal, else → YAML unmarshal (which currently is what happens, but the comment is misleading). |
| 3 | [MINOR] | `review/persona.go` | 55 | `LoadBuiltinPersona` calls `ListBuiltinPersonas()` to build an error message only on failure. This means if `embeddedPersonas.ReadFile` fails AND `embeddedPersonas.ReadDir` (inside `ListBuiltinPersonas`) also fails, the error message will be `unknown built-in persona "x" (available: )` with an empty list — which is confusing. Consider hardcoding the known persona names in the error message or handling the ReadDir error in ListBuiltinPersonas more gracefully. |
| 4 | [MINOR] | `review/formatter.go` | 36 | The comment says `displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).` While display names are set by repo owners (trusted), they come from YAML files that could be committed by contributors. If a malicious contributor adds a `display_name` containing `-->` or similar, it could break the sentinel comment or inject content into the review body. The sentinel is protected (uses `sentinelName` from `reviewerName` which is validated), but the header `# <displayName> Review` is unescaped. This is low severity given Gitea sanitizes Markdown, but the trust model statement in the comment is slightly overstated. |
| 5 | [NIT] | `review/persona.go` | 79 | Trailing whitespace after the `var p Persona` declaration (blank line with spaces before the comment). Run `gofmt`. |
| 6 | [NIT] | `review/persona.go` | 94 | Trailing whitespace after the closing brace of the else block. Run `gofmt`. |
| 7 | [NIT] | `docs/DESIGN-51-personas.md` | 1 | The design document mentions `gopkg.in/yaml.v3` but the implementation uses `github.com/goccy/go-yaml`. The design doc's 'Design Revision' section should be updated to reflect the actual library chosen, or the implementation should use gopkg.in/yaml.v3 as designed. |
| 8 | [NIT] | `review/persona_prompt.go` | 56 | `BuildSystemPromptWithPersona` is exported and documented as a convenience wrapper, but it's not used anywhere in the codebase (main.go assembles the prompt manually). This creates a parallel code path that could diverge. Either use it in main.go or make it unexported/remove it to avoid maintenance burden. |
## Recommendation
**REQUEST_CHANGES** — **REQUEST_CHANGES** — The external dependency on `github.com/goccy/go-yaml` directly violates the project's explicit 'Go standard library only' convention. This needs to be resolved before merging:
**Option A (recommended):** Drop YAML support entirely and support only JSON for persona files. The built-in personas can be written as JSON. JSON is part of the stdlib (`encoding/json`). This is the simplest fix and aligns with project conventions. The YAML support in the README can be documented as 'convert to JSON before use.'
**Option B:** Write a minimal YAML parser sufficient for persona files (only needs string scalars, lists, and maps — no anchors, etc.). This is non-trivial but keeps the dependency-free guarantee.
**Option C:** Explicitly update the project conventions to permit this specific dependency with justification, get buy-in, then re-merge.
Apart from the dependency issue, the feature design is sound: the persona loading/validation logic is clean, tests are thorough, the path traversal protection via `validateWorkspacePath` is a good refactor, and the display-name vs sentinel-name separation is well-reasoned. Address the MAJOR finding and the code is close to mergeable.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 57e62a34*
[NIT] The design document mentions gopkg.in/yaml.v3 but the implementation uses github.com/goccy/go-yaml. The design doc's 'Design Revision' section should be updated to reflect the actual library chosen, or the implementation should use gopkg.in/yaml.v3 as designed.
**[NIT]** The design document mentions `gopkg.in/yaml.v3` but the implementation uses `github.com/goccy/go-yaml`. The design doc's 'Design Revision' section should be updated to reflect the actual library chosen, or the implementation should use gopkg.in/yaml.v3 as designed.
[MAJOR] The project's stated convention is 'Go standard library only — no external dependencies,' but this PR adds github.com/goccy/go-yaml v1.19.2. The design doc mentions gopkg.in/yaml.v3 but the implementation uses a different third-party library. YAML parsing can be done with the stdlib by requiring JSON format for all persona files, or by writing a minimal YAML-subset parser. Alternatively, the project convention must be explicitly updated before adding dependencies.
**[MAJOR]** The project's stated convention is 'Go standard library only — no external dependencies,' but this PR adds `github.com/goccy/go-yaml v1.19.2`. The design doc mentions gopkg.in/yaml.v3 but the implementation uses a different third-party library. YAML parsing can be done with the stdlib by requiring JSON format for all persona files, or by writing a minimal YAML-subset parser. Alternatively, the project convention must be explicitly updated before adding dependencies.
[MINOR] The comment says displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input). While display names are set by repo owners (trusted), they come from YAML files that could be committed by contributors. If a malicious contributor adds a display_name containing --> or similar, it could break the sentinel comment or inject content into the review body. The sentinel is protected (uses sentinelName from reviewerName which is validated), but the header # <displayName> Review is unescaped. This is low severity given Gitea sanitizes Markdown, but the trust model statement in the comment is slightly overstated.
**[MINOR]** The comment says `displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).` While display names are set by repo owners (trusted), they come from YAML files that could be committed by contributors. If a malicious contributor adds a `display_name` containing `-->` or similar, it could break the sentinel comment or inject content into the review body. The sentinel is protected (uses `sentinelName` from `reviewerName` which is validated), but the header `# <displayName> Review` is unescaped. This is low severity given Gitea sanitizes Markdown, but the trust model statement in the comment is slightly overstated.
[MINOR]LoadBuiltinPersona calls ListBuiltinPersonas() to build an error message only on failure. This means if embeddedPersonas.ReadFile fails AND embeddedPersonas.ReadDir (inside ListBuiltinPersonas) also fails, the error message will be unknown built-in persona "x" (available: ) with an empty list — which is confusing. Consider hardcoding the known persona names in the error message or handling the ReadDir error in ListBuiltinPersonas more gracefully.
**[MINOR]** `LoadBuiltinPersona` calls `ListBuiltinPersonas()` to build an error message only on failure. This means if `embeddedPersonas.ReadFile` fails AND `embeddedPersonas.ReadDir` (inside `ListBuiltinPersonas`) also fails, the error message will be `unknown built-in persona "x" (available: )` with an empty list — which is confusing. Consider hardcoding the known persona names in the error message or handling the ReadDir error in ListBuiltinPersonas more gracefully.
[MINOR] The parsePersona function uses filepath.Ext(source) to decide the format, but for built-in personas the source is "builtin:security" which has no file extension — it correctly falls through to the YAML path. However, for user-provided files without a .json extension (e.g., .yml), it also falls through to YAML, which is correct behavior but is undocumented. More importantly, the comment says 'YAML is a superset of JSON' as justification for the fallback, but this is only approximately true and the actual behavior depends on the library. The logic would be clearer as: if JSON extension → JSON unmarshal, else → YAML unmarshal (which currently is what happens, but the comment is misleading).
**[MINOR]** The `parsePersona` function uses `filepath.Ext(source)` to decide the format, but for built-in personas the source is `"builtin:security"` which has no file extension — it correctly falls through to the YAML path. However, for user-provided files without a `.json` extension (e.g., `.yml`), it also falls through to YAML, which is correct behavior but is undocumented. More importantly, the comment says 'YAML is a superset of JSON' as justification for the fallback, but this is only approximately true and the actual behavior depends on the library. The logic would be clearer as: if JSON extension → JSON unmarshal, else → YAML unmarshal (which currently is what happens, but the comment is misleading).
[NIT]BuildSystemPromptWithPersona is exported and documented as a convenience wrapper, but it's not used anywhere in the codebase (main.go assembles the prompt manually). This creates a parallel code path that could diverge. Either use it in main.go or make it unexported/remove it to avoid maintenance burden.
**[NIT]** `BuildSystemPromptWithPersona` is exported and documented as a convenience wrapper, but it's not used anywhere in the codebase (main.go assembles the prompt manually). This creates a parallel code path that could diverge. Either use it in main.go or make it unexported/remove it to avoid maintenance burden.
The persona feature is well-integrated overall (flags, prompt assembly, formatting, tests), but there are two blocking issues: an external YAML dependency violates repository conventions, and a unit test expectation conflicts with the new path validation logic. Address these to proceed.
Findings
#
Severity
File
Line
Finding
1
[MAJOR]
go.mod
5
Introduces an external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'.
2
[MAJOR]
cmd/review-bot/main_test.go
96
TestValidateWorkspacePath case "absolute path gets normalized to relative" expects error text containing "failed to resolve", but validateWorkspacePath now returns an error like "resolves outside workspace" for absolute paths outside the workspace. This mismatch will cause the test to fail on Linux.
3
[NIT]
cmd/review-bot/main_test.go
78
The comment "filepath.Join strips leading / making it /etc/passwd" is inaccurate on Unix-like systems — filepath.Join with an absolute path ignores the base and yields the absolute path. Update the comment for clarity.
4
[MINOR]
review/persona.go
18
Persona.ModelPref is parsed but not used anywhere. Consider documenting intended usage or removing until needed to reduce API surface.
Recommendation
REQUEST_CHANGES — To align with repository conventions and ensure tests pass:
Remove the external YAML dependency (MAJOR):
Either restrict persona files to JSON only (using encoding/json), and convert built-in persona files to JSON, or
If YAML support is essential, consider an alternative approach that keeps stdlib-only (e.g., accept only JSON for now and document this; YAML can be added later behind a build tag or reconsider the convention). Update review/persona.go to drop github.com/goccy/go-yaml and remove it from go.mod/go.sum.
Fix the path validation test mismatch (MAJOR):
Update TestValidateWorkspacePath case "absolute path gets normalized to relative" to reflect current behavior: rename to something like "absolute path outside workspace" and set errMatch to "resolves outside workspace". Also correct the misleading comment about filepath.Join behavior on absolute paths.
Minor cleanup (optional):
Either implement ModelPref handling (e.g., log a warning if requested model differs) or remove the field until used.
With these changes, the implementation should meet project conventions, keep tests green, and maintain the new persona functionality as designed.
# Gpt Review
## Summary
The persona feature is well-integrated overall (flags, prompt assembly, formatting, tests), but there are two blocking issues: an external YAML dependency violates repository conventions, and a unit test expectation conflicts with the new path validation logic. Address these to proceed.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MAJOR] | `go.mod` | 5 | Introduces an external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'. |
| 2 | [MAJOR] | `cmd/review-bot/main_test.go` | 96 | TestValidateWorkspacePath case "absolute path gets normalized to relative" expects error text containing "failed to resolve", but validateWorkspacePath now returns an error like "resolves outside workspace" for absolute paths outside the workspace. This mismatch will cause the test to fail on Linux. |
| 3 | [NIT] | `cmd/review-bot/main_test.go` | 78 | The comment "filepath.Join strips leading / making it <workspace>/etc/passwd" is inaccurate on Unix-like systems — filepath.Join with an absolute path ignores the base and yields the absolute path. Update the comment for clarity. |
| 4 | [MINOR] | `review/persona.go` | 18 | Persona.ModelPref is parsed but not used anywhere. Consider documenting intended usage or removing until needed to reduce API surface. |
## Recommendation
**REQUEST_CHANGES** — To align with repository conventions and ensure tests pass:
1) Remove the external YAML dependency (MAJOR):
- Either restrict persona files to JSON only (using encoding/json), and convert built-in persona files to JSON, or
- If YAML support is essential, consider an alternative approach that keeps stdlib-only (e.g., accept only JSON for now and document this; YAML can be added later behind a build tag or reconsider the convention). Update review/persona.go to drop github.com/goccy/go-yaml and remove it from go.mod/go.sum.
2) Fix the path validation test mismatch (MAJOR):
- Update TestValidateWorkspacePath case "absolute path gets normalized to relative" to reflect current behavior: rename to something like "absolute path outside workspace" and set errMatch to "resolves outside workspace". Also correct the misleading comment about filepath.Join behavior on absolute paths.
3) Minor cleanup (optional):
- Either implement ModelPref handling (e.g., log a warning if requested model differs) or remove the field until used.
With these changes, the implementation should meet project conventions, keep tests green, and maintain the new persona functionality as designed.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 57e62a34*
[NIT] The comment "filepath.Join strips leading / making it /etc/passwd" is inaccurate on Unix-like systems — filepath.Join with an absolute path ignores the base and yields the absolute path. Update the comment for clarity.
**[NIT]** The comment "filepath.Join strips leading / making it <workspace>/etc/passwd" is inaccurate on Unix-like systems — filepath.Join with an absolute path ignores the base and yields the absolute path. Update the comment for clarity.
[MAJOR] TestValidateWorkspacePath case "absolute path gets normalized to relative" expects error text containing "failed to resolve", but validateWorkspacePath now returns an error like "resolves outside workspace" for absolute paths outside the workspace. This mismatch will cause the test to fail on Linux.
**[MAJOR]** TestValidateWorkspacePath case "absolute path gets normalized to relative" expects error text containing "failed to resolve", but validateWorkspacePath now returns an error like "resolves outside workspace" for absolute paths outside the workspace. This mismatch will cause the test to fail on Linux.
[MAJOR] Introduces an external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'.
**[MAJOR]** Introduces an external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'.
Overall, the changes introduce role-based personas safely and improve path handling by validating and constraining file access to the CI workspace. No exploitable vulnerabilities were identified. I noted two minor hardening points around dependency risk and Markdown display name handling.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
go.mod
4
A new external dependency (github.com/goccy/go-yaml v1.19.2) increases the supply-chain attack surface. While not a direct vulnerability, it diverges from the repo’s "stdlib only" convention and can introduce risks if the library later has issues.
2
[MINOR]
review/formatter.go
25
Display names are intentionally not HTML-escaped and are included in Markdown. If a persona is loaded from a repo file, a crafted display_name could embed Markdown constructs (e.g., images) that cause remote content loads when rendered. Consider restricting display_name to a conservative character set or escaping/sanitizing to reduce content-injection/tracking risks, even if Gitea sanitizes most HTML.
Recommendation
APPROVE — Approve the PR. The new persona feature is implemented with sensible security controls, notably robust path validation preventing traversal and symlink escapes for persona and system-prompt files. To further harden the system, consider (1) reviewing the necessity of the new YAML dependency or pinning and regularly auditing it to mitigate supply-chain risk, and (2) sanitizing or restricting persona display_name to reduce the chance of Markdown-based content injection or tracking, especially when personas are sourced from repository files. No blocking security issues were found.
# Security Review
## Summary
Overall, the changes introduce role-based personas safely and improve path handling by validating and constraining file access to the CI workspace. No exploitable vulnerabilities were identified. I noted two minor hardening points around dependency risk and Markdown display name handling.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `go.mod` | 4 | A new external dependency (github.com/goccy/go-yaml v1.19.2) increases the supply-chain attack surface. While not a direct vulnerability, it diverges from the repo’s "stdlib only" convention and can introduce risks if the library later has issues. |
| 2 | [MINOR] | `review/formatter.go` | 25 | Display names are intentionally not HTML-escaped and are included in Markdown. If a persona is loaded from a repo file, a crafted display_name could embed Markdown constructs (e.g., images) that cause remote content loads when rendered. Consider restricting display_name to a conservative character set or escaping/sanitizing to reduce content-injection/tracking risks, even if Gitea sanitizes most HTML. |
## Recommendation
**APPROVE** — Approve the PR. The new persona feature is implemented with sensible security controls, notably robust path validation preventing traversal and symlink escapes for persona and system-prompt files. To further harden the system, consider (1) reviewing the necessity of the new YAML dependency or pinning and regularly auditing it to mitigate supply-chain risk, and (2) sanitizing or restricting persona display_name to reduce the chance of Markdown-based content injection or tracking, especially when personas are sourced from repository files. No blocking security issues were found.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 57e62a34*
[MINOR] A new external dependency (github.com/goccy/go-yaml v1.19.2) increases the supply-chain attack surface. While not a direct vulnerability, it diverges from the repo’s "stdlib only" convention and can introduce risks if the library later has issues.
**[MINOR]** A new external dependency (github.com/goccy/go-yaml v1.19.2) increases the supply-chain attack surface. While not a direct vulnerability, it diverges from the repo’s "stdlib only" convention and can introduce risks if the library later has issues.
[MINOR] Display names are intentionally not HTML-escaped and are included in Markdown. If a persona is loaded from a repo file, a crafted display_name could embed Markdown constructs (e.g., images) that cause remote content loads when rendered. Consider restricting display_name to a conservative character set or escaping/sanitizing to reduce content-injection/tracking risks, even if Gitea sanitizes most HTML.
**[MINOR]** Display names are intentionally not HTML-escaped and are included in Markdown. If a persona is loaded from a repo file, a crafted display_name could embed Markdown constructs (e.g., images) that cause remote content loads when rendered. Consider restricting display_name to a conservative character set or escaping/sanitizing to reduce content-injection/tracking risks, even if Gitea sanitizes most HTML.
MAJOR fixes:
- Remove external YAML dependency (github.com/goccy/go-yaml)
Per project convention: Go standard library only, zero dependencies.
Convert all persona files from YAML to JSON format.
- Fix TestValidateWorkspacePath error expectation
Go 1.21+ filepath.Join normalizes absolute paths differently.
MINOR fixes:
- Remove custom contains helper in persona_test.go (use strings.Contains)
- Add Unicode-safe CapitalizeFirst function for header titles
- ListBuiltinPersonas returns empty slice instead of nil on error
- Fix test comment about filepath.Join behavior
Documentation:
- Update README to reflect JSON-only persona format
- Update design doc with note about JSON decision
- Fix action.yml description for persona-file input
This PR adds a well-designed role-based persona system to review-bot. The implementation is clean, follows Go idioms, maintains the zero-external-dependencies constraint by using JSON instead of YAML, and includes comprehensive tests. There are a few minor issues worth noting but nothing blocking.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/formatter.go
34
The comment says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown' and 'Persona display names are controlled by repo owners (trusted input)'. However, displayName can come from a --persona-file whose path is validated for workspace containment but whose contents (the display_name field) are not sanitized. A persona file checked into a repo could contain something like display_name: "] Review\n\n<script> in a pathological case. Since Gitea does sanitize Markdown-rendered HTML, this is low risk, but the trust assertion in the comment ('controlled by repo owners') is slightly inaccurate — it could also come from any file a developer adds to the repo. The comment should be more precise or dropped.
2
[MINOR]
docs/DESIGN-51-personas.md
76
The design document shows a YAML persona file format as the primary example (with # .review/personas/security.yaml header and YAML syntax like name: security) but the implementation uses JSON. The design revision note at the bottom explains this, but the main body of the document is misleading — it still shows YAML syntax in the code block under '2. Persona File Format'. This will confuse future contributors reading the design doc.
3
[MINOR]
review/persona.go
59
The inline comment on embeddedPersonas.ReadFile("personas/" + filename) says 'embed.FS paths use forward slashes per io/fs spec'. While correct and useful, this comment is placed on the wrong line — it belongs on the ReadFile call line (line 59), which is where it appears. This is fine. However, the function silently falls through to listing available personas when the embedded file is not found, which means a typo in the name produces an error message that lists alternatives. This is good UX, but the ListBuiltinPersonas() call inside LoadBuiltinPersona adds a small O(n) directory scan on every failed lookup — not a real concern at this scale but worth noting.
4
[NIT]
review/persona.go
97
CapitalizeFirst is placed in persona.go but is used by formatter.go. It's a general string utility that doesn't have a conceptual relationship with persona loading. It would be more discoverable in formatter.go or a shared strings.go/util.go file within the review package.
5
[NIT]
review/persona_prompt.go
62
BuildSystemPromptWithPersona is exported but not used in main.go — main.go calls BuildPersonaSystemPrompt directly and then appends patterns/conventions via the budget system. This exported function implements a different (non-budget-aware) path. It's only tested, not used in production. Either document clearly that it's an alternative API for callers who don't need the budget system, or make it unexported if it's just for testing convenience.
6
[NIT]
cmd/review-bot/main_test.go
96
The TestValidateWorkspacePath test creates a symlink to /etc/passwd and doesn't have OS-specific guards. On Windows, os.Symlink to /etc/passwd would fail or behave differently. The existing tests in the repo don't appear to target Windows, so this is likely acceptable, but it's worth noting for cross-platform CI.
Recommendation
APPROVE — APPROVE — The implementation is solid. The persona system is well-structured, backwards-compatible, and follows the project's zero-dependencies convention. The refactoring of validateWorkspacePath into a shared function is a nice improvement. The FormatMarkdownWithDisplay separation of display name from sentinel name is clean and correct. Tests are comprehensive and cover edge cases including Unicode. The minor issues flagged (misleading design doc, misplaced utility function, unexported-but-public convenience function) are housekeeping items that don't affect correctness or behavior.
# Sonnet Review
## Summary
This PR adds a well-designed role-based persona system to review-bot. The implementation is clean, follows Go idioms, maintains the zero-external-dependencies constraint by using JSON instead of YAML, and includes comprehensive tests. There are a few minor issues worth noting but nothing blocking.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/formatter.go` | 34 | The comment says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown' and 'Persona display names are controlled by repo owners (trusted input)'. However, `displayName` can come from a `--persona-file` whose path is validated for workspace containment but whose *contents* (the `display_name` field) are not sanitized. A persona file checked into a repo could contain something like `display_name: "] Review\n\n<script>` in a pathological case. Since Gitea does sanitize Markdown-rendered HTML, this is low risk, but the trust assertion in the comment ('controlled by repo owners') is slightly inaccurate — it could also come from any file a developer adds to the repo. The comment should be more precise or dropped. |
| 2 | [MINOR] | `docs/DESIGN-51-personas.md` | 76 | The design document shows a YAML persona file format as the primary example (with `# .review/personas/security.yaml` header and YAML syntax like `name: security`) but the implementation uses JSON. The design revision note at the bottom explains this, but the main body of the document is misleading — it still shows YAML syntax in the code block under '2. Persona File Format'. This will confuse future contributors reading the design doc. |
| 3 | [MINOR] | `review/persona.go` | 59 | The inline comment on `embeddedPersonas.ReadFile("personas/" + filename)` says 'embed.FS paths use forward slashes per io/fs spec'. While correct and useful, this comment is placed on the wrong line — it belongs on the `ReadFile` call line (line 59), which is where it appears. This is fine. However, the function silently falls through to listing available personas when the embedded file is not found, which means a typo in the name produces an error message that lists alternatives. This is good UX, but the `ListBuiltinPersonas()` call inside `LoadBuiltinPersona` adds a small O(n) directory scan on every failed lookup — not a real concern at this scale but worth noting. |
| 4 | [NIT] | `review/persona.go` | 97 | `CapitalizeFirst` is placed in `persona.go` but is used by `formatter.go`. It's a general string utility that doesn't have a conceptual relationship with persona loading. It would be more discoverable in `formatter.go` or a shared `strings.go`/`util.go` file within the `review` package. |
| 5 | [NIT] | `review/persona_prompt.go` | 62 | `BuildSystemPromptWithPersona` is exported but not used in `main.go` — `main.go` calls `BuildPersonaSystemPrompt` directly and then appends patterns/conventions via the budget system. This exported function implements a different (non-budget-aware) path. It's only tested, not used in production. Either document clearly that it's an alternative API for callers who don't need the budget system, or make it unexported if it's just for testing convenience. |
| 6 | [NIT] | `cmd/review-bot/main_test.go` | 96 | The `TestValidateWorkspacePath` test creates a symlink to `/etc/passwd` and doesn't have OS-specific guards. On Windows, `os.Symlink` to `/etc/passwd` would fail or behave differently. The existing tests in the repo don't appear to target Windows, so this is likely acceptable, but it's worth noting for cross-platform CI. |
## Recommendation
**APPROVE** — APPROVE — The implementation is solid. The persona system is well-structured, backwards-compatible, and follows the project's zero-dependencies convention. The refactoring of `validateWorkspacePath` into a shared function is a nice improvement. The `FormatMarkdownWithDisplay` separation of display name from sentinel name is clean and correct. Tests are comprehensive and cover edge cases including Unicode. The minor issues flagged (misleading design doc, misplaced utility function, unexported-but-public convenience function) are housekeeping items that don't affect correctness or behavior.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 4dd67742*
[NIT] The TestValidateWorkspacePath test creates a symlink to /etc/passwd and doesn't have OS-specific guards. On Windows, os.Symlink to /etc/passwd would fail or behave differently. The existing tests in the repo don't appear to target Windows, so this is likely acceptable, but it's worth noting for cross-platform CI.
**[NIT]** The `TestValidateWorkspacePath` test creates a symlink to `/etc/passwd` and doesn't have OS-specific guards. On Windows, `os.Symlink` to `/etc/passwd` would fail or behave differently. The existing tests in the repo don't appear to target Windows, so this is likely acceptable, but it's worth noting for cross-platform CI.
[MINOR] The design document shows a YAML persona file format as the primary example (with # .review/personas/security.yaml header and YAML syntax like name: security) but the implementation uses JSON. The design revision note at the bottom explains this, but the main body of the document is misleading — it still shows YAML syntax in the code block under '2. Persona File Format'. This will confuse future contributors reading the design doc.
**[MINOR]** The design document shows a YAML persona file format as the primary example (with `# .review/personas/security.yaml` header and YAML syntax like `name: security`) but the implementation uses JSON. The design revision note at the bottom explains this, but the main body of the document is misleading — it still shows YAML syntax in the code block under '2. Persona File Format'. This will confuse future contributors reading the design doc.
[MINOR] The comment says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown' and 'Persona display names are controlled by repo owners (trusted input)'. However, displayName can come from a --persona-file whose path is validated for workspace containment but whose contents (the display_name field) are not sanitized. A persona file checked into a repo could contain something like display_name: "] Review\n\n<script> in a pathological case. Since Gitea does sanitize Markdown-rendered HTML, this is low risk, but the trust assertion in the comment ('controlled by repo owners') is slightly inaccurate — it could also come from any file a developer adds to the repo. The comment should be more precise or dropped.
**[MINOR]** The comment says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown' and 'Persona display names are controlled by repo owners (trusted input)'. However, `displayName` can come from a `--persona-file` whose path is validated for workspace containment but whose *contents* (the `display_name` field) are not sanitized. A persona file checked into a repo could contain something like `display_name: "] Review\n\n<script>` in a pathological case. Since Gitea does sanitize Markdown-rendered HTML, this is low risk, but the trust assertion in the comment ('controlled by repo owners') is slightly inaccurate — it could also come from any file a developer adds to the repo. The comment should be more precise or dropped.
[MINOR] The inline comment on embeddedPersonas.ReadFile("personas/" + filename) says 'embed.FS paths use forward slashes per io/fs spec'. While correct and useful, this comment is placed on the wrong line — it belongs on the ReadFile call line (line 59), which is where it appears. This is fine. However, the function silently falls through to listing available personas when the embedded file is not found, which means a typo in the name produces an error message that lists alternatives. This is good UX, but the ListBuiltinPersonas() call inside LoadBuiltinPersona adds a small O(n) directory scan on every failed lookup — not a real concern at this scale but worth noting.
**[MINOR]** The inline comment on `embeddedPersonas.ReadFile("personas/" + filename)` says 'embed.FS paths use forward slashes per io/fs spec'. While correct and useful, this comment is placed on the wrong line — it belongs on the `ReadFile` call line (line 59), which is where it appears. This is fine. However, the function silently falls through to listing available personas when the embedded file is not found, which means a typo in the name produces an error message that lists alternatives. This is good UX, but the `ListBuiltinPersonas()` call inside `LoadBuiltinPersona` adds a small O(n) directory scan on every failed lookup — not a real concern at this scale but worth noting.
[NIT]CapitalizeFirst is placed in persona.go but is used by formatter.go. It's a general string utility that doesn't have a conceptual relationship with persona loading. It would be more discoverable in formatter.go or a shared strings.go/util.go file within the review package.
**[NIT]** `CapitalizeFirst` is placed in `persona.go` but is used by `formatter.go`. It's a general string utility that doesn't have a conceptual relationship with persona loading. It would be more discoverable in `formatter.go` or a shared `strings.go`/`util.go` file within the `review` package.
[NIT]BuildSystemPromptWithPersona is exported but not used in main.go — main.go calls BuildPersonaSystemPrompt directly and then appends patterns/conventions via the budget system. This exported function implements a different (non-budget-aware) path. It's only tested, not used in production. Either document clearly that it's an alternative API for callers who don't need the budget system, or make it unexported if it's just for testing convenience.
**[NIT]** `BuildSystemPromptWithPersona` is exported but not used in `main.go` — `main.go` calls `BuildPersonaSystemPrompt` directly and then appends patterns/conventions via the budget system. This exported function implements a different (non-budget-aware) path. It's only tested, not used in production. Either document clearly that it's an alternative API for callers who don't need the budget system, or make it unexported if it's just for testing convenience.
gpt-review-bot
approved these changes 2026-05-10 17:03:29 +00:00
Solid, well-structured implementation of role-based personas with clean CLI/action integration, safe file path validation, and comprehensive tests. No functional issues found; only minor documentation inconsistencies and a small clarity nit.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
docs/DESIGN-51-personas.md
320
Design doc contradicts repository conventions and implementation: it states a 'Design Revision: YAML with gopkg.in/yaml.v3' (adding an external dependency), while the actual implementation uses JSON and maintains zero external dependencies. This may confuse contributors.
2
[NIT]
README.md
360
JSON example snippet under 'Custom Personas' includes a comment line ('// .review/personas/trading.json') which is not valid JSON; users copy-pasting may encounter parse errors. Consider moving the comment outside the code block or noting that it's illustrative.
3
[NIT]
README.md
345
The 'Using Built-in Personas' example specifies an Anthropic model (claude-opus-4-20250514) without setting llm-provider to 'anthropic'. While covered elsewhere, adding it here would reduce potential user confusion.
Recommendation
APPROVE — The changes are well-implemented, idiomatic, and thoroughly tested. The persona feature is integrated cleanly via flags and the composite action, with robust path validation and good reuse of shared prompt schema. Approve this PR. As follow-ups, clarify the design doc to remove the YAML/dependency contradiction, adjust the README JSON snippet to avoid invalid comments, and consider adding llm-provider to the persona example for clarity.
# Gpt Review
## Summary
Solid, well-structured implementation of role-based personas with clean CLI/action integration, safe file path validation, and comprehensive tests. No functional issues found; only minor documentation inconsistencies and a small clarity nit.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `docs/DESIGN-51-personas.md` | 320 | Design doc contradicts repository conventions and implementation: it states a 'Design Revision: YAML with gopkg.in/yaml.v3' (adding an external dependency), while the actual implementation uses JSON and maintains zero external dependencies. This may confuse contributors. |
| 2 | [NIT] | `README.md` | 360 | JSON example snippet under 'Custom Personas' includes a comment line ('// .review/personas/trading.json') which is not valid JSON; users copy-pasting may encounter parse errors. Consider moving the comment outside the code block or noting that it's illustrative. |
| 3 | [NIT] | `README.md` | 345 | The 'Using Built-in Personas' example specifies an Anthropic model (claude-opus-4-20250514) without setting llm-provider to 'anthropic'. While covered elsewhere, adding it here would reduce potential user confusion. |
## Recommendation
**APPROVE** — The changes are well-implemented, idiomatic, and thoroughly tested. The persona feature is integrated cleanly via flags and the composite action, with robust path validation and good reuse of shared prompt schema. Approve this PR. As follow-ups, clarify the design doc to remove the YAML/dependency contradiction, adjust the README JSON snippet to avoid invalid comments, and consider adding llm-provider to the persona example for clarity.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 4dd67742*
[NIT] The 'Using Built-in Personas' example specifies an Anthropic model (claude-opus-4-20250514) without setting llm-provider to 'anthropic'. While covered elsewhere, adding it here would reduce potential user confusion.
**[NIT]** The 'Using Built-in Personas' example specifies an Anthropic model (claude-opus-4-20250514) without setting llm-provider to 'anthropic'. While covered elsewhere, adding it here would reduce potential user confusion.
[NIT] JSON example snippet under 'Custom Personas' includes a comment line ('// .review/personas/trading.json') which is not valid JSON; users copy-pasting may encounter parse errors. Consider moving the comment outside the code block or noting that it's illustrative.
**[NIT]** JSON example snippet under 'Custom Personas' includes a comment line ('// .review/personas/trading.json') which is not valid JSON; users copy-pasting may encounter parse errors. Consider moving the comment outside the code block or noting that it's illustrative.
[MINOR] Design doc contradicts repository conventions and implementation: it states a 'Design Revision: YAML with gopkg.in/yaml.v3' (adding an external dependency), while the actual implementation uses JSON and maintains zero external dependencies. This may confuse contributors.
**[MINOR]** Design doc contradicts repository conventions and implementation: it states a 'Design Revision: YAML with gopkg.in/yaml.v3' (adding an external dependency), while the actual implementation uses JSON and maintains zero external dependencies. This may confuse contributors.
security-review-bot
requested review from security-review-bot 2026-05-10 17:03:56 +00:00
Overall, the changes add persona support with sound security measures, notably robust path validation for local files and symlink traversal defenses. No exploitable vulnerabilities were identified; only minor hardening opportunities exist.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/formatter.go
28
Persona display_name is inserted into the Markdown header and footer without escaping. While Gitea sanitizes rendered Markdown, if a custom or differently configured renderer is used, a crafted display_name could inject Markdown (e.g., images/links) and cause external resource loads in the PR UI. Consider stripping newlines and restricting characters or explicitly escaping to reduce injection surface.
2
[MINOR]
review/persona.go
39
LoadPersona reads the entire persona file with os.ReadFile without a size cap. In CI contexts where persona-file points to a repository file, a very large JSON could cause excessive memory usage. Consider adding a size check (e.g., via os.Stat) or a reasonable read limit before loading.
Recommendation
APPROVE — The persona functionality and associated security measures look solid, particularly the mutual exclusivity of persona flags and the validateWorkspacePath checks that prevent path traversal and symlink escapes. Approve the PR as-is. For defense-in-depth, consider sanitizing or restricting persona display_name to avoid potential Markdown-based injections in non-Gitea contexts, and add a simple size check before reading persona/system prompt files to mitigate resource exhaustion from unusually large inputs.
# Security Review
## Summary
Overall, the changes add persona support with sound security measures, notably robust path validation for local files and symlink traversal defenses. No exploitable vulnerabilities were identified; only minor hardening opportunities exist.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/formatter.go` | 28 | Persona display_name is inserted into the Markdown header and footer without escaping. While Gitea sanitizes rendered Markdown, if a custom or differently configured renderer is used, a crafted display_name could inject Markdown (e.g., images/links) and cause external resource loads in the PR UI. Consider stripping newlines and restricting characters or explicitly escaping to reduce injection surface. |
| 2 | [MINOR] | `review/persona.go` | 39 | LoadPersona reads the entire persona file with os.ReadFile without a size cap. In CI contexts where persona-file points to a repository file, a very large JSON could cause excessive memory usage. Consider adding a size check (e.g., via os.Stat) or a reasonable read limit before loading. |
## Recommendation
**APPROVE** — The persona functionality and associated security measures look solid, particularly the mutual exclusivity of persona flags and the validateWorkspacePath checks that prevent path traversal and symlink escapes. Approve the PR as-is. For defense-in-depth, consider sanitizing or restricting persona display_name to avoid potential Markdown-based injections in non-Gitea contexts, and add a simple size check before reading persona/system prompt files to mitigate resource exhaustion from unusually large inputs.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 4dd67742*
[MINOR] Persona display_name is inserted into the Markdown header and footer without escaping. While Gitea sanitizes rendered Markdown, if a custom or differently configured renderer is used, a crafted display_name could inject Markdown (e.g., images/links) and cause external resource loads in the PR UI. Consider stripping newlines and restricting characters or explicitly escaping to reduce injection surface.
**[MINOR]** Persona display_name is inserted into the Markdown header and footer without escaping. While Gitea sanitizes rendered Markdown, if a custom or differently configured renderer is used, a crafted display_name could inject Markdown (e.g., images/links) and cause external resource loads in the PR UI. Consider stripping newlines and restricting characters or explicitly escaping to reduce injection surface.
[MINOR] LoadPersona reads the entire persona file with os.ReadFile without a size cap. In CI contexts where persona-file points to a repository file, a very large JSON could cause excessive memory usage. Consider adding a size check (e.g., via os.Stat) or a reasonable read limit before loading.
**[MINOR]** LoadPersona reads the entire persona file with os.ReadFile without a size cap. In CI contexts where persona-file points to a repository file, a very large JSON could cause excessive memory usage. Consider adding a size check (e.g., via os.Stat) or a reasonable read limit before loading.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implement role-based review personas that provide specialized review focus, addressing issue #51.
Problem
Current review-bot performs generic code review. Every reviewer uses the same base prompt and evaluates the same concerns. This leads to:
Solution
Add role-based personas with:
Built-in Personas
securityarchitectdocsChanges
review/persona.go: Persona struct + JSON loadingreview/persona_prompt.go: BuildPersonaSystemPromptreview/personas/*.json: Built-in personas (embedded via go:embed)review/formatter.go: FormatMarkdownWithDisplay for persona display namescmd/review-bot/main.go: --persona and --persona-file flags.gitea/actions/review/action.yml: persona and persona-file inputsREADME.md: Comprehensive persona documentationUsage
Testing
Backwards Compatibility
No breaking changes. When --persona and --persona-file are both omitted, behavior is unchanged (uses generic review prompt).
Closes #51
Security Review
Summary
The persona feature is implemented cleanly and integrates with the existing prompt/budget workflow without introducing major security risks. One minor hardening opportunity exists around loading persona files from disk.
Findings
cmd/review-bot/main.goRecommendation
APPROVE — Overall, the changes are sound and CI passed. To harden against potential path traversal or unintended file access, apply the same workspace-bounding and symlink resolution checks to persona-file that you already use for system-prompt-file. Specifically, resolve the absolute path under the repository workspace, call filepath.EvalSymlinks, and verify the resolved path remains within the workspace before reading. This mitigates accidental or malicious reads of arbitrary files on the runner. With that adjustment, the feature is safe to merge.
Review by security
Evaluated against
d7d5151a@@ -94,0 +106,4 @@var err errorpersona, err = review.LoadBuiltinPersona(*personaName)if err != nil {slog.Error("failed to load persona", "persona", *personaName, "error", err)[MINOR] The --persona-file path is read directly from the filesystem without workspace or symlink checks, allowing arbitrary file reads on the runner if the input were ever user-controlled. Mirror the path traversal protections used for --system-prompt-file (resolve to absolute path within the workspace, EvalSymlinks, and reject paths escaping the workspace) to prevent unintended access.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
d7d5151a)Gpt Review
Summary
Good addition of role-based personas with clean Go implementation and tests. However, the composite action YAML has a malformed inputs section that can break action parsing and misconfigure inputs.
Findings
.gitea/actions/review/action.ymlcmd/review-bot/main.goreview/formatter.goRecommendation
REQUEST_CHANGES — Fix the malformed composite action YAML. Under inputs, keep the system-prompt-file's description/required/default directly beneath the system-prompt-file key, and ensure persona and persona-file each have their own properly scoped properties without duplicated keys. After correcting the YAML, consider applying the same workspace and symlink traversal checks to persona-file that are already used for system-prompt-file to avoid reading paths outside the workspace. Optionally, make the header capitalization in FormatMarkdown and FormatMarkdownWithDisplay Unicode-safe. Once the action.yml is fixed, the persona feature and associated Go code/tests look solid and should be good to merge.
Review by gpt
Evaluated against
d7d5151a@@ -71,6 +71,14 @@ inputs:required: falsedefault: 'true'system-prompt-file:persona:[MAJOR] Malformed inputs block: properties for system-prompt-file are misplaced under persona-file due to insertion. The keys 'description', 'required', and 'default' are duplicated under persona-file, and system-prompt-file lacks its properties. This can cause YAML parsing issues and incorrect input definitions.
@@ -94,0 +107,4 @@persona, err = review.LoadBuiltinPersona(*personaName)if err != nil {slog.Error("failed to load persona", "persona", *personaName, "error", err)os.Exit(1)[MINOR] Persona file path is read directly from --persona-file without the workspace/symlink traversal safeguards applied to system-prompt-file. For consistency and security, restrict persona-file to the workspace and guard against symlink traversal.
Security Review
Summary
The new persona feature is well-scoped and CI passes, but there is a security-sensitive gap in path handling for persona files. Unlike system-prompt-file, persona-file paths are not constrained to the workspace or protected against symlink traversal, creating a potential path traversal/data exfiltration vector in CI environments.
Findings
review/persona.gocmd/review-bot/main.goRecommendation
REQUEST_CHANGES — Restrict persona-file to repository workspace paths and defend against symlink traversal, matching the approach used for system-prompt-file. Specifically: resolve the runner workspace (GITHUB_WORKSPACE or CWD), join and Clean the provided persona-file path, verify it resides within the workspace, EvalSymlinks, and re-verify it remains within the workspace before reading. Consider logging a clear error and exiting if validation fails. This will prevent potential path traversal and unintended file reads, and avoid exfiltrating sensitive local files into LLM prompts. No other security-relevant issues were found in the changes.
Review by security
Evaluated against
e4cde3ed@@ -94,0 +102,4 @@// Load persona if specifiedvar persona *review.Personaif *personaName != "" {[MINOR] The persona-file code path accepts and loads a file path directly into review.LoadPersona without the workspace-bound and symlink checks applied to system-prompt-file. While LoadPersona performs JSON parsing (reducing accidental leakage), adding the same path/symlink validation here would provide consistent defense-in-depth and reduce the risk of reading unintended files.
@@ -0,0 +30,4 @@Major string `json:"major"`Minor string `json:"minor"`Nit string `json:"nit"`}[MAJOR] LoadPersona reads an arbitrary file path from the local filesystem without any path normalization, workspace boundary checks, or symlink resolution. In CI, a misconfigured workflow or malicious change could cause the bot to read files outside the repository workspace and include their contents (via persona fields) in the LLM system prompt, risking data exfiltration. Mirror the strict workspace + symlink checks used for system-prompt-file.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
e4cde3ed)Gpt Review
Summary
The persona feature is well-implemented with solid tests and CI is passing, but the composite action YAML has a structural error in the inputs section that can break input parsing. Additionally, there are a couple of minor polish items.
Findings
.gitea/actions/review/action.ymlinputssection is malformed:system-prompt-fileis declared without any properties, and the subsequentdescription/required/defaultfields appear underpersona-file, resulting in duplicate keys forpersona-fileand leavingsystem-prompt-filewith a null value. In YAML this can cause the last duplicate key to win and may lead to GitHub/Gitea Actions rejecting or mis-parsing these inputs.cmd/review-bot/main.go--persona-file) accepts an arbitrary path without the same workspace-bound path validation that is applied to--system-prompt-file. For consistency and safety in CI environments, consider applying similar workspace containment checks to avoid accidentally reading outside the repository workspace.review/formatter.goFormatMarkdownWithDisplaylargely duplicatesFormatMarkdown. Consider refactoring to reduce duplication (e.g., have one function take both display and sentinel names, or have one call the other with appropriate parameters).Recommendation
REQUEST_CHANGES — Please fix the malformed inputs block in .gitea/actions/review/action.yml. Specifically, move the
description,required, anddefaultfields back undersystem-prompt-fileand ensurepersona-filehas a single, correct set of fields without duplicates. This prevents ambiguous or rejected inputs by the actions runner. Additionally, consider adding workspace-bound path validation for--persona-filesimilar to what you implemented for--system-prompt-file, and optionally refactor the duplicated markdown formatting logic. Once the YAML is corrected, the implementation and tests for personas look good and should be ready to merge.Review by gpt
Evaluated against
e4cde3ed@@ -72,2 +72,4 @@default: 'true'system-prompt-file:persona:description: 'Built-in persona name (security, architect, docs)'[MAJOR] The
inputssection is malformed:system-prompt-fileis declared without any properties, and the subsequentdescription/required/defaultfields appear underpersona-file, resulting in duplicate keys forpersona-fileand leavingsystem-prompt-filewith a null value. In YAML this can cause the last duplicate key to win and may lead to GitHub/Gitea Actions rejecting or mis-parsing these inputs.@@ -94,0 +111,4 @@}slog.Info("loaded built-in persona", "persona", persona.Name, "display", persona.DisplayName)} else if *personaFile != "" {var err error[MINOR] Persona file loading (
--persona-file) accepts an arbitrary path without the same workspace-bound path validation that is applied to--system-prompt-file. For consistency and safety in CI environments, consider applying similar workspace containment checks to avoid accidentally reading outside the repository workspace.@@ -56,0 +57,4 @@// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.// displayName is used for the header title, sentinelName is used for the cleanup sentinel.// If displayName is empty, sentinelName is used for both.func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string {[NIT] The new
FormatMarkdownWithDisplaylargely duplicatesFormatMarkdown. Consider refactoring to reduce duplication (e.g., have one function take both display and sentinel names, or have one call the other with appropriate parameters).Self-review: NEEDS_WORK
Findings
[completeness] MAJOR:
.gitea/actions/review/action.ymlhas a structural error. When thepersonaandpersona-fileinputs were added, thesystem-prompt-fileinput definition was broken. Currently:system-prompt-file:has no attributes (empty value) — it will be null/undefined as an inputpersona-file:has duplicatedescription,required, anddefaultkeys (the originalsystem-prompt-fileattributes were left dangling)persona-filewill have the wrong descriptionFix: Restore
system-prompt-fileattributes and ensure proper YAML structure:[fit] NIT: Double blank line at
cmd/review-bot/main.go:95-96(minor style inconsistency with rest of codebase)[completeness] NIT:
BuildSystemPromptWithPersonainreview/persona_prompt.gois defined and tested but never called frommain.go. It's a reasonable public API for callers not using the budget system, but could be documented as such or removed if truly unused.What's Good
docs/DESIGN-51-personas.mdRecommendation
The action.yml issue is a blocker — it will break existing users of
system-prompt-fileand givepersona-filethe wrong description. Fix that, and the PR is ready.Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
5682d824)Gpt Review
Summary
Solid persona feature implementation with clean embedding, prompt construction, and tests. However, loading a persona file lacks the same workspace-bound path validation and symlink checks applied to system-prompt files, creating a potential secret exfiltration risk in CI.
Findings
cmd/review-bot/main.goreview/formatter.godocs/DESIGN-51-personas.mdRecommendation
REQUEST_CHANGES — Address the security gap when loading persona files: restrict persona-file to the repository workspace and guard against symlink traversal just like system-prompt-file (resolve absolute path, clean it, ensure it’s within the workspace prefix, then EvalSymlinks and re-check). This prevents unintended disclosure of runner files to the LLM. After adding this validation, the implementation appears robust: embedded built-in personas, mutually exclusive flags, prompt assembly, and tests are well done. Optionally, refactor markdown formatting to reduce duplication and tidy the design doc to emphasize JSON personas up front.
Review by gpt
Evaluated against
5682d824@@ -94,0 +109,4 @@os.Exit(1)}slog.Info("loaded built-in persona", "persona", persona.Name, "display", persona.DisplayName)} else if *personaFile != "" {[MAJOR] persona-file is read from the filesystem without validating it resides within the checkout workspace or guarding against symlink traversal. A malicious config could point to arbitrary files (e.g., /etc/*) and exfiltrate their contents to the LLM. Apply the same absolute path, prefix, and symlink-resolution checks used for system-prompt-file.
@@ -0,0 +1,353 @@# Design: Role-based Review Personas (Issue #51)[NIT] The design doc initially describes YAML personas before noting a switch to JSON at the end. This could confuse readers; consider moving the JSON revision note to the top or removing YAML snippets.
@@ -56,0 +59,4 @@// If displayName is empty, sentinelName is used for both.func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string {var sb strings.Builder[MINOR] FormatMarkdown and FormatMarkdownWithDisplay duplicate substantial formatting logic. Consider refactoring to a shared helper to reduce drift and maintenance overhead.
Security Review
Summary
Good feature addition with clean integration and tests; however, loading a persona from an arbitrary filesystem path lacks the same path/symlink validation applied to system-prompt files. This enables arbitrary file reads on the CI runner and can lead to denial-of-service by reading device files, and potential sensitive data exfiltration to the LLM.
Findings
cmd/review-bot/main.goreview/persona.goRecommendation
REQUEST_CHANGES — Harden persona-file handling to match (and reuse) the protections already implemented for system-prompt-file:
Optionally, sanitize or constrain persona.DisplayName if you intend to render it in Markdown in contexts where untrusted persona files may be used; though current usage is low risk, constraining to a reasonable character set would be consistent with reviewer-name validation.
Once these changes are in place, re-run tests to confirm no regressions and that persona loading behaves correctly under path validation.
Review by security
Evaluated against
5682d824@@ -94,0 +97,4 @@if *personaName != "" && *personaFile != "" {slog.Error("--persona and --persona-file are mutually exclusive")os.Exit(1)}[MAJOR] Unvalidated persona-file path is read without restricting to the workspace or validating symlinks. An attacker who can modify workflow inputs could point to arbitrary files (e.g., /dev/zero causing DoS, or JSON config files), risking resource exhaustion and potential data exfiltration to the LLM.
@@ -0,0 +38,4 @@if err != nil {return nil, fmt.Errorf("read persona file %s: %w", path, err)}return parsePersona(data, path)[MAJOR] LoadPersona uses os.ReadFile on the provided path without checks. Combined with the caller’s lack of path/symlink validation, this permits reading arbitrary paths on the runner (including device nodes or large files), enabling DoS and possible sensitive data leakage if the file parses as JSON.
Self-review: PASS
ci.yml(disabling unavailable models) are noted via a comment and don't disrupt the feature scope.encoding/json(standard library) instead of YAML dependencies, matching the project's zero-external-deps policy. Error handling follows existing patterns.go:embedfor built-in personas is idiomatic Go 1.16+ pattern.containshelper inpersona_test.goreimplementsstrings.Contains(test-only, harmless); FormatMarkdown/FormatMarkdownWithDisplay share duplicated logic (refactoring opportunity, not a blocker).No issues found — ready for human review.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
9b57a538)Gpt Review
Summary
Well-structured addition of role-based personas with clean CLI, action, and README integration. Persona loading via embedded JSON is dependency-free, path validation is secure, tests are comprehensive, and CI passed.
Recommendation
APPROVE — The implementation is solid and idiomatic: personas are embedded and validated, mutually exclusive flags are enforced, path handling prevents traversal, and the formatter cleanly separates display name from sentinel. Tests cover new functionality thoroughly and CI is green. Proceed to merge.
Review by gpt
Evaluated against
9b57a538Security Review
Summary
Adds role-based review personas with safe file loading, proper path validation against workspace and symlink traversal, and clean integration into prompt assembly and formatting. CI passed and no exploitable vulnerabilities were found.
Findings
review/formatter.goRecommendation
APPROVE — Overall the changes are sound from a security perspective: persona files are validated to reside within the workspace and symlink escapes are blocked; reviewer-name is validated to safe characters; and built-in personas are embedded. No injection, auth, or secrets issues were identified. As a minor hardening step, consider sanitizing or restricting the persona display_name before rendering it in review comments to mitigate theoretical XSS if upstream sanitization were to weaken. Otherwise, proceed with merge.
Review by security
Evaluated against
9b57a538Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
ca1e17de)Sonnet Review
Summary
This PR cleanly implements role-based review personas with good separation of concerns: persona definition (JSON + embed), prompt construction, and formatter extension. The implementation is backwards-compatible, well-tested, and follows the repository's zero-dependency convention. A few minor issues worth noting but none are blockers.
Findings
review/persona_test.gocontains/containsHelperfunctions in persona_test.go are reinventingstrings.Contains. The standard library already provides this. The custom implementation is error-prone (thecontainswrapper has a subtle redundancy: it checkslen(s) >= len(substr)ands == substrbefore callingcontainsHelper, which already handles the equality case in its loop). Just usestrings.Containsdirectly.review/formatter.goFormatMarkdownWithDisplayis almost a complete copy ofFormatMarkdown. The two functions share ~90% of their logic (summary, findings table, recommendation, sentinel). This violates DRY and means future changes to the review format must be applied in two places.FormatMarkdowncould simply delegate toFormatMarkdownWithDisplay(result, "", reviewerName)after capitalizing the name for display, orFormatMarkdownWithDisplaycould be the single implementation that both call.review/formatter.gostrings.ToUpper(headerName[:1]) + headerName[1:]is duplicated in bothFormatMarkdown(line 12) andFormatMarkdownWithDisplay(line 65). This byte-slice indexing also breaks on multi-byte (non-ASCII) first characters. Usestrings.ToUpper(string([]rune(headerName)[:1])) + string([]rune(headerName)[1:])orunicode.ToUpperfor correctness, and extract it into a helper.review/persona_prompt.gosb.WriteStringcalls with manual newlines. This is verbose and fragile. A single multi-line string literal (raw string orconst) would be cleaner and easier to maintain, following the pattern already used inBuildSystemBase.docs/DESIGN-51-personas.mdyaml:annotations) even after the revision section at the bottom switched to JSON. The checklist items referencesecurity.yaml,architect.yaml,docs.yaml. These inconsistencies are confusing for future readers. The doc should be updated to consistently reflect the JSON decision.cmd/review-bot/main_test.gopath/filepathimport is not in alphabetical order relative to the other imports in the block (os,os/exec,strings,path/filepath).goimports/gofmtwould sort this. Minor but the project convention is canonical formatting.Recommendation
APPROVE — APPROVE — CI passes, the implementation is correct, backwards-compatible, well-tested, and respects the zero-dependency convention. The MINOR findings (code duplication in formatter, reinvented
strings.Containsin tests, non-ASCII-safe capitalisation) are worth addressing in a follow-up or in the same PR if the author prefers, but they don't block merging.Review by sonnet
Evaluated against
ca1e17de@@ -7,6 +7,7 @@ import ("os""os/exec"[NIT] The
path/filepathimport is not in alphabetical order relative to the other imports in the block (os,os/exec,strings,path/filepath).goimports/gofmtwould sort this. Minor but the project convention is canonical formatting.@@ -0,0 +1,353 @@# Design: Role-based Review Personas (Issue #51)[NIT] The design doc still references YAML throughout (phase checklist, finding #2, struct tags use
yaml:annotations) even after the revision section at the bottom switched to JSON. The checklist items referencesecurity.yaml,architect.yaml,docs.yaml. These inconsistencies are confusing for future readers. The doc should be updated to consistently reflect the JSON decision.@@ -56,0 +56,4 @@// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.// displayName is used for the header title, sentinelName is used for the cleanup sentinel.// If displayName is empty, sentinelName is used for both.[MINOR]
FormatMarkdownWithDisplayis almost a complete copy ofFormatMarkdown. The two functions share ~90% of their logic (summary, findings table, recommendation, sentinel). This violates DRY and means future changes to the review format must be applied in two places.FormatMarkdowncould simply delegate toFormatMarkdownWithDisplay(result, "", reviewerName)after capitalizing the name for display, orFormatMarkdownWithDisplaycould be the single implementation that both call.@@ -56,0 +62,4 @@// Use display name for header, or fall back to sentinel nameheaderName := displayNameif headerName == "" {[MINOR] The title capitalisation
strings.ToUpper(headerName[:1]) + headerName[1:]is duplicated in bothFormatMarkdown(line 12) andFormatMarkdownWithDisplay(line 65). This byte-slice indexing also breaks on multi-byte (non-ASCII) first characters. Usestrings.ToUpper(string([]rune(headerName)[:1])) + string([]rune(headerName)[1:])orunicode.ToUpperfor correctness, and extract it into a helper.@@ -0,0 +83,4 @@sb.WriteString("- If the diff has no changes relevant to your focus areas, APPROVE with no findings.\n")// Custom output format if providedif p.OutputFormat != "" {[NIT] The output format JSON is constructed via a long sequence of individual
sb.WriteStringcalls with manual newlines. This is verbose and fragile. A single multi-line string literal (raw string orconst) would be cleaner and easier to maintain, following the pattern already used inBuildSystemBase.@@ -0,0 +188,4 @@dir := t.TempDir()path := filepath.Join(dir, "invalid.json")if err := os.WriteFile(path, []byte("not json"), 0644); err != nil {t.Fatalf("failed to write test file: %v", err)[MINOR] The
contains/containsHelperfunctions in persona_test.go are reinventingstrings.Contains. The standard library already provides this. The custom implementation is error-prone (thecontainswrapper has a subtle redundancy: it checkslen(s) >= len(substr)ands == substrbefore callingcontainsHelper, which already handles the equality case in its loop). Just usestrings.Containsdirectly.Security Review
Summary
The changes add role-based personas and integrate them safely into prompt construction and the CLI. Path handling for persona and system prompt files includes defenses against traversal and symlink attacks. One minor concern is untrusted display names from repo-controlled persona files being injected into review Markdown without escaping.
Findings
cmd/review-bot/main.goRecommendation
APPROVE — Overall, the implementation is sound and improves functionality without introducing major security risks. The newly added validateWorkspacePath correctly mitigates path traversal and symlink attacks when loading files from the workspace. To harden against potential content injection, ensure persona.DisplayName is sanitized or escaped before inserting into Markdown (or restrict it to alphanumerics, spaces, dashes/underscores). With that minor adjustment, the feature set looks secure and ready to merge.
Review by security
Evaluated against
ca1e17de@@ -293,7 +311,12 @@ func main() {slog.Info("review parsed", "verdict", result.Verdict, "findings", len(result.Findings))[MINOR] Untrusted persona display_name (from a repo-controlled persona-file) is passed directly to FormatMarkdownWithDisplay and rendered into review Markdown without escaping. If the hosting platform's sanitizer is bypassable, this could enable Markdown/HTML injection in the review header/footer. Consider sanitizing or escaping displayName (and restricting to a safe character set) before inclusion.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
ca1e17de)Gpt Review
Summary
Solid implementation of role-based personas with clean integration into flags, action inputs, prompt assembly, and formatting. Tests and docs are comprehensive, and CI is passing.
Findings
cmd/review-bot/main.goreview/formatter.gocmd/review-bot/main_test.goRecommendation
APPROVE — Overall the persona feature is well integrated and follows repository conventions. The path validation helper is a good security addition; consider switching to filepath.Rel for a more robust containment check. Adjust the header capitalization to handle Unicode reviewer or display names gracefully. Lastly, update the absolute-path test expectation and its comment in TestValidateWorkspacePath to match the function’s behavior. With these minor refinements, the changes are ready to merge.
Review by gpt
Evaluated against
ca1e17de@@ -590,0 +627,4 @@fullPath = filepath.Clean(fullPath)// Check path is within workspaceif !strings.HasPrefix(fullPath, absWorkspace+string(filepath.Separator)) && fullPath != absWorkspace {return "", fmt.Errorf("%s resolves outside workspace: path=%s workspace=%s", pathName, fullPath, absWorkspace)[MINOR] validateWorkspacePath uses a string HasPrefix check to ensure paths are within the workspace; this can be brittle across platforms and case-insensitive filesystems. Consider using filepath.Rel and verifying the resulting path does not start with ".." to robustly confirm containment.
@@ -48,0 +92,4 @@{name: "valid nested path",workspace: tmpDir,path: "subdir/nested.json",[MINOR] The TestValidateWorkspacePath case labeled "absolute path gets normalized to relative" expects an error substring "failed to resolve", but validateWorkspacePath returns an earlier "resolves outside workspace" error for absolute paths. The expectation and test comment are inconsistent with actual behavior and may cause confusion; align the expected error substring to the actual branch.
@@ -56,0 +57,4 @@// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.// displayName is used for the header title, sentinelName is used for the cleanup sentinel.// If displayName is empty, sentinelName is used for both.func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string {[NIT] Header title capitalization uses byte slicing (headerName[:1]) which can mis-handle non-ASCII names (multi-byte runes). Consider using unicode/utf8 to upper-case the first rune safely.
ca1e17de16tod33a45329cSonnet Review
Summary
This PR adds a well-designed role-based review persona feature. The implementation is clean, follows Go stdlib patterns, maintains zero external dependencies, and includes comprehensive tests. CI passed. A few minor issues worth noting but none are blockers.
Findings
.gitea/actions/review/action.ymlsystem-prompt-fileinput is malformed — thepersonaandpersona-fileinputs are inserted between thesystem-prompt-filekey and itsdescription/required/defaultfields. This meanssystem-prompt-fileloses its description/metadata in the action definition. The two new inputs should be placed after the completesystem-prompt-fileblock, not in the middle of it.review/persona.gopath/filepathis imported and used inLoadBuiltinPersonato constructfilepath.Join("personas", filename). On Windows this would producepersonas\filename.json, butembed.FSalways uses forward slashes. This should use"personas/" + filenamedirectly (orpath.Joinfrom thepathpackage, notfilepath). This is a correctness bug on non-Unix platforms, though it won't affect Linux CI runners.review/persona_test.gocontains/containsHelperhelpers duplicate functionality fromstrings.Contains. The test file is in thereviewpackage (notreview_test), so it could just usestrings.Containsdirectly — or at minimum importstrings. Adding custom substring matching helpers in tests is unnecessary complexity and a maintenance burden.cmd/review-bot/main.goos.Exit(1)block (the required-fields validation) and before the persona-flags validation comment. This is a minor style issue (double blank line) thatgofmtwould not flag but deviates from the existing code style where single blank lines separate logical sections.review/formatter.goFormatMarkdownWithDisplayis nearly identical toFormatMarkdown— the only difference is the two-name parameter split. Consider refactoringFormatMarkdownto delegate toFormatMarkdownWithDisplay(result, reviewerName, reviewerName)to avoid duplicating the findings table and recommendation rendering logic.review/persona_prompt.goBuildSystemPromptWithPersonaexists as a helper but is never called frommain.go— the main function manually assemblessystemBasethen appendsadditionalPrompt, patterns, and conventions separately via the budget system. This exported function is unused dead code in practice. Either wire it into the main flow or unexport/remove it.docs/DESIGN-51-personas.mdyaml:tags, the design says 'personas/*.yaml', the completion checklist asks 'Persona struct matches YAML schema exactly?') even though the design was revised to JSON. The doc is internally inconsistent. Since this is a docs file it doesn't affect correctness, but it will confuse future readers.Recommendation
APPROVE — APPROVE — The implementation is correct, well-tested, follows established patterns (embed.FS, encoding/json, zero deps), and maintains backward compatibility. The YAML action.yml structural bug (Finding 1) and the filepath vs path bug (Finding 2) are the most actionable items. The filepath issue won't affect Linux CI but is technically wrong. All other findings are style/cleanup nits that can be addressed in a follow-up. The feature works as designed and CI passes.
Review by sonnet
Evaluated against
d33a4532@@ -71,6 +71,14 @@ inputs:required: falsedefault: 'true'system-prompt-file:persona:[MINOR] The YAML for the
system-prompt-fileinput is malformed — thepersonaandpersona-fileinputs are inserted between thesystem-prompt-filekey and itsdescription/required/defaultfields. This meanssystem-prompt-fileloses its description/metadata in the action definition. The two new inputs should be placed after the completesystem-prompt-fileblock, not in the middle of it.@@ -91,6 +93,33 @@ func main() {os.Exit(1)}[MINOR] There is a blank line immediately after the
os.Exit(1)block (the required-fields validation) and before the persona-flags validation comment. This is a minor style issue (double blank line) thatgofmtwould not flag but deviates from the existing code style where single blank lines separate logical sections.@@ -0,0 +1,353 @@# Design: Role-based Review Personas (Issue #51)[NIT] The design doc still references YAML in several places (e.g. the Persona struct uses
yaml:tags, the design says 'personas/*.yaml', the completion checklist asks 'Persona struct matches YAML schema exactly?') even though the design was revised to JSON. The doc is internally inconsistent. Since this is a docs file it doesn't affect correctness, but it will confuse future readers.@@ -53,3 +53,48 @@ func GiteaEvent(verdict string) string {return "COMMENT"}}[NIT]
FormatMarkdownWithDisplayis nearly identical toFormatMarkdown— the only difference is the two-name parameter split. Consider refactoringFormatMarkdownto delegate toFormatMarkdownWithDisplay(result, reviewerName, reviewerName)to avoid duplicating the findings table and recommendation rendering logic.@@ -0,0 +4,4 @@"embed""encoding/json""fmt""os"[MINOR]
path/filepathis imported and used inLoadBuiltinPersonato constructfilepath.Join("personas", filename). On Windows this would producepersonas\filename.json, butembed.FSalways uses forward slashes. This should use"personas/" + filenamedirectly (orpath.Joinfrom thepathpackage, notfilepath). This is a correctness bug on non-Unix platforms, though it won't affect Linux CI runners.@@ -0,0 +82,4 @@sb.WriteString("- Line numbers should reference the new file line numbers from the diff headers.\n")sb.WriteString("- If the diff has no changes relevant to your focus areas, APPROVE with no findings.\n")// Custom output format if provided[NIT]
BuildSystemPromptWithPersonaexists as a helper but is never called frommain.go— the main function manually assemblessystemBasethen appendsadditionalPrompt, patterns, and conventions separately via the budget system. This exported function is unused dead code in practice. Either wire it into the main flow or unexport/remove it.@@ -0,0 +191,4 @@t.Fatalf("failed to write test file: %v", err)}_, err := LoadPersona(path)[MINOR] The
contains/containsHelperhelpers duplicate functionality fromstrings.Contains. The test file is in thereviewpackage (notreview_test), so it could just usestrings.Containsdirectly — or at minimum importstrings. Adding custom substring matching helpers in tests is unnecessary complexity and a maintenance burden.d33a45329cto4776b22194Sonnet Review
Summary
This PR cleanly implements role-based review personas with good separation of concerns: JSON-based persona definitions, embedded built-ins, workspace path validation extracted into a reusable helper, and a separate
FormatMarkdownWithDisplayfunction for persona display names. CI passes. The implementation is idiomatic Go, maintains zero external dependencies, and is fully backwards compatible.Findings
review/persona_test.gocontainsandcontainsHelperhelpers are reinventingstrings.Contains. The project already importsstringselsewhere and the stdlibstrings.Containsis available in test files. Usingstrings.Containsdirectly would be clearer and eliminate ~10 lines of custom code.review/persona_prompt.goBuildPersonaSystemPromptbuilds the JSON output format spec using ~20 individualsb.WriteStringcalls on string literals. This duplicates logic that already exists inBuildSystemBase()(or the base prompt builder). A shared constant or helper for the JSON output spec would reduce the risk of the persona and generic prompts diverging silently.review/formatter.goFormatMarkdownWithDisplayduplicates nearly all ofFormatMarkdown— the only differences are the header/footer name split. Consider refactoringFormatMarkdownto callFormatMarkdownWithDisplay(result, reviewerName, reviewerName)to keep the two in sync, or extract the shared table-building logic.cmd/review-bot/main_test.gopath/filepathimport is not in alphabetical order relative to the other imports (os,os/exec,strings).goimportswould place it beforeos. Minor style issue.docs/DESIGN-51-personas.mdRecommendation
APPROVE — APPROVE — The implementation is correct, well-tested, and follows established project patterns (zero dependencies, table-driven tests, early returns, proper error wrapping). The minor findings are quality improvements but none block merging. The
containsreimplementation in the test file is the most unnecessary piece of code, but it doesn't affect correctness.Review by sonnet
Evaluated against
4776b221@@ -7,6 +7,7 @@ import ("os"[NIT] The
path/filepathimport is not in alphabetical order relative to the other imports (os,os/exec,strings).goimportswould place it beforeos. Minor style issue.@@ -0,0 +1,353 @@# Design: Role-based Review Personas (Issue #51)[NIT] The design doc references YAML throughout (personas/*.yaml, gopkg.in/yaml.v3) before the revision section at the bottom switches to JSON. The doc would be cleaner if the YAML references in the early sections were updated to reflect the final JSON decision, rather than leaving the revision as an addendum. As a static doc this is low priority.
@@ -53,3 +53,48 @@ func GiteaEvent(verdict string) string {return "COMMENT"}}[NIT]
FormatMarkdownWithDisplayduplicates nearly all ofFormatMarkdown— the only differences are the header/footer name split. Consider refactoringFormatMarkdownto callFormatMarkdownWithDisplay(result, reviewerName, reviewerName)to keep the two in sync, or extract the shared table-building logic.@@ -0,0 +82,4 @@sb.WriteString("- Line numbers should reference the new file line numbers from the diff headers.\n")sb.WriteString("- If the diff has no changes relevant to your focus areas, APPROVE with no findings.\n")// Custom output format if provided[MINOR]
BuildPersonaSystemPromptbuilds the JSON output format spec using ~20 individualsb.WriteStringcalls on string literals. This duplicates logic that already exists inBuildSystemBase()(or the base prompt builder). A shared constant or helper for the JSON output spec would reduce the risk of the persona and generic prompts diverging silently.@@ -0,0 +194,4 @@_, err := LoadPersona(path)if err == nil {t.Error("expected error for invalid JSON")}[MINOR] The
containsandcontainsHelperhelpers are reinventingstrings.Contains. The project already importsstringselsewhere and the stdlibstrings.Containsis available in test files. Usingstrings.Containsdirectly would be clearer and eliminate ~10 lines of custom code.4776b22194to9c7538cafaSonnet Review
Summary
This PR cleanly implements role-based review personas with good separation of concerns, comprehensive tests, and zero new dependencies (JSON over YAML is the right call). The implementation is well-structured, backwards-compatible, and follows the repository's patterns. A few minor issues worth noting but none are blockers.
Findings
review/persona_test.gocontainsandcontainsHelperhelpers re-implementstrings.Containsfrom scratch. This is unnecessary —strings.Containsis already in the standard library and is used everywhere else in the test files. This also violates the principle of using standard library functions.review/formatter.goFormatMarkdownWithDisplayandFormatMarkdownshare almost identical logic (only the header/footer differ). This duplication means any change to the findings table, summary format, etc. must be made in two places. Consider havingFormatMarkdowndelegate toFormatMarkdownWithDisplay, or extracting the shared body-building logic into a private helper.review/persona.gofilepathis imported but used only forfilepath.Join("personas", filename)inLoadBuiltinPersona. Sinceembed.FSuses forward-slash paths on all platforms (per Go spec), using"personas/" + filename(plain string concatenation) would be both simpler and more correct —filepath.Joinuses the OS separator on Windows, which would break the embed.FS lookup.cmd/review-bot/main_test.go"path/filepath"is not in alphabetical order with the other imports — it appears after"strings"instead of between"os/exec"and"strings".goimportswould fix this automatically.review/persona_prompt.goBuildPersonaSystemPromptfunction builds the JSON output format using many individualsb.WriteStringcalls for what is essentially a multi-line string constant. This is harder to read and maintain than a singleconstor backtick string. Consider using aconstfor the static instructions block.docs/DESIGN-51-personas.mdyaml:tags,.review/personas/trading.yaml,security.yamlphase items) after the JSON revision was made. These are internal doc inconsistencies but worth cleaning up for accuracy.Recommendation
APPROVE — The implementation is solid and ready to merge. The
filepath.Joinissue withembed.FS(MINOR) is the most substantive concern — on Windows this would produce a backslash path thatembed.FScannot find, making built-in personas fail. All other findings are style/duplication issues. The core design decisions are sound: JSON over YAML keeps zero dependencies, the persona/system-prompt-file distinction is well-documented, backwards compatibility is preserved, and the workspace path validation was correctly refactored into a shared helper.Review by sonnet
Evaluated against
9c7538ca@@ -7,6 +7,7 @@ import ("os""os/exec""strings"[NIT] Import
"path/filepath"is not in alphabetical order with the other imports — it appears after"strings"instead of between"os/exec"and"strings".goimportswould fix this automatically.@@ -0,0 +1,353 @@# Design: Role-based Review Personas (Issue #51)[NIT] The design doc still references YAML in several places (Persona struct with
yaml:tags,.review/personas/trading.yaml,security.yamlphase items) after the JSON revision was made. These are internal doc inconsistencies but worth cleaning up for accuracy.@@ -53,3 +53,48 @@ func GiteaEvent(verdict string) string {return "COMMENT"}}[MINOR]
FormatMarkdownWithDisplayandFormatMarkdownshare almost identical logic (only the header/footer differ). This duplication means any change to the findings table, summary format, etc. must be made in two places. Consider havingFormatMarkdowndelegate toFormatMarkdownWithDisplay, or extracting the shared body-building logic into a private helper.@@ -0,0 +1,98 @@package reviewimport ([MINOR]
filepathis imported but used only forfilepath.Join("personas", filename)inLoadBuiltinPersona. Sinceembed.FSuses forward-slash paths on all platforms (per Go spec), using"personas/" + filename(plain string concatenation) would be both simpler and more correct —filepath.Joinuses the OS separator on Windows, which would break the embed.FS lookup.@@ -0,0 +55,4 @@sb.WriteString("CONTEXT:\n")sb.WriteString("- You will receive the full content of modified files for reference, followed by the diff showing what changed.\n")sb.WriteString("- The diff shows ONLY what was added/removed. The full file content provides complete context.\n")sb.WriteString("- Focus your review on the CHANGES (the diff), using the full files for context.\n\n")[NIT] The
BuildPersonaSystemPromptfunction builds the JSON output format using many individualsb.WriteStringcalls for what is essentially a multi-line string constant. This is harder to read and maintain than a singleconstor backtick string. Consider using aconstfor the static instructions block.@@ -0,0 +193,4 @@_, err := LoadPersona(path)if err == nil {t.Error("expected error for invalid JSON")[MINOR] The
containsandcontainsHelperhelpers re-implementstrings.Containsfrom scratch. This is unnecessary —strings.Containsis already in the standard library and is used everywhere else in the test files. This also violates the principle of using standard library functions.Security Review
Summary
Persona support is added with careful path validation for local files and embedded built-ins. No exploitable vulnerabilities are apparent; CI passed and the changes improve security posture by adding safe path handling. A couple of minor hardening opportunities exist around unescaped display strings and path checks.
Findings
review/formatter.gocmd/review-bot/main.goRecommendation
APPROVE — Overall, the persona feature and related prompt assembly are implemented safely, with good defense against path traversal and symlink escapes when reading local files. Approve the PR. For defense-in-depth, consider sanitizing persona display_name before rendering it in review markdown to avoid potential mention/format injection, and strengthen validateWorkspacePath by using filepath.Rel-based checks (and drive/volume matching on Windows) instead of HasPrefix to make the containment check more robust across platforms.
Review by security
Evaluated against
9c7538ca@@ -587,6 +610,36 @@ func validateReviewerName(name string) error {return nil[MINOR] validateWorkspacePath relies on string prefix checks for containment. Although it also resolves symlinks and uses a path separator guard, on case-insensitive filesystems and certain UNC/volume edge cases, prefix checks can be brittle. Consider using filepath.Rel to compute the relative path and ensuring it does not start with ".." and that the volume/drive matches, for stronger cross-platform guarantees.
@@ -56,0 +63,4 @@// Use display name for header, or fall back to sentinel nameheaderName := displayNameif headerName == "" {headerName = sentinelName[MINOR] Persona display_name is used directly in the markdown header/footer without escaping. While the cleanup sentinel uses the validated reviewer-name, a custom persona file could inject markdown mentions or formatting into the posted review. Consider sanitizing or constraining display_name (e.g., limit characters or escape markdown) to reduce potential content/mention abuse.
Gpt Review
Summary
Overall the persona feature is well integrated, with clean flag handling, secure path validation, embedded built-in personas, and comprehensive tests. However, there's a blocking test expectation mismatch in TestValidateWorkspacePath that will fail on typical systems, and a minor Unicode-safety issue in header capitalization.
Findings
cmd/review-bot/main_test.goreview/formatter.goreview/formatter.goRecommendation
REQUEST_CHANGES — Address the failing test expectation in TestValidateWorkspacePath by aligning the expected error substring with the actual behavior of validateWorkspacePath for absolute paths (e.g., expect "resolves outside workspace"). This will prevent test failures on typical Unix environments where /etc/passwd exists. Additionally, consider making header capitalization Unicode-safe in FormatMarkdownWithDisplay and refactoring duplicated formatting code for maintainability. With these fixes, the PR should be ready to merge.
Review by gpt
Evaluated against
9c7538ca@@ -45,6 +46,113 @@ func TestValidateReviewerName(t *testing.T) {}[MAJOR] TestValidateWorkspacePath case "absolute path gets normalized to relative" expects an error containing "failed to resolve", but validateWorkspacePath returns an error "resolves outside workspace" for an absolute path like /etc/passwd. Update the test to match the actual error message (or adjust validateWorkspacePath to return a consistent error in this scenario).
9c7538cafato83441bfbacSonnet Review
Summary
A well-designed feature addition implementing role-based review personas. The code is idiomatic Go, follows the repository's zero-dependency convention, has comprehensive test coverage, and the refactoring of
validateWorkspacePathinto a reusable helper is a nice improvement. CI passes.Findings
review/persona_test.gocontainsandcontainsHelperhelpers are reimplementingstrings.Contains, which is already available in the standard library. The helpers add no value and the custom implementation has a subtle bug:containsHelperis only called whenlen(s) > 0, but the outercontainschecklen(s) >= len(substr)already handles the empty-substr case. Just usestrings.Containsdirectly — it handles all edge cases correctly and is idiomatic Go.review/formatter.goFormatMarkdownWithDisplayduplicates nearly all of the logic inFormatMarkdown. The only difference is theheaderName/sentinelNamesplit.FormatMarkdowncould simply delegate toFormatMarkdownWithDisplay(result, reviewerName, reviewerName), eliminating ~35 lines of duplication. Duplicated formatting logic is a maintenance hazard — a future change (e.g., adding a new section) must be applied in two places.review/persona_prompt.goBuildPersonaSystemPromptfunction has 30+ consecutivesb.WriteString(...)calls for the hardcoded JSON output format schema. This duplicates the output format specification that already lives in the baseBuildSystemBase()function. If the JSON schema changes (e.g., adding a new field), both functions must be updated. Consider extracting the shared output format section into a package-level constant or helper function to keep the schema definition in one place.review/persona.gopath/filepathis imported but only used forfilepath.Join("personas", filename)inLoadBuiltinPersona. Since the path separator for embedded FS is always/(regardless of OS),embed.FSpaths must use forward slashes per theio/fsspec. Usingfilepath.Joinhere works on Linux/macOS but would producepersonas\securityon Windows if that ever becomes a build target. Using"personas/" + filenamedirectly would be safer and removes the need for thepath/filepathimport.docs/DESIGN-51-personas.mdPersona structGo code block usesyaml:struct tags, the persona file format sections show YAML examples). The design revision section at the bottom explains the switch to JSON, but the earlier sections weren't updated. This creates confusion for anyone reading the doc top-to-bottom. Consider updating or striking through the YAML sections, or noting "see Design Revision below" at the first YAML mention.cmd/review-bot/main_test.gopath/filepathimport was added but placed out of order — the Go convention (andgoimports) groups stdlib imports alphabetically. It should appear between"os/exec"and"strings", not after"strings". Minor butgoimportswould reorder this.Recommendation
APPROVE — APPROVE — This is a clean, well-structured feature. The design decision to use JSON instead of YAML preserves the zero-dependency constraint correctly. The
validateWorkspacePathrefactoring is a genuine improvement. The minor findings (duplication in formatter, customcontainshelper, filepath.Join on embed.FS paths) are worth addressing but don't block merging. The stale design doc is a documentation-only issue.Review by sonnet
Evaluated against
83441bfb@@ -7,6 +7,7 @@ import ("os"[NIT] The
path/filepathimport was added but placed out of order — the Go convention (andgoimports) groups stdlib imports alphabetically. It should appear between"os/exec"and"strings", not after"strings". Minor butgoimportswould reorder this.@@ -0,0 +1,353 @@# Design: Role-based Review Personas (Issue #51)[NIT] The design document still references YAML as the persona format in multiple places (e.g., the
Persona structGo code block usesyaml:struct tags, the persona file format sections show YAML examples). The design revision section at the bottom explains the switch to JSON, but the earlier sections weren't updated. This creates confusion for anyone reading the doc top-to-bottom. Consider updating or striking through the YAML sections, or noting "see Design Revision below" at the first YAML mention.@@ -53,3 +53,48 @@ func GiteaEvent(verdict string) string {return "COMMENT"}}[MINOR]
FormatMarkdownWithDisplayduplicates nearly all of the logic inFormatMarkdown. The only difference is theheaderName/sentinelNamesplit.FormatMarkdowncould simply delegate toFormatMarkdownWithDisplay(result, reviewerName, reviewerName), eliminating ~35 lines of duplication. Duplicated formatting logic is a maintenance hazard — a future change (e.g., adding a new section) must be applied in two places.@@ -0,0 +2,4 @@import ("embed""encoding/json"[NIT]
path/filepathis imported but only used forfilepath.Join("personas", filename)inLoadBuiltinPersona. Since the path separator for embedded FS is always/(regardless of OS),embed.FSpaths must use forward slashes per theio/fsspec. Usingfilepath.Joinhere works on Linux/macOS but would producepersonas\securityon Windows if that ever becomes a build target. Using"personas/" + filenamedirectly would be safer and removes the need for thepath/filepathimport.@@ -0,0 +67,4 @@sb.WriteString(" \"findings\": [\n")sb.WriteString(" {\n")sb.WriteString(" \"severity\": \"MAJOR\" or \"MINOR\" or \"NIT\",\n")sb.WriteString(" \"file\": \"path/to/file\",\n")[MINOR] The
BuildPersonaSystemPromptfunction has 30+ consecutivesb.WriteString(...)calls for the hardcoded JSON output format schema. This duplicates the output format specification that already lives in the baseBuildSystemBase()function. If the JSON schema changes (e.g., adding a new field), both functions must be updated. Consider extracting the shared output format section into a package-level constant or helper function to keep the schema definition in one place.@@ -0,0 +193,4 @@_, err := LoadPersona(path)if err == nil {t.Error("expected error for invalid JSON")[MINOR] The
containsandcontainsHelperhelpers are reimplementingstrings.Contains, which is already available in the standard library. The helpers add no value and the custom implementation has a subtle bug:containsHelperis only called whenlen(s) > 0, but the outercontainschecklen(s) >= len(substr)already handles the empty-substr case. Just usestrings.Containsdirectly — it handles all edge cases correctly and is idiomatic Go.Gpt Review
Summary
Solid implementation of role-based personas with clean CLI/action wiring, secure file path validation, embedded built-ins, and comprehensive tests. The changes are cohesive, backward compatible, and CI passed.
Findings
review/persona.goreview/persona_prompt.go.gitea/actions/review/action.ymlRecommendation
APPROVE — Overall, this is a well-designed and well-tested feature addition. The persona system integrates cleanly with the existing prompt/budget pipeline, the action and CLI flags are mutually exclusive and validated, path handling is secure via workspace-bound resolution, and the embedded built-in personas are convenient and covered by tests. Consider addressing the small nits: either use Persona.ModelPref (e.g., log a warning or influence model choice) or remove it to reduce ambiguity, and consider centralizing the common JSON output schema between persona and default prompts to minimize duplication. Otherwise, this is good to merge.
Review by gpt
Evaluated against
83441bfb@@ -0,0 +16,4 @@type Persona struct {Name string `json:"name"`DisplayName string `json:"display_name"`ModelPref string `json:"model_preference,omitempty"`[NIT] Persona.ModelPref (model_preference) is defined but not used anywhere. Consider either wiring it into model selection or removing it to avoid confusion.
@@ -0,0 +1,118 @@package review[NIT] BuildPersonaSystemPrompt re-specifies the output schema that likely mirrors BuildSystemBase; consider refactoring to share a common formatter for the JSON output specification to avoid future drift.
Security Review
Summary
The persona feature is implemented cleanly with sensible security boundaries. Paths for persona and prompt files are validated against the workspace with symlink checks, and built-in personas are embedded to avoid remote fetches. No exploitable vulnerabilities were found.
Findings
cmd/review-bot/main.goreview/formatter.goRecommendation
APPROVE — Approve this change. The new persona system uses embedded JSON for built-in roles and validates local file inputs with a clear workspace-bound and symlink-safe check. No injection, auth, or secret-handling issues were identified. As defense-in-depth, consider switching the path containment check to use filepath.Rel to avoid relying on prefix comparisons, and optionally document or sanitize persona display names to prevent unexpected Markdown/HTML rendering. Otherwise, the implementation is sound and CI has passed.
Review by security
Evaluated against
83441bfb@@ -590,0 +625,4 @@// Join and clean the pathfullPath := filepath.Join(absWorkspace, path)fullPath = filepath.Clean(fullPath)// Check path is within workspace[MINOR] Workspace boundary check relies on strings.HasPrefix for path containment. While mitigated by filepath.Clean and EvalSymlinks, using filepath.Rel (and verifying it does not start with "..") would be a more robust, cross-platform defense-in-depth against subtle path normalization edge cases.
83441bfbacto3d50707332Sonnet Review
Summary
The PR adds a useful persona feature but critically violates the project's stated convention of 'Go standard library only — no external dependencies' by introducing gopkg.in/yaml.v3. The design document even acknowledges this conflict and resolves to use JSON, but the implementation proceeds to use YAML anyway. This is the primary blocker; all other issues are secondary.
Findings
go.modreview/persona.goreview/persona.goreview/persona.goreview/persona_prompt.gocmd/review-bot/main.goreview/persona.goreview/persona_prompt.gocmd/review-bot/main_test.goREADME.mdRecommendation
REQUEST_CHANGES — REQUEST_CHANGES. The blocking issue is the introduction of gopkg.in/yaml.v3 in direct violation of the project's 'Go standard library only — no external dependencies' convention. Ironically, the design document that ships with this PR explicitly documents this conflict and resolves to use JSON instead of YAML — but the implementation ignores that resolution. The fix is straightforward: convert the built-in persona files from .yaml to .json, update the embed glob to *.json, remove the yaml.v3 import, and use only encoding/json. Once the dependency is removed, also clean up: (1) the --persona-file description to accurately state the supported format, (2) the README note about 'backwards compatibility' which doesn't apply, and (3) consider whether BuildSystemPromptWithPersona should be used in main.go or removed. The overall feature design is sound and the code quality is good — this is a well-structured addition that just needs the dependency violation corrected.
Review by sonnet
Evaluated against
3d507073@@ -332,0 +404,4 @@- Decimal precision for moneyignore:- Code style[NIT] The README custom persona example shows YAML format and says 'JSON format is also supported for backwards compatibility' — but this is a new feature with no prior format, so there's no backwards compatibility concern. After removing the yaml.v3 dependency and going JSON-only per the design document's own resolution, this sentence should be removed or changed to accurately describe which format is canonical.
@@ -72,2 +72,4 @@llmProvider := flag.String("llm-provider", envOrDefault("LLM_PROVIDER", "openai"), "LLM API provider: openai or anthropic")personaName := flag.String("persona", envOrDefault("PERSONA", ""), "Built-in persona name (security, architect, docs)")personaFile := flag.String("persona-file", envOrDefault("PERSONA_FILE", ""), "Path to persona JSON file")[MINOR] The --persona-file flag description says 'Path to persona JSON file' but the implementation also accepts YAML. The description should say 'Path to persona JSON or YAML file' (or after the dependency fix, just 'JSON file').
@@ -48,2 +154,4 @@func makeReview(id int64, login, state string, stale bool, body string) gitea.Review {r := gitea.Review{[NIT] Double blank line after the closing brace of TestValidateWorkspacePath before the makeReview helper. Minor style inconsistency.
@@ -2,2 +2,4 @@go 1.26.2require gopkg.in/yaml.v3 v3.0.1 // indirect[MAJOR] Introduces gopkg.in/yaml.v3 as a dependency. The repository conventions explicitly state 'Go standard library only — no external dependencies.' The design document (DESIGN-51-personas.md) even documents this conflict under 'Design Revision: JSON Instead of YAML' and resolves to use JSON to maintain zero dependencies — but the implementation ignores that resolution and uses YAML anyway. The persona loading code must be changed to use only JSON, or the YAML dependency must be removed.
@@ -0,0 +9,4 @@"strings""gopkg.in/yaml.v3")[MAJOR] Imports gopkg.in/yaml.v3 in violation of the zero-external-dependencies convention. The parsePersona function should use only encoding/json. Built-in personas stored as .yaml files should either be converted to .json or parsed with a minimal hand-rolled YAML subset — though the simplest fix consistent with the design document's own resolution is to store built-in personas as .json and remove the yaml.v3 import entirely.
@@ -0,0 +75,4 @@}}return names}[MINOR] ListBuiltinPersonas silently returns nil when ReadDir fails. The caller (LoadBuiltinPersona error message) would then say 'available: ' with no names, which is confusing. Consider logging or returning an error, or at minimum ensuring the error path doesn't produce a misleading 'available: ' suffix in the error message.
@@ -0,0 +86,4 @@if err := json.Unmarshal(data, &p); err != nil {return nil, fmt.Errorf("parse persona %s: %w", source, err)}} else {[NIT] Trailing whitespace on line 89 (blank line with spaces inside parsePersona before the ext detection block). Minor style issue.
@@ -0,0 +93,4 @@}}if err := validatePersona(&p, source); err != nil {[MINOR] parsePersona uses filepath.Ext(source) to detect JSON vs YAML, but for built-in personas the source is 'builtin:security' (no file extension), so it falls through to the YAML path. This works today only because yaml.v3 is a superset of JSON, but it's fragile: if YAML is replaced with JSON-only, built-in persona sources would need to be renamed (e.g., 'builtin:security.yaml') or the detection logic changed. The dispatch logic is unclear about its contract.
@@ -0,0 +65,4 @@sb.WriteString(" \"verdict\": \"APPROVE\" or \"REQUEST_CHANGES\",\n")sb.WriteString(" \"summary\": \"Brief overall assessment (1-3 sentences)\",\n")sb.WriteString(" \"findings\": [\n")sb.WriteString(" {\n")[NIT] The JSON output format is written as a series of sb.WriteString calls with manually formatted JSON. This is the same pattern used in BuildSystemBase and is consistent, but the inline JSON template is duplicated between BuildPersonaSystemPrompt and BuildSystemBase. A shared constant or helper would reduce drift risk if the output schema ever changes.
@@ -0,0 +91,4 @@return sb.String()}// BuildSystemPromptWithPersona constructs the full system prompt, using either[MINOR] BuildSystemPromptWithPersona has the conventions and patterns arguments in the wrong order relative to BuildSystemBase's documented usage in the budget assembly (where patterns come before conventions in the prompt). More importantly, the argument order is (persona, conventions, patterns) but the caller in main.go assembles the prompt manually rather than using this function — making BuildSystemPromptWithPersona a dead export that duplicates logic without being used. Either use it in main.go or remove it.
Gpt Review
Summary
The persona feature is well integrated (flags, prompt selection, formatting, tests) and CI passes, but the change introduces an external YAML dependency which violates the repository’s zero-dependency convention. There are also minor documentation inconsistencies and a small portability concern in workspace path validation.
Findings
go.modreview/persona.go.gitea/actions/review/action.ymldocs/DESIGN-51-personas.mdcmd/review-bot/main.goRecommendation
REQUEST_CHANGES — The personas feature is thoughtfully implemented and well tested, but it conflicts with the repository’s "Go standard library only — no external dependencies" rule. To proceed:
Remove the YAML dependency:
Documentation consistency:
Path validation hardening (optional but recommended):
Once YAML is removed and docs are aligned, the feature will comply with project conventions and can be approved.
Review by gpt
Evaluated against
3d507073@@ -77,2 +83,4 @@required: falsedefault: ''runs:[MINOR] The persona-file input description states "Path to persona JSON file" but the implementation supports both YAML and JSON. Update the description to reflect YAML/JSON support for consistency.
@@ -587,6 +610,36 @@ func validateReviewerName(name string) error {return nil[MINOR] validateWorkspacePath uses strings.HasPrefix for path containment checks, which can be brittle on case-insensitive filesystems. Prefer using filepath.Rel and ensuring the result does not start with ".." for more robust containment checks.
@@ -0,0 +297,4 @@## Design Review Findings (Self-Review)### Finding 1: Severity Mapping[MINOR] The design document states a "Design Revision: JSON Instead of YAML" to preserve zero dependencies, but the implementation retains YAML parsing and adds a YAML dependency. Align docs and code (prefer JSON-only) to match project conventions.
@@ -2,2 +2,4 @@go 1.26.2require gopkg.in/yaml.v3 v3.0.1 // indirect[MAJOR] Introduces external dependency gopkg.in/yaml.v3, which violates the repository convention of using Go standard library only (zero dependencies).
@@ -0,0 +14,4 @@//go:embed personas/*.yamlvar embeddedPersonas embed.FS// Persona defines a specialized review role with focused expertise.[MAJOR] Imports gopkg.in/yaml.v3 and parses YAML personas, conflicting with the project’s zero-dependency requirement. Either remove YAML support or replace with JSON-only parsing.
Security Review
Summary
The changes add role-based review personas with proper path validation and clean integration into the CLI and action. No exploitable vulnerabilities are evident, and CI has passed.
Findings
review/persona.gogo.modreview/formatter.goRecommendation
APPROVE — Overall, the persona feature is implemented securely, with good path traversal and symlink defenses via validateWorkspacePath. Approve the changes. As hardening steps, enforce a reasonable maximum size on persona and system prompt files before unmarshaling to mitigate potential CI resource exhaustion, maintain vigilance on the new YAML dependency (audit and pin versions), and consider sanitizing persona DisplayName outputs to guard against potential markdown injection should downstream sanitization be bypassed.
Review by security
Evaluated against
3d507073@@ -2,2 +2,4 @@go 1.26.2require gopkg.in/yaml.v3 v3.0.1 // indirect[MINOR] An external dependency (gopkg.in/yaml.v3 v3.0.1) is introduced for YAML support. While common and currently patched against known alias-expansion issues, any new dependency increases supply-chain risk. Ensure this version is regularly audited and pinned, and consider preferring JSON where possible to minimize the attack surface.
@@ -56,0 +58,4 @@// displayName is used for the header title, sentinelName is used for the cleanup sentinel.// If displayName is empty, sentinelName is used for both.func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string {var sb strings.Builder[NIT] Persona DisplayName is inserted into the review header and footer without escaping. Although Gitea typically sanitizes markdown, an attacker-controlled DisplayName from a repo persona file could inject undesirable markdown. Consider sanitizing or constraining DisplayName to a safe character set to reduce reliance on downstream sanitization.
@@ -0,0 +30,4 @@// These are prompt guidance for the LLM, not output format changes.type Severity struct {Major string `json:"major" yaml:"major"`Minor string `json:"minor" yaml:"minor"`[MINOR] Persona files from the repository are read and unmarshaled without an explicit size limit. A very large YAML/JSON persona file could cause elevated memory/CPU usage during os.ReadFile and yaml/json unmarshaling, enabling a potential CI resource exhaustion vector. Consider enforcing a maximum file size before reading/unmarshaling and rejecting oversized files.
3d50707332toa36c3da05ba36c3da05bto57e62a345fSonnet Review
Summary
The implementation is well-structured and feature-complete, but it introduces an external dependency (
github.com/goccy/go-yaml) in a repository that explicitly requires Go standard library only. This is a direct violation of the repository's stated conventions. All other aspects of the code are high quality.Findings
go.modgithub.com/goccy/go-yaml v1.19.2as an external dependency. The design doc (docs/DESIGN-51-personas.md) even notes a 'Design Revision' acknowledging this decision, but the justification (multi-line strings, comments in YAML) does not override the stated project constraint. YAML parsing can be handled with the stdlibencoding/jsonpackage (JSON-only persona files) or by writing a minimal YAML subset parser, but bringing in an external library violates the project's zero-dependencies rule.review/persona.goparsePersonafunction usesfilepath.Ext(source)to detect the format, but for built-in personas thesourceis"builtin:security"(no file extension). The comment says 'YAML (also handles .yaml, .yml, and builtin: prefix)' but this only works by coincidence —filepath.Ext("builtin:security")returns""which falls into the else-YAML branch. This is fragile. A more explicit check likesource == ".json"or extracting the logic to handle thebuiltin:prefix explicitly would be clearer and more robust.review/persona.goLoadBuiltinPersonacallsListBuiltinPersonas()to build an error message when the persona isn't found. However,ListBuiltinPersonasreads the directory from the embedded FS — if that somehow fails, it returnsniland the error message becomes 'unknown built-in persona "foo" (available: )'. This is a minor usability issue, but it would be better to hardcode the list of known personas or at least handle the empty-list case.review/formatter.goFormatMarkdownWithDisplaysays 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).' While the trust reasoning is sound for built-in personas,--persona-fileallows the display name to come from any YAML file in the workspace. If a workspace file is somehow malicious (e.g., a compromised dependency's config), the unescaped display name could inject markdown. This is a low-risk but worth noting given the security context of this tool.review/persona.govar p Personabefore the blank line. Minor style issue.cmd/review-bot/main_test.goTestValidateWorkspacePathandmakeReview. Consistent single blank lines between test functions would match the rest of the file.review/persona_test.goTestLoadPersonaInvalidYAMLuses"not: valid: yaml: here"as invalid YAML, but this is actually valid YAML (it's a keynotwith valuevalid: yaml: hereas a string). The test may pass only because the YAML parses to a struct with empty required fields, triggering the validation error rather than a parse error. The test description says 'invalid YAML' but it's really testing 'YAML missing required fields'. The test still provides coverage but the intent is misleading.Recommendation
REQUEST_CHANGES — REQUEST_CHANGES — The external dependency on
github.com/goccy/go-yamlmust be removed before this can be merged. The project'sconventionsdocument is unambiguous: 'Go standard library only — no external dependencies.' The design document itself acknowledges this was a deliberate decision, but the decision contradicts the project's rules. The fix is straightforward: either (1) support only JSON format for persona files usingencoding/jsonfrom stdlib, or (2) write a minimal YAML parser for the simple key-value/list structure used in persona files. Given that JSON is already supported and the built-in personas could be shipped as JSON, option 1 is the path of least resistance. The rest of the implementation is clean, well-tested, and correctly designed.Review by sonnet
Evaluated against
57e62a34@@ -48,3 +155,4 @@func makeReview(id int64, login, state string, stale bool, body string) gitea.Review {r := gitea.Review{ID: id,[NIT] There is a double blank line between
TestValidateWorkspacePathandmakeReview. Consistent single blank lines between test functions would match the rest of the file.@@ -2,2 +2,4 @@go 1.26.2require github.com/goccy/go-yaml v1.19.2[MAJOR] The repository conventions explicitly state 'Go standard library only — no external dependencies.' This PR adds
github.com/goccy/go-yaml v1.19.2as an external dependency. The design doc (docs/DESIGN-51-personas.md) even notes a 'Design Revision' acknowledging this decision, but the justification (multi-line strings, comments in YAML) does not override the stated project constraint. YAML parsing can be handled with the stdlibencoding/jsonpackage (JSON-only persona files) or by writing a minimal YAML subset parser, but bringing in an external library violates the project's zero-dependencies rule.@@ -10,0 +24,4 @@// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.// Note: displayName is not HTML-escaped as Gitea sanitizes rendered Markdown.// Persona display names are controlled by repo owners (trusted input).[MINOR] The comment on
FormatMarkdownWithDisplaysays 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).' While the trust reasoning is sound for built-in personas,--persona-fileallows the display name to come from any YAML file in the workspace. If a workspace file is somehow malicious (e.g., a compromised dependency's config), the unescaped display name could inject markdown. This is a low-risk but worth noting given the security context of this tool.@@ -0,0 +57,4 @@}// ListBuiltinPersonas returns the names of all built-in personas.func ListBuiltinPersonas() []string {[MINOR]
LoadBuiltinPersonacallsListBuiltinPersonas()to build an error message when the persona isn't found. However,ListBuiltinPersonasreads the directory from the embedded FS — if that somehow fails, it returnsniland the error message becomes 'unknown built-in persona "foo" (available: )'. This is a minor usability issue, but it would be better to hardcode the list of known personas or at least handle the empty-list case.@@ -0,0 +79,4 @@func parsePersona(data []byte, source string) (*Persona, error) {var p Persona[NIT] There is a trailing whitespace after
var p Personabefore the blank line. Minor style issue.@@ -0,0 +83,4 @@// Determine format by extension or try YAML first (it's a superset of JSON)ext := strings.ToLower(filepath.Ext(source))if ext == ".json" {if err := json.Unmarshal(data, &p); err != nil {[MINOR] The
parsePersonafunction usesfilepath.Ext(source)to detect the format, but for built-in personas thesourceis"builtin:security"(no file extension). The comment says 'YAML (also handles .yaml, .yml, and builtin: prefix)' but this only works by coincidence —filepath.Ext("builtin:security")returns""which falls into the else-YAML branch. This is fragile. A more explicit check likesource == ".json"or extracting the logic to handle thebuiltin:prefix explicitly would be clearer and more robust.@@ -0,0 +223,4 @@func TestLoadPersonaFileNotFound(t *testing.T) {_, err := LoadPersona("/nonexistent/path/persona.yaml")if err == nil {[NIT]
TestLoadPersonaInvalidYAMLuses"not: valid: yaml: here"as invalid YAML, but this is actually valid YAML (it's a keynotwith valuevalid: yaml: hereas a string). The test may pass only because the YAML parses to a struct with empty required fields, triggering the validation error rather than a parse error. The test description says 'invalid YAML' but it's really testing 'YAML missing required fields'. The test still provides coverage but the intent is misleading.Security Review
Summary
The persona feature is integrated cleanly with proper path validation, and CI has passed. Security-wise, there are no exploitable issues in the changes; only minor hardening opportunities exist.
Findings
review/persona.goreview/formatter.goRecommendation
APPROVE — Approve. The additions for role-based personas are implemented with attention to safety: persona-file and system-prompt-file paths are validated against workspace roots and symlink traversal, reviewer-name is strictly validated for sentinel safety, and built-in personas are embedded. As a defense-in-depth improvement, enforce a size limit before parsing persona YAML and prefer JSON where possible to reduce parser complexity. Otherwise, no exploitable vulnerabilities were identified.
Review by security
Evaluated against
57e62a34@@ -10,0 +18,4 @@case "REQUEST_CHANGES":return "REQUEST_CHANGES"default:return "COMMENT"[NIT] DisplayName is inserted into the markdown header/body without escaping. While Gitea typically sanitizes markdown, if this output is ever reused in a different renderer, unescaped content could cause formatting issues. Consider conservative sanitization or documenting the assumption that Gitea sanitization is always applied.
@@ -0,0 +97,4 @@return nil, err}return &p, nil}[MINOR] Persona YAML is parsed without limits using a third‑party YAML library. If a workflow references a persona file from the PR branch, a malicious PR could supply a very large or adversarial YAML (e.g., anchor expansion) to cause excessive CPU/memory consumption during unmarshalling. Consider enforcing a maximum file size before reading/parsing and/or preferring JSON (stdlib) for untrusted inputs.
Gpt Review
Summary
Solid feature addition with clear tests and documentation, but it introduces a new external dependency in violation of repository conventions that require Go standard library only. Minor inconsistencies and small edge-case issues should also be addressed.
Findings
go.modcmd/review-bot/main_test.godocs/DESIGN-51-personas.mdreview/formatter.go.gitea/actions/review/action.ymlreview/persona.goRecommendation
REQUEST_CHANGES — The role-based persona feature is well implemented with clear prompt assembly, tests, and documentation. However, to comply with repository conventions, remove the external YAML dependency or restrict personas to JSON parsing via the standard library, or update the repository policy to allow vetted dependencies. Align TestValidateWorkspacePath’s expected error message with validateWorkspacePath’s current behavior (or adjust the function if you prefer the alternative behavior). Update the design doc to reflect the actual YAML library used, fix the action input description for persona-file to mention YAML/JSON, and consider Unicode-safe capitalization in formatter. Once the dependency convention issue is resolved and minor inconsistencies are addressed, this change can be approved.
Review by gpt
Evaluated against
57e62a34@@ -77,2 +83,4 @@required: falsedefault: ''runs:[NIT] The 'persona-file' input description says 'Path to persona JSON file', but the implementation supports YAML and JSON. Update the description to avoid misleading users (e.g., 'YAML/JSON').
@@ -48,0 +77,4 @@defer os.Setenv("GITHUB_WORKSPACE", origWorkspace)tests := []struct {name string[MINOR] TestValidateWorkspacePath expects the error substring 'failed to resolve' for an absolute path ("/etc/passwd"), but validateWorkspacePath returns 'resolves outside workspace' for absolute paths. This mismatch can cause brittle or failing tests; align the expected substring with the actual error or adjust the function to match the test intent.
@@ -0,0 +301,4 @@The persona YAML allows `critical` severity, but the LLM output parser (`review/parser.go`) only accepts MAJOR/MINOR/NIT.**Resolution:** Keep standard output format. Persona severity section is ONLY for calibrating the LLM's judgment (prompt guidance). Output must still use MAJOR/MINOR/NIT. Document this clearly in persona format docs.[MINOR] Design doc states 'Decision: Add gopkg.in/yaml.v3' but the implementation uses github.com/goccy/go-yaml. Update the design document to reflect the actual library choice or switch the implementation to match the doc.
@@ -2,2 +2,4 @@go 1.26.2require github.com/goccy/go-yaml v1.19.2[MAJOR] Adds external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'. Either remove this dependency (e.g., support only JSON) or update the repo conventions accordingly.
@@ -10,0 +25,4 @@// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.// Note: displayName is not HTML-escaped as Gitea sanitizes rendered Markdown.// Persona display names are controlled by repo owners (trusted input).// displayName is used for the header title, sentinelName is used for the cleanup sentinel.[NIT] Header capitalization uses byte indexing (strings.ToUpper(headerName[:1]) + headerName[1:]) which is not Unicode-safe for multi-byte runes. Consider using utf8.DecodeRuneInString to handle non-ASCII display names correctly.
@@ -0,0 +45,4 @@}// LoadBuiltinPersona loads a built-in persona by name.// Returns an error if the persona doesn't exist.[NIT] ListBuiltinPersonas returns nil on error, which may require nil checks at call sites. Returning an empty slice is often more ergonomic and safer.
Sonnet Review
Summary
The PR adds a well-structured persona feature with good test coverage, but introduces an external dependency (github.com/goccy/go-yaml) that violates the project's explicit 'Go standard library only — no external dependencies' convention. This is a blocking issue. There are also a few minor design issues worth addressing.
Findings
go.modgithub.com/goccy/go-yaml v1.19.2. The design doc mentions gopkg.in/yaml.v3 but the implementation uses a different third-party library. YAML parsing can be done with the stdlib by requiring JSON format for all persona files, or by writing a minimal YAML-subset parser. Alternatively, the project convention must be explicitly updated before adding dependencies.review/persona.goparsePersonafunction usesfilepath.Ext(source)to decide the format, but for built-in personas the source is"builtin:security"which has no file extension — it correctly falls through to the YAML path. However, for user-provided files without a.jsonextension (e.g.,.yml), it also falls through to YAML, which is correct behavior but is undocumented. More importantly, the comment says 'YAML is a superset of JSON' as justification for the fallback, but this is only approximately true and the actual behavior depends on the library. The logic would be clearer as: if JSON extension → JSON unmarshal, else → YAML unmarshal (which currently is what happens, but the comment is misleading).review/persona.goLoadBuiltinPersonacallsListBuiltinPersonas()to build an error message only on failure. This means ifembeddedPersonas.ReadFilefails ANDembeddedPersonas.ReadDir(insideListBuiltinPersonas) also fails, the error message will beunknown built-in persona "x" (available: )with an empty list — which is confusing. Consider hardcoding the known persona names in the error message or handling the ReadDir error in ListBuiltinPersonas more gracefully.review/formatter.godisplayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).While display names are set by repo owners (trusted), they come from YAML files that could be committed by contributors. If a malicious contributor adds adisplay_namecontaining-->or similar, it could break the sentinel comment or inject content into the review body. The sentinel is protected (usessentinelNamefromreviewerNamewhich is validated), but the header# <displayName> Reviewis unescaped. This is low severity given Gitea sanitizes Markdown, but the trust model statement in the comment is slightly overstated.review/persona.govar p Personadeclaration (blank line with spaces before the comment). Rungofmt.review/persona.gogofmt.docs/DESIGN-51-personas.mdgopkg.in/yaml.v3but the implementation usesgithub.com/goccy/go-yaml. The design doc's 'Design Revision' section should be updated to reflect the actual library chosen, or the implementation should use gopkg.in/yaml.v3 as designed.review/persona_prompt.goBuildSystemPromptWithPersonais exported and documented as a convenience wrapper, but it's not used anywhere in the codebase (main.go assembles the prompt manually). This creates a parallel code path that could diverge. Either use it in main.go or make it unexported/remove it to avoid maintenance burden.Recommendation
REQUEST_CHANGES — REQUEST_CHANGES — The external dependency on
github.com/goccy/go-yamldirectly violates the project's explicit 'Go standard library only' convention. This needs to be resolved before merging:Option A (recommended): Drop YAML support entirely and support only JSON for persona files. The built-in personas can be written as JSON. JSON is part of the stdlib (
encoding/json). This is the simplest fix and aligns with project conventions. The YAML support in the README can be documented as 'convert to JSON before use.'Option B: Write a minimal YAML parser sufficient for persona files (only needs string scalars, lists, and maps — no anchors, etc.). This is non-trivial but keeps the dependency-free guarantee.
Option C: Explicitly update the project conventions to permit this specific dependency with justification, get buy-in, then re-merge.
Apart from the dependency issue, the feature design is sound: the persona loading/validation logic is clean, tests are thorough, the path traversal protection via
validateWorkspacePathis a good refactor, and the display-name vs sentinel-name separation is well-reasoned. Address the MAJOR finding and the code is close to mergeable.Review by sonnet
Evaluated against
57e62a34@@ -0,0 +1,330 @@# Design: Role-based Review Personas (Issue #51)[NIT] The design document mentions
gopkg.in/yaml.v3but the implementation usesgithub.com/goccy/go-yaml. The design doc's 'Design Revision' section should be updated to reflect the actual library chosen, or the implementation should use gopkg.in/yaml.v3 as designed.@@ -2,2 +2,4 @@go 1.26.2require github.com/goccy/go-yaml v1.19.2[MAJOR] The project's stated convention is 'Go standard library only — no external dependencies,' but this PR adds
github.com/goccy/go-yaml v1.19.2. The design doc mentions gopkg.in/yaml.v3 but the implementation uses a different third-party library. YAML parsing can be done with the stdlib by requiring JSON format for all persona files, or by writing a minimal YAML-subset parser. Alternatively, the project convention must be explicitly updated before adding dependencies.@@ -14,0 +33,4 @@// Use display name for header, or fall back to sentinel nameheaderName := displayNameif headerName == "" {headerName = sentinelName[MINOR] The comment says
displayName is not HTML-escaped as Gitea sanitizes rendered Markdown. Persona display names are controlled by repo owners (trusted input).While display names are set by repo owners (trusted), they come from YAML files that could be committed by contributors. If a malicious contributor adds adisplay_namecontaining-->or similar, it could break the sentinel comment or inject content into the review body. The sentinel is protected (usessentinelNamefromreviewerNamewhich is validated), but the header# <displayName> Reviewis unescaped. This is low severity given Gitea sanitizes Markdown, but the trust model statement in the comment is slightly overstated.@@ -0,0 +52,4 @@if err != nil {available := ListBuiltinPersonas()return nil, fmt.Errorf("unknown built-in persona %q (available: %s)", name, strings.Join(available, ", "))}[MINOR]
LoadBuiltinPersonacallsListBuiltinPersonas()to build an error message only on failure. This means ifembeddedPersonas.ReadFilefails ANDembeddedPersonas.ReadDir(insideListBuiltinPersonas) also fails, the error message will beunknown built-in persona "x" (available: )with an empty list — which is confusing. Consider hardcoding the known persona names in the error message or handling the ReadDir error in ListBuiltinPersonas more gracefully.@@ -0,0 +76,4 @@}return names}[NIT] Trailing whitespace after the
var p Personadeclaration (blank line with spaces before the comment). Rungofmt.@@ -0,0 +81,4 @@var p Persona// Determine format by extension or try YAML first (it's a superset of JSON)ext := strings.ToLower(filepath.Ext(source))[MINOR] The
parsePersonafunction usesfilepath.Ext(source)to decide the format, but for built-in personas the source is"builtin:security"which has no file extension — it correctly falls through to the YAML path. However, for user-provided files without a.jsonextension (e.g.,.yml), it also falls through to YAML, which is correct behavior but is undocumented. More importantly, the comment says 'YAML is a superset of JSON' as justification for the fallback, but this is only approximately true and the actual behavior depends on the library. The logic would be clearer as: if JSON extension → JSON unmarshal, else → YAML unmarshal (which currently is what happens, but the comment is misleading).@@ -0,0 +91,4 @@if err := yaml.Unmarshal(data, &p); err != nil {return nil, fmt.Errorf("parse persona %s: %w", source, err)}}[NIT] Trailing whitespace after the closing brace of the else block. Run
gofmt.@@ -0,0 +53,4 @@// Output format instructions (shared schema from prompt.go)sb.WriteString("## Review Instructions\n\n")sb.WriteString("CONTEXT:\n")sb.WriteString("- You will receive the full content of modified files for reference, followed by the diff showing what changed.\n")[NIT]
BuildSystemPromptWithPersonais exported and documented as a convenience wrapper, but it's not used anywhere in the codebase (main.go assembles the prompt manually). This creates a parallel code path that could diverge. Either use it in main.go or make it unexported/remove it to avoid maintenance burden.Gpt Review
Summary
The persona feature is well-integrated overall (flags, prompt assembly, formatting, tests), but there are two blocking issues: an external YAML dependency violates repository conventions, and a unit test expectation conflicts with the new path validation logic. Address these to proceed.
Findings
go.modcmd/review-bot/main_test.gocmd/review-bot/main_test.goreview/persona.goRecommendation
REQUEST_CHANGES — To align with repository conventions and ensure tests pass:
With these changes, the implementation should meet project conventions, keep tests green, and maintain the new persona functionality as designed.
Review by gpt
Evaluated against
57e62a34@@ -48,0 +75,4 @@// Save and restore GITHUB_WORKSPACEorigWorkspace := os.Getenv("GITHUB_WORKSPACE")defer os.Setenv("GITHUB_WORKSPACE", origWorkspace)[NIT] The comment "filepath.Join strips leading / making it /etc/passwd" is inaccurate on Unix-like systems — filepath.Join with an absolute path ignores the base and yields the absolute path. Update the comment for clarity.
@@ -48,0 +93,4 @@name: "valid nested path",workspace: tmpDir,path: "subdir/nested.json",wantErr: false,[MAJOR] TestValidateWorkspacePath case "absolute path gets normalized to relative" expects error text containing "failed to resolve", but validateWorkspacePath now returns an error like "resolves outside workspace" for absolute paths outside the workspace. This mismatch will cause the test to fail on Linux.
@@ -2,2 +2,4 @@go 1.26.2require github.com/goccy/go-yaml v1.19.2[MAJOR] Introduces an external dependency github.com/goccy/go-yaml, which violates the repository convention of 'Go standard library only — no external dependencies'.
@@ -0,0 +15,4 @@var embeddedPersonas embed.FS// Persona defines a specialized review role with focused expertise.type Persona struct {[MINOR] Persona.ModelPref is parsed but not used anywhere. Consider documenting intended usage or removing until needed to reduce API surface.
Security Review
Summary
Overall, the changes introduce role-based personas safely and improve path handling by validating and constraining file access to the CI workspace. No exploitable vulnerabilities were identified. I noted two minor hardening points around dependency risk and Markdown display name handling.
Findings
go.modreview/formatter.goRecommendation
APPROVE — Approve the PR. The new persona feature is implemented with sensible security controls, notably robust path validation preventing traversal and symlink escapes for persona and system-prompt files. To further harden the system, consider (1) reviewing the necessity of the new YAML dependency or pinning and regularly auditing it to mitigate supply-chain risk, and (2) sanitizing or restricting persona display_name to reduce the chance of Markdown-based content injection or tracking, especially when personas are sourced from repository files. No blocking security issues were found.
Review by security
Evaluated against
57e62a34@@ -1,3 +1,5 @@module gitea.weiker.me/rodin/review-botgo 1.26.2[MINOR] A new external dependency (github.com/goccy/go-yaml v1.19.2) increases the supply-chain attack surface. While not a direct vulnerability, it diverges from the repo’s "stdlib only" convention and can introduce risks if the library later has issues.
@@ -10,0 +22,4 @@}}// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.[MINOR] Display names are intentionally not HTML-escaped and are included in Markdown. If a persona is loaded from a repo file, a crafted display_name could embed Markdown constructs (e.g., images) that cause remote content loads when rendered. Consider restricting display_name to a conservative character set or escaping/sanitizing to reduce content-injection/tracking risks, even if Gitea sanitizes most HTML.
Sonnet Review
Summary
This PR adds a well-designed role-based persona system to review-bot. The implementation is clean, follows Go idioms, maintains the zero-external-dependencies constraint by using JSON instead of YAML, and includes comprehensive tests. There are a few minor issues worth noting but nothing blocking.
Findings
review/formatter.godisplayNamecan come from a--persona-filewhose path is validated for workspace containment but whose contents (thedisplay_namefield) are not sanitized. A persona file checked into a repo could contain something likedisplay_name: "] Review\n\n<script>in a pathological case. Since Gitea does sanitize Markdown-rendered HTML, this is low risk, but the trust assertion in the comment ('controlled by repo owners') is slightly inaccurate — it could also come from any file a developer adds to the repo. The comment should be more precise or dropped.docs/DESIGN-51-personas.md# .review/personas/security.yamlheader and YAML syntax likename: security) but the implementation uses JSON. The design revision note at the bottom explains this, but the main body of the document is misleading — it still shows YAML syntax in the code block under '2. Persona File Format'. This will confuse future contributors reading the design doc.review/persona.goembeddedPersonas.ReadFile("personas/" + filename)says 'embed.FS paths use forward slashes per io/fs spec'. While correct and useful, this comment is placed on the wrong line — it belongs on theReadFilecall line (line 59), which is where it appears. This is fine. However, the function silently falls through to listing available personas when the embedded file is not found, which means a typo in the name produces an error message that lists alternatives. This is good UX, but theListBuiltinPersonas()call insideLoadBuiltinPersonaadds a small O(n) directory scan on every failed lookup — not a real concern at this scale but worth noting.review/persona.goCapitalizeFirstis placed inpersona.gobut is used byformatter.go. It's a general string utility that doesn't have a conceptual relationship with persona loading. It would be more discoverable informatter.goor a sharedstrings.go/util.gofile within thereviewpackage.review/persona_prompt.goBuildSystemPromptWithPersonais exported but not used inmain.go—main.gocallsBuildPersonaSystemPromptdirectly and then appends patterns/conventions via the budget system. This exported function implements a different (non-budget-aware) path. It's only tested, not used in production. Either document clearly that it's an alternative API for callers who don't need the budget system, or make it unexported if it's just for testing convenience.cmd/review-bot/main_test.goTestValidateWorkspacePathtest creates a symlink to/etc/passwdand doesn't have OS-specific guards. On Windows,os.Symlinkto/etc/passwdwould fail or behave differently. The existing tests in the repo don't appear to target Windows, so this is likely acceptable, but it's worth noting for cross-platform CI.Recommendation
APPROVE — APPROVE — The implementation is solid. The persona system is well-structured, backwards-compatible, and follows the project's zero-dependencies convention. The refactoring of
validateWorkspacePathinto a shared function is a nice improvement. TheFormatMarkdownWithDisplayseparation of display name from sentinel name is clean and correct. Tests are comprehensive and cover edge cases including Unicode. The minor issues flagged (misleading design doc, misplaced utility function, unexported-but-public convenience function) are housekeeping items that don't affect correctness or behavior.Review by sonnet
Evaluated against
4dd67742@@ -48,0 +93,4 @@name: "valid nested path",workspace: tmpDir,path: "subdir/nested.json",wantErr: false,[NIT] The
TestValidateWorkspacePathtest creates a symlink to/etc/passwdand doesn't have OS-specific guards. On Windows,os.Symlinkto/etc/passwdwould fail or behave differently. The existing tests in the repo don't appear to target Windows, so this is likely acceptable, but it's worth noting for cross-platform CI.@@ -0,0 +73,4 @@output_format: |For each finding:- Severity: [CRITICAL|MAJOR|MINOR|NIT]- Attack vector: How could this be exploited?[MINOR] The design document shows a YAML persona file format as the primary example (with
# .review/personas/security.yamlheader and YAML syntax likename: security) but the implementation uses JSON. The design revision note at the bottom explains this, but the main body of the document is misleading — it still shows YAML syntax in the code block under '2. Persona File Format'. This will confuse future contributors reading the design doc.@@ -12,2 +33,2 @@if reviewerName != "" {title := strings.ToUpper(reviewerName[:1]) + reviewerName[1:]// Use display name for header, or fall back to sentinel nameheaderName := displayName[MINOR] The comment says 'displayName is not HTML-escaped as Gitea sanitizes rendered Markdown' and 'Persona display names are controlled by repo owners (trusted input)'. However,
displayNamecan come from a--persona-filewhose path is validated for workspace containment but whose contents (thedisplay_namefield) are not sanitized. A persona file checked into a repo could contain something likedisplay_name: "] Review\n\n<script>in a pathological case. Since Gitea does sanitize Markdown-rendered HTML, this is low risk, but the trust assertion in the comment ('controlled by repo owners') is slightly inaccurate — it could also come from any file a developer adds to the repo. The comment should be more precise or dropped.@@ -0,0 +56,4 @@// ListBuiltinPersonas returns the names of all built-in personas.// Returns an empty slice if the embedded directory cannot be read.func ListBuiltinPersonas() []string {entries, err := embeddedPersonas.ReadDir("personas")[MINOR] The inline comment on
embeddedPersonas.ReadFile("personas/" + filename)says 'embed.FS paths use forward slashes per io/fs spec'. While correct and useful, this comment is placed on the wrong line — it belongs on theReadFilecall line (line 59), which is where it appears. This is fine. However, the function silently falls through to listing available personas when the embedded file is not found, which means a typo in the name produces an error message that lists alternatives. This is good UX, but theListBuiltinPersonas()call insideLoadBuiltinPersonaadds a small O(n) directory scan on every failed lookup — not a real concern at this scale but worth noting.@@ -0,0 +94,4 @@// DisplayName defaults to Name if not setif p.DisplayName == "" {p.DisplayName = p.Name}[NIT]
CapitalizeFirstis placed inpersona.gobut is used byformatter.go. It's a general string utility that doesn't have a conceptual relationship with persona loading. It would be more discoverable informatter.goor a sharedstrings.go/util.gofile within thereviewpackage.@@ -0,0 +59,4 @@sb.WriteString("Your task:\n")sb.WriteString("1. Review the diff for issues within YOUR focus areas only.\n")sb.WriteString("2. Consider the CI status — if CI has failed, that is an automatic REQUEST_CHANGES regardless of code quality.\n")sb.WriteString("3. Output your review as structured JSON (and ONLY JSON, no markdown fences or other text).\n\n")[NIT]
BuildSystemPromptWithPersonais exported but not used inmain.go—main.gocallsBuildPersonaSystemPromptdirectly and then appends patterns/conventions via the budget system. This exported function implements a different (non-budget-aware) path. It's only tested, not used in production. Either document clearly that it's an alternative API for callers who don't need the budget system, or make it unexported if it's just for testing convenience.Gpt Review
Summary
Solid, well-structured implementation of role-based personas with clean CLI/action integration, safe file path validation, and comprehensive tests. No functional issues found; only minor documentation inconsistencies and a small clarity nit.
Findings
docs/DESIGN-51-personas.mdREADME.mdREADME.mdRecommendation
APPROVE — The changes are well-implemented, idiomatic, and thoroughly tested. The persona feature is integrated cleanly via flags and the composite action, with robust path validation and good reuse of shared prompt schema. Approve this PR. As follow-ups, clarify the design doc to remove the YAML/dependency contradiction, adjust the README JSON snippet to avoid invalid comments, and consider adding llm-provider to the persona example for clarity.
Review by gpt
Evaluated against
4dd67742@@ -332,0 +342,4 @@|---------|-------|| `security` | Vulnerabilities, auth bypass, secrets exposure, injection attacks || `architect` | Design patterns, code organization, API contracts, testability || `docs` | Documentation quality, API clarity, error messages |[NIT] The 'Using Built-in Personas' example specifies an Anthropic model (claude-opus-4-20250514) without setting llm-provider to 'anthropic'. While covered elsewhere, adding it here would reduce potential user confusion.
@@ -332,0 +357,4 @@### Multiple Personas in Parallel```yaml[NIT] JSON example snippet under 'Custom Personas' includes a comment line ('// .review/personas/trading.json') which is not valid JSON; users copy-pasting may encounter parse errors. Consider moving the comment outside the code block or noting that it's illustrative.
@@ -0,0 +317,4 @@### Finding 3: display_name vs reviewer-nameDesign says header shows "persona display name" but sentinel uses "reviewer-name". This is correct - they serve different purposes:- `display_name` → human-readable header ("Security Specialist Review")[MINOR] Design doc contradicts repository conventions and implementation: it states a 'Design Revision: YAML with gopkg.in/yaml.v3' (adding an external dependency), while the actual implementation uses JSON and maintains zero external dependencies. This may confuse contributors.
Security Review
Summary
Overall, the changes add persona support with sound security measures, notably robust path validation for local files and symlink traversal defenses. No exploitable vulnerabilities were identified; only minor hardening opportunities exist.
Findings
review/formatter.goreview/persona.goRecommendation
APPROVE — The persona functionality and associated security measures look solid, particularly the mutual exclusivity of persona flags and the validateWorkspacePath checks that prevent path traversal and symlink escapes. Approve the PR as-is. For defense-in-depth, consider sanitizing or restricting persona display_name to avoid potential Markdown-based injections in non-Gitea contexts, and add a simple size check before reading persona/system prompt files to mitigate resource exhaustion from unusually large inputs.
Review by security
Evaluated against
4dd67742@@ -10,0 +25,4 @@// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.// Note: displayName is not HTML-escaped as Gitea sanitizes rendered Markdown.// Persona display names are controlled by repo owners (trusted input).// displayName is used for the header title, sentinelName is used for the cleanup sentinel.[MINOR] Persona display_name is inserted into the Markdown header and footer without escaping. While Gitea sanitizes rendered Markdown, if a custom or differently configured renderer is used, a crafted display_name could inject Markdown (e.g., images/links) and cause external resource loads in the PR UI. Consider stripping newlines and restricting characters or explicitly escaping to reduce injection surface.
@@ -0,0 +36,4 @@func LoadPersona(path string) (*Persona, error) {data, err := os.ReadFile(path)if err != nil {return nil, fmt.Errorf("read persona file %s: %w", path, err)[MINOR] LoadPersona reads the entire persona file with os.ReadFile without a size cap. In CI contexts where persona-file points to a repository file, a very large JSON could cause excessive memory usage. Consider adding a size check (e.g., via os.Stat) or a reasonable read limit before loading.