wicket

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2026 License: MIT Imports: 23 Imported by: 0

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

View Source
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

func BeadIDFor(repo string, number int) (string, bool)

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

func FetchRateLimitRemaining(ctx context.Context) (remaining int, resetAt time.Time, err error)

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

type AddLabelCall struct {
	Repo   string
	Number int
	Labels []string
}

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 CloseCall

type CloseCall struct {
	Repo   string
	Number int
}

CloseCall records arguments from a CloseIssue invocation.

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

type CommentCall struct {
	Repo   string
	Number int
	Body   string
}

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) AddLabels

func (m *MockGitHubClient) AddLabels(ctx context.Context, repo string, number int, labels []string) error

func (*MockGitHubClient) CloseIssue

func (m *MockGitHubClient) CloseIssue(ctx context.Context, repo string, number int, reason string) error

func (*MockGitHubClient) CommentOnIssue

func (m *MockGitHubClient) CommentOnIssue(ctx context.Context, repo string, number int, body string) error

func (*MockGitHubClient) GetIssue

func (m *MockGitHubClient) GetIssue(ctx context.Context, repo string, number int) (*Issue, error)

func (*MockGitHubClient) ListComments

func (m *MockGitHubClient) ListComments(ctx context.Context, repo string, number int) ([]Comment, error)

func (*MockGitHubClient) ListIssues

func (m *MockGitHubClient) ListIssues(ctx context.Context, repo string, labels []string) ([]Issue, error)

func (*MockGitHubClient) ListReactions

func (m *MockGitHubClient) ListReactions(ctx context.Context, repo string, number int) ([]Reaction, error)

func (*MockGitHubClient) RemoveLabel

func (m *MockGitHubClient) RemoveLabel(ctx context.Context, repo string, number int, label string) error

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 New

func New(cfg *config.Config, db *state.DB) *Monitor

New creates a Wicket issue triage monitor with the default GitHub CLI client.

func (*Monitor) AnvilForRepo

func (m *Monitor) AnvilForRepo(repo string) (string, bool)

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

func (m *Monitor) HandlePRCreated(ctx context.Context, beadID, prURL string, prNumber int)

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

func (m *Monitor) Run(ctx context.Context) error

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

func (m *Monitor) UpdateConfig(cfg *config.Config)

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

type RemoveCall struct {
	Repo   string
	Number int
	Label  string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL