Skip to content

Commit c08a4d2

Browse files
sylvansysclaudeconstellos[bot]
authored
feat(github-orchestration): Improve stop hook issue display with URLs (#328)
* feat(github-orchestration): Improve stop hook issue display with URLs and session issues - Add LinkedIssueInfo interface for rich issue data (number, title, url, repo) - Add extractLinkedIssuesWithInfo() for full issue details with cross-repo support - Update stop hook to display linked issues nested under PR with clickable URLs - Add "Other Issues Created" section for session issues not linked to the PR - Support issues from any repo (not just current one) with proper owner/repo#num format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add Constellos review workflow Auto-provisioned by Constellos app --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: constellos[bot] <253833643+constellos[bot]@users.noreply.github.com>
1 parent 453365f commit c08a4d2

3 files changed

Lines changed: 315 additions & 32 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Constellos Review Workflow
2+
# AI-powered code reviews on PRs using Claude
3+
#
4+
# This workflow is provisioned by the Constellos app.
5+
# Configure agents via .constellos/config.json in your repo.
6+
7+
name: Constellos Review
8+
on:
9+
pull_request:
10+
types: [opened, synchronize, reopened]
11+
12+
permissions:
13+
contents: read
14+
issues: read
15+
actions: read
16+
17+
jobs:
18+
# Read config and determine which agents to run
19+
config:
20+
runs-on: ubuntu-latest
21+
outputs:
22+
agents: ${{ steps.read.outputs.agents }}
23+
steps:
24+
- uses: actions/checkout@v4
25+
- id: read
26+
run: |
27+
if [ -f ".constellos/config.json" ]; then
28+
AGENTS=$(jq -c '[.agents | to_entries[] | select(.value.enabled) | .key]' .constellos/config.json)
29+
else
30+
# Default: run both agents
31+
AGENTS='["requirements","code-quality"]'
32+
fi
33+
echo "agents=$AGENTS" >> $GITHUB_OUTPUT
34+
35+
# Run each enabled agent in parallel
36+
review:
37+
needs: config
38+
runs-on: ubuntu-latest
39+
strategy:
40+
fail-fast: false
41+
matrix:
42+
agent: ${{ fromJson(needs.config.outputs.agents) }}
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
# Requirements reviewer
47+
- name: Run requirements review
48+
id: requirements
49+
if: matrix.agent == 'requirements'
50+
uses: constellos/github-agents/.github/actions/requirements-reviewer@main
51+
with:
52+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
53+
pr_number: ${{ github.event.number }}
54+
branch: ${{ github.head_ref }}
55+
github_token: ${{ secrets.GITHUB_TOKEN }}
56+
57+
- name: Post requirements comment
58+
if: matrix.agent == 'requirements'
59+
run: |
60+
curl -X POST "https://github.constellos.ai/results" \
61+
-H "Content-Type: application/json" \
62+
-d '{
63+
"owner": "'${{ github.repository_owner }}'",
64+
"repo": "'${{ github.event.repository.name }}'",
65+
"pr_number": ${{ github.event.number }},
66+
"sha": "'${{ github.event.pull_request.head.sha }}'",
67+
"run_id": ${{ github.run_id }},
68+
"review_name": "Requirements",
69+
"results": {
70+
"passed": ${{ steps.requirements.outputs.passed }},
71+
"result_json": ${{ steps.requirements.outputs.result }},
72+
"checks_passed": ${{ steps.requirements.outputs.checks_passed }},
73+
"checks_failed": ${{ steps.requirements.outputs.checks_failed }},
74+
"checks_skipped": ${{ steps.requirements.outputs.checks_skipped }},
75+
"prompt_file": ".constellos/agents/requirements.md"
76+
}
77+
}'
78+
79+
# Code quality reviewer
80+
- name: Run code-quality review
81+
id: code-quality
82+
if: matrix.agent == 'code-quality'
83+
uses: constellos/github-agents/.github/actions/code-quality-reviewer@main
84+
with:
85+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
86+
pr_number: ${{ github.event.number }}
87+
github_token: ${{ secrets.GITHUB_TOKEN }}
88+
89+
- name: Post code-quality comment
90+
if: matrix.agent == 'code-quality'
91+
run: |
92+
curl -X POST "https://github.constellos.ai/results" \
93+
-H "Content-Type: application/json" \
94+
-d '{
95+
"owner": "'${{ github.repository_owner }}'",
96+
"repo": "'${{ github.event.repository.name }}'",
97+
"pr_number": ${{ github.event.number }},
98+
"sha": "'${{ github.event.pull_request.head.sha }}'",
99+
"run_id": ${{ github.run_id }},
100+
"review_name": "Code Quality",
101+
"results": {
102+
"passed": ${{ steps.code-quality.outputs.passed }},
103+
"result_json": ${{ steps.code-quality.outputs.result }},
104+
"checks_passed": ${{ steps.code-quality.outputs.checks_passed }},
105+
"checks_failed": ${{ steps.code-quality.outputs.checks_failed }},
106+
"checks_skipped": ${{ steps.code-quality.outputs.checks_skipped }},
107+
"prompt_file": ".constellos/agents/code-quality.md"
108+
}
109+
}'
110+
111+
# Context reviewer
112+
- name: Run context review
113+
id: context
114+
if: matrix.agent == 'context'
115+
uses: constellos/github-agents/.github/actions/context-reviewer@main
116+
with:
117+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
118+
pr_number: ${{ github.event.number }}
119+
github_token: ${{ secrets.GITHUB_TOKEN }}
120+
121+
- name: Post context comment
122+
if: matrix.agent == 'context'
123+
run: |
124+
curl -X POST "https://github.constellos.ai/results" \
125+
-H "Content-Type: application/json" \
126+
-d '{
127+
"owner": "'${{ github.repository_owner }}'",
128+
"repo": "'${{ github.event.repository.name }}'",
129+
"pr_number": ${{ github.event.number }},
130+
"sha": "'${{ github.event.pull_request.head.sha }}'",
131+
"run_id": ${{ github.run_id }},
132+
"review_name": "Context",
133+
"results": {
134+
"passed": ${{ steps.context.outputs.passed }},
135+
"result_json": ${{ steps.context.outputs.result }},
136+
"checks_passed": ${{ steps.context.outputs.checks_passed }},
137+
"checks_failed": ${{ steps.context.outputs.checks_failed }},
138+
"checks_skipped": ${{ steps.context.outputs.checks_skipped }},
139+
"prompt_file": ".constellos/agents/context.md"
140+
}
141+
}'

plugins/github-orchestration/hooks/commit-session-await-ci-status.ts

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ import {
5353
awaitCIWithFailFast,
5454
getLatestCIRun as getCIRunDetails,
5555
extractAllPreviews,
56-
extractLinkedIssuesFromPR,
56+
extractLinkedIssuesWithInfo,
5757
type GroupedPreviewUrls,
58+
type LinkedIssueInfo,
5859
} from '../shared/hooks/utils/ci-status.js';
60+
import { getSessionIssues, type IssueReference } from '../shared/hooks/utils/session-issues.js';
5961
import { addPRToState } from '../shared/hooks/utils/github-state.js';
6062
import { exec } from 'child_process';
6163
import { promisify } from 'util';
@@ -725,6 +727,33 @@ Session-Timestamp: ${timestamp}${branch ? `\nBranch: ${branch}` : ''}
725727
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`;
726728
}
727729

730+
/**
731+
* Format a linked issue for display
732+
* @param issue - Issue info with number, url, and repo
733+
* @param prRepo - PR's repository for comparison
734+
* @returns Formatted issue string
735+
*/
736+
function formatLinkedIssue(issue: LinkedIssueInfo, prRepo: string): string {
737+
// If same repo as PR, show just #number
738+
// If different repo, show owner/repo#number
739+
const prefix = issue.repo === prRepo ? `#${issue.number}` : `${issue.repo}#${issue.number}`;
740+
const titlePart = issue.title ? ` - ${issue.title}` : '';
741+
return `${prefix}${titlePart}${issue.url}`;
742+
}
743+
744+
/**
745+
* Format a session issue for display (other issues section)
746+
* @param issue - Issue reference from session tracking
747+
* @param currentRepo - Current repository for comparison
748+
* @returns Formatted issue string
749+
*/
750+
function formatSessionIssue(issue: IssueReference, currentRepo: string): string {
751+
// If same repo, show just #number; if different, show owner/repo#number
752+
const prefix = issue.repo === currentRepo ? `#${issue.number}` : `${issue.repo}#${issue.number}`;
753+
const titlePart = issue.title ? ` - ${issue.title}` : '';
754+
return `${prefix}${titlePart}${issue.url}`;
755+
}
756+
728757
/**
729758
* Format PR status message when commit was made and PR exists
730759
* @param commitSha - Commit SHA
@@ -737,7 +766,9 @@ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`;
737766
* @param ciRun.conclusion - CI run conclusion
738767
* @param ciRun.name - CI run name
739768
* @param groupedPreviews - Preview URLs grouped by provider
740-
* @param linkedIssues - Issue numbers linked from PR body
769+
* @param linkedIssues - Issues linked from PR body (closes when merged)
770+
* @param otherIssues - Other issues created during session but not linked to PR
771+
* @param prRepo - PR's repository name (owner/repo)
741772
* @returns Formatted message
742773
* @example
743774
*/
@@ -746,7 +777,9 @@ function formatPRStatusWithCommit(
746777
prCheck: { prNumber: number; prUrl: string },
747778
ciRun: { url?: string; status?: string; conclusion?: string; name?: string },
748779
groupedPreviews: GroupedPreviewUrls,
749-
linkedIssues: number[]
780+
linkedIssues: LinkedIssueInfo[],
781+
otherIssues: IssueReference[],
782+
prRepo: string
750783
): string {
751784
const ciPassed = ciRun.conclusion === 'success';
752785
const ciFailed = ciRun.conclusion === 'failure';
@@ -762,11 +795,19 @@ function formatPRStatusWithCommit(
762795
message += `🔄 CI: ${ciRun.url} ${statusIcon} ${ciRun.conclusion || ciRun.status || 'pending'}\n`;
763796
}
764797

765-
// Linked issues (if any)
798+
// Linked issues (closes when merged) - nested under PR
766799
if (linkedIssues.length > 0) {
767-
message += '\n📌 Linked Issue(s):\n';
768-
for (const issueNum of linkedIssues) {
769-
message += ` • #${issueNum}\n`;
800+
message += '\n 📌 Linked Issues (closes when merged):\n';
801+
for (const issue of linkedIssues) {
802+
message += ` • ${formatLinkedIssue(issue, prRepo)}\n`;
803+
}
804+
}
805+
806+
// Other issues created during session (not linked to PR)
807+
if (otherIssues.length > 0) {
808+
message += '\n📝 Other Issues Created:\n';
809+
for (const issue of otherIssues) {
810+
message += ` • ${formatSessionIssue(issue, prRepo)}\n`;
770811
}
771812
}
772813

@@ -809,15 +850,19 @@ function formatPRStatusWithCommit(
809850
* @param ciRun.conclusion - CI run conclusion
810851
* @param ciRun.name - CI run name
811852
* @param groupedPreviews - Preview URLs grouped by provider
812-
* @param linkedIssues - Issue numbers linked from PR body
853+
* @param linkedIssues - Issues linked from PR body (closes when merged)
854+
* @param otherIssues - Other issues created during session but not linked to PR
855+
* @param prRepo - PR's repository name (owner/repo)
813856
* @returns Formatted message
814857
* @example
815858
*/
816859
function formatPRStatusInfo(
817860
prCheck: { prNumber: number; prUrl: string },
818861
ciRun: { url?: string; status?: string; conclusion?: string; name?: string },
819862
groupedPreviews: GroupedPreviewUrls,
820-
linkedIssues: number[]
863+
linkedIssues: LinkedIssueInfo[],
864+
otherIssues: IssueReference[],
865+
prRepo: string
821866
): string {
822867
// Header (simplified - this function only called when CI passed or pending)
823868
let message = '✅ PR Ready for Review\n\n';
@@ -830,11 +875,19 @@ function formatPRStatusInfo(
830875
message += `🔄 [View CI Run](${ciRun.url}) ✅ success\n`;
831876
}
832877

833-
// Linked issues (if any)
878+
// Linked issues (closes when merged) - nested under PR
834879
if (linkedIssues.length > 0) {
835-
message += '\n📌 Linked Issue(s):\n';
836-
for (const issueNum of linkedIssues) {
837-
message += ` • #${issueNum}\n`;
880+
message += '\n 📌 Linked Issues (closes when merged):\n';
881+
for (const issue of linkedIssues) {
882+
message += ` • ${formatLinkedIssue(issue, prRepo)}\n`;
883+
}
884+
}
885+
886+
// Other issues created during session (not linked to PR)
887+
if (otherIssues.length > 0) {
888+
message += '\n📝 Other Issues Created:\n';
889+
for (const issue of otherIssues) {
890+
message += ` • ${formatSessionIssue(issue, prRepo)}\n`;
838891
}
839892
}
840893

@@ -1215,22 +1268,33 @@ ${checksTable}
12151268
checks: ciResult.checks
12161269
});
12171270

1218-
// Fetch PR details, previews, and linked issues in parallel
1219-
const [ciRun, groupedPreviews, linkedIssues] = await Promise.all([
1271+
// Fetch PR details, previews, linked issues, and session issues in parallel
1272+
const [ciRun, groupedPreviews, linkedIssues, sessionIssues] = await Promise.all([
12201273
getCIRunDetails(prCheck.prNumber, repoRoot).then(r => r ?? {}),
12211274
extractAllPreviews(prCheck.prNumber, repoRoot),
1222-
extractLinkedIssuesFromPR(prCheck.prNumber, repoRoot),
1275+
extractLinkedIssuesWithInfo(prCheck.prNumber, repoRoot),
1276+
getSessionIssues(input.session_id, repoRoot),
12231277
]);
12241278

1225-
// Track PR in github.json
1279+
// Extract PR repo from URL for display formatting
1280+
const prRepoMatch = prCheck.prUrl.match(/github\.com\/([^/]+\/[^/]+)\/pull/);
1281+
const prRepo = prRepoMatch ? prRepoMatch[1] : '';
1282+
1283+
// Filter session issues to find "other issues" not linked to the PR
1284+
const linkedIssueKeys = new Set(linkedIssues.map(i => `${i.repo}#${i.number}`));
1285+
const otherIssues = sessionIssues.filter(
1286+
issue => !linkedIssueKeys.has(`${issue.repo}#${issue.number}`)
1287+
);
1288+
1289+
// Track PR in github.json (store just issue numbers for backwards compatibility)
12261290
await addPRToState(
12271291
input.session_id,
12281292
{
12291293
number: prCheck.prNumber,
12301294
url: prCheck.prUrl,
12311295
title: '', // We don't have title here, could fetch if needed
12321296
createdAt: new Date().toISOString(),
1233-
linkedIssues,
1297+
linkedIssues: linkedIssues.map(i => i.number),
12341298
},
12351299
repoRoot
12361300
);
@@ -1252,19 +1316,19 @@ ${checksTable}
12521316
// Show PR status after commit (non-blocking)
12531317
return {
12541318
decision: 'approve',
1255-
systemMessage: formatPRStatusWithCommit(commitSha, { prNumber: prCheck.prNumber, prUrl: prCheck.prUrl }, ciRun, groupedPreviews, linkedIssues),
1319+
systemMessage: formatPRStatusWithCommit(commitSha, { prNumber: prCheck.prNumber, prUrl: prCheck.prUrl }, ciRun, groupedPreviews, linkedIssues, otherIssues, prRepo),
12561320
};
12571321
} else if (commitsAheadOfMain > 0) {
12581322
// Show PR status to user (non-blocking)
12591323
return {
12601324
decision: 'approve',
1261-
systemMessage: formatPRStatusInfo({ prNumber: prCheck.prNumber, prUrl: prCheck.prUrl }, ciRun, groupedPreviews, linkedIssues),
1325+
systemMessage: formatPRStatusInfo({ prNumber: prCheck.prNumber, prUrl: prCheck.prUrl }, ciRun, groupedPreviews, linkedIssues, otherIssues, prRepo),
12621326
};
12631327
} else {
12641328
// Always show PR status when PR exists, even if no new commits this session (non-blocking)
12651329
return {
12661330
decision: 'approve',
1267-
systemMessage: formatPRStatusInfo({ prNumber: prCheck.prNumber, prUrl: prCheck.prUrl }, ciRun, groupedPreviews, linkedIssues),
1331+
systemMessage: formatPRStatusInfo({ prNumber: prCheck.prNumber, prUrl: prCheck.prUrl }, ciRun, groupedPreviews, linkedIssues, otherIssues, prRepo),
12681332
};
12691333
}
12701334
}

0 commit comments

Comments
 (0)