Documentation
¶
Overview ¶
Package wicket implements the GitHub issue triage monitor. It polls configured repositories for new issues, runs an AI triage pass to decide whether each issue should become a bead, needs clarification, or should be flagged for a human, and then acts on the decision.
Index ¶
- Constants
- func BeadIDFor(repo string, number int) (string, bool)
- func CreateBead(ctx context.Context, db *state.DB, decision TriageDecision, issue Issue, ...) (string, error)
- func FetchRateLimitRemaining(ctx context.Context) (remaining int, resetAt time.Time, err error)
- func RenderAlreadyFixed(data AlreadyFixedData) (string, error)
- func RenderBeadCreated(data BeadCreatedData) (string, error)
- func RenderClarificationNeeded(data ClarificationNeededData) (string, error)
- func RenderDispatchConfirmed(data DispatchConfirmedData) (string, error)
- func RenderDuplicate(data DuplicateData) (string, error)
- func RenderFlaggedForHuman(data FlaggedForHumanData) (string, error)
- func RenderGenericNonTrustedUser(data GenericNonTrustedUserData) (string, error)
- func RenderLabelApplied(data LabelAppliedData) (string, error)
- func RenderOutOfScope(data OutOfScopeData) (string, error)
- func RenderPRCreated(data PRCreatedData) (string, error)
- func RenderPRMerged(data PRMergedData) (string, error)
- func RenderStaleWarning(data StaleWarningData) (string, error)
- type AddLabelCall
- type AlreadyFixedData
- type AnvilWicketConfig
- type BeadCreatedData
- type BeadSummary
- type ClarificationNeededData
- type CloseCall
- type Comment
- type CommentCall
- type DispatchConfirmedData
- type DuplicateData
- type FlaggedForHumanData
- type GenericNonTrustedUserData
- type GitHubClient
- type Issue
- type LabelAppliedData
- type MockGitHubClient
- func (m *MockGitHubClient) AddLabels(ctx context.Context, repo string, number int, labels []string) error
- func (m *MockGitHubClient) CloseIssue(ctx context.Context, repo string, number int, reason string) error
- func (m *MockGitHubClient) CommentOnIssue(ctx context.Context, repo string, number int, body string) error
- func (m *MockGitHubClient) GetIssue(ctx context.Context, repo string, number int) (*Issue, error)
- func (m *MockGitHubClient) ListComments(ctx context.Context, repo string, number int) ([]Comment, error)
- func (m *MockGitHubClient) ListIssues(ctx context.Context, repo string, labels []string) ([]Issue, error)
- func (m *MockGitHubClient) ListReactions(ctx context.Context, repo string, number int) ([]Reaction, error)
- func (m *MockGitHubClient) RemoveLabel(ctx context.Context, repo string, number int, label string) error
- type Monitor
- func (m *Monitor) AnvilForRepo(repo string) (string, bool)
- func (m *Monitor) HandlePRCreated(ctx context.Context, beadID, prURL string, prNumber int)
- func (m *Monitor) HandlePRMerged(ctx context.Context, beadID, prURL, baseBranch string, prNumber int)
- func (m *Monitor) Run(ctx context.Context) error
- func (m *Monitor) Stop()
- func (m *Monitor) TriggerScan()
- func (m *Monitor) UpdateConfig(cfg *config.Config)
- type OutOfScopeData
- type PRCreatedData
- type PRMergedData
- type RateLimitError
- type Reaction
- type RemoveCall
- type RepoResolver
- type StaleWarningData
- type TriageAction
- type TriageConfig
- type TriageDecision
Constants ¶
const ( StateBeadCreated = "bead_created" StateAskClarify = "ask_clarify" StateNeedsHuman = "needs_human" StateRejected = "rejected" StateDispatched = "dispatched" StateStale = "stale" StatePRCreated = "pr_created" StateMerged = "merged" StateClosed = "closed" )
Wicket issue lifecycle state constants.
Variables ¶
This section is empty.
Functions ¶
func BeadIDFor ¶
BeadIDFor returns the bead ID previously recorded for the given issue, and whether one exists.
func CreateBead ¶
func CreateBead(ctx context.Context, db *state.DB, decision TriageDecision, issue Issue, priority int, anvilName, anvilPath string) (string, error)
CreateBead creates a bead from the given triage decision and GitHub issue, stores the issue→bead mapping in both wicket_issues (via db) and an in-memory cache, and returns the new bead ID.
decision.Action must be ActionCreateBead and both BeadTitle and BeadDescription must be non-empty; otherwise CreateBead returns an error immediately so that misuse of the function fails fast.
priority is the bd priority (0–4); values outside that range are rejected.
anvilName is the name of the monitoring anvil that owns this issue; it is embedded in the bead's metadata so downstream code can route the bead back to the correct anvil. Pass an empty string to omit the metadata field.
db may be nil, in which case persistence to state.db is skipped and only the in-memory cache is updated.
func FetchRateLimitRemaining ¶
FetchRateLimitRemaining queries the GitHub rate_limit API and returns the current remaining core API requests and the reset time. Returns -1 and a zero time when the call fails or the response cannot be parsed.
func RenderAlreadyFixed ¶
func RenderAlreadyFixed(data AlreadyFixedData) (string, error)
RenderAlreadyFixed renders the AlreadyFixed comment template.
func RenderBeadCreated ¶
func RenderBeadCreated(data BeadCreatedData) (string, error)
RenderBeadCreated renders the BeadCreated comment template with the given data.
func RenderClarificationNeeded ¶
func RenderClarificationNeeded(data ClarificationNeededData) (string, error)
RenderClarificationNeeded renders the ClarificationNeeded comment template.
func RenderDispatchConfirmed ¶
func RenderDispatchConfirmed(data DispatchConfirmedData) (string, error)
RenderDispatchConfirmed renders the DispatchConfirmed comment template.
func RenderDuplicate ¶
func RenderDuplicate(data DuplicateData) (string, error)
RenderDuplicate renders the Duplicate comment template.
func RenderFlaggedForHuman ¶
func RenderFlaggedForHuman(data FlaggedForHumanData) (string, error)
RenderFlaggedForHuman renders the FlaggedForHuman comment template.
func RenderGenericNonTrustedUser ¶
func RenderGenericNonTrustedUser(data GenericNonTrustedUserData) (string, error)
RenderGenericNonTrustedUser renders the generic response template posted to issues from non-trusted contributors.
func RenderLabelApplied ¶
func RenderLabelApplied(data LabelAppliedData) (string, error)
RenderLabelApplied renders the LabelApplied comment template.
func RenderOutOfScope ¶
func RenderOutOfScope(data OutOfScopeData) (string, error)
RenderOutOfScope renders the OutOfScope comment template. The reason is normalized to a single line so the Markdown blockquote renders correctly regardless of whether the AI returns multi-line text.
func RenderPRCreated ¶
func RenderPRCreated(data PRCreatedData) (string, error)
RenderPRCreated renders the PRCreated follow-up comment template.
func RenderPRMerged ¶
func RenderPRMerged(data PRMergedData) (string, error)
RenderPRMerged renders the PRMerged auto-close comment template.
func RenderStaleWarning ¶
func RenderStaleWarning(data StaleWarningData) (string, error)
RenderStaleWarning renders the stale warning comment template.
Types ¶
type AddLabelCall ¶
AddLabelCall records arguments from an AddLabels invocation.
type AlreadyFixedData ¶
type AlreadyFixedData struct {
// ReferencePR is the PR URL or bead ID that resolved the issue.
ReferencePR string
}
AlreadyFixedData holds the template data for the AlreadyFixed comment.
type AnvilWicketConfig ¶
type AnvilWicketConfig struct {
// Enabled controls whether Wicket scans this anvil. When nil, the
// global WicketEnabled setting is used. Set to false to opt out.
Enabled *bool `yaml:"wicket_enabled,omitempty" mapstructure:"wicket_enabled"`
// TrustedUsers is the list of GitHub logins whose issues are
// auto-dispatched without extra review. Issues from other authors
// follow the normal triage flow.
TrustedUsers []string `yaml:"wicket_trusted_users,omitempty" mapstructure:"wicket_trusted_users"`
// AutoDispatch, when true, automatically dispatches triaged beads
// without waiting for a human to approve the queue entry.
AutoDispatch bool `yaml:"wicket_auto_dispatch,omitempty" mapstructure:"wicket_auto_dispatch"`
// IssueLabels is the list of GitHub label names that must be present
// on an issue for Wicket to consider it. An empty list means all
// unlabelled/labelled issues are eligible (subject to WicketTriggerLabel).
IssueLabels []string `yaml:"wicket_issue_labels,omitempty" mapstructure:"wicket_issue_labels"`
// Repos is the list of "owner/repo" strings to scan. When empty, the
// anvil's primary repository (derived from its git remote) is used.
Repos []string `yaml:"wicket_repos,omitempty" mapstructure:"wicket_repos"`
// TriagePrompt is an optional freeform prompt suffix appended to the
// default Wicket triage system prompt. Use it to add project-specific
// context or constraints (e.g. "This is a public API — be conservative
// about accepting feature requests from external contributors.").
TriagePrompt string `yaml:"wicket_triage_prompt,omitempty" mapstructure:"wicket_triage_prompt"`
}
AnvilWicketConfig holds the per-anvil Wicket configuration. Fields use the same yaml/mapstructure tags as config.AnvilConfig so that config loading can populate them automatically via mapstructure. Consumers in the wicket package should read from this struct rather than calling config.AnvilConfig directly, to keep the dependency surface narrow.
type BeadCreatedData ¶
type BeadCreatedData struct {
// BeadID is the newly created bead identifier (e.g. "Forge-abc1").
BeadID string
// Reason is the triage AI's explanation for creating the bead.
Reason string
}
BeadCreatedData holds the template data for the BeadCreated comment.
type BeadSummary ¶
type BeadSummary struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
}
BeadSummary is a compact representation of a bead used to provide context to the triage AI for duplicate and already-fixed detection.
type ClarificationNeededData ¶
type ClarificationNeededData struct {
// Reason is the triage AI's explanation for requesting clarification.
Reason string
}
ClarificationNeededData holds the template data for the ClarificationNeeded comment.
type Comment ¶
type Comment struct {
// ID is the platform-assigned comment identifier.
ID int64
// Author is the GitHub login of the comment author.
Author string
// Body is the comment text.
Body string
// CreatedAt is when the comment was posted.
CreatedAt time.Time
}
Comment represents a single comment on a GitHub issue.
type CommentCall ¶
CommentCall records arguments from a CommentOnIssue invocation.
type DispatchConfirmedData ¶
type DispatchConfirmedData struct {
// BeadID is the bead being dispatched.
BeadID string
}
DispatchConfirmedData holds the template data for the DispatchConfirmed comment.
type DuplicateData ¶
type DuplicateData struct {
// DuplicateID is the ID of the existing bead that already covers this issue.
DuplicateID string
}
DuplicateData holds the template data for the Duplicate comment.
type FlaggedForHumanData ¶
type FlaggedForHumanData struct {
// Reason is the triage AI's explanation for escalating to a human.
Reason string
}
FlaggedForHumanData holds the template data for the FlaggedForHuman comment.
type GenericNonTrustedUserData ¶
type GenericNonTrustedUserData struct {
// Author is the GitHub login of the issue author.
Author string
}
GenericNonTrustedUserData holds the template data for the generic response posted to issues from non-trusted contributors.
type GitHubClient ¶
type GitHubClient interface {
// ListIssues returns open issues for the given repository. If labels is
// non-empty, only issues that carry all of the listed label names are
// returned.
ListIssues(ctx context.Context, repo string, labels []string) ([]Issue, error)
// GetIssue returns a single issue by number from the given repository.
GetIssue(ctx context.Context, repo string, number int) (*Issue, error)
// ListComments returns all comments on the specified issue, ordered by
// creation time ascending.
ListComments(ctx context.Context, repo string, number int) ([]Comment, error)
// ListReactions returns all emoji reactions on the specified issue.
ListReactions(ctx context.Context, repo string, number int) ([]Reaction, error)
// CommentOnIssue posts body as a new comment on the specified issue.
CommentOnIssue(ctx context.Context, repo string, number int, body string) error
// AddLabels attaches the given label names to the specified issue.
AddLabels(ctx context.Context, repo string, number int, labels []string) error
// RemoveLabel removes a single label from the specified issue.
RemoveLabel(ctx context.Context, repo string, number int, label string) error
// CloseIssue closes the specified issue. reason is passed as --reason to gh
// (e.g. "completed", "not planned"); an empty string omits the flag.
CloseIssue(ctx context.Context, repo string, number int, reason string) error
}
GitHubClient defines the GitHub operations used by the wicket package. All methods operate on a specific repository identified by "owner/repo".
func NewGitHubClient ¶
func NewGitHubClient() GitHubClient
NewGitHubClient returns a GitHubClient backed by the gh CLI.
type Issue ¶
type Issue struct {
// Number is the issue number within the repository.
Number int
// Repo is the full repository name in "owner/repo" format.
Repo string
// Title is the issue title.
Title string
// Body is the issue body (description).
Body string
// Author is the GitHub login of the issue author.
Author string
// Labels is the list of label names attached to the issue.
Labels []string
// CreatedAt is when the issue was opened.
CreatedAt time.Time
}
Issue represents a GitHub issue retrieved during a Wicket scan.
type LabelAppliedData ¶
type LabelAppliedData struct {
// Tag is the label/tag that was applied.
Tag string
// BeadID is the associated bead identifier.
BeadID string
}
LabelAppliedData holds the template data for the LabelApplied comment.
type MockGitHubClient ¶
type MockGitHubClient struct {
// OnListIssues is called by ListIssues if non-nil.
OnListIssues func(ctx context.Context, repo string, labels []string) ([]Issue, error)
// OnGetIssue is called by GetIssue if non-nil.
OnGetIssue func(ctx context.Context, repo string, number int) (*Issue, error)
// OnListComments is called by ListComments if non-nil.
OnListComments func(ctx context.Context, repo string, number int) ([]Comment, error)
// OnListReactions is called by ListReactions if non-nil.
OnListReactions func(ctx context.Context, repo string, number int) ([]Reaction, error)
// OnCommentOnIssue is called by CommentOnIssue if non-nil.
OnCommentOnIssue func(ctx context.Context, repo string, number int, body string) error
// OnAddLabels is called by AddLabels if non-nil.
OnAddLabels func(ctx context.Context, repo string, number int, labels []string) error
// OnRemoveLabel is called by RemoveLabel if non-nil.
OnRemoveLabel func(ctx context.Context, repo string, number int, label string) error
// OnCloseIssue is called by CloseIssue if non-nil.
OnCloseIssue func(ctx context.Context, repo string, number int, reason string) error
// Recorded calls — populated regardless of whether an On* func is set.
CommentCalls []CommentCall
AddLabelCalls []AddLabelCall
RemoveCalls []RemoveCall
CloseCalls []CloseCall
}
MockGitHubClient is a test double for GitHubClient. Tests set the On* fields to inject canned responses or capture calls.
func (*MockGitHubClient) CloseIssue ¶
func (*MockGitHubClient) CommentOnIssue ¶
func (*MockGitHubClient) ListComments ¶
func (*MockGitHubClient) ListIssues ¶
func (*MockGitHubClient) ListReactions ¶
func (*MockGitHubClient) RemoveLabel ¶
type Monitor ¶
type Monitor struct {
// contains filtered or unexported fields
}
Monitor periodically polls GitHub repositories for new issues, triages them using an AI provider, and dispatches the appropriate action: create a bead, ask for clarification, or flag for human review.
func (*Monitor) AnvilForRepo ¶
AnvilForRepo returns the anvil name that owns the given "owner/repo" string, and whether a mapping exists. The mapping is populated during scanAnvil and is consumed by downstream workers (Wicket phases 5b and 5c) that need to route GitHub events back to the correct anvil without re-resolving the git remote on every call.
func (*Monitor) HandlePRCreated ¶
HandlePRCreated is called by the daemon when a PR is created for a bead that may have been sourced from a Wicket GitHub issue. It looks up the bead ID in wicket_issues, posts a follow-up comment on the linked GitHub issue, and updates the wicket_issues state to "pr_created".
This is a no-op when the bead ID is not found in wicket_issues.
func (*Monitor) HandlePRMerged ¶
func (m *Monitor) HandlePRMerged(ctx context.Context, beadID, prURL, baseBranch string, prNumber int)
HandlePRMerged is called by the daemon when a PR is merged for a bead that may have been sourced from a Wicket GitHub issue. It posts a closure comment on the linked GitHub issue, closes the issue, and updates wicket_issues state to "merged".
This is a no-op when the bead ID is not found in wicket_issues.
func (*Monitor) Run ¶
Run starts the periodic issue triage loop. Blocks until ctx is canceled. The poll interval is dynamically adjusted: when quota is low (<100 remaining) it is doubled; when a rate-limit backoff is active the loop waits until the backoff expires before scanning again.
func (*Monitor) Stop ¶
func (m *Monitor) Stop()
Stop is a convenience no-op. Cancelling the context passed to Run is the primary shutdown mechanism; this method exists for API symmetry with other monitors.
func (*Monitor) TriggerScan ¶ added in v0.12.0
func (m *Monitor) TriggerScan()
TriggerScan requests an immediate scan outside the normal interval. If a trigger is already pending (not yet consumed by the Run loop) this is a no-op, preventing duplicate concurrent scans.
func (*Monitor) UpdateConfig ¶
UpdateConfig replaces the monitor configuration. Safe to call while Run is active; the new configuration takes effect on the next scan cycle.
type OutOfScopeData ¶
type OutOfScopeData struct {
// Reason is the AI's explanation for why this issue is out of scope.
Reason string
}
OutOfScopeData holds the template data for the OutOfScope comment.
type PRCreatedData ¶
type PRCreatedData struct {
// PRURL is the URL of the created pull request.
PRURL string
// BeadID is the associated bead identifier.
BeadID string
}
PRCreatedData holds the template data for the PRCreated follow-up comment.
type PRMergedData ¶
type PRMergedData struct {
// PRURL is the URL of the merged pull request.
PRURL string
// BaseBranch is the branch the PR was merged into.
BaseBranch string
}
PRMergedData holds the template data for the PRMerged auto-close comment.
type RateLimitError ¶
type RateLimitError struct {
// Message is the raw error text from the gh CLI stderr.
Message string
// Remaining is the X-RateLimit-Remaining value if available, -1 if unknown.
Remaining int
// ResetAt is when the rate-limit window resets, zero if unknown.
ResetAt time.Time
}
RateLimitError is returned by GitHub client calls when the GitHub API rate limit has been exceeded (HTTP 403 or a secondary rate-limit signal).
func (*RateLimitError) Error ¶
func (e *RateLimitError) Error() string
type Reaction ¶
type Reaction struct {
// Content is the reaction emoji identifier (e.g. "rocket", "+1", "heart").
Content string
// User is the GitHub login of the user who reacted.
User string
}
Reaction represents a single emoji reaction by a user on a GitHub issue.
type RemoveCall ¶
RemoveCall records arguments from a RemoveLabel invocation.
type RepoResolver ¶
type RepoResolver struct {
// contains filtered or unexported fields
}
RepoResolver resolves the list of GitHub repositories ("owner/repo") for a given anvil configuration. When WicketRepos is explicitly set it is returned as-is; otherwise the repository is derived from the anvil's git origin remote.
func NewRepoResolver ¶
func NewRepoResolver() *RepoResolver
NewRepoResolver returns a RepoResolver that uses the real git CLI.
func (*RepoResolver) ResolveRepos ¶
func (r *RepoResolver) ResolveRepos(ctx context.Context, anvil config.AnvilConfig) ([]string, error)
ResolveRepos returns the list of "owner/repo" strings for the given anvil.
When anvil.WicketRepos is non-empty, those values are returned directly and no git subprocess is spawned. Otherwise the repository is inferred from the origin remote URL of anvil.Path.
type StaleWarningData ¶
type StaleWarningData struct{}
StaleWarningData holds the template data for the stale warning comment.
type TriageAction ¶
type TriageAction string
TriageAction is the outcome of a triage decision for a single issue.
const ( // ActionCreateBead instructs Wicket to create a bead from the issue. ActionCreateBead TriageAction = "create_bead" // ActionAskClarify instructs Wicket to post a clarification comment on // the issue asking the author for more detail before proceeding. ActionAskClarify TriageAction = "ask_clarify" // ActionFlagHuman instructs Wicket to label the issue for human review // and skip automated processing. ActionFlagHuman TriageAction = "flag_human" // ActionReject instructs Wicket to silently discard the issue without // posting any public comment. Used for obvious spam or off-topic issues. ActionReject TriageAction = "reject" // ActionDuplicate instructs Wicket to post a comment referencing the // existing open bead that already covers this issue. DuplicateID in the // TriageDecision carries the matching bead identifier. ActionDuplicate TriageAction = "duplicate" // ActionAlreadyFixed instructs Wicket to post a comment stating the issue // was resolved in a previous PR or bead. ReferencePR in the TriageDecision // carries the relevant PR URL or bead ID. ActionAlreadyFixed TriageAction = "already_fixed" // ActionOutOfScope instructs Wicket to post a rejection comment explaining // that the issue is outside the project's scope. ActionOutOfScope TriageAction = "out_of_scope" )
type TriageConfig ¶
type TriageConfig struct {
// Providers is the ordered list of AI providers to try.
// Defaults to provider.Defaults() when empty.
Providers []provider.Provider
// ExtraPrompt is appended to the default triage prompt to provide
// project-specific context or constraints.
ExtraPrompt string
// AnvilPath is the local filesystem path of the anvil repository. It is
// passed as cmd.Dir to `bd list` so the correct .beads/ config is used.
// When empty, the daemon's working directory is used (may query the wrong DB).
AnvilPath string
// AnvilRepo is the primary GitHub repository ("owner/repo") derived from
// the anvil's own git remote. When set, RunTriage compares this against
// issue.Repo to detect issues from external (monitored) repos and
// prepends the anvil's README.md and AGENTS.md to the triage prompt so
// the AI can contextualise the foreign issue against the anvil's domain.
AnvilRepo string
// AllAnvilPaths is the list of ALL configured anvil paths (including the
// current anvil). When non-empty, RunTriage performs a cross-anvil Source
// URL lookup before calling the AI so that issues already tracked in a
// different anvil are detected as duplicates immediately.
AllAnvilPaths []string
// MonitoredAnvilPaths is the list of local filesystem paths for all anvils
// that correspond to repos monitored by the current anvil (via the 5a
// wicket_repos mapping). When non-empty, open and closed beads are
// aggregated from all these paths and included in the triage prompt,
// giving the AI context across all related repositories. When empty, only
// AnvilPath is used for bead context.
MonitoredAnvilPaths []string
// contains filtered or unexported fields
}
TriageConfig holds configuration for a RunTriage call.
type TriageDecision ¶
type TriageDecision struct {
// Action is the chosen triage outcome.
Action TriageAction
// Reason is a human-readable explanation of why this action was chosen.
Reason string
// BeadTitle is the proposed bead title; only populated when Action is
// ActionCreateBead.
BeadTitle string
// BeadDescription is the proposed bead description; only populated when
// Action is ActionCreateBead.
BeadDescription string
// DuplicateID is the existing bead ID that this issue duplicates; only
// populated when Action is ActionDuplicate.
DuplicateID string
// ReferencePR is the PR URL or bead ID that already resolved this issue;
// only populated when Action is ActionAlreadyFixed.
ReferencePR string
}
TriageDecision is the structured output from the AI triage step.
func RunTriage ¶
func RunTriage(ctx context.Context, issue Issue, cfg TriageConfig) TriageDecision
RunTriage calls the AI provider with a triage prompt for the given issue and returns a TriageDecision. It retries once if the first response cannot be parsed (parse failure only), and defaults to ActionFlagHuman on persistent failure. Runner errors (provider failures) cause an immediate fallback without retrying to avoid doubling cost on provider outages.
func RunTriageWithComments ¶
func RunTriageWithComments(ctx context.Context, issue Issue, comments []Comment, cfg TriageConfig) TriageDecision
RunTriageWithComments is like RunTriage but includes the issue's comment history in the prompt. Used for clarification re-triage when the author has replied with additional details.