@exaudeus/workrail 3.32.0 → 3.34.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.
Files changed (98) hide show
  1. package/dist/cli/commands/index.d.ts +1 -0
  2. package/dist/cli/commands/index.js +3 -1
  3. package/dist/cli/commands/worktrain-await.js +11 -9
  4. package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
  5. package/dist/cli/commands/worktrain-daemon-install.js +291 -0
  6. package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
  7. package/dist/cli/commands/worktrain-daemon.js +272 -0
  8. package/dist/cli/commands/worktrain-spawn.js +11 -9
  9. package/dist/cli-worktrain.js +488 -0
  10. package/dist/cli.js +1 -22
  11. package/dist/console/standalone-console.d.ts +28 -0
  12. package/dist/console/standalone-console.js +142 -0
  13. package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-C1JXnwZS.js} +1 -1
  14. package/dist/{console → console-ui}/index.html +1 -1
  15. package/dist/daemon/agent-loop.d.ts +27 -0
  16. package/dist/daemon/agent-loop.js +39 -1
  17. package/dist/daemon/daemon-events.d.ts +63 -1
  18. package/dist/daemon/workflow-runner.d.ts +3 -2
  19. package/dist/daemon/workflow-runner.js +285 -46
  20. package/dist/infrastructure/session/HttpServer.js +133 -34
  21. package/dist/manifest.json +136 -104
  22. package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
  23. package/dist/mcp/handlers/v2-error-mapping.js +2 -0
  24. package/dist/mcp/handlers/v2-execution/advance.js +25 -0
  25. package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
  26. package/dist/mcp/output-schemas.d.ts +30 -30
  27. package/dist/mcp/transports/fatal-exit.js +4 -0
  28. package/dist/mcp/transports/http-entry.js +0 -5
  29. package/dist/mcp/transports/stdio-entry.js +24 -12
  30. package/dist/mcp/v2/tools.d.ts +4 -4
  31. package/dist/mcp-server.d.ts +0 -2
  32. package/dist/mcp-server.js +1 -42
  33. package/dist/trigger/adapters/github-poller.d.ts +44 -0
  34. package/dist/trigger/adapters/github-poller.js +190 -0
  35. package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
  36. package/dist/trigger/adapters/gitlab-poller.js +81 -0
  37. package/dist/trigger/index.d.ts +4 -1
  38. package/dist/trigger/index.js +5 -1
  39. package/dist/trigger/polled-event-store.d.ts +22 -0
  40. package/dist/trigger/polled-event-store.js +173 -0
  41. package/dist/trigger/polling-scheduler.d.ts +20 -0
  42. package/dist/trigger/polling-scheduler.js +249 -0
  43. package/dist/trigger/trigger-listener.d.ts +3 -0
  44. package/dist/trigger/trigger-listener.js +47 -3
  45. package/dist/trigger/trigger-store.js +114 -33
  46. package/dist/trigger/types.d.ts +17 -1
  47. package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
  48. package/dist/v2/durable-core/domain/observation-builder.js +2 -2
  49. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
  50. package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
  51. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
  52. package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
  53. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  54. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
  55. package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
  56. package/dist/v2/usecases/console-routes.js +3 -3
  57. package/dist/v2/usecases/console-service.js +185 -10
  58. package/dist/v2/usecases/console-types.d.ts +8 -0
  59. package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
  60. package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
  61. package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
  62. package/docs/design/daemon-conversation-logging-plan.md +98 -0
  63. package/docs/design/daemon-conversation-logging-review.md +55 -0
  64. package/docs/design/daemon-conversation-logging.md +129 -0
  65. package/docs/design/github-polling-adapter-design-candidates.md +226 -0
  66. package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
  67. package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
  68. package/docs/design/implementation_plan.md +192 -0
  69. package/docs/design/workflow-id-validation-at-startup.md +146 -0
  70. package/docs/design/workflow-id-validation-design-review.md +87 -0
  71. package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
  72. package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
  73. package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
  74. package/docs/discovery/design-candidates.md +180 -0
  75. package/docs/discovery/design-review-findings.md +110 -0
  76. package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
  77. package/docs/ideas/backlog.md +627 -0
  78. package/package.json +1 -1
  79. package/workflows/architecture-scalability-audit.json +1 -1
  80. package/workflows/bug-investigation.agentic.v2.json +3 -3
  81. package/workflows/coding-task-workflow-agentic.json +32 -32
  82. package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
  83. package/workflows/coding-task-workflow-agentic.v2.json +7 -7
  84. package/workflows/mr-review-workflow.agentic.v2.json +21 -12
  85. package/workflows/personal-learning-materials-creation-branched.json +2 -2
  86. package/workflows/production-readiness-audit.json +1 -1
  87. package/workflows/relocation-workflow-us.json +2 -2
  88. package/workflows/ui-ux-design-workflow.json +14 -14
  89. package/workflows/workflow-for-workflows.json +3 -3
  90. package/workflows/workflow-for-workflows.v2.json +2 -2
  91. package/workflows/wr.discovery.json +59 -8
  92. package/dist/mcp/transports/bridge-entry.d.ts +0 -102
  93. package/dist/mcp/transports/bridge-entry.js +0 -454
  94. package/dist/mcp/transports/bridge-events.d.ts +0 -51
  95. package/dist/mcp/transports/bridge-events.js +0 -24
  96. package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
  97. package/dist/mcp/transports/primary-tombstone.js +0 -51
  98. /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
@@ -0,0 +1,98 @@
1
+ # Implementation Plan: Daemon Conversation Logging
2
+
3
+ ## Problem Statement
4
+
5
+ The WorkRail daemon runs workflows autonomously but provides minimal visibility into what the agent is actually doing. Today you can see `session_started`, `tool_called`, and `session_completed` in the JSONL event log - but you cannot see what the LLM decided, which tools it requested, how long each tool took, or whether a tool succeeded. Adding `llm_turn_started`, `llm_turn_completed`, `tool_call_started`, `tool_call_completed`, and `tool_call_failed` events - plus a `worktrain logs` CLI command - turns the event file into a real-time audit trail of agent behavior.
6
+
7
+ ## Acceptance Criteria
8
+
9
+ 1. After an LLM API call in `_runLoop()`, `llm_turn_started` is written before the call and `llm_turn_completed` after the response.
10
+ 2. For every tool execution via `_executeTools()`, `tool_call_started` is written before `tool.execute()`, and either `tool_call_completed` or `tool_call_failed` is written after.
11
+ 3. `tool_call_started` args are truncated to max 200 chars. `tool_call_completed` result summary truncated to max 200 chars.
12
+ 4. All new events appear in the same daily JSONL file as existing events.
13
+ 5. `worktrain logs` reads today's log file and prints each event formatted for humans.
14
+ 6. `worktrain logs --follow` polls the file every 500ms and prints new events as they arrive.
15
+ 7. `worktrain logs --session <id>` filters events to those with matching `sessionId`.
16
+ 8. `worktrain logs --follow` handles midnight file rotation (switches to new date file).
17
+ 9. If the log file doesn't exist, `worktrain logs` prints a helpful message; `--follow` waits for the file.
18
+ 10. TypeScript compiles without errors. Existing tests pass.
19
+
20
+ ## Non-Goals
21
+
22
+ - NOT putting events in the v2 session event store
23
+ - NOT adding a Console Timeline tab
24
+ - NOT deprecating `tool_called` events (backward compat)
25
+ - NOT implementing accurate pre-call token counting (message count proxy is sufficient)
26
+ - NOT searching across multiple day files for `--session` filter
27
+
28
+ ## Philosophy-Driven Constraints
29
+
30
+ - **Fire-and-forget invariant**: All callbacks in AgentLoop are wrapped in try/catch that swallow errors.
31
+ - **DI for boundaries**: AgentLoop receives callbacks, not DaemonEventEmitter itself.
32
+ - **Make illegal states unrepresentable**: New event kinds added to `DaemonEvent` discriminated union.
33
+ - **YAGNI**: Only the specified event kinds and fields.
34
+
35
+ ## Invariants
36
+
37
+ 1. `tool_call_started` is always followed by either `tool_call_completed` or `tool_call_failed`.
38
+ 2. `llm_turn_started` may have no matching `llm_turn_completed` on API error - this is intentional signal.
39
+ 3. Callbacks in AgentLoop never propagate exceptions to the caller.
40
+ 4. `DaemonEvent` union remains exhaustive.
41
+
42
+ ## Selected Approach
43
+
44
+ AgentLoopOptions callbacks: 5 optional callback properties on `AgentLoopOptions` called in `_runLoop()` and `_executeTools()`. workflow-runner.ts wires them to `emitter?.emit()`.
45
+
46
+ ## Vertical Slices
47
+
48
+ ### Slice 1: New event types in daemon-events.ts
49
+ - Add interfaces: `LlmTurnStartedEvent`, `LlmTurnCompletedEvent`, `ToolCallStartedEvent`, `ToolCallCompletedEvent`, `ToolCallFailedEvent`
50
+ - Extend `DaemonEvent` union with all 5
51
+
52
+ ### Slice 2: AgentLoopOptions callbacks + emission in agent-loop.ts
53
+ - Add 5 optional callbacks to `AgentLoopOptions`
54
+ - Call with try/catch in `_runLoop()` before/after `client.messages.create()`
55
+ - Call with try/catch in `_executeTools()` before/after `tool.execute()`
56
+ - Add `Date.now()` timing for tool calls
57
+
58
+ ### Slice 3: Wire callbacks in workflow-runner.ts
59
+ - In `runWorkflow()`, pass `AgentLoop` constructor the 5 callbacks
60
+ - Each callback calls `emitter?.emit()` with the appropriate new event kind
61
+
62
+ ### Slice 4: `worktrain logs` CLI command
63
+ - Add `program.command('logs')` with `--follow` and `--session <id>` options
64
+ - Read daily JSONL, format each line, handle ENOENT
65
+ - Polling loop with midnight rotation
66
+
67
+ ### Slice 5: Tests
68
+ - `daemon-events.test.ts`: Add 5 new event kinds to exhaustiveness test
69
+ - `agent-loop.test.ts`: Add tests for callback timing, completion, failure, and try/catch guards
70
+
71
+ ## Test Design
72
+
73
+ - onToolCallStarted fires before tool execute (verified via call order recording)
74
+ - onToolCallCompleted fires after successful execute (verified with durationMs > 0)
75
+ - onToolCallFailed fires when tool throws (loop continues normally)
76
+ - onLlmTurnStarted fires with correct messageCount before API call
77
+ - onLlmTurnCompleted fires with actual token counts from API response
78
+ - Callbacks that throw do not crash the loop
79
+
80
+ ## Risk Register
81
+
82
+ | Risk | Mitigation |
83
+ |---|---|
84
+ | Callback throws crash the session | try/catch on all 5 callback invocations |
85
+ | --follow misses events at midnight | Date-check on each poll iteration |
86
+
87
+ ## PR Strategy
88
+
89
+ Single PR: `feat/daemon-conversation-logging`
90
+
91
+ ## Philosophy Alignment
92
+
93
+ - DI for boundaries: Satisfied (callbacks, not DaemonEventEmitter in AgentLoop)
94
+ - Make illegal states unrepresentable: Satisfied (discriminated union)
95
+ - Errors are data: Satisfied (tool throws -> tool_call_failed, not propagated)
96
+ - Fire-and-forget: Satisfied (try/catch guards)
97
+ - YAGNI: Satisfied
98
+ - Exhaustiveness: Satisfied (union extended + test updated)
@@ -0,0 +1,55 @@
1
+ # Daemon Conversation Logging: Design Review Findings
2
+
3
+ ## Tradeoff Review
4
+
5
+ | Tradeoff | Assessment | Conditions for failure |
6
+ |---|---|---|
7
+ | AgentLoopOptions gains 5 optional callbacks | Acceptable - all optional, zero cost when absent | Would matter if AgentLoop were a versioned public library |
8
+ | Dual `tool_called` + `tool_call_started` events in log | Minor duplication, harmless - different fields, different consumers | If a consumer enforced "one event per tool execution" |
9
+ | `llm_turn_started` uses message count (proxy) | Spec-compliant - user explicitly said "estimate from message count" | If accurate pre-call token counts were needed for routing |
10
+ | `--follow` polls at 500ms interval | Acceptable for human-readable monitoring | If sub-100ms stream was required |
11
+
12
+ ## Failure Mode Review
13
+
14
+ | Failure mode | Status | Mitigation |
15
+ |---|---|---|
16
+ | Callback throws, propagates into agent loop | **UNMITIGATED - REQUIRES FIX** | Add try/catch around all 5 callback invocations in agent-loop.ts |
17
+ | `tool_call_started` without matching `tool_call_completed` | Handled - catch block emits `tool_call_failed` | No action needed |
18
+ | `llm_turn_started` without matching `llm_turn_completed` (API error) | Acceptable - unmatched started = API error signal | No action needed |
19
+ | `--follow` misses events at midnight file rotation | **REQUIRES FIX** | Check `new Date()` on each poll; switch to new file when date changes |
20
+ | Log file doesn't exist (daemon not started) | Handled - ENOENT returns graceful message | No action needed |
21
+
22
+ ## Runner-Up / Simpler Alternative Review
23
+
24
+ - **Runner-up (Candidate B, per-tool factories)**: No elements worth borrowing. Centralizing in `_executeTools()` is strictly better.
25
+ - **Simpler alternative (no AgentLoop changes + `turn_end` subscriber for LLM events)**: Fails spec - `turn_end` fires after tool results, not after API response. Not a valid simplification.
26
+ - **Hybrid (callbacks for LLM, per-factory for tools)**: Two patterns for the same concern. Worse than either pure approach.
27
+
28
+ ## Philosophy Alignment
29
+
30
+ **Satisfied**: DI for boundaries, immutability, make illegal states unrepresentable, errors as data, determinism, YAGNI, validate at boundaries.
31
+
32
+ **Under tension (acceptable)**:
33
+ - Type safety: `argsSummary` is deliberately a truncated string - this is spec-required (max 200 chars) and appropriate for JSONL serialization.
34
+ - Exhaustiveness: DaemonEvent union grows by 5; no switch consumers exist so this is theoretical only.
35
+
36
+ ## Findings
37
+
38
+ ### Red (blocking)
39
+ None.
40
+
41
+ ### Orange (should fix before implementation)
42
+ 1. **Missing try/catch around callbacks in agent-loop.ts**: A buggy callback passed to `AgentLoop` would propagate a throw into the agent loop and crash the session. This violates the fire-and-forget invariant that all observability in the daemon upholds. Fix: wrap each of the 5 callback invocations with `try { callback(info); } catch { /* swallow */ }`.
43
+
44
+ ### Yellow (fix during implementation)
45
+ 2. **Midnight file rotation in `--follow`**: The polling loop should check `new Date().toISOString().slice(0, 10)` on each iteration and switch to the new file when the date changes. 3-line fix in the polling loop.
46
+
47
+ ## Recommended Revisions
48
+
49
+ 1. Add try/catch guards around all callback invocations in `_runLoop()` and `_executeTools()` in `agent-loop.ts`.
50
+ 2. Add date-aware file switching in the `--follow` polling loop in `cli-worktrain.ts`.
51
+
52
+ ## Residual Concerns
53
+
54
+ - The `tool_called` + `tool_call_started` dual events: a future cleanup task could deprecate `tool_called` once all consumers migrate to `tool_call_started`. Not in scope for this PR.
55
+ - The `worktrain logs` command reads from the daily JSONL file directly. If sessions span multiple days, `--session <id>` would only find events in the current day's file. A future improvement could search across all files. Not in scope.
@@ -0,0 +1,129 @@
1
+ # Daemon Conversation Logging: Design Candidates
2
+
3
+ ## Problem Understanding
4
+
5
+ ### Core tensions
6
+
7
+ 1. **AgentLoop decoupling vs. LLM turn visibility**: AgentLoop is intentionally decoupled from all observability infrastructure (no DaemonEventEmitter import). To emit LLM turn events FROM inside `_runLoop()`, we need to bridge this gap without coupling AgentLoop to daemon-specific types. Options: inject callbacks, use the existing AgentEvent subscriber system, or violate the boundary. The subscriber system fires at `turn_end` (after tool results), which is not the right boundary for `llm_turn_started` / `llm_turn_completed`. Callbacks are the right choice.
8
+
9
+ 2. **Single-source vs. dual-source tool events**: Today each tool factory (`makeBashTool`, `makeReadTool`, etc.) emits `tool_called` directly. Adding `tool_call_started/completed/failed` in `_executeTools()` creates a single centralized emission point. The existing `tool_called` events remain for backward compatibility; new event kinds are additive.
10
+
11
+ 3. **Input token estimation**: True token counts require a tokenizer (tiktoken or the API's usage field). The API returns `response.usage.input_tokens` and `response.usage.output_tokens` in the response. For `llm_turn_started`, emit message count as proxy. For `llm_turn_completed`, emit actual token counts from the API response.
12
+
13
+ 4. **`worktrain logs --follow` streaming**: Node.js file watching is noisy; polling every 500ms is reliable and simple for MVP.
14
+
15
+ ### What makes this hard
16
+
17
+ Nothing is architecturally hard. The tricky parts are:
18
+ - Getting tool event timing exactly right (started before execute, completed/failed after)
19
+ - For `worktrain logs --follow`: handling the case where the log file doesn't exist yet
20
+ - TypeScript type checking: callback signatures must be precise for ts-strict
21
+
22
+ ### Likely seam
23
+
24
+ The real seam for tool events is `_executeTools()` in `agent-loop.ts` - it's the single place all tools execute. The real seam for LLM turn events is the `client.messages.create()` call in `_runLoop()`. Both are in `agent-loop.ts`.
25
+
26
+ ## Philosophy Constraints
27
+
28
+ From `CLAUDE.md` (system-wide):
29
+ - **DI for boundaries**: inject external effects (observability) to keep core logic testable
30
+ - **YAGNI with discipline**: no speculative fields beyond what's in the spec
31
+ - **Exhaustiveness everywhere**: new event kinds extend the `DaemonEvent` discriminated union
32
+ - **Fire-and-forget invariant**: `emit()` is void, errors swallowed - observability never affects correctness
33
+ - **Prefer fakes over mocks**: FakeAnthropicClient pattern in agent-loop tests
34
+
35
+ No philosophy conflicts found between stated principles and existing repo patterns.
36
+
37
+ ## Impact Surface
38
+
39
+ - `runWorkflow()` in `workflow-runner.ts`: constructs AgentLoop, must pass new callbacks
40
+ - `AgentLoopOptions` interface: extended with optional callbacks (non-breaking)
41
+ - `DaemonEvent` union: extended with new members (exhaustiveness tests must update)
42
+ - `tests/unit/daemon-events.test.ts`: the exhaustiveness test at line 169 must list new event kinds
43
+ - `tests/unit/agent-loop.test.ts`: needs tests for callback invocation timing
44
+ - No public API changes - all daemon-internal
45
+
46
+ ## Candidates
47
+
48
+ ### Candidate A: AgentLoopOptions callbacks (recommended)
49
+
50
+ **Summary**: Add 5 optional callback properties to `AgentLoopOptions` in `agent-loop.ts`. Call them synchronously in `_runLoop()` and `_executeTools()`. Wire in `workflow-runner.ts` to call `emitter?.emit()`.
51
+
52
+ **New properties on AgentLoopOptions**:
53
+ ```typescript
54
+ onLlmTurnStarted?: (info: { messageCount: number }) => void
55
+ onLlmTurnCompleted?: (info: {
56
+ stopReason: string;
57
+ outputTokens: number;
58
+ inputTokens: number;
59
+ toolNamesRequested: string[];
60
+ }) => void
61
+ onToolCallStarted?: (info: { toolName: string; argsSummary: string }) => void
62
+ onToolCallCompleted?: (info: { toolName: string; durationMs: number; resultSummary: string }) => void
63
+ onToolCallFailed?: (info: { toolName: string; durationMs: number; errorMessage: string }) => void
64
+ ```
65
+
66
+ **Tensions resolved**: AgentLoop stays decoupled from DaemonEventEmitter. Single source of truth for tool event timing.
67
+
68
+ **Boundary**: AgentLoop / workflow-runner.ts interface. Correct seam - AgentLoop is a reusable primitive; workflow-runner.ts is the daemon-specific orchestrator.
69
+
70
+ **Failure mode**: If a callback throws, it propagates into the agent loop. Mitigated by: callbacks call `emitter?.emit()` which is fire-and-forget and never throws.
71
+
72
+ **Follows existing pattern**: `DaemonRegistry` uses the same inject-as-optional pattern. `toolExecution: 'sequential'` is already a strategy parameter on `AgentLoopOptions`.
73
+
74
+ **Gains**: Central timing; no changes to individual tool factories; clean separation; new tools get events automatically.
75
+ **Gives up**: `AgentLoopOptions` interface is slightly heavier (5 optional callbacks). Callbacks are less discoverable than per-tool pattern.
76
+
77
+ **Scope**: best-fit.
78
+
79
+ **Philosophy**: honors DI-for-boundaries, YAGNI, exhaustiveness. No conflicts.
80
+
81
+ ---
82
+
83
+ ### Candidate B: Extend per-tool factory pattern (adapt existing)
84
+
85
+ **Summary**: Keep the existing per-tool `emitter?.emit({ kind: 'tool_called' })` approach. Add `tool_call_started` emit before `tool.execute()` and `tool_call_completed`/`tool_call_failed` after, inside each of the 5 tool factory closures. Add LLM turn callbacks to `AgentLoopOptions` only for the LLM-specific events.
86
+
87
+ **Tensions resolved**: Minimizes changes to AgentLoop (only 2 callbacks instead of 5). Follows the exact existing pattern.
88
+
89
+ **Boundary**: Each tool factory is the emission point.
90
+
91
+ **Failure mode**: 5 tool factories x 3 events each = 15 new emit calls. Duplication risk. New tools added later won't automatically get events.
92
+
93
+ **Follows existing pattern**: Pure adaptation of the existing `tool_called` pattern.
94
+
95
+ **Gains**: No callbacks for tool events in AgentLoopOptions; no risk of propagated errors.
96
+ **Gives up**: DRY principle - timing logic duplicated 5x. Maintenance trap.
97
+
98
+ **Scope**: best-fit for existing tools, but creates technical debt.
99
+
100
+ **Philosophy**: conflicts with "compose with small, pure functions" (duplication). Honors DI-for-boundaries.
101
+
102
+ ## Comparison and Recommendation
103
+
104
+ **Recommendation: Candidate A**
105
+
106
+ Candidate A wins on every meaningful dimension:
107
+ - **Best-fit boundary**: `_executeTools()` is the single canonical execution point for all tools.
108
+ - **Most manageable failure mode**: callbacks call `emitter?.emit()` which can never throw.
109
+ - **Best philosophy fit**: "Compose with small, pure functions" and "DI for boundaries" both point to A.
110
+ - **Easiest to evolve**: Adding a 6th tool gets events automatically.
111
+ - **Consistent with repo patterns**: Same pattern as `DaemonRegistry` injection.
112
+
113
+ ## Self-Critique
114
+
115
+ **Strongest argument against**: Candidate A adds 5 callback properties to `AgentLoopOptions`. If `AgentLoop` is used in tests without an emitter, the interface is heavier. Counter: all 5 are optional (`?`), zero cost when absent.
116
+
117
+ **Narrower option that was considered**: Only add LLM turn callbacks (skip `tool_call_started/completed/failed`). Doesn't satisfy the spec.
118
+
119
+ **Broader option**: Put the emitter directly in `AgentLoopOptions`. Would require `AgentLoop` to import `DaemonEventEmitter`, coupling the modules. Unjustified.
120
+
121
+ **Invalidating assumption**: None. `_executeTools()` is the only tool execution path in `AgentLoop`.
122
+
123
+ ## Open Questions for Implementation
124
+
125
+ 1. The existing `tool_called` events in per-tool factories (`makeBashTool`, `makeReadTool`, `makeWriteTool`, `makeContinueWorkflowTool`) - keep them as-is for backward compat, or remove them now that `tool_call_started` supersedes them? Decision: keep for backward compat since consumers may depend on them.
126
+
127
+ 2. For the `worktrain logs --follow` command, should it print historical lines first then follow? Yes - show existing entries then poll for new ones.
128
+
129
+ 3. Should `worktrain logs --session <id>` filter by exact sessionId match? Yes.
@@ -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.