Skip to content

Commit b86a7fd

Browse files
authored
feat(core,github): PollerDeps host-hook parity — six execution-pipeline hooks + config-driven board wiring (#71 PR-C) (#76)
- core: TaskChecker, ExecutionChecker, PreFlightJudger (+Verdict), ExecutionSaver, IssueMetricsRecorder, RateLimitScheduler interfaces + 7 PollerDeps fields (incl. ProjectPath). RateLimitScheduler is a deliberate seam, not a port: the host classifies rate-limit errors and queues retries on its own scheduler, so the SDK never imports host task/rate-limit types. - github poller: wire all six hooks into both dispatch paths mirroring the host-side semantics — pre-flight judge (fail-open, needs-clarification label + comment + declined record on reject, no markProcessed), task-queued gate after retry grace, completed-execution guard (fail-open, marks processed on hit), InvalidateCompletion on retry-ready re-dispatch, rate-limited handler errors stay marked with 'rate_limited' outcome metric. - github adapter: bridge the new deps; construct ProjectBoardSource/Sync from ProjectBoardConfig (new SourceEnabled field) so board mode is config-driven. - sdk/core api.golden re-blessed: additive-only (+39 lines, no deletions).
1 parent 28c9cf1 commit b86a7fd

6 files changed

Lines changed: 870 additions & 2 deletions

File tree

sdk/core/api.golden

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ type ChatDeps struct {
5555
Handler MessageHandler
5656
}
5757

58+
type ExecutionChecker interface {
59+
HasCompletedExecution(taskID, projectPath string) (bool, error)
60+
InvalidateCompletion(taskID, projectPath string) error
61+
}
62+
63+
type ExecutionSaver interface {
64+
SaveDeclinedExecution(taskID, projectPath, status, reason string) error
65+
}
66+
5867
type Identity struct {
5968
UserID string
6069
DisplayName string
@@ -78,6 +87,10 @@ type IssueHandler interface {
7887

7988
type IssueHandlerFunc func(ctx context.Context, ev IssueEvent) (*IssueResult, error)
8089

90+
type IssueMetricsRecorder interface {
91+
RecordIssueProcessed(result string)
92+
}
93+
8194
type IssueResult struct {
8295
Success bool
8396
Skipped bool
@@ -143,6 +156,17 @@ type PollerDeps struct {
143156
MaxConcurrent int
144157
Handler IssueHandler
145158
OnPRCreated func(ev PRCreatedEvent)
159+
TaskChecker TaskChecker
160+
ExecutionChecker ExecutionChecker
161+
ProjectPath string
162+
PreFlightJudge PreFlightJudger
163+
ExecutionSaver ExecutionSaver
164+
IssueMetricsRecorder IssueMetricsRecorder
165+
RateLimitScheduler RateLimitScheduler
166+
}
167+
168+
type PreFlightJudger interface {
169+
JudgeIssue(ctx context.Context, title, body, repoContext string) (Verdict, error)
146170
}
147171

148172
type ProcessedStore interface {
@@ -152,6 +176,21 @@ type ProcessedStore interface {
152176
Load(source, repo string) (map[string]time.Time, error)
153177
}
154178

179+
type RateLimitScheduler interface {
180+
QueueRetryIfRateLimited(taskID, title, body, errText string) bool
181+
}
182+
183+
type TaskChecker interface {
184+
IsTaskQueued(taskID string) bool
185+
}
186+
187+
type Verdict struct {
188+
Accepted bool
189+
Decision string
190+
Reason string
191+
Confidence float64
192+
}
193+
155194
type WebhookCapable interface {
156195
Adapter
157196
WebhookSource() string

sdk/core/registry.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,64 @@ type ActiveExecutionLister interface {
119119
ListActiveTaskIDs(ctx context.Context) ([]string, error)
120120
}
121121

122+
// TaskChecker reports whether a task is currently queued or in-progress in
123+
// the consuming application's execution pipeline. Pollers consult it during
124+
// retry-grace evaluation so an issue whose task is still running is not
125+
// re-dispatched.
126+
type TaskChecker interface {
127+
IsTaskQueued(taskID string) bool
128+
}
129+
130+
// ExecutionChecker verifies whether a completed execution record exists for a
131+
// task, preventing re-dispatch when a tracker-side "done" marker failed to
132+
// apply. InvalidateCompletion deletes a stale completed record so an explicit
133+
// retry is not silently no-op'd.
134+
type ExecutionChecker interface {
135+
HasCompletedExecution(taskID, projectPath string) (bool, error)
136+
InvalidateCompletion(taskID, projectPath string) error
137+
}
138+
139+
// Verdict is the result of a pre-flight judgment on an issue.
140+
type Verdict struct {
141+
Accepted bool
142+
Decision string
143+
Reason string
144+
Confidence float64
145+
}
146+
147+
// PreFlightJudger evaluates issues before dispatch so pollers do not burn
148+
// worker slots on vague, ambiguous, or otherwise unactionable issues.
149+
type PreFlightJudger interface {
150+
JudgeIssue(ctx context.Context, title, body, repoContext string) (Verdict, error)
151+
}
152+
153+
// ExecutionSaver persists pre-flight rejection records for observability.
154+
type ExecutionSaver interface {
155+
SaveDeclinedExecution(taskID, projectPath, status, reason string) error
156+
}
157+
158+
// IssueMetricsRecorder records issue processing outcomes (e.g. "rate_limited").
159+
type IssueMetricsRecorder interface {
160+
RecordIssueProcessed(result string)
161+
}
162+
163+
// RateLimitScheduler lets the consuming application classify a handler error
164+
// as a rate limit and queue the issue for a timed retry on its own scheduler.
165+
// QueueRetryIfRateLimited returns true when the error was recognized as a
166+
// rate limit and a retry was queued — the poller then leaves the issue
167+
// unmarked so the scheduler owns the retry. Returning false hands the error
168+
// back to the poller's standard failure path.
169+
//
170+
// This is a deliberate seam rather than a concrete scheduler dependency:
171+
// error classification and task construction stay host-side, so the SDK never
172+
// imports the application's task or rate-limit types.
173+
type RateLimitScheduler interface {
174+
QueueRetryIfRateLimited(taskID, title, body, errText string) bool
175+
}
176+
122177
// PollerDeps provides shared infrastructure to adapter pollers. Consuming
123-
// applications supply these — the SDK never constructs them itself.
178+
// applications supply these — the SDK never constructs them itself. All
179+
// fields except Handler are optional; nil disables the corresponding hook.
124180
type PollerDeps struct {
125181
// ProcessedStore deduplicates issues across restarts.
126182
ProcessedStore ProcessedStore
@@ -131,6 +187,23 @@ type PollerDeps struct {
131187
Handler IssueHandler
132188
// OnPRCreated fires after an adapter opens a PR/MR for an issue.
133189
OnPRCreated func(ev PRCreatedEvent)
190+
// TaskChecker skips retry re-dispatch while a task is still queued/running.
191+
TaskChecker TaskChecker
192+
// ExecutionChecker prevents re-dispatch when a completed execution record
193+
// exists. Its lookups (and ExecutionSaver records) are scoped by ProjectPath.
194+
ExecutionChecker ExecutionChecker
195+
// ProjectPath identifies the local project checkout used to scope
196+
// ExecutionChecker/ExecutionSaver records.
197+
ProjectPath string
198+
// PreFlightJudge evaluates issue quality before dispatch; rejections are
199+
// surfaced on the tracker and the issue is not dispatched.
200+
PreFlightJudge PreFlightJudger
201+
// ExecutionSaver persists pre-flight rejection records.
202+
ExecutionSaver ExecutionSaver
203+
// IssueMetricsRecorder records issue processing outcomes.
204+
IssueMetricsRecorder IssueMetricsRecorder
205+
// RateLimitScheduler queues timed retries for rate-limited handler errors.
206+
RateLimitScheduler RateLimitScheduler
134207
}
135208

136209
// --- Registry ---

sdk/integrations/github/adapter.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,39 @@ func (a *Adapter) NewPoller(deps core.PollerDeps) core.Poller {
109109
})
110110
}))
111111
}
112+
if deps.TaskChecker != nil {
113+
opts = append(opts, WithTaskChecker(deps.TaskChecker))
114+
}
115+
if deps.ExecutionChecker != nil {
116+
opts = append(opts, WithExecutionChecker(deps.ExecutionChecker, deps.ProjectPath))
117+
}
118+
if deps.PreFlightJudge != nil {
119+
opts = append(opts, WithPreFlightJudge(deps.PreFlightJudge))
120+
}
121+
if deps.ExecutionSaver != nil {
122+
opts = append(opts, WithExecutionSaver(deps.ExecutionSaver))
123+
}
124+
if deps.IssueMetricsRecorder != nil {
125+
opts = append(opts, WithIssueMetricsRecorder(deps.IssueMetricsRecorder))
126+
}
127+
if deps.RateLimitScheduler != nil {
128+
opts = append(opts, WithRateLimitScheduler(deps.RateLimitScheduler))
129+
}
130+
131+
// Board layer is config-driven, not a PollerDeps hook: construct the board
132+
// source/sync from ProjectBoardConfig so hosts get board mode by config alone.
133+
if pb := a.config.ProjectBoard; pb != nil && pb.Enabled {
134+
parts := strings.SplitN(repo, "/", 2)
135+
if len(parts) == 2 {
136+
ownerName, repoName := parts[0], parts[1]
137+
if bs := NewProjectBoardSync(client, pb, ownerName); bs != nil && pb.GetStatuses().InProgress != "" {
138+
opts = append(opts, WithBoardSync(bs, pb.GetStatuses().InProgress))
139+
}
140+
if pb.SourceEnabled {
141+
opts = append(opts, WithProjectBoardSource(NewProjectBoardSource(client, pb, ownerName, repoName)))
142+
}
143+
}
144+
}
112145

113146
// NewPoller can only fail on invalid repo format; DefaultConfig guarantees "unknown/unknown"
114147
// as a fallback so this is always valid after the check above.

0 commit comments

Comments
 (0)