@exaudeus/workrail 3.32.0 → 3.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-await.js +11 -9
- package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
- package/dist/cli/commands/worktrain-daemon-install.js +291 -0
- package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
- package/dist/cli/commands/worktrain-daemon.js +272 -0
- package/dist/cli/commands/worktrain-spawn.js +11 -9
- package/dist/cli-worktrain.js +329 -0
- package/dist/cli.js +1 -22
- package/dist/console/standalone-console.d.ts +28 -0
- package/dist/console/standalone-console.js +142 -0
- package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-BuJFLLfY.js} +1 -1
- package/dist/{console → console-ui}/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +26 -0
- package/dist/daemon/agent-loop.js +39 -1
- package/dist/daemon/daemon-events.d.ts +47 -1
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +205 -41
- package/dist/infrastructure/session/HttpServer.js +133 -34
- package/dist/manifest.json +118 -62
- package/dist/mcp/output-schemas.d.ts +30 -30
- package/dist/mcp/transports/bridge-events.d.ts +4 -0
- package/dist/mcp/transports/fatal-exit.js +4 -0
- package/dist/mcp/transports/http-entry.js +2 -0
- package/dist/mcp/transports/stdio-entry.js +26 -6
- package/dist/mcp/v2/tools.d.ts +4 -4
- package/dist/trigger/adapters/github-poller.d.ts +44 -0
- package/dist/trigger/adapters/github-poller.js +190 -0
- package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
- package/dist/trigger/adapters/gitlab-poller.js +81 -0
- package/dist/trigger/index.d.ts +4 -1
- package/dist/trigger/index.js +5 -1
- package/dist/trigger/polled-event-store.d.ts +22 -0
- package/dist/trigger/polled-event-store.js +173 -0
- package/dist/trigger/polling-scheduler.d.ts +20 -0
- package/dist/trigger/polling-scheduler.js +249 -0
- package/dist/trigger/trigger-listener.d.ts +3 -0
- package/dist/trigger/trigger-listener.js +47 -3
- package/dist/trigger/trigger-store.js +114 -33
- package/dist/trigger/types.d.ts +17 -1
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
- package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
- package/dist/v2/usecases/console-routes.js +3 -3
- package/dist/v2/usecases/console-service.js +133 -9
- package/dist/v2/usecases/console-types.d.ts +7 -0
- package/docs/design/daemon-conversation-logging-plan.md +98 -0
- package/docs/design/daemon-conversation-logging-review.md +55 -0
- package/docs/design/daemon-conversation-logging.md +129 -0
- package/docs/design/github-polling-adapter-design-candidates.md +226 -0
- package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
- package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
- package/docs/design/implementation_plan.md +192 -0
- package/docs/design/workflow-id-validation-at-startup.md +146 -0
- package/docs/design/workflow-id-validation-design-review.md +87 -0
- package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
- package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
- package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
- package/docs/ideas/backlog.md +361 -0
- package/package.json +1 -1
- package/workflows/architecture-scalability-audit.json +1 -1
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/coding-task-workflow-agentic.json +32 -32
- package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
- package/workflows/coding-task-workflow-agentic.v2.json +7 -7
- package/workflows/mr-review-workflow.agentic.v2.json +21 -12
- package/workflows/personal-learning-materials-creation-branched.json +2 -2
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +2 -2
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-for-workflows.json +3 -3
- package/workflows/workflow-for-workflows.v2.json +2 -2
- package/workflows/wr.discovery.json +1 -1
- /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# GitHub Polling Adapter: Design Candidates
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-15
|
|
4
|
+
**Type:** Design Candidates
|
|
5
|
+
**Status:** Draft -- for review
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem Understanding
|
|
10
|
+
|
|
11
|
+
### The Ask
|
|
12
|
+
|
|
13
|
+
Add GitHub Issues and GitHub PRs polling adapters that mirror the GitLab adapter pattern. Poll GitHub APIs on a configurable schedule, deduplicate via `PolledEventStore`, dispatch workflows via `TriggerRouter.dispatch()`. Must include `excludeAuthors` filter to prevent self-review loops.
|
|
14
|
+
|
|
15
|
+
### Infrastructure Context
|
|
16
|
+
|
|
17
|
+
The GitLab adapter infrastructure lives in the `feat-polling-triggers` worktree (PR #404, in-flight). It is NOT merged to `main` yet. The GitHub adapter work must be built on top of that branch. Files in scope:
|
|
18
|
+
|
|
19
|
+
- `src/trigger/adapters/gitlab-poller.ts` -- the direct template
|
|
20
|
+
- `src/trigger/polling-scheduler.ts` -- orchestration; calls adapters and dispatches
|
|
21
|
+
- `src/trigger/polled-event-store.ts` -- per-trigger deduplication state on disk
|
|
22
|
+
- `src/trigger/types.ts` -- `GitLabPollingSource`, `TriggerDefinition.pollingSource`
|
|
23
|
+
- `src/trigger/trigger-store.ts` -- YAML parser, `SUPPORTED_PROVIDERS` set
|
|
24
|
+
|
|
25
|
+
### Core Tensions
|
|
26
|
+
|
|
27
|
+
**1. Shared `source:` YAML block vs. type-safe discriminated union.**
|
|
28
|
+
The YAML parser uses one `source:` block with shared field names. GitLab uses `projectId`; GitHub uses `repo`. If reusing the same block, the raw parsed type is a union of optional fields -- structurally looser than the assembled typed model. The discriminated union only kicks in after assembly; the raw parser type cannot be discriminated cleanly.
|
|
29
|
+
|
|
30
|
+
**2. `pollingSource` field migration vs. backward compatibility.**
|
|
31
|
+
The current `TriggerDefinition.pollingSource?: GitLabPollingSource` field has a code comment requesting migration to a discriminated union at adapter #2 (this change). All existing narrowing in `polling-scheduler.ts` and `trigger-store.ts` must be updated. The impact surface is small but must be handled atomically.
|
|
32
|
+
|
|
33
|
+
**3. GitHub PR `updated_at` vs. GitLab `updated_after` API semantics.**
|
|
34
|
+
GitLab issues endpoint accepts `updated_after=<ISO8601>` as a server-side filter. GitHub PRs endpoint does NOT accept a `since` parameter -- must fetch all open PRs sorted by `updated` desc, then filter client-side where `updated_at > lastPollAt`. This means more items are fetched per cycle and the deduplication load shifts to the client.
|
|
35
|
+
|
|
36
|
+
**4. `notLabels` client-side filter vs. pagination.**
|
|
37
|
+
GitHub API supports `labels=foo,bar` (include filter) but has no native exclude. The `notLabels` filter runs client-side. But with only 100 items per page and no pagination, filtering out many items means some new items may be missed. Accepted limitation -- same as the GitLab adapter's pagination limitation.
|
|
38
|
+
|
|
39
|
+
### What Makes This Hard
|
|
40
|
+
|
|
41
|
+
A junior developer would:
|
|
42
|
+
1. Skip the discriminated union migration and leave `pollingSource` as a bare `GitLabPollingSource | GitHubPollingSource` union -- the compiler cannot enforce which source type accompanies which provider string
|
|
43
|
+
2. Forget to add `github_issues_poll` and `github_prs_poll` to `SUPPORTED_PROVIDERS` in `trigger-store.ts` -- triggers would silently fail with `unknown_provider` at config load
|
|
44
|
+
3. Use `lastPollAt` as a GitHub PRs `since` parameter (the API ignores it -- no such parameter exists for PRs)
|
|
45
|
+
4. Miss the rate limit header check -- at 5000 req/hour the math is fine for normal use, but a burst or misconfiguration could exhaust it without warning
|
|
46
|
+
5. Place the `excludeAuthors` filter AFTER the dispatch call, creating the self-loop it was meant to prevent
|
|
47
|
+
|
|
48
|
+
### Likely Seam
|
|
49
|
+
|
|
50
|
+
`polling-scheduler.ts` is the coordination point. It must route to the correct adapter based on `trigger.provider`. The current code calls `pollGitLabMRs` directly -- this must become provider-aware dispatch.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Philosophy Constraints
|
|
55
|
+
|
|
56
|
+
From `CLAUDE.md` (hard rules):
|
|
57
|
+
|
|
58
|
+
- **Errors are data** -- all public functions return `Result<T, E>`, never throw
|
|
59
|
+
- **Immutability by default** -- all types `readonly`, all interfaces use `readonly` fields
|
|
60
|
+
- **Make illegal states unrepresentable** -- the TODO in `types.ts` for discriminated union is exactly this principle; the union migration fulfills it
|
|
61
|
+
- **Validate at boundaries, trust inside** -- `trigger-store.ts` validates all fields at parse time; adapters receive already-validated config
|
|
62
|
+
- **Type safety as first line of defense** -- branded `TriggerId`, explicit `Result` types, type guards
|
|
63
|
+
- **Dependency injection for boundaries** -- `fetchFn` is injectable in `gitlab-poller.ts`; required for the GitHub adapter too
|
|
64
|
+
- **Prefer fakes over mocks** -- tests use `vi.fn().mockResolvedValue()` for fetch; maintain this pattern
|
|
65
|
+
- **YAGNI with discipline** -- no registry pattern (premature for 2 providers)
|
|
66
|
+
|
|
67
|
+
**Philosophy conflict:** The backlog example uses `excludeAuthors: "worktrain-*"` suggesting glob matching. The `CLAUDE.md` principle of determinism and the YAML parser's lack of glob support argue for exact string matching only. **Resolution:** exact string match for MVP; glob support is a documented TODO.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Impact Surface
|
|
72
|
+
|
|
73
|
+
Files that must remain consistent:
|
|
74
|
+
|
|
75
|
+
- `src/trigger/types.ts` -- type shape changes here cascade to the assembler and scheduler
|
|
76
|
+
- `src/trigger/trigger-store.ts` -- `SUPPORTED_PROVIDERS`, `ParsedTriggerRaw.source`, assembly code
|
|
77
|
+
- `src/trigger/polling-scheduler.ts` -- `isPollingTrigger`, `buildWorkflowTrigger`, `doPoll` routing
|
|
78
|
+
- `tests/unit/trigger-store.test.ts` -- tests for `github_issues_poll` and `github_prs_poll` providers
|
|
79
|
+
- `tests/unit/gitlab-poller.test.ts` -- template; existing tests must continue to pass
|
|
80
|
+
|
|
81
|
+
No other files outside `src/trigger/` read `pollingSource` today.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Candidates
|
|
86
|
+
|
|
87
|
+
### Candidate A: Minimal extension -- bare union type, if/else in scheduler
|
|
88
|
+
|
|
89
|
+
**Summary:** Add `GitHubPollingSource` to `types.ts` as a bare union (`pollingSource?: GitLabPollingSource | GitHubPollingSource`), add new providers to `SUPPORTED_PROVIDERS`, extend the `source:` parser, add `if/else` branches in `PollingScheduler.doPoll`.
|
|
90
|
+
|
|
91
|
+
**Tensions resolved:** Zero migration cost. No changes to existing narrowing code.
|
|
92
|
+
|
|
93
|
+
**Tensions accepted:** The discriminated union TODO is ignored. TypeScript cannot prevent a `GitLabPollingSource` being used with a `github_issues_poll` trigger at the assembled type level. Illegal states remain representable.
|
|
94
|
+
|
|
95
|
+
**Boundary solved at:** `polling-scheduler.ts` routing -- additive `if/else` only.
|
|
96
|
+
|
|
97
|
+
**Why this boundary:** Minimum change surface.
|
|
98
|
+
|
|
99
|
+
**Failure mode:** A future refactor could accidentally call `pollGitLabMRs` with a `GitHubPollingSource` because the compiler cannot distinguish them without the tag. Silent wrong behavior, not a compile error.
|
|
100
|
+
|
|
101
|
+
**Repo-pattern relationship:** Adapts existing pattern but ignores the explicit codebase TODO for discriminated union.
|
|
102
|
+
|
|
103
|
+
**Gains:** Zero migration cost, zero risk of breaking existing code.
|
|
104
|
+
|
|
105
|
+
**Losses:** Correctness guarantee at the type level. The codebase's own comment requests this migration.
|
|
106
|
+
|
|
107
|
+
**Impact surface:** `polling-scheduler.ts` only (additive). `types.ts` additive.
|
|
108
|
+
|
|
109
|
+
**Scope judgment:** Correct scope, but incomplete honoring of stated invariants.
|
|
110
|
+
|
|
111
|
+
**Philosophy fit:** Honors YAGNI. Violates "Make illegal states unrepresentable".
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### Candidate B: Tagged union migration (RECOMMENDED)
|
|
116
|
+
|
|
117
|
+
**Summary:** Migrate `TriggerDefinition.pollingSource` to a discriminated union tagged by `provider`:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
export type PollingSource =
|
|
121
|
+
| (GitLabPollingSource & { readonly provider: 'gitlab_poll' })
|
|
122
|
+
| (GitHubPollingSource & { readonly provider: 'github_issues_poll' })
|
|
123
|
+
| (GitHubPollingSource & { readonly provider: 'github_prs_poll' });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`trigger-store.ts` assembler produces the tagged object (adds `provider` field to the assembled source). `polling-scheduler.ts` uses `switch(trigger.pollingSource.provider)` or typed `if/else` on `provider` to narrow. `github-poller.ts` is a pure function matching the GitLab adapter signature.
|
|
127
|
+
|
|
128
|
+
**Tensions resolved:** Discriminated union TODO fulfilled. Illegal states unrepresentable at the assembled type level. `switch` on `provider` is compiler-enforced exhaustive.
|
|
129
|
+
|
|
130
|
+
**Tensions accepted:** Small migration: ~20 lines of changes in existing files. The raw `ParsedTriggerRaw.source` type in `trigger-store.ts` is still an untagged bag of optional fields (the discrimination only applies after assembly, not during parsing).
|
|
131
|
+
|
|
132
|
+
**Boundary solved at:** `types.ts` (new union), `trigger-store.ts` (tagged assembly), `polling-scheduler.ts` (switch routing).
|
|
133
|
+
|
|
134
|
+
**Why this boundary is best fit:** The `types.ts` comment explicitly requests this migration at adapter #2. This IS adapter #2. Not doing it defers a known debt item.
|
|
135
|
+
|
|
136
|
+
**Failure mode:** If a future consumer outside `src/trigger/` reads `pollingSource` and was not updated during migration. Verified: no such consumer exists today.
|
|
137
|
+
|
|
138
|
+
**Repo-pattern relationship:** Directly follows what the codebase's own TODO comment requests. Mirrors `TriggerId` branded string: same philosophy of making types carry guarantees.
|
|
139
|
+
|
|
140
|
+
**Gains:** Compile-time guarantee that GitLab source never reaches the GitHub adapter. Exhaustive `switch` on provider. Clean type-level documentation of which sources exist.
|
|
141
|
+
|
|
142
|
+
**Losses:** ~20 lines of existing code must be updated. The `pollingSource` field shape changes.
|
|
143
|
+
|
|
144
|
+
**Impact surface:** `trigger-store.ts` assembler, `polling-scheduler.ts` narrowing, `types.ts`. No external consumers.
|
|
145
|
+
|
|
146
|
+
**Scope judgment:** Best-fit -- the TODO explicitly scopes this to adapter #2.
|
|
147
|
+
|
|
148
|
+
**Philosophy fit:** Honors "Make illegal states unrepresentable", "Type safety as first line of defense", "Exhaustiveness everywhere". Minor YAGNI tension (resolved by the existing TODO).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Candidate C: Generic `PollingAdapter` interface with registry
|
|
153
|
+
|
|
154
|
+
**Summary:** Define `interface PollingAdapter<TItem, TSource>` with `poll(source: TSource, since: string, fetchFn?) => Result<TItem[], error>`. A registry `Map<string, PollingAdapter<unknown, unknown>>` holds all adapters. The scheduler looks up by `trigger.provider`. Adding a new provider = registering a new adapter; no scheduler changes.
|
|
155
|
+
|
|
156
|
+
**Tensions resolved:** Open/closed: adding providers requires zero scheduler changes. Perfect separation of concerns.
|
|
157
|
+
|
|
158
|
+
**Tensions accepted:** The registry carries `unknown` typed adapters. The scheduler must cast when building `WorkflowTrigger` context -- type safety lost at the registry boundary. Runtime invariant, not compile-time.
|
|
159
|
+
|
|
160
|
+
**Boundary solved at:** New `adapter-registry.ts`. Scheduler depends on registry, not specific adapters.
|
|
161
|
+
|
|
162
|
+
**Why this boundary:** Maximum extensibility for future providers.
|
|
163
|
+
|
|
164
|
+
**Failure mode:** A misregistered adapter (wrong source type registered under wrong provider key) silently produces wrong context at dispatch time. No compile-time catch.
|
|
165
|
+
|
|
166
|
+
**Repo-pattern relationship:** Departs from existing patterns. Codebase has no registry or DI container for adapters. Introduces a new abstraction not justified by existing use.
|
|
167
|
+
|
|
168
|
+
**Gains:** Maximum extensibility (Jira, Linear, Sentry adapters add zero scheduler code).
|
|
169
|
+
|
|
170
|
+
**Losses:** Type safety at the registry boundary. Complexity for 2 providers. Violates YAGNI and the explicit CLAUDE.md warning against speculative abstractions.
|
|
171
|
+
|
|
172
|
+
**Impact surface:** New `adapter-registry.ts`, changes to `polling-scheduler.ts`, new abstract type in `types.ts`.
|
|
173
|
+
|
|
174
|
+
**Scope judgment:** Too broad -- 2 providers do not justify a registry.
|
|
175
|
+
|
|
176
|
+
**Philosophy fit:** Violates YAGNI, violates "Type safety as first line of defense". Honors open/closed but prematurely.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Comparison and Recommendation
|
|
181
|
+
|
|
182
|
+
**Recommendation: Candidate B (tagged union migration)**
|
|
183
|
+
|
|
184
|
+
| Dimension | A (minimal) | B (tagged union) | C (registry) |
|
|
185
|
+
|---|---|---|---|
|
|
186
|
+
| Illegal states representable | Yes (bug risk) | No (compile-time) | Partially (cast gap) |
|
|
187
|
+
| Migration cost | Zero | ~20 lines | High |
|
|
188
|
+
| Exhaustive narrowing | No | Yes | No |
|
|
189
|
+
| YAGNI | Honors | Minor (TODO overrides) | Violates |
|
|
190
|
+
| Consistent with TODO | No | Yes | No |
|
|
191
|
+
| Future extensibility | If/else grows | Union grows | Registry extends |
|
|
192
|
+
|
|
193
|
+
Candidate B is the correct choice for three reasons:
|
|
194
|
+
|
|
195
|
+
1. **The TODO is a commitment.** `types.ts` already says "TODO(follow-up): migrate to discriminated union at adapter #2." This is adapter #2. Skipping this is explicitly deferring committed technical debt.
|
|
196
|
+
2. **The migration cost is bounded.** `pollingSource` is only read inside `src/trigger/`. No external consumers. The ~20 lines of changes are confined to three files.
|
|
197
|
+
3. **The gain is real.** The `switch(pollingSource.provider)` pattern gives the TypeScript compiler full narrowing -- it is impossible to call `pollGitLabMRs` with a `GitHubPollingSource` after this change. Candidate A has no equivalent safety.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Self-Critique
|
|
202
|
+
|
|
203
|
+
**Strongest argument against B:** The raw `ParsedTriggerRaw.source` type in `trigger-store.ts` is still an untagged bag of optional fields -- both `projectId` (GitLab) and `repo` (GitHub) fields exist in the same raw type. The discriminated union only applies after assembly. So the "illegal states unrepresentable" benefit does NOT apply during YAML parsing -- only after. Candidate A has identical parse-time behavior. The union migration adds safety only at dispatch time.
|
|
204
|
+
|
|
205
|
+
Counter-counter: dispatch time is where safety matters. The assembler is the parser boundary. After the assembler, the system should be able to trust the types -- that's where the discriminated union matters.
|
|
206
|
+
|
|
207
|
+
**Narrower option that almost works:** Candidate A with a runtime assertion in the scheduler: `assert(trigger.provider === 'gitlab_poll')` before calling `pollGitLabMRs`. But TypeScript structural typing means both source types share enough optional fields that the assert is not enforced by the compiler -- it's a runtime check, not a compile-time one.
|
|
208
|
+
|
|
209
|
+
**Broader option that might be justified:** Candidate C (registry), but only if 4+ providers are added within 6 months. At 2 providers, the registry is premature. Evidence required: 3+ new adapter types confirmed in the near-term roadmap.
|
|
210
|
+
|
|
211
|
+
**Pivot conditions:**
|
|
212
|
+
- If `pollingSource` is used outside `src/trigger/` in a future PR (expands migration scope)
|
|
213
|
+
- If the `excludeAuthors` exact-string match is insufficient for the WorkTrain bot naming convention (requires glob support)
|
|
214
|
+
- If the GitHub rate limit at 5000 req/hour proves insufficient for high-volume repos (requires backoff strategy)
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Open Questions for the Main Agent
|
|
219
|
+
|
|
220
|
+
1. **`excludeAuthors` matching**: Should it be exact string match (e.g., `worktrain-bot`) or glob pattern (e.g., `worktrain-*`)? The backlog example uses `worktrain-*`. Exact match is simpler and safer for MVP.
|
|
221
|
+
|
|
222
|
+
2. **`repo` field format**: Should it be `owner/repo` (single string, split at `/`) or two separate fields `owner` and `repo`? Single string `owner/repo` is consistent with how GitHub URLs work and matches the GitLab `projectId: namespace/project` pattern.
|
|
223
|
+
|
|
224
|
+
3. **Rate limit skip**: When `X-RateLimit-Remaining < 100`, should this be a `GitHubPollError` kind or a silent skip (log only)? The prompt says "skip the current cycle and log a warning" -- so log only, no error return. Confirm this matches desired behavior.
|
|
225
|
+
|
|
226
|
+
4. **`notLabels` documentation**: The pagination limitation means `notLabels` can silently miss items on busy repos. Should this be documented in the config schema comment, or is it acceptable as an implicit limitation?
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# GitHub Polling Adapter: Design Review Findings
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-15
|
|
4
|
+
**Design reviewed:** Candidate B -- tagged `PollingSource` discriminated union
|
|
5
|
+
**Status:** Complete
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tradeoff Review
|
|
10
|
+
|
|
11
|
+
### Tradeoff 1: Parse-time raw type is still untagged (accepted)
|
|
12
|
+
|
|
13
|
+
The `ParsedTriggerRaw.source` bag holds both `projectId` (GitLab) and `repo` (GitHub) as optional fields. Discrimination only applies after assembly. This is valid: the assembler validates required fields with explicit `missing_field` errors. A `github_issues_poll` trigger with `projectId` instead of `repo` will fail at assembly with a clear error.
|
|
14
|
+
|
|
15
|
+
**Hidden assumption verified:** The assembler must explicitly validate `repo` as required for `github_issues_poll`/`github_prs_poll` (not inherited from GitLab path). Must be in the implementation.
|
|
16
|
+
|
|
17
|
+
### Tradeoff 2: `excludeAuthors` exact string match (accepted)
|
|
18
|
+
|
|
19
|
+
Valid for a single controlled bot account. Breaks if the bot naming convention uses unpredictable suffixes. Mitigated by: prominent warning in `GitHubPollingSource` comment, TODO for glob support.
|
|
20
|
+
|
|
21
|
+
### Tradeoff 3: 100 items per page, no pagination (accepted)
|
|
22
|
+
|
|
23
|
+
Issues API has native `since` filter -- pagination almost never needed. PRs API lacks `since` -- all open PRs fetched and filtered client-side. Burst risk (>100 PRs in one interval) is low for typical repos. Must be documented.
|
|
24
|
+
|
|
25
|
+
### Tradeoff 4: GitHub PRs client-side `updated_at` filter (accepted)
|
|
26
|
+
|
|
27
|
+
Higher API cost per cycle (fetch up to 100 PRs vs. only updated ones). Within rate limit budget at normal poll intervals (42 req/hour for PRs at 5-min interval). Rate limit check guards the threshold.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Failure Mode Review
|
|
32
|
+
|
|
33
|
+
### RED: Self-loop if `excludeAuthors` not configured
|
|
34
|
+
|
|
35
|
+
**Risk level:** HIGH. An unconfigured `excludeAuthors` creates an infinite loop: WorkTrain PR -> poll picks it up -> workflow dispatched -> another PR -> repeat. Cascading effects: rate limit exhaustion, unbounded sessions, potential cost impact.
|
|
36
|
+
|
|
37
|
+
**Current mitigation:** None (it's optional config). User must explicitly set it.
|
|
38
|
+
|
|
39
|
+
**Required mitigation:**
|
|
40
|
+
- `GitHubPollingSource.excludeAuthors` comment must contain: "IMPORTANT: include your WorkTrain bot account login here to prevent infinite self-review loops."
|
|
41
|
+
- `polling-scheduler.ts` dispatch path comment must repeat the warning.
|
|
42
|
+
- A default auto-detection TODO (WorkTrain could know its own GitHub login) should be filed.
|
|
43
|
+
|
|
44
|
+
### ORANGE: Rate limit skip has no alerting beyond log line
|
|
45
|
+
|
|
46
|
+
**Risk level:** Medium. When `X-RateLimit-Remaining < 100`, the cycle is silently skipped with only a console warning. Users who don't monitor logs may not notice the adapter has stopped polling.
|
|
47
|
+
|
|
48
|
+
**Current mitigation:** Log warning per skipped cycle.
|
|
49
|
+
|
|
50
|
+
**Recommended mitigation:** No code change needed for MVP, but the log message should include the `X-RateLimit-Reset` timestamp so users know when polling will resume.
|
|
51
|
+
|
|
52
|
+
### YELLOW: >100 PRs in one poll interval causes silent miss
|
|
53
|
+
|
|
54
|
+
**Risk level:** Low for typical repos, medium for dependency-bot-heavy repos.
|
|
55
|
+
|
|
56
|
+
**Mitigation:** Document in `GitHubPollingSource.pollIntervalSeconds` comment. No code change.
|
|
57
|
+
|
|
58
|
+
### YELLOW: `excludeAuthors` exact match may be insufficient for numbered bot accounts
|
|
59
|
+
|
|
60
|
+
**Risk level:** Low -- most bot accounts have stable names.
|
|
61
|
+
|
|
62
|
+
**Mitigation:** TODO comment for glob support. No code change.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Runner-Up / Simpler Alternative Review
|
|
67
|
+
|
|
68
|
+
**Candidate A (bare union + if/else):** Identified one borrow -- using `trigger.provider` for the dispatch switch is valid, but inside the switch arm TypeScript still needs the tag on `pollingSource` to narrow from `GitLabPollingSource | GitHubPollingSource` to the specific type. The hybrid (trigger.provider switch + untagged union) would require unsafe casts. Not worth pursuing.
|
|
69
|
+
|
|
70
|
+
**Merged `github_poll` provider:** Rejected. Issues and PRs have different endpoints, different shapes, different filter semantics. Merging adds conditional logic to the adapter. Two single-purpose adapters are cleaner.
|
|
71
|
+
|
|
72
|
+
**Simpler tagged union (add tag to source, but skip updating `isPollingTrigger` guard):** Already analyzed -- `isPollingTrigger` works unchanged when `pollingSource` becomes `PollingSource`. No simplification available.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Philosophy Alignment
|
|
77
|
+
|
|
78
|
+
| Principle | Status |
|
|
79
|
+
|---|---|
|
|
80
|
+
| Errors are data | Satisfied -- Result<T,E> everywhere |
|
|
81
|
+
| Immutability by default | Satisfied -- all fields readonly |
|
|
82
|
+
| Make illegal states unrepresentable | Satisfied -- tagged union |
|
|
83
|
+
| Type safety as first line of defense | Satisfied -- type guards at boundary |
|
|
84
|
+
| Validate at boundaries, trust inside | Satisfied -- assembler validates |
|
|
85
|
+
| Dependency injection for boundaries | Satisfied -- fetchFn injectable |
|
|
86
|
+
| Prefer fakes over mocks | Satisfied -- vi.fn() fake pattern |
|
|
87
|
+
| Document why not what | Satisfied -- invariant comments required |
|
|
88
|
+
| YAGNI | Minor tension -- overridden by existing TODO |
|
|
89
|
+
| Determinism over cleverness | Minor tension -- excludeAuthors exact match acceptable |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Findings
|
|
94
|
+
|
|
95
|
+
### RED
|
|
96
|
+
|
|
97
|
+
**Self-loop risk is not mitigated by default.** `excludeAuthors` is optional config with no default. A WorkTrain PR polling trigger with no `excludeAuthors` creates an infinite loop. The design must include prominent warnings in the `GitHubPollingSource` type comment and in the `polling-scheduler.ts` dispatch path comment.
|
|
98
|
+
|
|
99
|
+
### ORANGE
|
|
100
|
+
|
|
101
|
+
**Rate limit skip log message should include reset time.** When `X-RateLimit-Remaining < 100`, include `X-RateLimit-Reset` (Unix timestamp) in the log message so users know when polling resumes without digging into GitHub's documentation.
|
|
102
|
+
|
|
103
|
+
### YELLOW
|
|
104
|
+
|
|
105
|
+
**Both pagination and client-side filtering limitations must be documented in the type comment.** Not in code comments only -- in the `GitHubPollingSource` interface comment where users read it when configuring triggers.
|
|
106
|
+
|
|
107
|
+
### YELLOW
|
|
108
|
+
|
|
109
|
+
**`excludeAuthors` exact-match limitation must have a TODO for glob support.** The backlog example uses `worktrain-*` which implies glob. The implementation note should clarify that this is exact match and reference the backlog entry.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Recommended Revisions
|
|
114
|
+
|
|
115
|
+
1. **Add self-loop warning to `GitHubPollingSource.excludeAuthors` field comment** (required, not optional). Example text: "IMPORTANT: always include your WorkTrain bot account login here (e.g., `worktrain-bot`). Omitting this causes an infinite self-review loop where WorkTrain reviews its own PRs."
|
|
116
|
+
|
|
117
|
+
2. **Include `X-RateLimit-Reset` in the rate limit skip log message.** Change: `console.warn('[GH] Rate limit low: remaining=${remaining}')` to `console.warn('[GH] Rate limit low: remaining=${remaining}, resets at ${new Date(resetTs * 1000).toISOString()}')`.
|
|
118
|
+
|
|
119
|
+
3. **Document pagination limitation and client-side filter in `GitHubPollingSource` comments** -- one sentence each.
|
|
120
|
+
|
|
121
|
+
4. **Add TODO for glob support in `excludeAuthors` comment** -- "TODO: exact string match only. Glob support (e.g., worktrain-*) planned but not implemented."
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Residual Concerns
|
|
126
|
+
|
|
127
|
+
- **`github_prs_poll` redundancy with `github_issues_poll`:** GitHub Issues API returns PRs as well (a PR is also an issue). Using `github_issues_poll` with `labelFilter: my-label` could accidentally pick up PRs. Users should be aware that `state=open` on the issues endpoint includes open PRs. The `github_prs_poll` adapter uses the separate `/repos/:owner/:repo/pulls` endpoint which is PR-only. Both adapters should document this distinction.
|
|
128
|
+
|
|
129
|
+
- **Authentication:** Only personal access tokens are supported (same as GitLab). GitHub App tokens and fine-grained PATs are not addressed. This is an accepted MVP limitation.
|
|
130
|
+
|
|
131
|
+
- **`filter.notLabels` naming:** The backlog uses `filter.notLabels` as a nested object. The implementation uses `notLabels` as a top-level field on `GitHubPollingSource` for simplicity (consistent with how `events` and `labelFilter` are flat fields). No structural concern but naming should be consistent with any future filter fields.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# GitHub Polling Adapter: Implementation Plan
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-15
|
|
4
|
+
**Branch:** `feat/github-polling-adapter` (based on `feat-polling-triggers`)
|
|
5
|
+
**Status:** Ready for implementation
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Problem Statement
|
|
10
|
+
|
|
11
|
+
WorkRail can poll GitLab MRs for new/updated merge requests (PR #404, in-flight). There is no equivalent for GitHub. Users with GitHub repos must configure webhooks (requiring admin access + public URL) or forgo event-driven automation entirely. This plan implements GitHub Issues and GitHub PRs polling adapters that mirror the GitLab pattern exactly: poll on a schedule, deduplicate via `PolledEventStore`, dispatch via `TriggerRouter.dispatch()`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Acceptance Criteria
|
|
16
|
+
|
|
17
|
+
1. A trigger with `provider: github_issues_poll` polls `GET /repos/:owner/:repo/issues?state=open&since=<ISO8601>&sort=updated` on the configured interval.
|
|
18
|
+
2. A trigger with `provider: github_prs_poll` polls `GET /repos/:owner/:repo/pulls?state=open&sort=updated&direction=desc` and filters `updated_at > lastPollAt` client-side.
|
|
19
|
+
3. Events authored by users in `excludeAuthors` are never dispatched.
|
|
20
|
+
4. `notLabels` excludes items with matching labels (client-side filter).
|
|
21
|
+
5. `labelFilter` is passed as `labels=` query parameter to the issues/pulls endpoint.
|
|
22
|
+
6. When `X-RateLimit-Remaining < 100`, the poll cycle is skipped and a warning is logged with the reset timestamp.
|
|
23
|
+
7. Items already processed (by ID) are not re-dispatched across daemon restarts (deduplication via `PolledEventStore`).
|
|
24
|
+
8. `trigger-store.ts` parses `github_issues_poll` and `github_prs_poll` triggers from `triggers.yml` with all required fields validated.
|
|
25
|
+
9. `TriggerDefinition.pollingSource` is typed as a tagged `PollingSource` discriminated union.
|
|
26
|
+
10. All existing GitLab tests continue to pass unchanged.
|
|
27
|
+
11. `github-poller.test.ts` covers: success, empty response, HTTP 401/500, network error, invalid JSON, non-array response, malformed items, event filter, `excludeAuthors`, `notLabels`, rate limit skip, URL construction.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 3. Non-Goals
|
|
32
|
+
|
|
33
|
+
- Pagination beyond 100 items per page
|
|
34
|
+
- GitHub webhooks
|
|
35
|
+
- GitHub App token or fine-grained PAT support
|
|
36
|
+
- Glob pattern matching for `excludeAuthors`
|
|
37
|
+
- Response caching or request coalescing
|
|
38
|
+
- Monitoring or alerting beyond log messages
|
|
39
|
+
- Configuring `notLabels` as a server-side filter (API does not support it)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 4. Philosophy-Driven Constraints
|
|
44
|
+
|
|
45
|
+
- All public functions return `Result<T, E>`. No throws at adapter boundaries.
|
|
46
|
+
- All types are `readonly` on every field.
|
|
47
|
+
- `fetchFn` is injectable in both adapter functions (required for tests).
|
|
48
|
+
- Tests use `vi.fn().mockResolvedValue()` (fakes, not mocks of full HTTP layer).
|
|
49
|
+
- Rate limit skip is a log-and-return, not an error result (per stated requirement).
|
|
50
|
+
- `excludeAuthors` is exact string match only. Glob support filed as TODO.
|
|
51
|
+
- At-least-once ordering: `dispatch()` BEFORE `store.record()`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 5. Invariants
|
|
56
|
+
|
|
57
|
+
1. **At-least-once delivery**: dispatch is called BEFORE `PolledEventStore.record()`. If process crashes between dispatch and record, events re-fire. Silent miss is worse than duplicate.
|
|
58
|
+
2. **Fresh-start invariant**: `PolledEventStore` initializes `lastPollAt=now` on missing/corrupt file. Never re-fires historical events on daemon restart.
|
|
59
|
+
3. **Skip-cycle guard**: if a poll cycle is still running when the next interval fires, skip the new cycle. Never run two concurrent polls for the same trigger.
|
|
60
|
+
4. **`excludeAuthors` filter runs BEFORE dispatch**. Never dispatch an event for a filtered author.
|
|
61
|
+
5. **Rate limit skip**: if `X-RateLimit-Remaining < 100`, return early from the adapter (log warning). Do NOT call dispatch.
|
|
62
|
+
6. **No silent schema rejection**: type guards (`isGitHubIssueShape`, `isGitHubPRShape`) skip malformed items and return valid ones; they do not return errors.
|
|
63
|
+
7. **`github_issues_poll` and `github_prs_poll` must be in `SUPPORTED_PROVIDERS`** in `trigger-store.ts` or config parsing silently rejects them with `unknown_provider`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 6. Selected Approach + Rationale + Runner-Up
|
|
68
|
+
|
|
69
|
+
**Selected: Candidate B -- tagged `PollingSource` discriminated union**
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export type PollingSource =
|
|
73
|
+
| (GitLabPollingSource & { readonly provider: 'gitlab_poll' })
|
|
74
|
+
| (GitHubPollingSource & { readonly provider: 'github_issues_poll' })
|
|
75
|
+
| (GitHubPollingSource & { readonly provider: 'github_prs_poll' });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`TriggerDefinition.pollingSource` is typed as `PollingSource | undefined`. The assembler in `trigger-store.ts` adds the `provider` tag. The scheduler uses `switch(trigger.pollingSource.provider)` for exhaustive dispatch.
|
|
79
|
+
|
|
80
|
+
**Rationale:** `types.ts` already has the comment "TODO: migrate to discriminated union at adapter #2." This is adapter #2. The migration is bounded to 3 files with no external consumers of `pollingSource`.
|
|
81
|
+
|
|
82
|
+
**Runner-up: Candidate A** -- bare union without tag. Lost because TypeScript cannot narrow `GitLabPollingSource | GitHubPollingSource` inside a `switch(trigger.provider)` arm without unsafe casts. The tag is necessary for compiler-enforced safety.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 7. Vertical Slices
|
|
87
|
+
|
|
88
|
+
### Slice 1: Types -- `GitHubPollingSource` and `PollingSource` union
|
|
89
|
+
|
|
90
|
+
**Files:** `src/trigger/types.ts`
|
|
91
|
+
|
|
92
|
+
**Changes:**
|
|
93
|
+
- Add `GitHubPollingSource` interface (fields: `baseUrl`, `repo`, `token`, `events`, `pollIntervalSeconds`, `excludeAuthors`, `notLabels`, `labelFilter`)
|
|
94
|
+
- Add `PollingSource` discriminated union type
|
|
95
|
+
- Change `TriggerDefinition.pollingSource?: GitLabPollingSource` to `pollingSource?: PollingSource`
|
|
96
|
+
|
|
97
|
+
**Done when:** TypeScript compiles with no errors after the type change. All existing GitLab code that reads `pollingSource` still compiles (it will need narrowing updates in Slice 4).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### Slice 2: Trigger store -- parse `github_issues_poll` and `github_prs_poll`
|
|
102
|
+
|
|
103
|
+
**Files:** `src/trigger/trigger-store.ts`
|
|
104
|
+
|
|
105
|
+
**Changes:**
|
|
106
|
+
- Add `'github_issues_poll'` and `'github_prs_poll'` to `SUPPORTED_PROVIDERS`
|
|
107
|
+
- Add `repo`, `excludeAuthors`, `notLabels`, `labelFilter` to `ParsedTriggerRaw.source` optional fields
|
|
108
|
+
- Add assembly branch in `validateAndResolveTrigger` for GitHub providers: validate `repo` required, validate `token` required, parse `excludeAuthors` space-separated, parse `notLabels` space-separated, parse `labelFilter` space-separated, produce tagged `PollingSource`
|
|
109
|
+
- Add `provider` tag to the assembled GitLab source too (so `pollingSource.provider === 'gitlab_poll'` works)
|
|
110
|
+
- Warn on unrecognized `source` fields for non-polling providers (existing behavior -- no change)
|
|
111
|
+
|
|
112
|
+
**Done when:** `triggers.yml` with `provider: github_issues_poll` and a valid `source:` block parses without error. Missing `repo` returns `missing_field` error. `trigger-store.test.ts` updated tests pass.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Slice 3: GitHub poller adapter
|
|
117
|
+
|
|
118
|
+
**Files:** `src/trigger/adapters/github-poller.ts` (new)
|
|
119
|
+
|
|
120
|
+
**Exports:**
|
|
121
|
+
- `interface GitHubIssue` -- fields: `id`, `number`, `title`, `html_url`, `updated_at`, `state`, `user`, `labels`
|
|
122
|
+
- `interface GitHubPR` -- fields: `id`, `number`, `title`, `html_url`, `updated_at`, `state`, `user`, `draft`
|
|
123
|
+
- `type GitHubPollError` -- kinds: `http_error`, `network_error`, `parse_error`
|
|
124
|
+
- `async function pollGitHubIssues(source, since, fetchFn?): Promise<Result<GitHubIssue[], GitHubPollError>>`
|
|
125
|
+
- `async function pollGitHubPRs(source, since, fetchFn?): Promise<Result<GitHubPR[], GitHubPollError>>`
|
|
126
|
+
|
|
127
|
+
**Issues endpoint:** `GET https://api.github.com/repos/:owner/:repo/issues?state=open&since=<since>&sort=updated&direction=desc&per_page=100&labels=<labelFilter>`
|
|
128
|
+
|
|
129
|
+
**PRs endpoint:** `GET https://api.github.com/repos/:owner/:repo/pulls?state=open&sort=updated&direction=desc&per_page=100`
|
|
130
|
+
|
|
131
|
+
**Both functions:**
|
|
132
|
+
1. Build URL
|
|
133
|
+
2. Fetch with `Authorization: Bearer <token>` header
|
|
134
|
+
3. Check `X-RateLimit-Remaining` -- if < 100, log warning with reset timestamp, return `ok([])`
|
|
135
|
+
4. On non-2xx: return `err({ kind: 'http_error', status, message })`
|
|
136
|
+
5. Parse JSON array
|
|
137
|
+
6. Apply `isGitHubIssueShape` / `isGitHubPRShape` type guard
|
|
138
|
+
7. Filter `item.user?.login` against `source.excludeAuthors` (exact string match)
|
|
139
|
+
8. For PRs only: filter `item.updated_at > since` (client-side)
|
|
140
|
+
9. Apply `notLabels` filter: drop items where any label name is in `source.notLabels`
|
|
141
|
+
10. Return `ok(items)`
|
|
142
|
+
|
|
143
|
+
**Rate limit check implementation:**
|
|
144
|
+
```ts
|
|
145
|
+
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') ?? '9999', 10);
|
|
146
|
+
const resetTs = parseInt(response.headers.get('X-RateLimit-Reset') ?? '0', 10);
|
|
147
|
+
if (remaining < 100) {
|
|
148
|
+
console.warn(`[GitHubPoller] Rate limit low: remaining=${remaining}, resets at ${new Date(resetTs * 1000).toISOString()}. Skipping cycle.`);
|
|
149
|
+
return ok([]);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Done when:** `github-poller.test.ts` passes all cases (success, errors, filters, rate limit skip).
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Slice 4: Polling scheduler -- extend for GitHub
|
|
158
|
+
|
|
159
|
+
**Files:** `src/trigger/polling-scheduler.ts`
|
|
160
|
+
|
|
161
|
+
**Changes:**
|
|
162
|
+
- `isPollingTrigger` -- unchanged (still checks `pollingSource !== undefined`; type is now `PollingSource`)
|
|
163
|
+
- `doPoll` -- change dispatch routing to `switch(trigger.pollingSource.provider)` with cases for `gitlab_poll`, `github_issues_poll`, `github_prs_poll`. No `default` case that silently drops -- use exhaustive check with a logged warning.
|
|
164
|
+
- `buildWorkflowTrigger` -- split into:
|
|
165
|
+
- `buildGitLabWorkflowTrigger(trigger, mr: GitLabMR): WorkflowTrigger`
|
|
166
|
+
- `buildGitHubWorkflowTrigger(trigger, item: GitHubIssue | GitHubPR): WorkflowTrigger`
|
|
167
|
+
|
|
168
|
+
**GitHub context variables injected:**
|
|
169
|
+
```ts
|
|
170
|
+
{
|
|
171
|
+
itemId: item.id,
|
|
172
|
+
itemNumber: item.number,
|
|
173
|
+
itemTitle: item.title,
|
|
174
|
+
itemUrl: item.html_url,
|
|
175
|
+
itemUpdatedAt: item.updated_at,
|
|
176
|
+
itemAuthorLogin: item.user?.login,
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Done when:** A `github_issues_poll` trigger in a test fires `dispatch()` with the correct `WorkflowTrigger`. Existing `polling-scheduler.test.ts` GitLab cases still pass.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### Slice 5: Tests
|
|
185
|
+
|
|
186
|
+
**Files:**
|
|
187
|
+
- `tests/unit/github-poller.test.ts` (new)
|
|
188
|
+
- `tests/unit/trigger-store.test.ts` (extend)
|
|
189
|
+
- `tests/unit/polling-scheduler.test.ts` (extend)
|
|
190
|
+
|
|
191
|
+
**`github-poller.test.ts` cases:**
|
|
192
|
+
- Success: 2 issues returned from fake fetch
|
|
193
|
+
- Empty: API returns empty array
|
|
194
|
+
- HTTP 401: returns `http_error`
|
|
195
|
+
- HTTP 500: returns `http_error`
|
|
196
|
+
- Network error: returns `network_error`
|
|
197
|
+
- Invalid JSON: returns `parse_error`
|
|
198
|
+
- Non-array response: returns `parse_error`
|
|
199
|
+
- Malformed items: skipped, valid items returned
|
|
200
|
+
- `excludeAuthors` filter: bot-authored item excluded
|
|
201
|
+
- `notLabels` filter: labeled item excluded
|
|
202
|
+
- Rate limit skip: `X-RateLimit-Remaining=50` returns `ok([])` with log
|
|
203
|
+
- PR `updated_at` filter: item older than `since` excluded
|
|
204
|
+
- Issues URL construction: correct params including `since` and `labels`
|
|
205
|
+
- PRs URL construction: no `since` param, sort by updated
|
|
206
|
+
- Auth header: `Authorization: Bearer <token>`
|
|
207
|
+
|
|
208
|
+
**Done when:** All new tests pass, all existing trigger-related tests pass.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### Slice 6: Exports and documentation
|
|
213
|
+
|
|
214
|
+
**Files:**
|
|
215
|
+
- `src/trigger/index.ts` -- export `GitHubPollingSource` and `PollingSource` if needed
|
|
216
|
+
- `docs/design/github-polling-adapter-design-candidates.md` (already written)
|
|
217
|
+
- `docs/design/github-polling-adapter-design-review-findings.md` (already written)
|
|
218
|
+
|
|
219
|
+
**Done when:** Module exports are consistent. No breaking changes to existing exports.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 8. Test Design
|
|
224
|
+
|
|
225
|
+
**Framework:** Vitest (same as existing trigger tests)
|
|
226
|
+
|
|
227
|
+
**Pattern:** Inject `fetchFn: FetchFn` as a `vi.fn()` fake returning controlled responses. Never use real HTTP.
|
|
228
|
+
|
|
229
|
+
**Template:** Mirror `tests/unit/gitlab-poller.test.ts` structure exactly:
|
|
230
|
+
- Helper functions: `makeSource()`, `makeIssue()`, `makePR()`, `makeFetch()`
|
|
231
|
+
- Describe blocks per behavior group
|
|
232
|
+
- Explicit `result.kind === 'ok'` / `result.kind === 'err'` narrowing before assertions
|
|
233
|
+
|
|
234
|
+
**Coverage target:** All branches in both adapter functions. All filter logic (excludeAuthors, notLabels, updated_at).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 9. Risk Register
|
|
239
|
+
|
|
240
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
241
|
+
|---|---|---|---|
|
|
242
|
+
| Self-loop from unconfigured `excludeAuthors` | Medium | HIGH | Mandatory warning in config comment; document in triggers.yml example |
|
|
243
|
+
| Rate limit exhaustion on high-volume repos | Low | Medium | `X-RateLimit-Remaining < 100` skip + reset timestamp in log |
|
|
244
|
+
| `github_issues_poll` catches open PRs accidentally | Low | Low | Document in adapter comment: "includes open PRs (they are also issues)" |
|
|
245
|
+
| >100 PRs updated per poll interval silently missed | Low | Low | Document in `pollIntervalSeconds` comment |
|
|
246
|
+
| `feat-polling-triggers` PR changes conflict | Low | Low | Branch this work on `feat-polling-triggers`; rebase after #404 merges |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 10. PR Packaging Strategy
|
|
251
|
+
|
|
252
|
+
**Single PR** on branch `feat/github-polling-adapter`, based on `feat-polling-triggers`.
|
|
253
|
+
|
|
254
|
+
All 6 slices in one PR because:
|
|
255
|
+
- The discriminated union type change and the adapter implementation are tightly coupled
|
|
256
|
+
- Tests validate the full integration path
|
|
257
|
+
- The PR is digestible in size (est. 400-600 lines new/changed)
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 11. Philosophy Alignment Per Slice
|
|
262
|
+
|
|
263
|
+
| Slice | Principle | Status |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| 1 (types) | Make illegal states unrepresentable | Satisfied -- tagged union |
|
|
266
|
+
| 1 (types) | Immutability by default | Satisfied -- all fields readonly |
|
|
267
|
+
| 2 (trigger-store) | Validate at boundaries | Satisfied -- assembler validates all fields |
|
|
268
|
+
| 2 (trigger-store) | Errors are data | Satisfied -- missing_field, invalid_field_value errors |
|
|
269
|
+
| 3 (adapter) | Dependency injection | Satisfied -- fetchFn injectable |
|
|
270
|
+
| 3 (adapter) | Errors are data | Satisfied -- Result<T,E> return |
|
|
271
|
+
| 3 (adapter) | Determinism over cleverness | Satisfied -- exact string match for excludeAuthors |
|
|
272
|
+
| 4 (scheduler) | Exhaustiveness everywhere | Satisfied -- switch on provider with no silent default |
|
|
273
|
+
| 4 (scheduler) | Compose with small, pure functions | Satisfied -- split buildWorkflowTrigger |
|
|
274
|
+
| 5 (tests) | Prefer fakes over mocks | Satisfied -- vi.fn() fake fetchFn |
|
|
275
|
+
| 5 (tests) | Document why not what | Satisfied -- test case names describe behavior, not implementation |
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Plan Confidence
|
|
280
|
+
|
|
281
|
+
- `planConfidenceBand`: High
|
|
282
|
+
- `unresolvedUnknownCount`: 1 (whether `excludeAuthors` exact match is truly sufficient for the WorkTrain bot account naming convention -- acceptable for MVP)
|
|
283
|
+
- `estimatedPRCount`: 1
|
|
284
|
+
- `followUpTickets`: ["Add glob support to excludeAuthors", "Implement pagination for high-volume repos", "Add X-RateLimit-Reset-based backoff", "Auto-detect WorkTrain bot account login for default excludeAuthors"]
|