Skip to content

Commit 11c35bd

Browse files
holonbot[bot]Holon Botjolestar
authored
Issue #342 Implementation Summary (#344)
* Issue #342 Implementation Summary * Apply changes from Holon --------- Co-authored-by: Holon Bot <bot@holon.run> Co-authored-by: jolestar <jolestar@gmail.com>
1 parent e8e0030 commit 11c35bd

File tree

6 files changed

+289
-26
lines changed

6 files changed

+289
-26
lines changed

.github/workflows/holon-solve.yml

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -169,43 +169,45 @@ jobs:
169169
170170
SHOULD_RUN='false'
171171
REASON='Not eligible'
172+
GOAL_HINT=''
172173
173174
# Avoid bot loops
174175
if [ "$ACTOR" = 'holonbot[bot]' ]; then
175176
REASON='Actor is holonbot[bot]'
176177
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
177178
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
179+
echo "goal_hint=$GOAL_HINT" >> "$GITHUB_OUTPUT"
178180
exit 0
179181
fi
180182
181183
if [ "$EVENT_NAME" = 'issue_comment' ] && [ "$EVENT_ACTION" = 'created' ]; then
182184
FIRST_LINE="$(printf '%s\n' "$COMMENT_BODY" | head -n1 | xargs || true)"
183185
184-
if [ "$IS_PR" = 'true' ]; then
185-
if [ "$FIRST_LINE" = '@holonbot fix' ] || [ "$FIRST_LINE" = '@holonbot solve' ]; then
186-
:
187-
else
188-
REASON="Comment command not recognized for PR: '$FIRST_LINE'"
189-
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
190-
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
191-
exit 0
192-
fi
186+
# Check if the comment starts with "@holonbot " (with trailing space for free-form triggers)
187+
# This pattern matches "@holonbot " followed by any text, including "fix" and "solve"
188+
# The xargs command trims trailing whitespace, so multiple spaces are normalized to one
189+
if [[ "$FIRST_LINE" == "@holonbot "* ]]; then
190+
# Extract trailing text after "@holonbot " (with space)
191+
# If comment is exactly "@holonbot ", GOAL_HINT will be empty (acceptable)
192+
GOAL_HINT="${FIRST_LINE#@holonbot }"
193+
echo "✅ Trigger path: free-form command"
194+
echo " Goal hint: $GOAL_HINT"
195+
else
196+
REASON="Comment command not recognized: '$FIRST_LINE'"
197+
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
198+
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
199+
echo "goal_hint=$GOAL_HINT" >> "$GITHUB_OUTPUT"
200+
exit 0
201+
fi
193202
203+
if [ "$IS_PR" = 'true' ]; then
194204
# Disallow fork PRs
195205
IS_CROSS_REPO="$(gh api "repos/$REPO/pulls/$ISSUE_NUMBER" --jq '.head.repo.full_name != .base.repo.full_name' 2>/dev/null || echo 'true')"
196206
if [ "$IS_CROSS_REPO" = 'true' ]; then
197207
REASON='PR is from a fork/cross-repo head; not supported'
198208
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
199209
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
200-
exit 0
201-
fi
202-
else
203-
if [ "$FIRST_LINE" = '@holonbot fix' ] || [ "$FIRST_LINE" = '@holonbot solve' ]; then
204-
:
205-
else
206-
REASON="Comment command not recognized for Issue: '$FIRST_LINE'"
207-
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
208-
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
210+
echo "goal_hint=$GOAL_HINT" >> "$GITHUB_OUTPUT"
209211
exit 0
210212
fi
211213
fi
@@ -216,6 +218,7 @@ jobs:
216218
REASON="Insufficient permissions for $ACTOR: ${PERMISSION:-none}"
217219
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
218220
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
221+
echo "goal_hint=$GOAL_HINT" >> "$GITHUB_OUTPUT"
219222
exit 0
220223
fi
221224
@@ -245,6 +248,7 @@ jobs:
245248
246249
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
247250
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
251+
echo "goal_hint=$GOAL_HINT" >> "$GITHUB_OUTPUT"
248252
env:
249253
GH_TOKEN: ${{ secrets.holon_github_token || github.token }}
250254
EVENT_NAME: ${{ github.event_name }}
@@ -381,6 +385,14 @@ jobs:
381385
IS_PR='${{ steps.ctx.outputs.is_pr }}'
382386
SHOULD_RUN='${{ steps.gate.outputs.should_run }}'
383387
REASON='${{ steps.gate.outputs.reason }}'
388+
GOAL_HINT='${{ steps.gate.outputs.goal_hint }}'
389+
390+
# Get comment_id from input (if available)
391+
# Default to 0 if empty to ensure valid JSON for --argjson
392+
COMMENT_ID='${{ inputs.comment_id }}'
393+
if [ -z "$COMMENT_ID" ] || [ "$COMMENT_ID" = '' ]; then
394+
COMMENT_ID='0'
395+
fi
384396
385397
MODE=''
386398
if [ "$SHOULD_RUN" = 'true' ]; then
@@ -397,11 +409,14 @@ jobs:
397409
--arg should_run "$SHOULD_RUN" \
398410
--arg reason "$REASON" \
399411
--arg mode "$MODE" \
412+
--arg goal_hint "$GOAL_HINT" \
413+
--argjson comment_id "$COMMENT_ID" \
400414
'{
401415
event: {name: $event_name, action: $event_action, actor: $actor},
402-
target: {repository: $repository, issue_number: $issue_number, is_pr: $is_pr},
403-
gate: {should_run: $should_run, reason: $reason},
404-
mode: $mode
416+
target: {repository: $repository, issue_number: ($issue_number | tonumber), is_pr: ($is_pr == "true")},
417+
gate: {should_run: ($should_run == "true"), reason: $reason},
418+
mode: $mode,
419+
trigger: {goal_hint: $goal_hint, comment_id: $comment_id}
405420
}' > "$INPUT_DIR/workflow.json"
406421
407422
- name: Run Holon

cmd/holon/solve.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,31 @@ func runSolve(ctx context.Context, refStr, explicitType string) error {
490490
return fmt.Errorf("failed to create context directory: %w", err)
491491
}
492492

493+
// Read workflow.json for trigger metadata (if exists)
494+
// This is populated by the holon-solve workflow when triggered by comments
495+
// We read it once and pass the data to buildGoal to avoid duplicate reads
496+
type workflowMetadata struct {
497+
TriggerCommentID int64
498+
TriggerGoalHint string
499+
}
500+
var workflowMeta workflowMetadata
501+
workflowPath := filepath.Join(inputDir, "workflow.json")
502+
if workflowData, err := os.ReadFile(workflowPath); err == nil {
503+
var workflow struct {
504+
Trigger struct {
505+
CommentID int64 `json:"comment_id"`
506+
GoalHint string `json:"goal_hint"`
507+
} `json:"trigger"`
508+
}
509+
if err := json.Unmarshal(workflowData, &workflow); err == nil {
510+
workflowMeta.TriggerCommentID = workflow.Trigger.CommentID
511+
workflowMeta.TriggerGoalHint = workflow.Trigger.GoalHint
512+
if workflowMeta.TriggerCommentID > 0 {
513+
fmt.Printf("Found trigger comment ID: %d\n", workflowMeta.TriggerCommentID)
514+
}
515+
}
516+
}
517+
493518
// Collect context based on type
494519
prov := github.NewProvider()
495520
if refType == "pr" {
@@ -505,6 +530,7 @@ func runSolve(ctx context.Context, refStr, explicitType string) error {
505530
IncludeChecks: true,
506531
ChecksOnlyFailed: false,
507532
ChecksMax: 200,
533+
TriggerCommentID: workflowMeta.TriggerCommentID,
508534
},
509535
}
510536
if _, err := prov.Collect(ctx, req); err != nil {
@@ -521,7 +547,8 @@ func runSolve(ctx context.Context, refStr, explicitType string) error {
521547
Ref: fmt.Sprintf("%s/%s#%d", solveRef.Owner, solveRef.Repo, solveRef.Number),
522548
OutputDir: contextDir,
523549
Options: collector.Options{
524-
Token: token,
550+
Token: token,
551+
TriggerCommentID: workflowMeta.TriggerCommentID,
525552
},
526553
}
527554
if _, err := prov.Collect(ctx, req); err != nil {
@@ -564,7 +591,7 @@ func runSolve(ctx context.Context, refStr, explicitType string) error {
564591
}
565592

566593
// Determine goal from the reference
567-
goal := buildGoal(solveRef, refType)
594+
goal := buildGoal(inputDir, solveRef, refType, workflowMeta.TriggerGoalHint)
568595

569596
// Resolve output directory with precedence: CLI flag > temp dir
570597
// For solve command, we use a temp directory by default to avoid polluting the workspace
@@ -729,11 +756,21 @@ func getGitHubToken() (string, error) {
729756
}
730757

731758
// buildGoal builds a goal description from the reference
732-
func buildGoal(ref *pkggithub.SolveRef, refType string) string {
759+
// triggerGoalHint is the optional goal hint from free-form triggers (e.g., "@holonbot fix this bug")
760+
func buildGoal(inputDir string, ref *pkggithub.SolveRef, refType string, triggerGoalHint string) string {
761+
baseGoal := ""
733762
if refType == "pr" {
734-
return fmt.Sprintf("Fix the review comments and issues in PR %s. Address all unresolved review comments and make necessary code changes.", ref.URL())
763+
baseGoal = fmt.Sprintf("Fix the review comments and issues in PR %s. Address all unresolved review comments and make necessary code changes.", ref.URL())
764+
} else {
765+
baseGoal = fmt.Sprintf("Implement a solution for the issue described in %s. Make the necessary code changes to resolve the issue. Focus on implementing the solution; the system will handle committing changes and creating any pull requests.", ref.URL())
766+
}
767+
768+
// Append goal hint from free-form triggers if provided
769+
if triggerGoalHint != "" {
770+
return fmt.Sprintf("%s\n\nUser intent: %s", baseGoal, triggerGoalHint)
735771
}
736-
return fmt.Sprintf("Implement a solution for the issue described in %s. Make the necessary code changes to resolve the issue. Focus on implementing the solution; the system will handle committing changes and creating any pull requests.", ref.URL())
772+
773+
return baseGoal
737774
}
738775

739776
// publishResults publishes the holon execution results

pkg/context/collector/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ type Options struct {
5555
// ChecksMax is the maximum number of check runs to fetch (for PRs)
5656
// 0 or negative means no limit
5757
ChecksMax int
58+
59+
// TriggerCommentID is the ID of the comment that triggered holon (for marking is_trigger)
60+
TriggerCommentID int64
5861
}
5962

6063
// FileInfo represents metadata about a collected file

pkg/context/provider/github/collector.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,32 @@ func (p *Provider) collectPR(ctx context.Context, owner, repo string, number int
143143
if err != nil {
144144
return nil, fmt.Errorf("failed to fetch review threads: %w", err)
145145
}
146+
147+
// Mark trigger comment if provided
148+
if req.Options.TriggerCommentID > 0 {
149+
found := false
150+
for i := range reviewThreads {
151+
if reviewThreads[i].CommentID == req.Options.TriggerCommentID {
152+
reviewThreads[i].IsTrigger = true
153+
fmt.Printf(" Marked review thread comment #%d as trigger\n", req.Options.TriggerCommentID)
154+
found = true
155+
break
156+
}
157+
// Also check replies
158+
for j := range reviewThreads[i].Replies {
159+
if reviewThreads[i].Replies[j].CommentID == req.Options.TriggerCommentID {
160+
reviewThreads[i].Replies[j].IsTrigger = true
161+
fmt.Printf(" Marked review reply #%d as trigger\n", req.Options.TriggerCommentID)
162+
found = true
163+
break
164+
}
165+
}
166+
if found {
167+
break
168+
}
169+
}
170+
}
171+
146172
fmt.Printf(" Found %d review threads\n", len(reviewThreads))
147173

148174
// Fetch diff if requested
@@ -221,6 +247,18 @@ func (p *Provider) collectIssue(ctx context.Context, owner, repo string, number
221247
if err != nil {
222248
return nil, fmt.Errorf("failed to fetch comments: %w", err)
223249
}
250+
251+
// Mark trigger comment if provided
252+
if req.Options.TriggerCommentID > 0 {
253+
for i := range comments {
254+
if comments[i].CommentID == req.Options.TriggerCommentID {
255+
comments[i].IsTrigger = true
256+
fmt.Printf(" Marked comment #%d as trigger\n", req.Options.TriggerCommentID)
257+
break
258+
}
259+
}
260+
}
261+
224262
fmt.Printf(" Found %d comments\n", len(comments))
225263

226264
// Write context files

0 commit comments

Comments
 (0)