@exaudeus/workrail 3.46.0 → 3.48.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 (31) 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-trigger-test.d.ts +21 -0
  4. package/dist/cli/commands/worktrain-trigger-test.js +123 -0
  5. package/dist/cli-worktrain.js +65 -0
  6. package/dist/console-ui/assets/{index-BQFhoMcY.js → index-CecBgrR7.js} +1 -1
  7. package/dist/console-ui/index.html +1 -1
  8. package/dist/coordinators/modes/implement-shared.d.ts +2 -1
  9. package/dist/coordinators/modes/implement-shared.js +7 -3
  10. package/dist/manifest.json +44 -36
  11. package/dist/mcp/output-schemas.d.ts +2 -2
  12. package/dist/trigger/adapters/github-queue-poller.js +10 -7
  13. package/dist/trigger/github-queue-config.d.ts +1 -0
  14. package/dist/trigger/github-queue-config.js +9 -0
  15. package/dist/trigger/polling-scheduler.js +8 -1
  16. package/dist/trigger/trigger-listener.js +296 -1
  17. package/dist/trigger/trigger-router.d.ts +4 -2
  18. package/dist/trigger/trigger-router.js +19 -3
  19. package/dist/trigger/trigger-store.js +10 -0
  20. package/dist/trigger/types.d.ts +2 -0
  21. package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +5 -0
  22. package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +12 -0
  23. package/docs/design/connect-adaptive-dispatch-candidates.md +153 -0
  24. package/docs/design/connect-adaptive-dispatch-design-review.md +88 -0
  25. package/docs/design/connect-adaptive-dispatch-implementation-plan.md +209 -0
  26. package/docs/design/queue-label-support-candidates.md +83 -0
  27. package/docs/design/queue-label-support-design-review.md +62 -0
  28. package/docs/design/queue-label-support-implementation-plan.md +158 -0
  29. package/docs/ideas/backlog.md +147 -0
  30. package/package.json +1 -1
  31. package/workflows/mr-review-workflow.agentic.v2.json +1 -1
@@ -204,7 +204,7 @@ class Semaphore {
204
204
  }
205
205
  const DEFAULT_MAX_CONCURRENT_SESSIONS = 3;
206
206
  class TriggerRouter {
207
- constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService, steerRegistry) {
207
+ constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService, steerRegistry, coordinatorDeps, modeExecutors) {
208
208
  this.index = index;
209
209
  this.ctx = ctx;
210
210
  this.apiKey = apiKey;
@@ -214,6 +214,8 @@ class TriggerRouter {
214
214
  this.emitter = emitter;
215
215
  this.notificationService = notificationService;
216
216
  this.steerRegistry = steerRegistry;
217
+ this._coordinatorDeps = coordinatorDeps;
218
+ this._modeExecutors = modeExecutors;
217
219
  const requested = maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
218
220
  const cap = Number.isNaN(requested) ? DEFAULT_MAX_CONCURRENT_SESSIONS : requested;
219
221
  if (cap < 1) {
@@ -385,13 +387,27 @@ class TriggerRouter {
385
387
  listTriggers() {
386
388
  return [...this.index.values()];
387
389
  }
388
- async dispatchAdaptivePipeline(goal, workspace, coordinatorDeps, modeExecutors, context) {
390
+ async dispatchAdaptivePipeline(goal, workspace, context, coordinatorDeps, modeExecutors) {
391
+ const effectiveDeps = coordinatorDeps ?? this._coordinatorDeps;
392
+ const effectiveExecutors = modeExecutors ?? this._modeExecutors;
393
+ if (effectiveDeps === undefined || effectiveExecutors === undefined) {
394
+ console.warn('[TriggerRouter] dispatchAdaptivePipeline called but coordinatorDeps not injected -- ' +
395
+ 'adaptive dispatch disabled. Inject coordinatorDeps and modeExecutors in the ' +
396
+ 'TriggerRouter constructor to activate. Returning escalated outcome.');
397
+ return {
398
+ kind: 'escalated',
399
+ escalationReason: {
400
+ phase: 'dispatch',
401
+ reason: 'coordinatorDeps or modeExecutors not injected into TriggerRouter',
402
+ },
403
+ };
404
+ }
389
405
  const opts = {
390
406
  goal,
391
407
  workspace,
392
408
  taskCandidate: context,
393
409
  };
394
- return (0, adaptive_pipeline_js_1.runAdaptivePipeline)(coordinatorDeps, opts, modeExecutors);
410
+ return (0, adaptive_pipeline_js_1.runAdaptivePipeline)(effectiveDeps, opts, effectiveExecutors);
395
411
  }
396
412
  }
397
413
  exports.TriggerRouter = TriggerRouter;
@@ -400,6 +400,12 @@ function setTriggerField(trigger, key, value) {
400
400
  case 'branchPrefix':
401
401
  trigger.branchPrefix = value;
402
402
  break;
403
+ case 'queueType':
404
+ trigger.queueType = value;
405
+ break;
406
+ case 'queueLabel':
407
+ trigger.queueLabel = value;
408
+ break;
403
409
  default:
404
410
  break;
405
411
  }
@@ -780,11 +786,15 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
780
786
  }
781
787
  queuePollIntervalSeconds = asNumber;
782
788
  }
789
+ const rawQueueType = raw.queueType?.trim();
790
+ const rawQueueLabel = raw.queueLabel?.trim();
783
791
  pollingSource = {
784
792
  provider: 'github_queue_poll',
785
793
  repo: queueSrc.repo.trim(),
786
794
  token: queueTokenResult.value,
787
795
  pollIntervalSeconds: queuePollIntervalSeconds,
796
+ ...(rawQueueType ? { queueType: rawQueueType } : {}),
797
+ ...(rawQueueLabel ? { queueLabel: rawQueueLabel } : {}),
788
798
  };
789
799
  }
790
800
  else if (raw.source) {
@@ -38,6 +38,8 @@ export interface GitHubQueuePollingSource {
38
38
  readonly repo: string;
39
39
  readonly token: string;
40
40
  readonly pollIntervalSeconds: number;
41
+ readonly queueType?: string;
42
+ readonly queueLabel?: string;
41
43
  }
42
44
  export interface TaskCandidate {
43
45
  readonly issueNumber: number;
@@ -7,12 +7,15 @@ export declare const ReviewVerdictArtifactV1Schema: z.ZodObject<{
7
7
  findings: z.ZodArray<z.ZodObject<{
8
8
  severity: z.ZodEnum<["critical", "major", "minor", "nit"]>;
9
9
  summary: z.ZodString;
10
+ findingCategory: z.ZodOptional<z.ZodEnum<["correctness", "security", "architecture", "ux", "performance", "testing", "style"]>>;
10
11
  }, "strict", z.ZodTypeAny, {
11
12
  severity: "critical" | "minor" | "major" | "nit";
12
13
  summary: string;
14
+ findingCategory?: "correctness" | "security" | "architecture" | "ux" | "performance" | "testing" | "style" | undefined;
13
15
  }, {
14
16
  severity: "critical" | "minor" | "major" | "nit";
15
17
  summary: string;
18
+ findingCategory?: "correctness" | "security" | "architecture" | "ux" | "performance" | "testing" | "style" | undefined;
16
19
  }>, "many">;
17
20
  summary: z.ZodString;
18
21
  }, "strict", z.ZodTypeAny, {
@@ -23,6 +26,7 @@ export declare const ReviewVerdictArtifactV1Schema: z.ZodObject<{
23
26
  findings: {
24
27
  severity: "critical" | "minor" | "major" | "nit";
25
28
  summary: string;
29
+ findingCategory?: "correctness" | "security" | "architecture" | "ux" | "performance" | "testing" | "style" | undefined;
26
30
  }[];
27
31
  }, {
28
32
  kind: "wr.review_verdict";
@@ -32,6 +36,7 @@ export declare const ReviewVerdictArtifactV1Schema: z.ZodObject<{
32
36
  findings: {
33
37
  severity: "critical" | "minor" | "major" | "nit";
34
38
  summary: string;
39
+ findingCategory?: "correctness" | "security" | "architecture" | "ux" | "performance" | "testing" | "style" | undefined;
35
40
  }[];
36
41
  }>;
37
42
  export type ReviewVerdictArtifactV1 = z.infer<typeof ReviewVerdictArtifactV1Schema>;
@@ -14,6 +14,18 @@ exports.ReviewVerdictArtifactV1Schema = zod_1.z
14
14
  .object({
15
15
  severity: zod_1.z.enum(['critical', 'major', 'minor', 'nit']),
16
16
  summary: zod_1.z.string().min(1),
17
+ findingCategory: zod_1.z
18
+ .enum([
19
+ 'correctness',
20
+ 'security',
21
+ 'architecture',
22
+ 'ux',
23
+ 'performance',
24
+ 'testing',
25
+ 'style',
26
+ ])
27
+ .optional()
28
+ .describe('Category of the finding. Used by coordinators to route audit chains.'),
17
29
  })
18
30
  .strict()),
19
31
  summary: zod_1.z.string().min(1),
@@ -0,0 +1,153 @@
1
+ # Design Candidates: Connect adaptive dispatch in polling-scheduler.ts
2
+
3
+ **Task:** Connect `TriggerRouter.dispatchAdaptivePipeline()` in `doPollGitHubQueue()` so queue poll sessions route through the adaptive coordinator instead of generic `dispatch()`.
4
+
5
+ **Scope:** `src/trigger/polling-scheduler.ts` and `src/trigger/trigger-router.ts` only.
6
+
7
+ ---
8
+
9
+ ## Problem Understanding
10
+
11
+ ### Tensions
12
+
13
+ 1. **Fire-and-forget vs. async coordination**: `dispatch()` is synchronous and returns a `string` immediately. `dispatchAdaptivePipeline()` is `async` and returns `Promise<PipelineOutcome>`. The scheduler calls both as void fire-and-forget. The tension is that the pipeline outcome is never surfaced to the scheduler -- errors are only logged, never propagated.
14
+
15
+ 2. **Deps ownership vs. scheduler simplicity**: `dispatchAdaptivePipeline()` currently requires `AdaptiveCoordinatorDeps` and `ModeExecutors` from the caller. The scheduler has neither. Either the router must own these deps (stored via DI), or the scheduler must carry them (wrong boundary), or the method builds them internally (violates DI principle).
16
+
17
+ 3. **Type safety vs. runtime flexibility**: The task requests a duck-type guard (`'dispatchAdaptivePipeline' in this.router`) rather than relying on TypeScript's static type. This is unusual for a strongly-typed codebase but necessary for test fakes that may not implement the method.
18
+
19
+ 4. **Production readiness vs. YAGNI**: Wiring `AdaptiveCoordinatorDeps` in production requires `spawnSession`, `awaitSessions`, `getAgentResult`, etc. -- HTTP-based, port-dependent. Providing that wiring now is out of scope for a "connect" task.
20
+
21
+ ### Likely seam
22
+
23
+ `doPollGitHubQueue()` in `polling-scheduler.ts`, line 494: `this.router.dispatch(workflowTrigger)`. This is the correct seam -- it's where the dispatch decision is made, and where `context.taskCandidate` is available.
24
+
25
+ ### What makes this hard
26
+
27
+ - `dispatchAdaptivePipeline` was added incomplete by design (JSDoc: "intentionally unconnected until that branch is rebased onto main"). It's a placeholder with a signature that requires caller-supplied deps that the scheduler cannot provide.
28
+ - Production `AdaptiveCoordinatorDeps` wiring does not yet exist on `TriggerRouter`.
29
+ - A naive swap crashes at runtime (deps are `undefined`, `runAdaptivePipeline` calls `deps.now()` immediately).
30
+
31
+ ---
32
+
33
+ ## Philosophy Constraints
34
+
35
+ From CLAUDE.md and repo patterns:
36
+
37
+ - **DI for boundaries**: inject external effects (I/O, clocks) -- do not construct them inside logic modules
38
+ - **YAGNI with discipline**: avoid speculative abstractions; the task is to "connect" not to "fully implement"
39
+ - **Immutability by default**: all TriggerRouter fields are `private readonly`
40
+ - **Type safety first**: prefer compile-time guarantees -- but the type guard is an accepted runtime concession for mock compatibility
41
+
42
+ ### Conflicts
43
+
44
+ - Stated philosophy ("type safety first") vs. requested duck-type guard. The runtime guard exists because test fakes may not implement `dispatchAdaptivePipeline`. This is an intentional concession to test flexibility, not a philosophical violation.
45
+
46
+ ---
47
+
48
+ ## Impact Surface
49
+
50
+ - `TriggerRouter` constructor signature (minor -- optional params only, no breaking change)
51
+ - All existing `TriggerRouter` tests -- must continue to pass without supplying new optional params
52
+ - `PollingScheduler` -- minimal change at the dispatch call site
53
+ - All existing `PollingScheduler` tests -- must continue to pass; the type guard means mock routers without `dispatchAdaptivePipeline` silently fall back to `dispatch()`
54
+ - Production bootstrap (e.g. `src/trigger/trigger-listener.ts`) -- NOT changed in this PR (wiring `coordinatorDeps` is a follow-up task)
55
+
56
+ ---
57
+
58
+ ## Candidates
59
+
60
+ ### Candidate A: Store optional coordinator deps on TriggerRouter (DI pattern)
61
+
62
+ **Summary**: Add optional `coordinatorDeps?: AdaptiveCoordinatorDeps` and `modeExecutors?: ModeExecutors` constructor params to `TriggerRouter`. Simplify `dispatchAdaptivePipeline` to use stored fields (not caller-provided params). In `doPollGitHubQueue`, type-guard check and call `void this.router.dispatchAdaptivePipeline(goal, workspace, context)`; fall back to `dispatch()` when the method is absent or deps are missing.
63
+
64
+ **Tensions resolved**: DI boundary (router owns deps). Fire-and-forget preserved. Type guard enables test fallback.
65
+
66
+ **Tensions accepted**: Production adaptive dispatch deferred (always falls back to `dispatch()` until deps are wired). Adds optional params to TriggerRouter constructor.
67
+
68
+ **Boundary**: `TriggerRouter` -- correct. It already owns `execFn`, `emitter`, `notificationService`, `steerRegistry` via the same pattern.
69
+
70
+ **Why this boundary**: The scheduler's job is to poll and dispatch. It should not know what `AdaptiveCoordinatorDeps` is. The router is the infrastructure boundary.
71
+
72
+ **Failure mode**: If `coordinatorDeps` is never injected in production, adaptive dispatch silently falls back to `dispatch()`. Mitigated by a `console.warn` log.
73
+
74
+ **Repo pattern**: Follows the existing TriggerRouter optional DI pattern exactly. No new patterns invented.
75
+
76
+ **Gains**: Clean separation; minimal change; all existing tests pass unchanged.
77
+
78
+ **Gives up**: Production adaptive dispatch doesn't work until a follow-up PR wires `coordinatorDeps`.
79
+
80
+ **Scope judgment**: Best-fit. The task says "connect" -- this creates the wiring path without full production readiness.
81
+
82
+ **Philosophy fit**: Honors DI for boundaries, YAGNI, immutability. Minor concession: duck-type guard is necessary for mock compatibility.
83
+
84
+ ---
85
+
86
+ ### Candidate B: Add coordinator deps to PollingScheduler constructor
87
+
88
+ **Summary**: Leave `TriggerRouter.dispatchAdaptivePipeline` signature as-is. Add `coordinatorDeps?` and `modeExecutors?` to `PollingScheduler` constructor. Pass them through to `dispatchAdaptivePipeline` in `doPollGitHubQueue`.
89
+
90
+ **Tensions resolved**: No TriggerRouter API change.
91
+
92
+ **Tensions accepted**: `PollingScheduler` becomes responsible for coordinator wiring -- wrong boundary. Scheduler's constructor grows with unrelated concerns.
93
+
94
+ **Boundary**: `PollingScheduler` -- wrong. The scheduler manages polling intervals; it should not know about `AdaptiveCoordinatorDeps`.
95
+
96
+ **Failure mode**: Two-level optional param threading. If either is absent, must guard in two places (scheduler constructor + method call).
97
+
98
+ **Repo pattern**: Departs -- nothing in `PollingScheduler` currently takes coordinator-level dependencies.
99
+
100
+ **Scope judgment**: Too broad -- changes the wrong boundary.
101
+
102
+ **Philosophy conflict**: Violates DI for boundaries (wrong injection point) and YAGNI.
103
+
104
+ ---
105
+
106
+ ### Candidate C: Lazily construct AdaptiveCoordinatorDeps inside dispatchAdaptivePipeline
107
+
108
+ **Summary**: No constructor changes. `dispatchAdaptivePipeline` builds `AdaptiveCoordinatorDeps` lazily from `this.ctx`, dynamically imports mode executors (same as CLI pattern), and calls `runAdaptivePipeline` self-contained. In the scheduler, type-guard + call with just `(goal, workspace, context)`.
109
+
110
+ **Tensions resolved**: No constructor API change. Self-contained per call.
111
+
112
+ **Tensions accepted**: Requires full `AdaptiveCoordinatorDeps` implementation inside `trigger-router.ts` -- requires console port, HTTP client, `gh` CLI. Major scope increase. Violates DI principle (building deps inside module).
113
+
114
+ **Failure mode**: Incomplete impl crashes at `deps.now()` or later; requires entire `AdaptiveCoordinatorDeps` surface implemented.
115
+
116
+ **Scope judgment**: Too broad. This is a separate task ("implement coordinator deps for TriggerRouter").
117
+
118
+ **Philosophy conflict**: Violates DI for boundaries.
119
+
120
+ ---
121
+
122
+ ## Comparison and Recommendation
123
+
124
+ **Recommendation: Candidate A.**
125
+
126
+ All meaningful tensions favor Candidate A:
127
+
128
+ - **Right boundary**: TriggerRouter already uses the exact same DI pattern for `execFn`, `emitter`, `notificationService`, `steerRegistry`. Adding `coordinatorDeps` and `modeExecutors` extends an established pattern at the established boundary.
129
+ - **Most manageable failure mode**: The fallback to `dispatch()` with a warning log is explicit and visible. Candidate B has the same fallback complexity but at the wrong boundary. Candidate C crashes if not fully implemented.
130
+ - **Scope fit**: The task is explicitly a "connect" task. Candidate A makes the connection; production wiring is deferred to a follow-up (which is correct -- the pitch says "wire queue poller -> runAdaptivePipeline()" as a modification to `trigger-router.ts`, not as production bootstrap wiring).
131
+ - **Easiest to evolve**: When the production `AdaptiveCoordinatorDeps` is ready, it's injected once at the bootstrap level. No changes to scheduler or method internals required.
132
+
133
+ ---
134
+
135
+ ## Self-Critique
136
+
137
+ **Strongest counter-argument**: If `coordinatorDeps` is always `undefined` in production, this PR "connects" nothing in practice -- the fallback to `dispatch()` always fires. The connection exists in the code but not in behavior.
138
+
139
+ Response: This is the explicitly stated intent. The JSDoc says "intentionally unconnected until that branch is rebased onto main." The task is to create the call path; making it operational is a separate step.
140
+
141
+ **Narrower option considered**: Don't change `trigger-router.ts` at all -- add the type guard in `polling-scheduler.ts` and call the existing method with `undefined` for the `coordinatorDeps`/`modeExecutors` params. Problem: the existing method has those params as required (not optional). This requires making them optional in `trigger-router.ts` anyway, which is essentially the same as Candidate A without the constructor injection.
142
+
143
+ **Broader option that might be justified**: Candidate C, if the production deps implementation already existed elsewhere and just needed wiring. Evidence required: a `makeAdaptiveCoordinatorDeps(ctx, port)` factory function somewhere. None found.
144
+
145
+ **Assumption that could invalidate this design**: If `dispatchAdaptivePipeline` was intended to always receive fresh deps from the caller (not stored), and the production caller is NOT `PollingScheduler` but some higher-level bootstrap that wraps the router. In that case, the method signature should stay as-is and the scheduler should call a different wrapper. However, the task description explicitly says to call `router.dispatchAdaptivePipeline()` from the scheduler, which rules out this interpretation.
146
+
147
+ ---
148
+
149
+ ## Open Questions for Main Agent
150
+
151
+ 1. Should the fallback log a `console.warn` (visible noise) or `console.log` (informational)? The pattern for "expected missing optional dep" in this codebase is `console.warn` (see TriggerRouter constructor for missing `maxConcurrentSessions`).
152
+
153
+ 2. Should the async `dispatchAdaptivePipeline` fire-and-forget (same as `dispatch()`) or should errors be awaited and logged? The existing `dispatch()` pattern uses `void this.queue.enqueue(...)` for fire-and-forget with result logging inside the callback. The adaptive dispatch should do the same: `void (async () => { ... })()` with outcome logging inside.
@@ -0,0 +1,88 @@
1
+ # Design Review: Connect adaptive dispatch in polling-scheduler.ts
2
+
3
+ **Design under review:** `connect-adaptive-dispatch-candidates.md`, Candidate A (with hybrid revision)
4
+
5
+ ---
6
+
7
+ ## Tradeoff Review
8
+
9
+ | Tradeoff | Status |
10
+ |----------|--------|
11
+ | Production activation deferred | Acceptable -- documented, warned, follow-up task |
12
+ | Duck-type guard instead of static narrowing | Acceptable -- required for test mock compatibility |
13
+ | Signature change removes 2 required params | Safe -- no tests call `dispatchAdaptivePipeline` directly |
14
+ | Async fire-and-forget via void IIFE | Acceptable -- consistent with `dispatch()` pattern |
15
+
16
+ All tradeoffs were verified against acceptance criteria and invariants. None violates the task's stated requirements.
17
+
18
+ ---
19
+
20
+ ## Failure Mode Review
21
+
22
+ | Failure Mode | Mitigation | Risk Level |
23
+ |-------------|-----------|-----------|
24
+ | `coordinatorDeps` absent -- silent fallback | `console.warn` log | Low (documented deferral) |
25
+ | `runAdaptivePipeline` throws | Try-catch in IIFE + scheduler-level catch | Low |
26
+ | Type guard false for real TriggerRouter | Cannot happen -- method on prototype | N/A |
27
+ | Interface drift (new required deps method) | TypeScript compile-time catch | Low |
28
+ | Concurrent poll cycles | Skip-cycle guard already prevents | N/A |
29
+
30
+ No unmitigated failure modes identified.
31
+
32
+ ---
33
+
34
+ ## Runner-Up / Simpler Alternative Review
35
+
36
+ **Simpler variant considered**: Keep `dispatchAdaptivePipeline` signature with `coordinatorDeps` and `modeExecutors` as optional method params (not stored on constructor). Scheduler calls with `(goal, workspace, undefined, undefined, context)`.
37
+
38
+ **Why rejected as-is**: 5-param call site with two explicit `undefined` arguments at the scheduler call site is noisy and implies the caller is responsible for deps it cannot provide.
39
+
40
+ **Hybrid adopted**: Keep optional method params (for flexibility) AND store them as constructor fields (for default injection). The scheduler calls with `(goal, workspace, context)` -- 3 params, no explicit `undefined`. Production can inject deps at construction; tests can pass them per-call if needed. This is strictly more flexible than the original Candidate A.
41
+
42
+ ---
43
+
44
+ ## Philosophy Alignment
45
+
46
+ - **DI for boundaries**: Satisfied. Deps injected at construction, scheduler passes nothing.
47
+ - **Immutability by default**: Satisfied. New fields are `private readonly`.
48
+ - **YAGNI with discipline**: Satisfied. No production deps wiring in this PR.
49
+ - **Type safety first**: Minor tension with duck-type guard. Acceptable -- required for test mock compatibility.
50
+ - **Make illegal states unrepresentable**: Minor tension -- `coordinatorDeps` and `modeExecutors` are independent optionals. Acceptable -- consistent with existing router pattern.
51
+
52
+ ---
53
+
54
+ ## Findings
55
+
56
+ ### Yellow: Deferred production activation
57
+
58
+ **Severity**: Yellow (informational, not a blocker)
59
+
60
+ `coordinatorDeps` and `modeExecutors` will be `undefined` in production until a follow-up PR wires them into the `TriggerRouter` constructor. The `console.warn` log makes this visible. This is intentional per the pitch's "connect" framing, but should be tracked as a follow-up task.
61
+
62
+ **No revision required.** The warning log is sufficient mitigation.
63
+
64
+ ### Yellow: Method signature flexibility
65
+
66
+ **Severity**: Yellow (quality, not a correctness issue)
67
+
68
+ The hybrid approach (optional params + stored fields) adds slight complexity to `dispatchAdaptivePipeline` internals (must merge caller-provided and stored fields). The precedence rule (caller-provided params override stored fields when both are present) should be documented in the method's JSDoc to prevent future confusion.
69
+
70
+ **Revision required**: Add JSDoc note to `dispatchAdaptivePipeline` clarifying that caller-provided `coordinatorDeps`/`modeExecutors` override the stored fields.
71
+
72
+ ---
73
+
74
+ ## Recommended Revisions
75
+
76
+ 1. **Adopt hybrid approach**: `dispatchAdaptivePipeline` accepts optional `coordinatorDeps?` and `modeExecutors?` params (caller override) with fallback to `this._coordinatorDeps` and `this._modeExecutors` (stored fields). Three-param call site from scheduler: `(goal, workspace, context)`.
77
+
78
+ 2. **Add JSDoc clarification**: Document the precedence rule (caller params override stored fields) in `dispatchAdaptivePipeline`'s JSDoc.
79
+
80
+ 3. **Warning log text**: The fallback warning should include: triggerId, reason ("coordinatorDeps not injected -- falling back to dispatch()"), and suggest where to inject.
81
+
82
+ ---
83
+
84
+ ## Residual Concerns
85
+
86
+ - **Production wiring gap**: The adaptive coordinator will not activate in production until `AdaptiveCoordinatorDeps` is implemented and injected. This is tracked as a follow-up task by the task description. No action required in this PR.
87
+
88
+ - **Test coverage gap**: No test verifies that `dispatchAdaptivePipeline` is called instead of `dispatch()` for queue-poll sessions when deps are present. This would require adding `dispatchAdaptivePipeline` to the mock router in `polling-scheduler.test.ts`. Out of scope for this PR per the "small targeted change" instruction, but recommended as a follow-up.
@@ -0,0 +1,209 @@
1
+ # Implementation Plan: Connect adaptive dispatch in polling-scheduler.ts
2
+
3
+ ## Problem Statement
4
+
5
+ `TriggerRouter.dispatchAdaptivePipeline()` was added in PR #639 but left unconnected. `polling-scheduler.ts` still calls `router.dispatch()` for all sessions including `github_queue_poll` ones. The queue poll sessions run as generic workflow sessions instead of being routed through the adaptive coordinator. This PR creates the wiring connection.
6
+
7
+ ---
8
+
9
+ ## Acceptance Criteria
10
+
11
+ 1. In `doPollGitHubQueue()`, when a task candidate is dispatched, `router.dispatchAdaptivePipeline()` is called (via type guard) instead of `router.dispatch()` -- when `dispatchAdaptivePipeline` exists on the router.
12
+
13
+ 2. When `dispatchAdaptivePipeline` is absent on the router (test mocks), `dispatch()` is called as fallback -- no crash, no error.
14
+
15
+ 3. When `dispatchAdaptivePipeline` exists but `coordinatorDeps` is not injected, a `console.warn` is emitted and the method falls back gracefully (returns a dry_run outcome or delegates to dispatch-equivalent behavior). The polling loop continues normally.
16
+
17
+ 4. `TriggerRouter` accepts optional `coordinatorDeps?: AdaptiveCoordinatorDeps` and `modeExecutors?: ModeExecutors` constructor params. When provided, `dispatchAdaptivePipeline` uses them as defaults.
18
+
19
+ 5. `dispatchAdaptivePipeline` signature is simplified: takes `(goal, workspace, context?, coordinatorDeps?, modeExecutors?)` -- last two are caller overrides (take precedence over stored fields). Returns `Promise<PipelineOutcome>`.
20
+
21
+ 6. `npm run build` produces no TypeScript errors.
22
+
23
+ 7. `npx vitest run` -- all 353 test files pass, no regressions.
24
+
25
+ ---
26
+
27
+ ## Non-Goals
28
+
29
+ - Do NOT implement `AdaptiveCoordinatorDeps` production wiring inside `TriggerRouter` (that's a follow-up PR).
30
+ - Do NOT wire `coordinatorDeps` in `trigger-listener.ts` or any bootstrap file.
31
+ - Do NOT modify any `src/mcp/` files.
32
+ - Do NOT change `AdaptiveCoordinatorDeps` or `ModeExecutors` interface definitions.
33
+ - Do NOT add new tests for the adaptive dispatch path (test mock router doesn't implement `dispatchAdaptivePipeline`; the fallback path is the tested path).
34
+ - Do NOT change `PollingScheduler` constructor signature.
35
+
36
+ ---
37
+
38
+ ## Philosophy-Driven Constraints
39
+
40
+ - **DI for boundaries**: `coordinatorDeps` stored on `TriggerRouter`, not constructed inside it.
41
+ - **Immutability by default**: New fields are `private readonly`.
42
+ - **YAGNI**: Only the wiring is added; no production deps implementation.
43
+ - **Errors as data**: Absent deps produce a logged warning and graceful fallback, never a thrown error.
44
+ - **Determinism**: Fallback behavior (when deps absent) is always the same -- log warn + skip adaptive dispatch.
45
+
46
+ ---
47
+
48
+ ## Invariants
49
+
50
+ 1. At-least-once delivery ordering is preserved: adaptive dispatch fires BEFORE `appendQueuePollLog` records the event (same ordering as the `dispatch()` call it replaces).
51
+
52
+ 2. Fire-and-forget semantics preserved: `doPollGitHubQueue` does not await the adaptive dispatch outcome.
53
+
54
+ 3. Fallback to `dispatch()` when `dispatchAdaptivePipeline` is absent on the router (type guard false).
55
+
56
+ 4. `dispatchAdaptivePipeline` never throws: any internal errors are caught and logged; the method returns `PipelineOutcome { kind: 'escalated' }` on unexpected errors.
57
+
58
+ 5. Existing `TriggerRouter` tests pass without modification -- new constructor params are optional.
59
+
60
+ ---
61
+
62
+ ## Selected Approach
63
+
64
+ **Hybrid approach (Candidate A refined):**
65
+
66
+ ### In `trigger-router.ts`
67
+
68
+ 1. Add to constructor parameters (after `steerRegistry?`):
69
+ - `coordinatorDeps?: AdaptiveCoordinatorDeps`
70
+ - `modeExecutors?: ModeExecutors`
71
+
72
+ 2. Store as `private readonly _coordinatorDeps?: AdaptiveCoordinatorDeps` and `private readonly _modeExecutors?: ModeExecutors`.
73
+
74
+ 3. Change `dispatchAdaptivePipeline` signature from:
75
+ ```typescript
76
+ async dispatchAdaptivePipeline(
77
+ goal: string,
78
+ workspace: string,
79
+ coordinatorDeps: AdaptiveCoordinatorDeps,
80
+ modeExecutors: ModeExecutors,
81
+ context?: Readonly<Record<string, unknown>>,
82
+ )
83
+ ```
84
+ to:
85
+ ```typescript
86
+ async dispatchAdaptivePipeline(
87
+ goal: string,
88
+ workspace: string,
89
+ context?: Readonly<Record<string, unknown>>,
90
+ coordinatorDeps?: AdaptiveCoordinatorDeps,
91
+ modeExecutors?: ModeExecutors,
92
+ )
93
+ ```
94
+ Effective deps = caller-provided (override) OR stored fields (default).
95
+
96
+ 4. Inside `dispatchAdaptivePipeline`, if no effective deps are available:
97
+ - Log `console.warn('[TriggerRouter] dispatchAdaptivePipeline called but coordinatorDeps not injected -- adaptive dispatch disabled. Inject coordinatorDeps in TriggerRouter constructor to activate.')`
98
+ - Return `{ kind: 'escalated', escalationReason: { phase: 'dispatch', reason: 'coordinatorDeps not injected' } }` as PipelineOutcome.
99
+
100
+ 5. Update JSDoc to document:
101
+ - The `@see feat/github-queue-poll` comment (already there -- keep it)
102
+ - The caller-override vs. stored-field precedence rule
103
+ - That absent deps produce a warn + escalated outcome (not a throw)
104
+
105
+ ### In `polling-scheduler.ts`
106
+
107
+ 1. In `doPollGitHubQueue`, replace line:
108
+ ```typescript
109
+ this.router.dispatch(workflowTrigger);
110
+ ```
111
+ with:
112
+ ```typescript
113
+ if ('dispatchAdaptivePipeline' in this.router && typeof (this.router as { dispatchAdaptivePipeline?: unknown }).dispatchAdaptivePipeline === 'function') {
114
+ void (this.router as { dispatchAdaptivePipeline: (goal: string, workspace: string, context?: Readonly<Record<string, unknown>>) => Promise<unknown> }).dispatchAdaptivePipeline(
115
+ workflowTrigger.goal,
116
+ workflowTrigger.workspacePath,
117
+ workflowTrigger.context,
118
+ );
119
+ } else {
120
+ this.router.dispatch(workflowTrigger);
121
+ }
122
+ ```
123
+ Note: The type cast is necessary because `TriggerRouter` is imported as a type (not class) in `polling-scheduler.ts`. The duck-type check is the runtime safety mechanism.
124
+
125
+ 2. Add a log line after the adaptive dispatch call:
126
+ ```typescript
127
+ console.log(`[QueuePoll] dispatched via adaptivePipeline: goal="${workflowTrigger.goal.slice(0, 80)}"`);
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Runner-Up
133
+
134
+ **Candidate B** (PollingScheduler carries coordinator deps) -- rejected because it places infrastructure-level deps at the wrong boundary. PollingScheduler should only know about the router interface.
135
+
136
+ ---
137
+
138
+ ## Vertical Slices
139
+
140
+ ### Slice 1: Modify `trigger-router.ts`
141
+ - Add optional constructor params `coordinatorDeps?` and `modeExecutors?`
142
+ - Store as `private readonly` fields
143
+ - Simplify `dispatchAdaptivePipeline` signature (context before deps)
144
+ - Add absent-deps fallback with `console.warn` and `escalated` return
145
+ - Update JSDoc
146
+
147
+ **Done when**: `npm run build` succeeds; `dispatchAdaptivePipeline` method compiles with new signature.
148
+
149
+ ### Slice 2: Modify `polling-scheduler.ts`
150
+ - Replace `this.router.dispatch(workflowTrigger)` with type-guard + adaptive dispatch call + fallback
151
+ - Add log line for adaptive dispatch path
152
+
153
+ **Done when**: `npm run build` succeeds; existing `polling-scheduler.test.ts` tests pass (mock router only has `dispatch`, type guard falls back -- no regressions).
154
+
155
+ ---
156
+
157
+ ## Test Design
158
+
159
+ **No new test files.** The task says "small targeted change" and the adaptive path requires `coordinatorDeps` injection which the existing test infrastructure doesn't support. Existing tests verify:
160
+ - The fallback path: mock router only has `dispatch()`, type guard is `false`, `dispatch()` is called (existing tests already verify this behavior -- they just don't explicitly test the type guard).
161
+
162
+ **Manual verification**: After implementation, verify that `npm run build` and `npx vitest run` both pass cleanly.
163
+
164
+ **Follow-up test** (not in this PR): Add `dispatchAdaptivePipeline` to the mock router in `polling-scheduler.test.ts` and assert it's called for queue-poll sessions.
165
+
166
+ ---
167
+
168
+ ## Risk Register
169
+
170
+ | Risk | Likelihood | Impact | Mitigation |
171
+ |------|-----------|--------|-----------|
172
+ | Type cast for duck-type check causes TS error | Low | Build failure | Use `as { dispatchAdaptivePipeline?: unknown }` minimal cast; verify in Slice 2 |
173
+ | `dispatchAdaptivePipeline` return type mismatch after signature change | Low | Build failure | Verify `ReturnType<typeof runAdaptivePipeline>` still matches `Promise<PipelineOutcome>` |
174
+ | Missing `(this.router as TriggerRouter).dispatchAdaptivePipeline` direct call | None | N/A | The import in `polling-scheduler.ts` is `import type { TriggerRouter }` -- the type guard is the correct approach |
175
+ | Existing trigger-router.test.ts fails after constructor change | Low | Test failure | New params are optional -- all existing test construction sites pass `undefined` implicitly |
176
+
177
+ ---
178
+
179
+ ## PR Packaging Strategy
180
+
181
+ **Single PR**: `fix/connect-adaptive-dispatch`
182
+
183
+ Commit: `fix(trigger): connect queue poll dispatch to adaptive coordinator`
184
+
185
+ All changes in one commit: `trigger-router.ts` + `polling-scheduler.ts` + design docs.
186
+
187
+ ---
188
+
189
+ ## Philosophy Alignment
190
+
191
+ | Principle | Status | Reason |
192
+ |-----------|--------|--------|
193
+ | DI for boundaries | Satisfied | `coordinatorDeps` injected at construction, not built internally |
194
+ | Immutability by default | Satisfied | New fields are `private readonly` |
195
+ | YAGNI with discipline | Satisfied | No production deps wiring; only the call path is connected |
196
+ | Errors as data | Satisfied | Absent deps return `escalated` PipelineOutcome, never throw |
197
+ | Type safety first | Minor tension | Duck-type guard is runtime check; acceptable for mock compatibility |
198
+ | Make illegal states unrepresentable | Minor tension | `coordinatorDeps` and `modeExecutors` are independent optionals; acceptable per existing router pattern |
199
+
200
+ ---
201
+
202
+ ## Estimation
203
+
204
+ - **`unresolvedUnknownCount`**: 0
205
+ - **`planConfidenceBand`**: High
206
+ - **`estimatedPRCount`**: 1
207
+ - **`followUpTickets`**:
208
+ 1. Wire `AdaptiveCoordinatorDeps` into `TriggerRouter` constructor in `trigger-listener.ts` bootstrap
209
+ 2. Add test for `dispatchAdaptivePipeline` call path in `polling-scheduler.test.ts`