@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.
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-trigger-test.d.ts +21 -0
- package/dist/cli/commands/worktrain-trigger-test.js +123 -0
- package/dist/cli-worktrain.js +65 -0
- package/dist/console-ui/assets/{index-BQFhoMcY.js → index-CecBgrR7.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/modes/implement-shared.d.ts +2 -1
- package/dist/coordinators/modes/implement-shared.js +7 -3
- package/dist/manifest.json +44 -36
- package/dist/mcp/output-schemas.d.ts +2 -2
- package/dist/trigger/adapters/github-queue-poller.js +10 -7
- package/dist/trigger/github-queue-config.d.ts +1 -0
- package/dist/trigger/github-queue-config.js +9 -0
- package/dist/trigger/polling-scheduler.js +8 -1
- package/dist/trigger/trigger-listener.js +296 -1
- package/dist/trigger/trigger-router.d.ts +4 -2
- package/dist/trigger/trigger-router.js +19 -3
- package/dist/trigger/trigger-store.js +10 -0
- package/dist/trigger/types.d.ts +2 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +5 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +12 -0
- package/docs/design/connect-adaptive-dispatch-candidates.md +153 -0
- package/docs/design/connect-adaptive-dispatch-design-review.md +88 -0
- package/docs/design/connect-adaptive-dispatch-implementation-plan.md +209 -0
- package/docs/design/queue-label-support-candidates.md +83 -0
- package/docs/design/queue-label-support-design-review.md +62 -0
- package/docs/design/queue-label-support-implementation-plan.md +158 -0
- package/docs/ideas/backlog.md +147 -0
- package/package.json +1 -1
- 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
|
|
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)(
|
|
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) {
|
package/dist/trigger/types.d.ts
CHANGED
|
@@ -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`
|