@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.
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-await.js +11 -9
- package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
- package/dist/cli/commands/worktrain-daemon-install.js +291 -0
- package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
- package/dist/cli/commands/worktrain-daemon.js +272 -0
- package/dist/cli/commands/worktrain-spawn.js +11 -9
- package/dist/cli-worktrain.js +488 -0
- package/dist/cli.js +1 -22
- package/dist/console/standalone-console.d.ts +28 -0
- package/dist/console/standalone-console.js +142 -0
- package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-C1JXnwZS.js} +1 -1
- package/dist/{console → console-ui}/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +27 -0
- package/dist/daemon/agent-loop.js +39 -1
- package/dist/daemon/daemon-events.d.ts +63 -1
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +285 -46
- package/dist/infrastructure/session/HttpServer.js +133 -34
- package/dist/manifest.json +136 -104
- package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
- package/dist/mcp/handlers/v2-error-mapping.js +2 -0
- package/dist/mcp/handlers/v2-execution/advance.js +25 -0
- package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
- package/dist/mcp/output-schemas.d.ts +30 -30
- package/dist/mcp/transports/fatal-exit.js +4 -0
- package/dist/mcp/transports/http-entry.js +0 -5
- package/dist/mcp/transports/stdio-entry.js +24 -12
- package/dist/mcp/v2/tools.d.ts +4 -4
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +1 -42
- package/dist/trigger/adapters/github-poller.d.ts +44 -0
- package/dist/trigger/adapters/github-poller.js +190 -0
- package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
- package/dist/trigger/adapters/gitlab-poller.js +81 -0
- package/dist/trigger/index.d.ts +4 -1
- package/dist/trigger/index.js +5 -1
- package/dist/trigger/polled-event-store.d.ts +22 -0
- package/dist/trigger/polled-event-store.js +173 -0
- package/dist/trigger/polling-scheduler.d.ts +20 -0
- package/dist/trigger/polling-scheduler.js +249 -0
- package/dist/trigger/trigger-listener.d.ts +3 -0
- package/dist/trigger/trigger-listener.js +47 -3
- package/dist/trigger/trigger-store.js +114 -33
- package/dist/trigger/types.d.ts +17 -1
- package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
- package/dist/v2/durable-core/domain/observation-builder.js +2 -2
- package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
- package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
- package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
- package/dist/v2/usecases/console-routes.js +3 -3
- package/dist/v2/usecases/console-service.js +185 -10
- package/dist/v2/usecases/console-types.d.ts +8 -0
- package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
- package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
- package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
- package/docs/design/daemon-conversation-logging-plan.md +98 -0
- package/docs/design/daemon-conversation-logging-review.md +55 -0
- package/docs/design/daemon-conversation-logging.md +129 -0
- package/docs/design/github-polling-adapter-design-candidates.md +226 -0
- package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
- package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
- package/docs/design/implementation_plan.md +192 -0
- package/docs/design/workflow-id-validation-at-startup.md +146 -0
- package/docs/design/workflow-id-validation-design-review.md +87 -0
- package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
- package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
- package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
- package/docs/discovery/design-candidates.md +180 -0
- package/docs/discovery/design-review-findings.md +110 -0
- package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
- package/docs/ideas/backlog.md +627 -0
- package/package.json +1 -1
- package/workflows/architecture-scalability-audit.json +1 -1
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/coding-task-workflow-agentic.json +32 -32
- package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
- package/workflows/coding-task-workflow-agentic.v2.json +7 -7
- package/workflows/mr-review-workflow.agentic.v2.json +21 -12
- package/workflows/personal-learning-materials-creation-branched.json +2 -2
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +2 -2
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-for-workflows.json +3 -3
- package/workflows/workflow-for-workflows.v2.json +2 -2
- package/workflows/wr.discovery.json +59 -8
- package/dist/mcp/transports/bridge-entry.d.ts +0 -102
- package/dist/mcp/transports/bridge-entry.js +0 -454
- package/dist/mcp/transports/bridge-events.d.ts +0 -51
- package/dist/mcp/transports/bridge-events.js +0 -24
- package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
- package/dist/mcp/transports/primary-tombstone.js +0 -51
- /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
|