@exaudeus/workrail 3.42.0 → 3.44.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 (36) hide show
  1. package/dist/console-ui/assets/{index-DwfWMKvv.js → index-Bi38ITiQ.js} +1 -1
  2. package/dist/console-ui/index.html +1 -1
  3. package/dist/daemon/workflow-runner.d.ts +15 -1
  4. package/dist/daemon/workflow-runner.js +86 -9
  5. package/dist/manifest.json +39 -23
  6. package/dist/trigger/adapters/github-queue-poller.d.ts +34 -0
  7. package/dist/trigger/adapters/github-queue-poller.js +200 -0
  8. package/dist/trigger/delivery-action.d.ts +2 -0
  9. package/dist/trigger/delivery-action.js +24 -0
  10. package/dist/trigger/github-queue-config.d.ts +18 -0
  11. package/dist/trigger/github-queue-config.js +155 -0
  12. package/dist/trigger/polling-scheduler.d.ts +1 -0
  13. package/dist/trigger/polling-scheduler.js +185 -6
  14. package/dist/trigger/trigger-router.js +24 -1
  15. package/dist/trigger/trigger-store.js +77 -2
  16. package/dist/trigger/types.d.ts +19 -0
  17. package/docs/design/adaptive-coordinator-context-candidates.md +265 -0
  18. package/docs/design/adaptive-coordinator-context-review.md +101 -0
  19. package/docs/design/adaptive-coordinator-context.md +504 -0
  20. package/docs/design/adaptive-coordinator-routing-candidates.md +340 -0
  21. package/docs/design/adaptive-coordinator-routing-design-review.md +135 -0
  22. package/docs/design/adaptive-coordinator-routing-review.md +156 -0
  23. package/docs/design/adaptive-coordinator-routing.md +660 -0
  24. package/docs/design/context-assembly-layer-design-review.md +110 -0
  25. package/docs/design/context-assembly-layer.md +622 -0
  26. package/docs/design/stuck-escalation-candidates.md +176 -0
  27. package/docs/design/stuck-escalation-design-review.md +70 -0
  28. package/docs/design/stuck-escalation.md +326 -0
  29. package/docs/design/worktrain-task-queue-candidates.md +252 -0
  30. package/docs/design/worktrain-task-queue-design-review.md +109 -0
  31. package/docs/design/worktrain-task-queue.md +443 -0
  32. package/docs/design/worktree-review-findings-candidates.md +101 -0
  33. package/docs/design/worktree-review-findings-design-review.md +65 -0
  34. package/docs/design/worktree-review-findings-implementation-plan.md +153 -0
  35. package/docs/ideas/backlog.md +148 -0
  36. package/package.json +3 -3
@@ -0,0 +1,622 @@
1
+ # Context Assembly Layer -- Discovery
2
+
3
+ **Status:** Discovery complete (wr.discovery workflow)
4
+ **Date:** 2026-04-19
5
+ **Workspace scope:** WorkTrain ONLY (`src/daemon/`, `src/trigger/`, `src/coordinators/`, `src/cli/`). NOT the WorkRail MCP server (`src/mcp/`).
6
+
7
+ ---
8
+
9
+ ## Context / Ask
10
+
11
+ ### Stated goal (original framing)
12
+
13
+ Design the context assembly layer for WorkTrain -- the missing abstraction between the trigger/dispatch layer and the orchestration (coordinator) layer.
14
+
15
+ ### Problem statement (reframed)
16
+
17
+ WorkTrain sessions start with insufficient context: a raw goal string plus sparse payload fields from `contextMapping` dot-path extraction. Agents repeatedly rediscover information that is statically knowable before spawning -- repo conventions (`CLAUDE.md`/`AGENTS.md`), upstream specs (pitch/PRD/BRD), affected files (git diff), and prior session outcomes -- wasting turns, missing conventions, and ignoring prior work.
18
+
19
+ ### Desired outcome
20
+
21
+ A concrete design for a `ContextAssembler` abstraction that:
22
+ 1. Can be called before spawning any WorkTrain session
23
+ 2. Returns a typed context bundle consumable by multiple coordinators
24
+ 3. Plugs into the existing `WorkflowTrigger` / `CoordinatorDeps` architecture without breaking existing code
25
+ 4. Is concrete enough to feed directly into wr.shaping
26
+
27
+ ### What is NOT in scope
28
+
29
+ - WorkRail MCP server changes (`src/mcp/`)
30
+ - Any changes to the workflow engine (`src/v2/durable-core/`)
31
+ - Production implementation (this is discovery/design only)
32
+ - Knowledge graph implementation (deferred until this design is complete, per Apr 19 backlog decision)
33
+
34
+ ---
35
+
36
+ ## Path Recommendation
37
+
38
+ **Selected path:** `full_spectrum`
39
+
40
+ **Rationale:** The goal was a solution statement (pre-framed as "context assembly layer"). `design_first` would risk designing an abstraction in isolation from the actual landscape. `landscape_first` alone would miss the reframing work. `full_spectrum` grounds the design in the existing codebase structure, validates the proposed abstraction against alternatives, and produces a concrete enough design to hand to wr.shaping.
41
+
42
+ The stated solution (context assembly layer) is likely the right answer for the god-class / separation-of-concerns problem, but the interface shape, integration point, and v1 source set are genuinely uncertain and require landscape work.
43
+
44
+ ---
45
+
46
+ ## Constraints / Anti-goals
47
+
48
+ ### Hard constraints
49
+
50
+ - Must not require changes to the WorkRail engine or MCP server
51
+ - Must be injectable via `CoordinatorDeps` pattern (no direct I/O in coordinator core)
52
+ - Must not add `ts-morph` or DuckDB to production build in v1 (knowledge graph is a v2 source)
53
+ - Must be usable by `pr-review.ts` coordinator without breaking its existing `CoordinatorDeps` interface
54
+
55
+ ### Anti-goals
56
+
57
+ - Do NOT add context-gathering logic directly to `pr-review.ts` or any other coordinator script (the god-class anti-pattern)
58
+ - Do NOT require a new daemon process or network service for context assembly
59
+ - Do NOT couple the assembler to a specific trigger type (it must work for webhook triggers and polling triggers)
60
+ - Do NOT implement context window trimming/prioritization in v1 (200K context window is not the binding constraint)
61
+
62
+ ---
63
+
64
+ ## Landscape Packet
65
+
66
+ ### Current state: what WorkTrain sessions receive today
67
+
68
+ The `WorkflowTrigger` interface (`src/daemon/workflow-runner.ts`):
69
+ ```typescript
70
+ export interface WorkflowTrigger {
71
+ readonly workflowId: string;
72
+ readonly goal: string;
73
+ readonly workspacePath: string;
74
+ readonly context?: Readonly<Record<string, unknown>>; // from contextMapping dot-paths
75
+ readonly referenceUrls?: readonly string[]; // static URLs -> system prompt
76
+ readonly agentConfig?: { model?, maxSessionMinutes?, maxTurns?, maxSubagentDepth? };
77
+ readonly soulFile?: string;
78
+ }
79
+ ```
80
+
81
+ ### What the daemon already does at session startup
82
+
83
+ Critically, `workflow-runner.ts` already performs context loading before the first LLM turn:
84
+
85
+ 1. **CLAUDE.md / AGENTS.md** -- `loadWorkspaceContext(workspacePath)` scans for `.claude/CLAUDE.md`, `CLAUDE.md`, `AGENTS.md`, `.github/AGENTS.md` in order and injects them into the system prompt under `## Workspace Context`. Cap: 32 KB combined. This runs for EVERY session automatically.
86
+ 2. **Soul file** -- `loadDaemonSoul(trigger.soulFile)` loads the daemon's persona/rules. Cascade: trigger YAML soulFile -> workspace soulFile -> global `~/.workrail/daemon-soul.md`.
87
+ 3. **Prior step notes (same session)** -- `loadSessionNotes(startContinueToken)` reads step notes from the CURRENT session. For fresh sessions, returns `[]`. For checkpoint-resumed sessions, provides continuity notes.
88
+ 4. **referenceUrls** -- static URLs from `TriggerDefinition.referenceUrls` are appended to the system prompt as a "Before starting, fetch these documents" instruction.
89
+
90
+ These are all loaded in parallel (`Promise.all`) before the `Agent` is constructed.
91
+
92
+ ### Current injection points in `TriggerDefinition` (trigger-level config)
93
+
94
+ 1. **`referenceUrls`** -- STATIC URL list baked into triggers.yml at configure time. Agent is instructed to fetch them. Covers: known-ahead-of-time upstream docs. Does NOT cover: dynamic URLs from the webhook payload (e.g. PR description body may link to a design doc).
95
+ 2. **`contextMapping`** -- dot-path extraction from webhook payload at dispatch time. Maps payload fields to workflow context variables (accessible as `context.mrTitle`, `context.prNumber`, etc. in the goal template). Covers: PR metadata. Does NOT cover: repo state or cross-session history.
96
+ 3. **`goalTemplate`** -- Mustache substitution into the goal string. Covers: specific goal wording. Does NOT cover: structured context injection.
97
+
98
+ ### Current coordinator: what `pr-review.ts` spawns
99
+
100
+ `runPrReviewCoordinator` -> `spawnSession(workflowId, goal, workspace)` where:
101
+ - `workflowId`: `'mr-review-workflow-agentic'`
102
+ - `goal`: `'Review PR #N "title" before merge'`
103
+ - `workspace`: absolute path
104
+
105
+ The review agent gets: goal string, workspace path, CLAUDE.md injection (automatic), referenceUrls (if configured in triggers.yml). It does NOT get: the PR diff, the PR description body, affected file list, or prior review sessions for the same PR.
106
+
107
+ ### What is actually missing (revised after code reading)
108
+
109
+ CLAUDE.md injection was listed as a gap in the problem statement -- this was WRONG. The daemon already handles it. The real gaps are:
110
+
111
+ 1. **Git state (per-task)** -- no diff summary, no affected file list, no branch/commit context. This is dynamic: different PRs have different diffs. Cannot be baked into triggers.yml.
112
+ 2. **Dynamic upstream docs** -- referenceUrls in triggers.yml are static. If a GitHub issue body links to a design doc, the agent does not see it. The link exists in the webhook payload but contextMapping cannot fetch and embed document content.
113
+ 3. **Cross-session prior notes** -- `loadSessionNotes` loads prior notes for the SAME session (checkpoint resume). It does NOT load notes from PRIOR sessions on the same workspace/task (e.g., a previous review of the same PR). A second review starts completely cold.
114
+ 4. **Structured task metadata** -- the goal string is free-form text. There is no typed struct for "this is a PR review for PR #42, branch feature/foo, by author alice, with 3 affected files". Agents parse the goal string rather than reading typed fields.
115
+ 5. **No coordinator reuse** -- if two coordinators both need "git diff summary for the workspace" before spawning, each must independently implement the I/O. No shared assembly layer exists.
116
+
117
+ ### What the knowledge graph spike produced
118
+
119
+ `src/knowledge-graph/` exists (DuckDB in-memory + ts-morph indexer). NOT wired to any tool. ts-morph is in devDependencies. This is the future dynamic source for "what files import the file being changed" -- NOT a v1 source.
120
+
121
+ ### Session summary provider: the prior-session lookup infrastructure
122
+
123
+ `src/v2/infra/local/session-summary-provider/index.ts` implements `LocalSessionSummaryProviderV2`. It:
124
+ - Enumerates sessions from disk (most-recently-modified first)
125
+ - Projects health, run DAG, recap snippets, workspace observations (git branch/SHA)
126
+ - Returns typed `HealthySessionSummary[]` with `recapSnippet`, `sessionTitle`, `observations.gitBranch`
127
+
128
+ This infrastructure exists and is used by the MCP server (resume_session, session listing in console). It is NOT currently used by coordinators or by the context assembly layer. The data needed for "prior session notes for this workspace" already exists -- it just isn't wired to the dispatch path.
129
+
130
+ ### TriggerRouter dispatch flow
131
+
132
+ `route(event)` in `trigger-router.ts`:
133
+ 1. Look up trigger by ID
134
+ 2. Validate HMAC
135
+ 3. Apply `contextMapping` (dot-path extract from payload -> `workflowContext`)
136
+ 4. Interpolate `goalTemplate` (or use static `goal`)
137
+ 5. Build `WorkflowTrigger` = `{ workflowId, goal, workspacePath, context: workflowContext, referenceUrls, agentConfig, soulFile }`
138
+ 6. Enqueue `runWorkflowFn(workflowTrigger, ...)` asynchronously
139
+
140
+ Context assembly would fit between steps 4 and 5 -- after goal and payload are known, before `WorkflowTrigger` is built. This is the natural integration point.
141
+
142
+ ---
143
+
144
+ ## Problem Frame Packet
145
+
146
+ ### Root cause analysis
147
+
148
+ The gap is not "agents lack context" generically. The specific root cause is:
149
+
150
+ **WorkTrain's dispatch path builds `WorkflowTrigger` from a static trigger config plus a webhook payload, but has no mechanism to enrich it with runtime workspace state.**
151
+
152
+ The trigger system is designed to be stateless and fast (it must return 202 immediately). Context assembly is inherently stateful (reads disk, runs git commands, queries session store). These two concerns must be decoupled: the trigger system dispatches, a separate assembler enriches.
153
+
154
+ ### Forces at play
155
+
156
+ 1. **Coordinator specificity** -- `pr-review.ts` knows it's reviewing a PR and could in principle run `gh pr view --json body,diff` before spawning. But this would be coordinator-specific assembly that duplicates when a coding coordinator also needs git state.
157
+ 2. **Dependency direction** -- coordinators currently live above the dispatch layer. Context assembly must live at the same level or below so coordinators can delegate to it without knowing its implementation.
158
+ 3. **Failure isolation** -- context assembly failure (e.g., git command fails, session store unavailable) must never block session dispatch. The session should start with partial context rather than not starting at all.
159
+ 4. **Testability** -- coordinators already use `CoordinatorDeps` for DI. Context assembly must follow the same pattern or it becomes untestable.
160
+
161
+ ### The integration point question (primary uncertainty)
162
+
163
+ Three candidate integration points:
164
+
165
+ **Option A: In `TriggerRouter.route()` before building `WorkflowTrigger`**
166
+ - Pros: centralised; all workflows get enriched context automatically; no per-coordinator work
167
+ - Cons: `route()` must return 202 immediately; async context assembly would delay the response; context assembly for a PR trigger is useless for a generic webhook trigger that runs a different kind of task
168
+
169
+ **Option B: In each coordinator before `spawnSession()`**
170
+ - Pros: coordinator knows the task type and can request the right context; assembly is per-task
171
+ - Cons: assembly logic would be duplicated across coordinators if not extracted
172
+
173
+ **Option C: As a `ContextAssembler` service injected into coordinator deps**
174
+ - Pros: decoupled (coordinator calls `assembleContext(task, workspace)` and gets a bundle); reusable across coordinators; testable with fakes
175
+ - Cons: requires adding `assembleContext` to `CoordinatorDeps` or as a separate injectable
176
+
177
+ **Verdict:** Option C. The coordinator calls `assembleContext(task, workspace, triggerMetadata)` and receives a typed bundle. This keeps the TriggerRouter fast (no I/O delay) while making assembly reusable and testable.
178
+
179
+ ### Stakeholders / users
180
+
181
+ | Stakeholder | Job to be done | Pain today |
182
+ |---|---|---|
183
+ | pr-review coordinator | Start a review session with enough context that the agent reviews intelligently | Agent re-reads the PR description from GitHub, missing context from issue body; first review turn is setup |
184
+ | Future coding coordinator | Start a coding session with file-level context so the agent starts on the right files | Agent runs `find` and `grep` for the first 3-5 turns to discover where code lives |
185
+ | WorkTrain operator (Etienne) | Configure once, get useful sessions consistently | Must manually put every relevant URL into triggers.yml referenceUrls |
186
+ | WorkTrain daemon | Build `WorkflowTrigger` with enriched context without I/O blocking | No mechanism for per-dispatch enrichment today |
187
+
188
+ ### Pains and tensions
189
+
190
+ 1. **Tension: coordinator knows task type vs. generic assembly** -- A PR review coordinator knows it can run `gh pr view --json body,diff`. A generic `assembleContext()` doesn't know what sources are relevant. Resolution: typed `AssemblyTask` input discriminated by task kind.
191
+ 2. **Tension: failure isolation vs. completeness** -- Assembly that partially fails (git command unavailable, session store offline) must not block dispatch. But partial context may be worse than no context if the agent is misled by stale data.
192
+ 3. **Pain: `referenceUrls` are configure-time only** -- dynamic upstream docs (PR issue body, linked design doc) require fetching at dispatch time. Static config cannot cover this.
193
+ 4. **Pain: cross-session memory is invisible** -- If PR #42 was reviewed last week and found 3 issues, the next review session has no idea. The session summary infrastructure exists but isn't wired to dispatch.
194
+
195
+ ### HMW questions
196
+
197
+ 1. **HMW make context assembly a first-class step in the coordinator pipeline** without requiring every coordinator to independently implement I/O?
198
+ 2. **HMW express task type (PR review vs. coding task vs. issue triage) as a typed input** to assembly so the assembler knows which sources to query?
199
+
200
+ ### Primary framing risk
201
+
202
+ **The framing assumes agents are context-starved because the assembler doesn't exist.** The specific condition that would make this framing wrong: if an audit of live session logs shows that WorkTrain agents for the current pr-review workflow already spend fewer than 2 turns on setup (because CLAUDE.md is injected and referenceUrls covers the relevant docs), then the gap is primarily limited to cross-session memory -- and the right solution is a lightweight "prior session notes" injection rather than a full context assembly layer with typed task sources. In that case, a much simpler `PriorSessionContext` struct injected via `WorkflowTrigger.context['priorSessionNotes']` would suffice.
203
+
204
+ ---
205
+
206
+ ## Candidate Directions
207
+
208
+ ### Problem Understanding (tensions, seam, what makes it hard)
209
+
210
+ **Core tensions:**
211
+ 1. Generic interface vs. task-specific sources -- `assembleContext()` needs to know what to fetch, but only the coordinator knows the task type. Typed `AssemblyTask` discriminated union resolves this.
212
+ 2. Failure isolation vs. typed bundle -- a single `Result<ContextBundle, Error>` fails entirely if any source fails. Per-field `Result<T, string>` is correct but verbose.
213
+ 3. WorkflowTrigger injection boundary -- assembled bundle must reach `buildSystemPrompt()` in the daemon without changing the engine. Options: new optional field on WorkflowTrigger (cleanest), string in context map (no schema change), or append to goal string (ugliest).
214
+ 4. YAGNI vs. extensibility -- philosophy demands no speculation, but the backlog explicitly names knowledge graph as a v2 plugin. The interface must have a clear extension point without implementing v2.
215
+
216
+ **What makes it hard:**
217
+ - `buildSystemPrompt()` in `workflow-runner.ts` currently takes `workspaceContext: string | null`. Injecting structured context requires either changing this signature (engine boundary) or serializing the bundle to string before crossing.
218
+ - `CoordinatorDeps.spawnSession` is a narrow 3-argument interface. Passing richer context requires either extending it or encoding the bundle into existing fields.
219
+ - Per-source failure tracking with readonly field types is verbose but necessary for type safety.
220
+
221
+ **Real seam:** Between coordinator's `spawnSession()` call and the daemon's `buildSystemPrompt()`. Context must be assembled by the coordinator (it knows the task), passed through WorkflowTrigger, and consumed by the daemon.
222
+
223
+ ---
224
+
225
+ ### Candidate A: Minimal enriched WorkflowTrigger fields
226
+
227
+ **Summary:** Add one optional `assembledContext` field to `WorkflowTrigger`; populate it in the coordinator via a single `assembleTaskContext` injectable function in `CoordinatorDeps`; `buildSystemPrompt()` adds one new section.
228
+
229
+ ```typescript
230
+ // Addition to WorkflowTrigger in workflow-runner.ts
231
+ interface WorkflowTrigger {
232
+ // ... existing fields ...
233
+ readonly assembledContext?: {
234
+ readonly gitDiff?: string;
235
+ readonly priorSessionNotes?: readonly string[];
236
+ readonly dynamicReferenceUrls?: readonly string[];
237
+ readonly sourceErrors?: Readonly<Record<string, string>>; // source -> error message
238
+ };
239
+ }
240
+
241
+ // Addition to CoordinatorDeps
242
+ interface CoordinatorDeps {
243
+ // ... existing deps ...
244
+ readonly assembleTaskContext: (opts: {
245
+ readonly workspacePath: string;
246
+ readonly prNumber?: number;
247
+ readonly payloadBody?: string;
248
+ }) => Promise<WorkflowTrigger['assembledContext']>;
249
+ }
250
+ ```
251
+
252
+ **Tensions resolved:** Failure isolation (sourceErrors map), WorkflowTrigger injection (new optional field), minimal code.
253
+ **Tensions accepted:** Multi-coordinator reuse (assembleTaskContext is per-coordinator unless extracted manually), task-type routing (opts struct is not discriminated -- coordinator still decides what to request).
254
+ **Failure mode to watch:** `buildSystemPrompt()` grows ad-hoc injection blocks as sources expand.
255
+ **Repo-pattern relationship:** Follows existing WorkflowTrigger optional field pattern (soulFile, agentConfig). Adapts CoordinatorDeps.
256
+ **Gains:** Minimal code, no new module, backward-compatible.
257
+ **Losses:** sourceErrors is stringly-typed (key names are unchecked). Not a reusable service.
258
+ **Scope judgment:** Best-fit for v1 with single coordinator. Too narrow once a second coordinator needs the same sources.
259
+ **Philosophy fit:** Honors YAGNI, DI. Conflicts with 'make illegal states unrepresentable' (sourceErrors Record is stringly typed).
260
+
261
+ ---
262
+
263
+ ### Candidate B: ContextAssembler service with typed AssemblyTask input (RECOMMENDED)
264
+
265
+ **Summary:** A `ContextAssembler` service lives in `src/context-assembly/`; accepts a typed `AssemblyTask` discriminated union; returns a `ContextBundle` with per-field `Result` values; injected into `CoordinatorDeps`; bundle is serialized to a context summary string and passed via `WorkflowTrigger.context['assembledContextSummary']`; `buildSystemPrompt()` is NOT changed.
266
+
267
+ ```typescript
268
+ // src/context-assembly/types.ts
269
+
270
+ export type AssemblyTask =
271
+ | { readonly kind: 'pr_review'; readonly prNumber: number; readonly workspacePath: string; readonly payloadBody?: string }
272
+ | { readonly kind: 'coding_task'; readonly issueNumber?: number; readonly workspacePath: string; readonly payloadBody?: string };
273
+
274
+ export interface ContextBundle {
275
+ readonly task: AssemblyTask;
276
+ readonly gitDiff: Result<string, string>;
277
+ readonly priorSessionNotes: Result<readonly string[], string>;
278
+ readonly dynamicReferenceUrls: Result<readonly string[], string>;
279
+ readonly assembledAt: string; // ISO timestamp
280
+ }
281
+
282
+ export interface ContextAssembler {
283
+ assemble(task: AssemblyTask): Promise<ContextBundle>;
284
+ }
285
+
286
+ // src/context-assembly/deps.ts
287
+ export interface ContextAssemblerDeps {
288
+ readonly execGit: (args: readonly string[], cwd: string) => Promise<Result<string, string>>;
289
+ readonly listRecentSessions: (workspacePath: string, limit: number) => Promise<Result<readonly SessionNote[], string>>;
290
+ readonly extractUrlsFromText: (text: string) => readonly string[];
291
+ }
292
+
293
+ // src/context-assembly/index.ts
294
+ export function createContextAssembler(deps: ContextAssemblerDeps): ContextAssembler;
295
+
296
+ // Pure rendering function
297
+ export function renderContextBundle(bundle: ContextBundle): string;
298
+ // -> produces markdown string injected into WorkflowTrigger.context['assembledContextSummary']
299
+ ```
300
+
301
+ **Integration in coordinator:**
302
+ ```typescript
303
+ // CoordinatorDeps gains:
304
+ readonly contextAssembler: ContextAssembler;
305
+
306
+ // Before spawnSession():
307
+ const bundle = await deps.contextAssembler.assemble({
308
+ kind: 'pr_review',
309
+ prNumber: pr.number,
310
+ workspacePath: opts.workspace,
311
+ payloadBody: pr.description,
312
+ });
313
+ const contextSummary = renderContextBundle(bundle); // pure, testable
314
+ const spawnContext = { assembledContextSummary: contextSummary };
315
+ await deps.spawnSession(workflowId, goal, opts.workspace, spawnContext);
316
+ ```
317
+
318
+ **spawnSession signature extended (backward-compatible optional arg):**
319
+ ```typescript
320
+ // CoordinatorDeps.spawnSession extended:
321
+ readonly spawnSession: (
322
+ workflowId: string,
323
+ goal: string,
324
+ workspace: string,
325
+ context?: Readonly<Record<string, unknown>>, // optional extra context
326
+ ) => Promise<Result<string, string>>;
327
+ ```
328
+
329
+ **How the context reaches the agent:** `context['assembledContextSummary']` is passed to `start_workflow` as a context variable. The WorkRail engine already stores context variables in the session; the existing `## Workspace Context` injection in `buildSystemPrompt()` is not changed. The assembled context summary is visible to the agent as a workflow context variable accessible via `{{assembledContextSummary}}` in step prompts or injected directly.
330
+
331
+ **Tensions resolved:** Multi-coordinator reuse (ContextAssembler service shared), typed task-specific routing (AssemblyTask discriminated union), per-field failure isolation (Result<T, E>), extensibility (add AssemblyTask kind or source without touching callers).
332
+ **Tensions accepted:** Context serialized to string before WorkflowTrigger boundary (loses structure at agent boundary). `buildSystemPrompt()` unchanged means assembled context is in the context map, not a dedicated system prompt section -- may be less visible to the agent.
333
+ **Failure mode to watch:** `renderContextBundle()` becomes the accumulation point for formatting decisions. If each coordinator wants different rendering, either renderContextBundle takes options or each coordinator writes its own renderer.
334
+ **Repo-pattern relationship:** Follows CoordinatorDeps DI pattern exactly. New module `src/context-assembly/` is a familiar pattern (see `src/knowledge-graph/` for precedent). AssemblyTask discriminated union follows PollingSource precedent in `trigger/types.ts`.
335
+ **Gains:** Clean separation, testable with fake deps, multi-coordinator reusable, failure-isolated, extensible for knowledge graph v2, no engine changes.
336
+ **Losses:** More code than Candidate A (~150 LOC for the module). String serialization loses type structure at agent boundary. Adding a new dep to CoordinatorDeps requires updating the composition root.
337
+ **Scope judgment:** Best-fit for the multi-coordinator goal stated in the backlog decision.
338
+ **Philosophy fit:** Honors DI, errors-as-data, immutability, exhaustiveness (discriminated union), YAGNI (only 3 v1 sources). The assembly module itself follows 'small pure functions' (renderContextBundle is pure).
339
+
340
+ ---
341
+
342
+ ### Candidate C: Context contributions array on spawnSession
343
+
344
+ **Summary:** Extend `CoordinatorDeps.spawnSession` to accept `readonly ContextContrib[]`; each coord fetches its own sources and packages them as labeled string blobs; daemon loops over contribs in `buildSystemPrompt()`.
345
+
346
+ ```typescript
347
+ export interface ContextContrib {
348
+ readonly sourceLabel: string;
349
+ readonly content: string;
350
+ readonly injectionPoint: 'system_prompt_section' | 'context_variable';
351
+ }
352
+
353
+ // CoordinatorDeps.spawnSession extended:
354
+ readonly spawnSession: (
355
+ workflowId: string,
356
+ goal: string,
357
+ workspace: string,
358
+ contribs?: readonly ContextContrib[],
359
+ ) => Promise<Result<string, string>>;
360
+ ```
361
+
362
+ **Tensions resolved:** Formatting stays in daemon (not coordinator). Coordinator packages sources as blobs.
363
+ **Tensions accepted:** Source-fetching still duplicated per coordinator. No typed task routing. ContextContrib.sourceLabel is unchecked string.
364
+ **Failure mode:** Contribs array grows without coordination; system prompt expands unpredictably. No failure isolation per source (coordinator must handle errors before packaging).
365
+ **Repo-pattern relationship:** Partially follows WorkflowTrigger optional fields. Introduces a new concept (contribs) not found elsewhere.
366
+ **Gains:** Formatting consolidated in daemon. Simple coordinator usage.
367
+ **Losses:** Source-fetching not reusable. No typed structure.
368
+ **Scope judgment:** Too narrow -- doesn't solve the multi-coordinator reuse problem.
369
+ **Philosophy fit:** Conflicts with 'make illegal states unrepresentable' (string labels, raw content), 'prefer explicit domain types over primitives'.
370
+
371
+ ---
372
+
373
+ ### Comparison and Recommendation
374
+
375
+ | Criterion | A (minimal) | B (service) | C (contribs) |
376
+ |---|---|---|---|
377
+ | Multi-coordinator reuse | Weak | Strong | Weak |
378
+ | Per-source failure isolation | Adequate | Strong | Weak |
379
+ | Typed task routing | None | Strong | None |
380
+ | Engine changes required | Yes (WorkflowTrigger) | No (uses context map) | Yes (buildSystemPrompt loop) |
381
+ | New code volume | ~50 LOC | ~150 LOC | ~50 LOC |
382
+ | Extension point for knowledge graph | Ad-hoc | Typed (new AssemblyTask kind) | Ad-hoc |
383
+ | Philosophy alignment | Good | Excellent | Poor |
384
+
385
+ **Recommendation: Candidate B**
386
+
387
+ The Apr 19 backlog decision is specifically about the god-class problem and multi-coordinator reuse. Candidate B is the only design that solves both. The extra 100 LOC over Candidate A pays for itself when the second coordinator arrives.
388
+
389
+ ---
390
+
391
+ ### Self-Critique
392
+
393
+ **Strongest counter-argument:** If only one coordinator ever needs context assembly, Candidate A delivers 90% of the value with 30% of the code. YAGNI argues for A.
394
+
395
+ **Why B still wins:** The backlog explicitly identifies a future coding coordinator. The migration from A to B when that coordinator arrives requires refactoring inline functions to a service -- more disruptive than designing B now.
396
+
397
+ **Pivot conditions:**
398
+ - If live session audit shows < 2 setup turns today -> simplify to Candidate A (prior notes only)
399
+ - If buildSystemPrompt() cannot change -> use context map for all injection (B accommodates this)
400
+ - If knowledge graph is near-term (< 2 months) -> add `queryKnowledgeGraph` to `ContextAssemblerDeps` now
401
+
402
+ **Invalidating assumption:** If agents ignore assembled context because the system prompt is already at or near the attention threshold, the entire premise fails. Test with one source first (prior session notes) and measure whether agents cite it before wiring all three sources.
403
+
404
+ ---
405
+
406
+ ### Open Questions for the Main Agent
407
+
408
+ 1. Should `renderContextBundle()` be a pure function or should each coordinator provide its own renderer? (Affects whether Candidate B needs a rendering strategy pattern)
409
+ 2. Does `WorkflowTrigger.context['assembledContextSummary']` actually reach the agent as useful context, or does it need to be a system prompt section? (Affects whether buildSystemPrompt() must change)
410
+ 3. Is `SessionNote` available from `LocalSessionSummaryProviderV2` without changes, or does the assembler need a new port? (Affects implementation scope)
411
+ 4. Should `ContextAssemblerDeps.execGit` be the same as the existing `execAsync` pattern in `workflow-runner.ts`, or should it be a distinct dep? (Code reuse vs. coupling)
412
+
413
+ ---
414
+
415
+ ## Challenge Notes
416
+
417
+ ### Assumption 1: A new architectural layer is required
418
+
419
+ **Assumption:** Context assembly needs its own layer separate from trigger and orchestration.
420
+ **Challenge:** The trigger layer already provides `referenceUrls` injected into the system prompt. Richer `goalTemplate` plus carefully structured `referenceUrls` pointing to `CLAUDE.md` and upstream docs might deliver 80% of the value without any new abstraction.
421
+ **Evidence to confirm/refute:** Audit session logs for re-discovery turns. If agents spend under 3 turns on setup when `referenceUrls` already includes `CLAUDE.md`, a new layer adds marginal value. If they still rediscover conventions, pre-assembly is justified.
422
+ **Verdict:** Partially confirmed. `referenceUrls` handles static known-at-config-time URLs. A new layer is needed for: (a) dynamic context predictable only at dispatch time (git diff, affected files), (b) cross-session history (prior session notes), and (c) multi-coordinator reuse without duplication.
423
+
424
+ ### Assumption 2: Pre-fetch is better than on-demand
425
+
426
+ **Assumption:** Context should be assembled before spawning rather than fetched on demand by the agent mid-session.
427
+ **Challenge:** Pre-fetch assumes relevant context is predictable from the trigger. For open-ended coding tasks, relevant files emerge as the agent reads code. A `query_knowledge_graph` daemon tool called mid-session may be more accurate.
428
+ **Evidence to confirm/refute:** Inspect the fix-agent goal: `'Fix review findings in PR #N: finding1; finding2'`. The affected files are often implicit in the findings. Measure setup turns vs. work turns in live sessions.
429
+ **Verdict:** Pre-fetch wins for well-structured, predictable context (CLAUDE.md, git diff, PR description, prior session notes). On-demand wins for dynamic ad-hoc queries (cross-file dependency traversal). The context assembly layer should handle pre-fetch; a future daemon tool handles on-demand.
430
+
431
+ ### Assumption 3: Context window budget management is a v1 concern
432
+
433
+ **Assumption:** Sessions have a context window; the assembler must decide what to include and what to trim.
434
+ **Challenge:** With 200K context windows and WorkTrain sessions targeting 30-50 turns on focused tasks, trimming is not the binding constraint for v1.
435
+ **Verdict:** Confirmed -- defer trimming to v2. V1 should inject all assembled context without a budget gate. The success criterion for v1 is "does assembly run at all and does the agent use it" rather than "does the trimmer make optimal choices."
436
+
437
+ ---
438
+
439
+ ## Resolution Notes
440
+
441
+ ### Selected Direction: Candidate B-hybrid
442
+
443
+ **ContextAssembler service with typed AssemblyTask input + typed `assembledContext` field on WorkflowTrigger**
444
+
445
+ #### What it is
446
+
447
+ A new `src/context-assembly/` module containing:
448
+
449
+ 1. **Types** (`types.ts`):
450
+ ```typescript
451
+ export type AssemblyTask =
452
+ | { readonly kind: 'pr_review'; readonly prNumber: number; readonly workspacePath: string; readonly payloadBody?: string }
453
+ | { readonly kind: 'coding_task'; readonly issueNumber?: number; readonly workspacePath: string; readonly payloadBody?: string };
454
+
455
+ export interface SessionNote {
456
+ readonly sessionId: string;
457
+ readonly recapSnippet: string;
458
+ readonly sessionTitle: string | null;
459
+ readonly gitBranch: string | null;
460
+ readonly lastModifiedMs: number;
461
+ }
462
+
463
+ export interface ContextBundle {
464
+ readonly task: AssemblyTask;
465
+ readonly gitDiff: Result<string, string>; // git diff --stat summary
466
+ readonly priorSessionNotes: Result<readonly SessionNote[], string>;
467
+ readonly dynamicReferenceUrls: Result<readonly string[], string>;
468
+ readonly assembledAt: string; // ISO 8601
469
+ }
470
+
471
+ export interface RenderOpts {
472
+ // stub for v1; populated when coordinators need different rendering
473
+ }
474
+ ```
475
+
476
+ 2. **Deps interface** (`deps.ts`):
477
+ ```typescript
478
+ export interface ContextAssemblerDeps {
479
+ readonly execGit: (args: readonly string[], cwd: string) => Promise<Result<string, string>>;
480
+ readonly listRecentSessions: (workspacePath: string, limit: number) => Promise<Result<readonly SessionNote[], string>>;
481
+ readonly extractUrlsFromText: (text: string) => readonly string[];
482
+ }
483
+ ```
484
+
485
+ 3. **Assembler factory** (`index.ts`):
486
+ ```typescript
487
+ export function createContextAssembler(deps: ContextAssemblerDeps): ContextAssembler;
488
+ export function renderContextBundle(bundle: ContextBundle, opts?: RenderOpts): string;
489
+ ```
490
+
491
+ 4. **WorkflowTrigger change** (`src/daemon/workflow-runner.ts`):
492
+ ```typescript
493
+ // New optional field on WorkflowTrigger
494
+ readonly assembledContext?: import('../context-assembly/types.js').ContextBundle;
495
+ ```
496
+
497
+ 5. **buildSystemPrompt() change** (3 lines in `src/daemon/workflow-runner.ts`):
498
+ ```typescript
499
+ if (trigger.assembledContext) {
500
+ const rendered = renderContextBundle(trigger.assembledContext);
501
+ if (rendered) { lines.push('', '## Assembled Task Context', rendered); }
502
+ }
503
+ ```
504
+ Position: BEFORE the referenceUrls section, AFTER the workspace context section.
505
+
506
+ 6. **CoordinatorDeps change** (`src/coordinators/pr-review.ts`):
507
+ ```typescript
508
+ // Add to CoordinatorDeps:
509
+ readonly contextAssembler?: ContextAssembler; // optional in v1; undefined = no assembly
510
+
511
+ // spawnSession gains optional 4th arg:
512
+ readonly spawnSession: (
513
+ workflowId: string,
514
+ goal: string,
515
+ workspace: string,
516
+ assembledContext?: ContextBundle,
517
+ ) => Promise<Result<string, string>>;
518
+ ```
519
+
520
+ 7. **Coordinator usage** (before each `spawnSession()` call in `runPrReviewCoordinator`):
521
+ ```typescript
522
+ const bundle = deps.contextAssembler
523
+ ? await deps.contextAssembler.assemble({ kind: 'pr_review', prNumber: pr.number, workspacePath: opts.workspace, payloadBody: pr.description })
524
+ : undefined;
525
+ const spawnResult = await deps.spawnSession('mr-review-workflow-agentic', goal, opts.workspace, bundle);
526
+ ```
527
+
528
+ #### Why this design was selected
529
+
530
+ 1. **Solves the god-class problem** from the Apr 19 backlog decision: assembly logic is extracted from the coordinator into a reusable service
531
+ 2. **Multi-coordinator reuse** via injectable `ContextAssembler` service
532
+ 3. **Typed task routing** via `AssemblyTask` discriminated union (exhaustive switch, compile-time checked)
533
+ 4. **Per-source failure isolation** via `Result<T, string>` per field
534
+ 5. **Knowledge graph v2 extension** via new `AssemblyTask` kind and new `ContextAssemblerDeps` field
535
+ 6. **No engine changes** -- only `workflow-runner.ts` (daemon, in scope) and `pr-review.ts` change
536
+ 7. **Consistent with CoordinatorDeps DI pattern** -- injectable, testable with fakes
537
+
538
+ #### Strongest alternative (Candidate A)
539
+
540
+ Inline `assembleTaskContext` function in `CoordinatorDeps` without a service module. 50 LOC vs. 150 LOC. Sufficient if only one coordinator ever needs assembly. Preferable pivot path if second coordinator does not arrive within 6 months.
541
+
542
+ #### Confidence band: medium-high
543
+
544
+ One validation gate required before full implementation: **O1 pilot test** -- run a PR review session with only `priorSessionNotes` injected and verify the agent references it in its reasoning. If the agent ignores it, the prompt positioning needs adjustment before wiring all three sources.
545
+
546
+ #### Residual risks
547
+
548
+ 1. **O1 (ORANGE)**: Agent may not attend to the assembled context section in a dense system prompt. Mitigation: position before referenceUrls; add one-line reference in step prompt; pilot test first.
549
+ 2. **Second coordinator timeline unscheduled**: If no second coordinator within 6 months, migrate to Candidate A instead.
550
+ 3. **LocalSessionSummaryProviderV2 re-wiring**: The session summary provider is wired to the MCP server/console path today. Making it available to `ContextAssemblerDeps` requires extracting its port interface. Scope this in wr.shaping.
551
+ 4. **git diff strategy**: Use `git diff HEAD~1 --stat` (file names + change counts) for v1, NOT the full diff. Full diffs for large PRs could be 50-100KB.
552
+
553
+ ---
554
+
555
+ ## Decision Log
556
+
557
+ | Date | Decision | Rationale |
558
+ |------|----------|-----------|
559
+ | 2026-04-19 | Defer knowledge graph until context assembly is designed | Prevents god-class expansion of pr-review.ts; knowledge graph becomes a plugin source |
560
+ | 2026-04-19 | Pre-fetch over on-demand for v1 | Predictable sources (git diff, prior notes) are known at dispatch time; no agent turns wasted |
561
+ | 2026-04-19 | Defer context budget management to v2 | 200K context windows are not the binding constraint for focused WorkTrain sessions |
562
+ | 2026-04-19 | CLAUDE.md gap is not a real gap | loadWorkspaceContext() in workflow-runner.ts already injects CLAUDE.md/AGENTS.md into every session automatically |
563
+ | 2026-04-19 | Selected Candidate B-hybrid over Candidate A | B solves multi-coordinator reuse; god-class prevention requires extraction into a service, not just an inline function |
564
+ | 2026-04-19 | Typed assembledContext field on WorkflowTrigger over string-in-context-map | Type safety, self-documenting, directly available to buildSystemPrompt() without context variable lookup |
565
+ | 2026-04-19 | git diff strategy: --stat only for v1 | Full diff could be 50-100KB for large PRs; file names + change counts are sufficient for agent context |
566
+
567
+ ---
568
+
569
+ ## Final Summary
570
+
571
+ ### Discovery path
572
+
573
+ `full_spectrum` -- goal was a solution statement (pre-framed as "context assembly layer"); needed both landscape grounding and reframing.
574
+
575
+ ### Critical landscape finding
576
+
577
+ The problem statement listed CLAUDE.md injection as a gap. This was wrong. `loadWorkspaceContext()` in `src/daemon/workflow-runner.ts` already injects `.claude/CLAUDE.md`, `CLAUDE.md`, `AGENTS.md`, and `.github/AGENTS.md` into EVERY WorkTrain session's system prompt automatically before the first LLM turn (capped at 32KB). The same function was already solving this problem. This correction narrowed the design scope.
578
+
579
+ ### Real gaps (revised)
580
+
581
+ Three gaps remain after the landscape correction:
582
+ 1. Git state (diff, affected files) -- per-task, not known until dispatch time
583
+ 2. Dynamic upstream docs from webhook payload -- `referenceUrls` in triggers.yml are static config-time URLs only
584
+ 3. Cross-session prior notes -- `loadSessionNotes()` handles same-session resume only; a second review of the same PR starts cold
585
+
586
+ ### Chosen direction: Candidate B-hybrid
587
+
588
+ **ContextAssembler service** (`src/context-assembly/`) with:
589
+ - `AssemblyTask` discriminated union input (`pr_review | coding_task`)
590
+ - `ContextBundle` with per-field `Result<T, string>` failure isolation
591
+ - `ContextAssemblerDeps` interface with all I/O injectable (follows CoordinatorDeps pattern exactly)
592
+ - Typed `assembledContext?: ContextBundle` field added to `WorkflowTrigger`
593
+ - `buildSystemPrompt()` gains 3 lines to inject the rendered bundle as `## Assembled Task Context`
594
+ - `CoordinatorDeps.contextAssembler?: ContextAssembler` injectable (optional, backward-compatible)
595
+
596
+ ### Why it won
597
+
598
+ 1. Solves the god-class / separation-of-concerns problem (Apr 19 backlog decision) by extracting assembly into a reusable service
599
+ 2. Multi-coordinator reuse: both pr-review and a future coding coordinator call `assembleContext()` without duplicating I/O
600
+ 3. Typed task routing via discriminated union -- exhaustive switch at compile time
601
+ 4. Per-field failure isolation -- any source can fail without blocking dispatch
602
+ 5. Knowledge graph v2 extension point -- add new `AssemblyTask` kind + `ContextAssemblerDeps` field
603
+
604
+ ### Strongest alternative (Candidate A)
605
+
606
+ Inline `assembleTaskContext` function in `CoordinatorDeps` (~50 LOC vs. ~150 LOC for the module). Sufficient if only one coordinator ever needs assembly. Pivot to A if no second coordinator is planned within 6 months.
607
+
608
+ ### Confidence band: medium-high
609
+
610
+ ### Residual risks
611
+
612
+ 1. **(ORANGE) O1: Agent visibility** -- assembled context section appended to system prompt may not get sufficient attention in a dense prompt. **Required pre-implementation validation**: run one PR review session with only `priorSessionNotes` injected; verify agent references it in first-turn reasoning.
613
+ 2. Second coordinator timeline is unscheduled -- the multi-coordinator argument assumes the coding coordinator arrives within months
614
+ 3. `LocalSessionSummaryProviderV2` re-wiring scope -- currently wired to MCP server path only; scope the port extraction during wr.shaping
615
+ 4. git diff strategy: `--stat` only (file names + change counts) for v1 -- full diff too large for large PRs
616
+
617
+ ### Next actions
618
+
619
+ 1. **Pilot test (O1 mitigation)**: inject `priorSessionNotes` for one pr-review session; verify agent uses it
620
+ 2. **wr.shaping**: scope the implementation using this design doc as input
621
+ 3. **Implementation order**: prior session notes source first (lowest risk, highest value), then git diff, then dynamic URL extraction
622
+ 4. **Do not implement** knowledge graph as a v1 source -- it requires `ts-morph` moving to dependencies (separate tracked backlog item)