@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,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"]
@@ -0,0 +1,192 @@
1
+ # Implementation Plan: WorkTrain System Prompt Preamble + report_issue Tool
2
+
3
+ ## Problem Statement
4
+
5
+ The WorkTrain daemon's system prompt preamble is thin (15 lines) and relies on the soul file for behavioral guidance. This leaves unattended agents without explicit direction on self-directed reasoning, the oracle hierarchy, or what to do when things go wrong. Additionally, there is no structured way for agents to record issues/errors for a future auto-fix coordinator -- failures either go unrecorded or end up buried in step notes.
6
+
7
+ ---
8
+
9
+ ## Acceptance Criteria
10
+
11
+ 1. `buildSystemPrompt()` output contains a richer preamble (~55 lines) that:
12
+ - Opens with "You are WorkRail Auto, an autonomous agent..." (existing test assertion preserved)
13
+ - Includes `## Your tools` section listing all 5 tools (existing test assertion)
14
+ - Includes `## Execution contract` section (existing test assertion)
15
+ - Adds `## What you are`, `## Your oracle`, `## Self-directed reasoning`, `## The workflow is the contract`, `## Silent failure is the worst outcome`, `## Tools are your hands not your voice`, `## You don't have a user` sections
16
+ - All existing `workflow-runner-system-prompt.test.ts` tests pass without modification
17
+
18
+ 2. `makeReportIssueTool(sessionId, emitter?, issuesDirOverride?)` is exported from `workflow-runner.ts`:
19
+ - Tool name: `report_issue`
20
+ - Input schema accepts: `kind` (5-value literal enum), `severity` (4-value literal enum), `summary` (string, required), `context` (string, optional), `toolName` (string, optional), `command` (string, optional), `suggestedFix` (string, optional), `continueToken` (string, optional)
21
+ - `execute()` appends one JSON line to `~/.workrail/issues/<sessionId>.jsonl` (or `issuesDirOverride/<sessionId>.jsonl` in tests) -- fire-and-forget (void+catch)
22
+ - `execute()` emits a `DaemonEventEmitter` event with `kind: 'issue_reported'`
23
+ - For non-fatal severity: returns `"Issue recorded (severity=<severity>). Continue with your work unless this is fatal."`
24
+ - For fatal severity: returns `"FATAL issue recorded. Call continue_workflow with notes explaining the blocker, then the session will end."`
25
+ - Wired into `runWorkflow()` tools array
26
+
27
+ 3. `IssueReportedEvent` is added to `DaemonEvent` union in `daemon-events.ts`:
28
+ - `kind: 'issue_reported'`, `sessionId: string`, `issueKind` (5-value literal union), `severity` (4-value literal union), `summary: string`, `continueToken?: string`
29
+
30
+ 4. `npm run build` succeeds (no TS errors)
31
+ 5. `npx vitest run` passes (all existing tests + new tests)
32
+
33
+ ---
34
+
35
+ ## Non-Goals
36
+
37
+ - No auto-fix coordinator implementation
38
+ - No IssueStore class (YAGNI -- extract when coordinator needs it)
39
+ - No changes to `soul-template.ts`, `triggers.yml`, or `src/v2/`
40
+ - No changes to the soul file template/default
41
+ - No changes to AgentLoop behavior (fatal severity does not abort the loop)
42
+ - No async changes to `buildSystemPrompt()` (must remain synchronous and pure)
43
+
44
+ ---
45
+
46
+ ## Philosophy-Driven Constraints
47
+
48
+ - `buildSystemPrompt()` must remain a pure, synchronous function (no I/O, no side effects)
49
+ - All `DaemonEvent` variants must use `readonly` fields only
50
+ - `IssueReportedEvent.issueKind` and `.severity` must be literal union types (not `string`)
51
+ - JSONL write must be fire-and-forget: `void appendIssueAsync().catch(() => {})`
52
+ - `mkdir({ recursive: true })` before every appendFile (handles missing dir silently)
53
+ - `issuesDirOverride` parameter for test isolation (mirrors DaemonEventEmitter constructor)
54
+
55
+ ---
56
+
57
+ ## Invariants
58
+
59
+ 1. `buildSystemPrompt()` is pure and synchronous -- verified by existing tests calling it directly
60
+ 2. `'You are WorkRail Auto'` is present in `buildSystemPrompt()` output -- verified by test L29
61
+ 3. `'## Your tools'` is present in `buildSystemPrompt()` output -- verified by test L30
62
+ 4. `'## Execution contract'` is present in `buildSystemPrompt()` output -- verified by test L32
63
+ 5. All `DaemonEvent` variants use `readonly` fields -- verified by TS compiler
64
+ 6. `DaemonEvent` union is exhaustive -- TS compiler enforces at every switch site
65
+ 7. `report_issue.execute()` never throws -- returns `AgentToolResult` always
66
+ 8. JSONL write never blocks `execute()` return -- `void` Promise
67
+
68
+ ---
69
+
70
+ ## Selected Approach + Rationale
71
+
72
+ **Part 1:** Module-private `BASE_SYSTEM_PROMPT` string constant defined above `buildSystemPrompt()`. The function uses it as the start of the lines array. Rationale: named constant is readable as a document; testable via `buildSystemPrompt()` output; follows `soul-template.ts` precedent for stable-content constants.
73
+
74
+ **Part 2:** `makeReportIssueTool(sessionId, emitter?, issuesDirOverride?)` inline tool factory following the exact shape of `makeReadTool`/`makeWriteTool`. Private `appendIssueAsync()` helper for JSONL write. `issuesDirOverride` for test isolation (hybrid of inline factory + runner-up's dirOverride). Rationale: YAGNI -- no IssueStore class until coordinator exists; hybrid resolves testability without over-engineering.
75
+
76
+ **Runner-up:** IssueStore class (Candidate B). Lost to YAGNI -- one caller, no coordinator yet.
77
+
78
+ ---
79
+
80
+ ## Vertical Slices
81
+
82
+ ### Slice 1: Create feature branch
83
+ - Create `feat/worktrain-system-prompt-and-report-issue` from current main
84
+ - Verify clean state
85
+
86
+ ### Slice 2: Add IssueReportedEvent to daemon-events.ts
87
+ - Add `IssueReportedEvent` interface
88
+ - Add to `DaemonEvent` union
89
+ - Verify TS compiles
90
+
91
+ ### Slice 3: Replace buildSystemPrompt() preamble
92
+ - Define `BASE_SYSTEM_PROMPT` constant above `buildSystemPrompt()`
93
+ - Replace lines 1087-1108 to use the constant
94
+ - Verify all existing system-prompt tests pass
95
+
96
+ ### Slice 4: Implement makeReportIssueTool
97
+ - Add private `appendIssueAsync()` helper
98
+ - Add `makeReportIssueTool()` factory
99
+ - Wire into `runWorkflow()` tools array
100
+
101
+ ### Slice 5: Tests
102
+ - Add tests for `makeReportIssueTool` -- verify JSONL write with temp dir, verify event emitted, verify return strings, verify fatal vs non-fatal
103
+ - Verify all existing tests still pass
104
+
105
+ ### Slice 6: Build + full test run
106
+ - `npm run build` -- zero errors
107
+ - `npx vitest run` -- all pass
108
+
109
+ ### Slice 7: PR
110
+ - Commit with conventional commit message
111
+ - Open PR to main
112
+
113
+ ---
114
+
115
+ ## Test Design
116
+
117
+ ### Existing tests (must pass unchanged)
118
+ - `tests/unit/workflow-runner-system-prompt.test.ts` -- all 11 tests
119
+ - `tests/unit/daemon-events.test.ts` -- all existing tests
120
+
121
+ ### New tests to add
122
+ File: `tests/unit/workflow-runner-report-issue.test.ts`
123
+
124
+ Test cases:
125
+ 1. `makeReportIssueTool` -- returns correct tool name and description
126
+ 2. `execute()` with non-fatal severity -- returns confirmation string with severity
127
+ 3. `execute()` with fatal severity -- returns FATAL message
128
+ 4. `execute()` -- writes JSON line to issuesDirOverride/<sessionId>.jsonl
129
+ 5. `execute()` -- written JSON contains kind, severity, summary, ts, sessionId
130
+ 6. `execute()` -- creates dir if it doesn't exist (mkdir recursive)
131
+ 7. `execute()` -- emits `issue_reported` event via emitter
132
+ 8. `execute()` -- optional fields (context, toolName, command, suggestedFix, continueToken) present in JSON when provided
133
+ 9. `execute()` -- does not throw when write fails (fire-and-forget)
134
+
135
+ ---
136
+
137
+ ## Risk Register
138
+
139
+ | Risk | Likelihood | Impact | Mitigation |
140
+ |---|---|---|---|
141
+ | BASE_SYSTEM_PROMPT missing required test strings | Low | High (CI break) | Include `'You are WorkRail Auto'`, `'## Your tools'`, `'## Execution contract'` explicitly; tests catch immediately |
142
+ | IssueReportedEvent `issueKind` vs tool input `kind` confusion | Low | Medium (runtime behavior ok, TS shape wrong) | Use `issueKind` in event interface; keep `kind` in input schema |
143
+ | Silent JSONL write failure not caught in tests | Low | Low (fire-and-forget is intentional) | issuesDirOverride isolates write path; test case #9 verifies no throw |
144
+ | Agent ignores fatal severity | Medium | Medium (tokens wasted) | Out of scope; coordinator detects post-hoc |
145
+
146
+ ---
147
+
148
+ ## PR Packaging Strategy
149
+
150
+ Single PR: `feat/worktrain-system-prompt-and-report-issue`
151
+ - All 3 files changed: `src/daemon/workflow-runner.ts`, `src/daemon/daemon-events.ts`, `tests/unit/workflow-runner-report-issue.test.ts`
152
+ - Commit message: `feat(console): richer daemon system prompt and report_issue tool`
153
+
154
+ Wait -- scope is `daemon`, not `console`. Correct commit message:
155
+ `feat(mcp): richer daemon system prompt and report_issue tool for auto-fix coordinator`
156
+
157
+ Actually these are daemon changes. The allowed scopes from CLAUDE.md are: `console`, `mcp`, `workflows`, `engine`, `schema`, `docs`. The daemon lives under `mcp` in this codebase (daemon is part of the WorkRail server). Use scope `mcp`.
158
+
159
+ ---
160
+
161
+ ## Philosophy Alignment Per Slice
162
+
163
+ ### Slice 2 (daemon-events.ts)
164
+ - Exhaustiveness everywhere -> satisfied (new union variant, TS enforces handling)
165
+ - Make illegal states unrepresentable -> satisfied (literal unions for issueKind/severity)
166
+ - Immutability by default -> satisfied (readonly fields)
167
+
168
+ ### Slice 3 (BASE_SYSTEM_PROMPT)
169
+ - Functional core, imperative shell -> satisfied (buildSystemPrompt remains pure)
170
+ - Immutability by default -> satisfied (const)
171
+ - Document why not what -> satisfied (JSDoc on constant)
172
+ - YAGNI with discipline -> satisfied (no speculative additions)
173
+
174
+ ### Slice 4 (makeReportIssueTool)
175
+ - Observability as a constraint -> satisfied (fire-and-forget, never blocks)
176
+ - Errors are data -> satisfied (execute() returns AgentToolResult, never throws)
177
+ - Prefer fakes over mocks -> satisfied (issuesDirOverride for tests)
178
+ - YAGNI with discipline -> satisfied (no IssueStore class)
179
+ - Exhaustiveness everywhere -> satisfied (return value handles all severity levels)
180
+
181
+ ### Slice 5 (tests)
182
+ - Prefer fakes over mocks -> satisfied (temp dir, no fs mocking)
183
+ - Determinism -> satisfied (all test writes go to unique temp dirs)
184
+
185
+ ---
186
+
187
+ ## Summary
188
+
189
+ - `estimatedPRCount`: 1
190
+ - `planConfidenceBand`: High
191
+ - `unresolvedUnknownCount`: 0
192
+ - `followUpTickets`: Extract IssueStore class when auto-fix coordinator is built
@@ -0,0 +1,146 @@
1
+ # Design: Workflow ID Validation at Daemon Startup
2
+
3
+ **Status:** Decision made -- implement Candidate A
4
+ **Date:** 2026-04-16
5
+ **Context:** Backlog item "Workflow ID validation at startup" (Tier 1, groomed Apr 18)
6
+
7
+ ---
8
+
9
+ ## Problem Understanding
10
+
11
+ ### The Bug
12
+
13
+ A user writes `workflowId: coding-task-workflow-agentic.lean.v2` (filename without extension) instead of `coding-task-workflow-agentic` (the actual workflow ID). The daemon starts fine, accepts webhooks, but every dispatch silently fails with `workflow_not_found`. The error only surfaces in logs, not at startup. The operator has no way to know their trigger is broken until they watch logs during an actual webhook event.
14
+
15
+ ### Core Tensions
16
+
17
+ 1. **Testability vs. production simplicity** -- `ctx.workflowService.getWorkflowById` is available in production but tests use `FAKE_CTX = {} as V2ToolContext` where `workflowService` is `undefined`. Requires an injectable function approach, not direct ctx access.
18
+ 2. **Warn+skip consistency vs. fail-fast** -- `loadTriggerConfig` already chose warn+skip for invalid triggers. A hard-fail here would create two conflicting behaviors in the same startup path.
19
+ 3. **Where to wire the lookup** -- `StartTriggerListenerOptions` injectable (matches existing `runWorkflowFn` pattern) vs. direct `ctx` access.
20
+
21
+ ### Likely Seam
22
+
23
+ `startTriggerListener` in `src/trigger/trigger-listener.ts`, after `buildTriggerIndex()` returns ok (~line 235), before `new TriggerRouter(...)`. This is the correct seam -- triggers are loaded and indexed, but no webhooks can arrive yet.
24
+
25
+ ### What Makes This Hard
26
+
27
+ - `FAKE_CTX = {} as V2ToolContext` in tests -- direct `ctx.workflowService` use breaks existing test infrastructure without any compile-time warning.
28
+ - Need to decide what happens when `getWorkflowByIdFn` is not provided (backward compat: skip validation entirely).
29
+ - Workflows are static YAML files -- if not found at startup, they will never be found at dispatch time either. No "not found now, maybe later" case exists.
30
+
31
+ ---
32
+
33
+ ## Philosophy Constraints
34
+
35
+ **Sources:**
36
+ - `/Users/etienneb/CLAUDE.md`: "Dependency injection for boundaries -- inject external effects (I/O, clocks, randomness) to keep core logic testable"
37
+ - `/Users/etienneb/CLAUDE.md`: "Validate at boundaries, trust inside -- do input validation at system edges"
38
+ - Repo pattern: `runWorkflowFn?: RunWorkflowFn` in `StartTriggerListenerOptions` -- exact injectable pattern to follow
39
+ - Repo pattern: `loadTriggerConfig` warn+skip -- policy to remain consistent with
40
+
41
+ **No conflicts.** All sources agree on: DI injectable for testability, warn+skip policy, validate at the startup boundary.
42
+
43
+ ---
44
+
45
+ ## Impact Surface
46
+
47
+ - **`src/trigger/trigger-listener.ts`** -- primary change. New validation loop and new `StartTriggerListenerOptions` field.
48
+ - **`tests/unit/trigger-router.test.ts`** -- add new test cases. Existing tests unaffected (they don't provide `getWorkflowByIdFn`, so validation is skipped -- same behavior as today).
49
+ - **`src/trigger/trigger-router.ts`** -- no change. Router already handles `workflow_not_found` at dispatch; this is an earlier defense layer.
50
+ - **`src/trigger/trigger-store.ts`** -- no change. YAML parsing is separate from workflow ID resolution.
51
+ - **`src/trigger/types.ts`** -- no change. `TriggerDefinition` shape unchanged.
52
+
53
+ ---
54
+
55
+ ## Candidates
56
+
57
+ ### Candidate A -- Injectable function on StartTriggerListenerOptions (RECOMMENDED)
58
+
59
+ **Summary:** Add `getWorkflowByIdFn?: (id: string) => Promise<boolean>` to `StartTriggerListenerOptions`. Production path defaults to `(id) => ctx.workflowService.getWorkflowById(id).then(w => w !== null)`. When not provided, validation is skipped (backward compat for existing tests).
60
+
61
+ **Tensions resolved:** Testability (tests inject stub), warn+skip consistency, DI principle.
62
+ **Tensions accepted:** Slight verbosity (new option field). Validation silently skipped if fn not provided (intentional).
63
+
64
+ **Boundary:** `startTriggerListener`, after `buildTriggerIndex()` returns ok.
65
+ **Why this boundary:** Single assembly point before the router accepts any traffic. Earlier (store layer) would require making `loadTriggerConfig` async. Later (dispatch time) is too late -- that's the bug we're fixing.
66
+
67
+ **Failure mode:** Existing tests that don't inject `getWorkflowByIdFn` silently skip validation. This is intentional backward compat, not a latent bug -- they still test all other startup behavior.
68
+
69
+ **Repo pattern:** Exact match to `runWorkflowFn?: RunWorkflowFn` in the same `StartTriggerListenerOptions` interface.
70
+
71
+ **Gains:** Full testability, no changes to existing tests, clean DI seam, consistent with all philosophy principles.
72
+ **Losses:** Caller must inject the fn to get validation. If someone creates a new caller of `startTriggerListener` without providing it, they get no validation. (Low risk: only one production caller.)
73
+
74
+ **Scope judgment:** Best-fit. Changes only `trigger-listener.ts` and adds tests. No interface changes to store or router.
75
+
76
+ **Philosophy fit:** Honors "Dependency injection for boundaries", "Validate at boundaries, trust inside". No conflicts.
77
+
78
+ ---
79
+
80
+ ### Candidate B -- Use ctx.workflowService directly with null guard
81
+
82
+ **Summary:** Call `ctx.workflowService?.getWorkflowById(id)` directly in the validation loop, skipping the whole loop if `ctx.workflowService` is undefined.
83
+
84
+ **Tensions resolved:** Production simplicity (no new option field).
85
+ **Tensions accepted:** Testability gap -- the warn+skip behavior can't be tested without constructing a real `workflowService` in `ctx`.
86
+
87
+ **Failure mode:** New validation behavior is untestable with the existing `FAKE_CTX` test infrastructure.
88
+
89
+ **Repo pattern:** Departs from `runWorkflowFn` injectable pattern. Conflicts with DI principle.
90
+
91
+ **Scope judgment:** Best-fit for production behavior, too narrow for test coverage.
92
+
93
+ **Philosophy fit:** Conflicts with "Dependency injection for boundaries".
94
+
95
+ ---
96
+
97
+ ### Candidate C -- Validate inside loadTriggerConfig (store layer)
98
+
99
+ **Summary:** Add `workflowResolver?: (id: string) => Promise<boolean>` to `loadTriggerConfig`, filtering unknown workflowId triggers at parse time.
100
+
101
+ **Tensions resolved:** Centralizes all trigger validation.
102
+ **Tensions accepted:** `trigger-store.ts` is a pure synchronous YAML parser; making it async for the resolver breaks its pure/impure boundary and all existing sync call sites.
103
+
104
+ **Failure mode:** Breaks `loadTriggerConfig`'s synchronous interface contract. All existing callers would need updating.
105
+
106
+ **Repo pattern:** Departs from the pure-sync design of `trigger-store.ts`.
107
+
108
+ **Scope judgment:** Too broad -- adds async I/O to a pure parsing module with no justification beyond this feature.
109
+
110
+ **Philosophy fit:** Conflicts with "Compose with small, pure functions".
111
+
112
+ ---
113
+
114
+ ## Comparison and Recommendation
115
+
116
+ | Tension | A (Injectable) | B (ctx direct) | C (store layer) |
117
+ |---------|---------------|----------------|-----------------|
118
+ | Testability | Wins | Loses | N/A |
119
+ | Warn+skip consistency | Wins | Wins | Breaks pure boundary |
120
+ | DI principle | Honors | Conflicts | Conflicts |
121
+ | Repo pattern fit | Exact match | Departs | Departs |
122
+ | Reversibility | Easy | Easy | Hard |
123
+
124
+ **Recommendation: Candidate A.** It resolves all tensions, is a direct repo-pattern match, requires minimal code change, and leaves all existing tests unchanged.
125
+
126
+ ---
127
+
128
+ ## Self-Critique
129
+
130
+ **Strongest counter-argument:** "Why add a new option when `ctx.workflowService` is already there? That's extra API surface for a one-time startup check." -- Response: `FAKE_CTX = {} as V2ToolContext` (line 33, `trigger-router.test.ts`) means `ctx.workflowService` is `undefined` at test runtime. Without the injectable, the new validation behavior is untestable. Fixing a silent-failure bug without being able to test it is unacceptable.
131
+
132
+ **Narrower option that lost:** Candidate B (ctx direct with null guard). Loses because new behavior is untestable.
133
+
134
+ **Broader option that would need evidence:** Candidate C (store layer) would be justified if multiple callers of `loadTriggerConfig` needed workflow ID validation -- but there is only one production caller. The scope increase is not warranted.
135
+
136
+ **Invalidating assumption:** If `FAKE_CTX` were replaced by a real mock with a `workflowService`, Candidate B would be equally valid. But that's a larger test infrastructure change that's out of scope.
137
+
138
+ ---
139
+
140
+ ## Open Questions for the Main Agent
141
+
142
+ None. All design decisions are resolved. Implementation is straightforward:
143
+ 1. Add `getWorkflowByIdFn?: (id: string) => Promise<boolean>` to `StartTriggerListenerOptions`
144
+ 2. After `buildTriggerIndex()` returns ok, if `getWorkflowByIdFn` is provided, iterate `triggerIndex`, call fn for each `workflowId`, warn and delete unknowns
145
+ 3. Production default (when fn not provided): use `ctx.workflowService.getWorkflowById(id).then(w => w !== null)`
146
+ 4. Add test cases for: warn+skip on unknown workflowId, valid workflowId passes through, fn not provided skips validation