Skip to content

Commit acbd1e0

Browse files
sylvansysclaude
andauthored
feat: Add Constellos review workflow (#55)
* feat: Add Constellos review workflow Enable AI-powered code reviews on this repo using Claude. Closes #54 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Use conditional steps instead of dynamic uses path GitHub Actions doesn't allow template expressions in composite action uses fields. Use if conditions to route to the correct reviewer. Also use local action path (`./ `) in workflow for testing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Update workflow to call reviewers directly and post comments - Call reviewer actions directly with if conditions (no router) - Add review-comment step after each review to post PR comments - This fixes output handling issues with composite action routing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Add workflow permissions for issues and pull-requests The GITHUB_TOKEN needs explicit permissions to: - Read issues (for requirements review context) - Write pull-requests (for posting review comments) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug: Add logging to issue detection Add debug output to understand why issue extraction is failing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Improve agent prompt clarity for JSON output - Use concrete examples instead of "passed|failed|skipped" placeholders - Add clear instruction to replace example values with actual findings - Remove debug logging from requirements-reviewer The placeholder format was causing Claude to output the literal text instead of choosing one status value. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug: Add extraction logging * fix: Extract JSON from all assistant messages, not just last Claude Code may output text in multiple turns. The JSON result could be in any message, not necessarily the last one. Join all text content and search for the JSON block in the combined output. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug: Show issue fetch errors instead of suppressing * Session work\n\nAuto-commit at session end to preserve work in progress.\n\nSession-ID: dd9fc88a-7441-49e8-a3ff-88965138fb6f\nSession-Timestamp: 2026-01-23T05:09:58.182Z\nBranch: 54-fix/take-a-look-at-prs-49-53-and-see-how-we-\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: Add multi-strategy JSON extraction for reviewer output The requirements reviewer was failing to extract JSON when Claude didn't wrap the output in ```json blocks. This adds fallback strategies: 1. Try ```json block extraction (existing behavior) 2. Fallback: grep for raw JSON object with "checks" key 3. Fallback: perl range extraction for {"checks":[ pattern Fixes requirements reviewer showing "0 passed • 0 failed • 0 skipped" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Add debug logging, Python extraction, and improve issue linking - Add debug logging to requirements reviewer to diagnose ALL_TEXT extraction - Add Strategy 4: Python-based multiline JSON extraction for robustness - Add Fallback 3: Detect any #123 reference in PR body (not just closing keywords) - Update tagline to describe Constellos agents and their data sources Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Use single-line Python to avoid YAML parsing errors The multiline Python code had colons that YAML interpreted as key-value separators, causing action.yml to fail parsing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Add imperative instructions to requirements prompt Claude was stopping after 1 turn with just a greeting. Added explicit START IMMEDIATELY instruction and detailed step-by-step process to ensure Claude actually reads files and performs the review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 38a3b80 commit acbd1e0

8 files changed

Lines changed: 236 additions & 69 deletions

File tree

.claude/settings.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
{
2-
"enabledPlugins": {
3-
"github-orchestration@constellos": true,
4-
"project-context@constellos": true
5-
},
62
"hooks": {
73
"SessionStart": [
84
{
@@ -15,5 +11,9 @@
1511
]
1612
}
1713
]
14+
},
15+
"enabledPlugins": {
16+
"github-orchestration@constellos": true,
17+
"project-context@constellos": true
1818
}
1919
}

.constellos/agents/code-quality.md

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,61 +46,58 @@ You must evaluate each of these checks independently:
4646

4747
## Output Format
4848

49-
**CRITICAL**: Output ONLY the JSON block below. Each check MUST have a status.
49+
**CRITICAL**: After your review, output a single JSON block with your findings.
5050

51+
For each check, set `status` to exactly one of: `"passed"`, `"failed"`, or `"skipped"`.
52+
- Use `"passed"` if no issues found
53+
- Use `"failed"` if any issues found
54+
- Use `"skipped"` if check is not applicable
55+
56+
Example output:
5157
```json
5258
{
5359
"checks": [
5460
{
5561
"name": "DRY",
56-
"status": "passed|failed|skipped",
57-
"result": "Brief 1-line result",
58-
"reasoning": "Why this status was given",
59-
"files": [
60-
{
61-
"path": "path/to/file.ts",
62-
"line": 42,
63-
"note": "Specific issue or observation"
64-
}
65-
]
62+
"status": "passed",
63+
"result": "No code duplication found",
64+
"reasoning": "Each component has unique logic with no repeated patterns",
65+
"files": []
6666
},
6767
{
6868
"name": "YAGNI",
69-
"status": "passed|failed|skipped",
70-
"result": "Brief 1-line result",
71-
"reasoning": "Why this status was given",
69+
"status": "passed",
70+
"result": "No over-engineering detected",
71+
"reasoning": "Implementation matches requirements without unnecessary complexity",
7272
"files": []
7373
},
7474
{
7575
"name": "Modularity",
76-
"status": "passed|failed|skipped",
77-
"result": "Brief 1-line result",
78-
"reasoning": "Why this status was given",
76+
"status": "passed",
77+
"result": "Good separation of concerns",
78+
"reasoning": "Components have single responsibilities and clear interfaces",
7979
"files": []
8080
},
8181
{
8282
"name": "Maintainability",
83-
"status": "passed|failed|skipped",
84-
"result": "Brief 1-line result",
85-
"reasoning": "Why this status was given",
83+
"status": "passed",
84+
"result": "Code is readable and well-structured",
85+
"reasoning": "Clear naming, appropriate comments, logical organization",
8686
"files": []
8787
},
8888
{
8989
"name": "Error Handling",
90-
"status": "passed|failed|skipped",
91-
"result": "Brief 1-line result",
92-
"reasoning": "Why this status was given",
90+
"status": "skipped",
91+
"result": "No error handling code in changes",
92+
"reasoning": "Changes are configuration files only",
9393
"files": []
9494
}
9595
],
96-
"message": "Optional overall assessment (leave empty if not needed)"
96+
"message": ""
9797
}
9898
```
9999

100-
## Status Guidelines
101-
- **passed**: No issues found for this check
102-
- **failed**: One or more issues found - be strict, fail if there are ANY violations
103-
- **skipped**: Check not applicable (e.g., no error handling code to review)
100+
**Important**: Replace the example values above with your actual findings. The status must be one of: `passed`, `failed`, or `skipped`.
104101

105102
## Severity in Files
106103
When adding files, prioritize high-severity issues:

.constellos/agents/requirements.md

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
You are a Requirements reviewer ensuring PR changes align with issue specifications.
44

5+
**IMPORTANT**: Start IMMEDIATELY by reading the context files. Do NOT just introduce yourself - perform the actual review.
6+
57
## Your Role
68
Verify that code changes implement what was requested, nothing more, nothing less.
79

@@ -29,56 +31,54 @@ You must evaluate each of these checks independently:
2931
- Issue context: `.claude/review-context/issue.json`
3032

3133
## Review Process
32-
1. Read the issue requirements carefully
33-
2. Review each changed file
34-
3. Evaluate EACH check independently
35-
4. Mark check as "failed" if ANY issues found
36-
5. Mark check as "passed" only if NO issues found
37-
6. Mark check as "skipped" if not applicable (e.g., no issue context)
34+
1. FIRST: Read `.claude/review-context/issue.json` to understand requirements
35+
2. THEN: Read `.claude/review-context/changed.txt` to see what files changed
36+
3. Review each changed file against the issue requirements
37+
4. Evaluate EACH check independently
38+
5. Mark check as "failed" if ANY issues found
39+
6. Mark check as "passed" only if NO issues found
40+
7. Mark check as "skipped" if not applicable (e.g., no issue context)
41+
8. Output the JSON result
3842

3943
## Output Format
4044

41-
**CRITICAL**: Output ONLY the JSON block below. Each check MUST have a status.
45+
**CRITICAL**: After your review, output a single JSON block with your findings.
46+
47+
For each check, set `status` to exactly one of: `"passed"`, `"failed"`, or `"skipped"`.
48+
- Use `"passed"` if check criteria are fully satisfied
49+
- Use `"failed"` if any issues are found
50+
- Use `"skipped"` if check is not applicable (e.g., no issue context)
4251

4352
```json
4453
{
4554
"checks": [
4655
{
4756
"name": "Completeness",
48-
"status": "passed|failed|skipped",
49-
"result": "Brief 1-line result",
50-
"reasoning": "Why this status was given",
51-
"files": [
52-
{
53-
"path": "path/to/file.ts",
54-
"line": 42,
55-
"note": "Missing requirement X implementation"
56-
}
57-
]
57+
"status": "passed",
58+
"result": "All acceptance criteria implemented",
59+
"reasoning": "Each requirement from the issue is addressed in the code changes",
60+
"files": []
5861
},
5962
{
6063
"name": "Scope",
61-
"status": "passed|failed|skipped",
62-
"result": "Brief 1-line result",
63-
"reasoning": "Why this status was given",
64+
"status": "passed",
65+
"result": "Changes within scope",
66+
"reasoning": "All changes relate directly to the issue requirements",
6467
"files": []
6568
},
6669
{
6770
"name": "Traceability",
68-
"status": "passed|failed|skipped",
69-
"result": "Brief 1-line result",
70-
"reasoning": "Why this status was given",
71+
"status": "skipped",
72+
"result": "No tests required for config change",
73+
"reasoning": "This is a workflow configuration change, not a code change requiring tests",
7174
"files": []
7275
}
7376
],
74-
"message": "Optional overall assessment (leave empty if not needed)"
77+
"message": ""
7578
}
7679
```
7780

78-
## Status Guidelines
79-
- **passed**: Check criteria fully satisfied
80-
- **failed**: One or more issues found - be strict, fail if there are ANY violations
81-
- **skipped**: Check not applicable (e.g., no issue context available)
81+
**Important**: Replace the example values above with your actual findings. The status must be one of: `passed`, `failed`, or `skipped`.
8282

8383
## Guidelines
8484
- Be strict about scope - extra features should be flagged

.github/actions/code-quality-reviewer/action.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,21 @@ runs:
158158
159159
EXEC="${{ steps.review.outputs.execution_file }}"
160160
if [ -f "$EXEC" ]; then
161-
TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC" 2>/dev/null || echo "")
162-
JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -50)
161+
# Get ALL text from assistant messages, joined together
162+
ALL_TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | join("\n")' "$EXEC" 2>/dev/null || echo "")
163+
# Strategy 1: Extract from ```json block
164+
JSON=$(echo "$ALL_TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -100)
165+
166+
# Strategy 2: If no json block, try to find raw JSON object with "checks" key
167+
if ! echo "$JSON" | jq . >/dev/null 2>&1; then
168+
JSON=$(echo "$ALL_TEXT" | grep -oP '\{"checks":\s*\[.*\]\s*(,"message":[^}]*)?\}' | head -1)
169+
fi
170+
171+
# Strategy 3: Extract any JSON object starting with {"checks"
172+
if ! echo "$JSON" | jq . >/dev/null 2>&1; then
173+
JSON=$(echo "$ALL_TEXT" | perl -ne 'print if /\{"checks":\[/.../"message":/' | tr '\n' ' ')
174+
fi
175+
163176
if echo "$JSON" | jq . >/dev/null 2>&1; then
164177
# Count checks by status
165178
CHECKS_PASSED=$(echo "$JSON" | jq '[.checks[]? | select(.status == "passed")] | length')

.github/actions/requirements-reviewer/action.yml

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,26 @@ runs:
8383
[ "$ISSUE" = "null" ] && ISSUE=""
8484
fi
8585
86+
# Fallback 3: Find any issue reference (#123) in PR body
87+
if [ -z "$ISSUE" ]; then
88+
# PR_BODY was fetched in Fallback 1, reuse it
89+
ISSUE=$(echo "$PR_BODY" | grep -oP '#\K[0-9]+' | head -1 || echo "")
90+
fi
91+
8692
echo "issue=$ISSUE" >> $GITHUB_OUTPUT
8793
8894
# Get changed files (gracefully handle permission errors)
8995
gh pr view ${{ inputs.pr_number }} --json files --jq '.files[].path' > .claude/review-context/changed.txt 2>/dev/null || touch .claude/review-context/changed.txt
9096
9197
# Get issue context if available
9298
if [ -n "$ISSUE" ]; then
93-
gh issue view $ISSUE --json title,body > .claude/review-context/issue.json 2>/dev/null || echo "{}" > .claude/review-context/issue.json
99+
echo "Fetching issue $ISSUE..."
100+
if ! gh issue view "$ISSUE" --json title,body > .claude/review-context/issue.json; then
101+
echo "Warning: Failed to fetch issue $ISSUE"
102+
echo "{}" > .claude/review-context/issue.json
103+
fi
94104
else
105+
echo "No issue number detected, skipping issue context"
95106
echo "{}" > .claude/review-context/issue.json
96107
fi
97108
@@ -187,8 +198,34 @@ runs:
187198
188199
EXEC="${{ steps.review.outputs.execution_file }}"
189200
if [ -f "$EXEC" ]; then
190-
TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC" 2>/dev/null || echo "")
191-
JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -50)
201+
# Debug: Show what we're working with
202+
echo "DEBUG: Checking execution file..."
203+
echo "DEBUG: File exists, size: $(wc -c < "$EXEC") bytes"
204+
echo "DEBUG: Message types in file:"
205+
jq -r '.[].type' "$EXEC" 2>/dev/null | sort | uniq -c || echo "DEBUG: jq failed"
206+
207+
# Get ALL text from assistant messages, joined together
208+
ALL_TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | join("\n")' "$EXEC" 2>/dev/null || echo "")
209+
echo "DEBUG: ALL_TEXT length: ${#ALL_TEXT}"
210+
echo "DEBUG: ALL_TEXT preview: ${ALL_TEXT:0:500}"
211+
# Strategy 1: Extract from ```json block
212+
JSON=$(echo "$ALL_TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -100)
213+
214+
# Strategy 2: If no json block, try to find raw JSON object with "checks" key
215+
if ! echo "$JSON" | jq . >/dev/null 2>&1; then
216+
JSON=$(echo "$ALL_TEXT" | grep -oP '\{"checks":\s*\[.*\]\s*(,"message":[^}]*)?\}' | head -1)
217+
fi
218+
219+
# Strategy 3: Extract any JSON object starting with {"checks"
220+
if ! echo "$JSON" | jq . >/dev/null 2>&1; then
221+
JSON=$(echo "$ALL_TEXT" | perl -ne 'print if /\{"checks":\[/.../"message":/' | tr '\n' ' ')
222+
fi
223+
224+
# Strategy 4: Use Python for robust multiline JSON extraction
225+
if ! echo "$JSON" | jq . >/dev/null 2>&1; then
226+
JSON=$(echo "$ALL_TEXT" | python3 -c "import re,sys; t=sys.stdin.read(); m=re.search(r'\{[^{}]*\"checks\"[^{}]*\[.*?\][^{}]*\}',t,re.DOTALL); print(m.group() if m else '')" 2>/dev/null)
227+
fi
228+
192229
if echo "$JSON" | jq . >/dev/null 2>&1; then
193230
# Count checks by status
194231
CHECKS_PASSED=$(echo "$JSON" | jq '[.checks[]? | select(.status == "passed")] | length')

.github/actions/review-comment/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ runs:
194194
# Create new comment
195195
{
196196
echo "$MARKER"
197-
echo "Updates on your PR checks from AI reviewers."
197+
echo "PR review updates from your Constellos agents. Based on linked issue(s), context files, and committed changes."
198198
echo ""
199199
echo "$AGENT_BLOCK"
200200
} > /tmp/comment.md
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Constellos Review Workflow
2+
# Runs AI-powered code reviews on PRs using Claude
3+
4+
name: Constellos Review
5+
on:
6+
pull_request:
7+
types: [opened, synchronize, reopened]
8+
9+
permissions:
10+
contents: read
11+
issues: read
12+
pull-requests: write
13+
14+
jobs:
15+
# Read config and determine which agents to run
16+
config:
17+
runs-on: ubuntu-latest
18+
outputs:
19+
agents: ${{ steps.read.outputs.agents }}
20+
steps:
21+
- uses: actions/checkout@v4
22+
- id: read
23+
run: |
24+
if [ -f ".constellos/config.json" ]; then
25+
AGENTS=$(jq -c '[.agents | to_entries[] | select(.value.enabled) | .key]' .constellos/config.json)
26+
else
27+
# Default: run both agents
28+
AGENTS='["requirements","code-quality"]'
29+
fi
30+
echo "agents=$AGENTS" >> $GITHUB_OUTPUT
31+
32+
# Run each enabled agent in parallel
33+
review:
34+
needs: config
35+
runs-on: ubuntu-latest
36+
strategy:
37+
fail-fast: false
38+
matrix:
39+
agent: ${{ fromJson(needs.config.outputs.agents) }}
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
# Requirements reviewer
44+
- name: Run requirements review
45+
id: requirements
46+
if: matrix.agent == 'requirements'
47+
uses: ./.github/actions/requirements-reviewer
48+
with:
49+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
50+
pr_number: ${{ github.event.number }}
51+
branch: ${{ github.head_ref }}
52+
github_token: ${{ secrets.GITHUB_TOKEN }}
53+
54+
- name: Post requirements comment
55+
if: matrix.agent == 'requirements'
56+
uses: ./.github/actions/review-comment
57+
with:
58+
review_name: Requirements
59+
passed: ${{ steps.requirements.outputs.passed }}
60+
result_json: ${{ steps.requirements.outputs.result }}
61+
checks_passed: ${{ steps.requirements.outputs.checks_passed }}
62+
checks_failed: ${{ steps.requirements.outputs.checks_failed }}
63+
checks_skipped: ${{ steps.requirements.outputs.checks_skipped }}
64+
pr_number: ${{ github.event.number }}
65+
sha: ${{ github.event.pull_request.head.sha }}
66+
github_token: ${{ secrets.GITHUB_TOKEN }}
67+
prompt_file: .constellos/agents/requirements.md
68+
69+
# Code quality reviewer
70+
- name: Run code-quality review
71+
id: code-quality
72+
if: matrix.agent == 'code-quality'
73+
uses: ./.github/actions/code-quality-reviewer
74+
with:
75+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
76+
pr_number: ${{ github.event.number }}
77+
github_token: ${{ secrets.GITHUB_TOKEN }}
78+
79+
- name: Post code-quality comment
80+
if: matrix.agent == 'code-quality'
81+
uses: ./.github/actions/review-comment
82+
with:
83+
review_name: Code Quality
84+
passed: ${{ steps.code-quality.outputs.passed }}
85+
result_json: ${{ steps.code-quality.outputs.result }}
86+
checks_passed: ${{ steps.code-quality.outputs.checks_passed }}
87+
checks_failed: ${{ steps.code-quality.outputs.checks_failed }}
88+
checks_skipped: ${{ steps.code-quality.outputs.checks_skipped }}
89+
pr_number: ${{ github.event.number }}
90+
sha: ${{ github.event.pull_request.head.sha }}
91+
github_token: ${{ secrets.GITHUB_TOKEN }}
92+
prompt_file: .constellos/agents/code-quality.md

0 commit comments

Comments
 (0)