@exaudeus/workrail 3.39.0 → 3.41.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 (97) hide show
  1. package/dist/cli/commands/init.js +0 -3
  2. package/dist/cli-worktrain.js +58 -26
  3. package/dist/cli.js +0 -18
  4. package/dist/config/app-config.d.ts +0 -16
  5. package/dist/config/app-config.js +0 -14
  6. package/dist/config/config-file.js +0 -3
  7. package/dist/console-ui/assets/index-CQt4UhPB.js +28 -0
  8. package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
  9. package/dist/console-ui/index.html +2 -2
  10. package/dist/coordinators/pr-review.d.ts +23 -1
  11. package/dist/coordinators/pr-review.js +224 -5
  12. package/dist/daemon/daemon-events.d.ts +9 -1
  13. package/dist/daemon/soul-template.d.ts +2 -2
  14. package/dist/daemon/soul-template.js +11 -1
  15. package/dist/daemon/workflow-runner.d.ts +17 -3
  16. package/dist/daemon/workflow-runner.js +401 -28
  17. package/dist/di/container.js +1 -25
  18. package/dist/di/tokens.d.ts +0 -3
  19. package/dist/di/tokens.js +0 -3
  20. package/dist/engine/engine-factory.js +0 -1
  21. package/dist/infrastructure/console-defaults.d.ts +1 -0
  22. package/dist/infrastructure/console-defaults.js +4 -0
  23. package/dist/infrastructure/session/index.d.ts +0 -1
  24. package/dist/infrastructure/session/index.js +1 -3
  25. package/dist/manifest.json +124 -124
  26. package/dist/mcp/handlers/session.d.ts +1 -0
  27. package/dist/mcp/handlers/session.js +61 -13
  28. package/dist/mcp/output-schemas.d.ts +10 -10
  29. package/dist/mcp/server.js +1 -18
  30. package/dist/mcp/tools.d.ts +12 -12
  31. package/dist/mcp/transports/http-entry.js +0 -2
  32. package/dist/mcp/transports/stdio-entry.js +1 -2
  33. package/dist/mcp/types.d.ts +0 -2
  34. package/dist/trigger/daemon-console.d.ts +2 -0
  35. package/dist/trigger/daemon-console.js +1 -1
  36. package/dist/trigger/trigger-listener.d.ts +2 -0
  37. package/dist/trigger/trigger-listener.js +3 -1
  38. package/dist/trigger/trigger-router.d.ts +4 -3
  39. package/dist/trigger/trigger-router.js +13 -5
  40. package/dist/trigger/trigger-store.js +17 -4
  41. package/dist/types/workflow-source.d.ts +0 -1
  42. package/dist/types/workflow-source.js +3 -6
  43. package/dist/types/workflow.d.ts +1 -1
  44. package/dist/types/workflow.js +1 -2
  45. package/dist/v2/durable-core/domain/artifact-contract-validator.js +66 -0
  46. package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +25 -0
  47. package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.js +31 -0
  48. package/dist/v2/durable-core/schemas/artifacts/index.d.ts +3 -1
  49. package/dist/v2/durable-core/schemas/artifacts/index.js +14 -1
  50. package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +41 -0
  51. package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +30 -0
  52. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +236 -236
  53. package/dist/v2/durable-core/schemas/session/events.d.ts +50 -50
  54. package/dist/v2/durable-core/schemas/session/gaps.d.ts +2 -2
  55. package/dist/v2/durable-core/schemas/session/manifest.d.ts +4 -4
  56. package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
  57. package/dist/v2/usecases/console-routes.d.ts +2 -1
  58. package/dist/v2/usecases/console-routes.js +207 -5
  59. package/dist/v2/usecases/console-service.js +14 -0
  60. package/dist/v2/usecases/console-types.d.ts +1 -0
  61. package/docs/authoring.md +16 -16
  62. package/docs/design/coordinator-artifact-protocol-design-candidates.md +155 -0
  63. package/docs/design/coordinator-artifact-protocol-design-review.md +103 -0
  64. package/docs/design/coordinator-artifact-protocol-implementation-plan.md +259 -0
  65. package/docs/design/coordinator-message-queue-drain-plan.md +241 -0
  66. package/docs/design/coordinator-message-queue-drain-review.md +120 -0
  67. package/docs/design/coordinator-message-queue-drain.md +289 -0
  68. package/docs/design/shaping-workflow-external-research.md +119 -0
  69. package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
  70. package/docs/discovery/late-bound-goals-review.md +82 -0
  71. package/docs/discovery/late-bound-goals.md +118 -0
  72. package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
  73. package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
  74. package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
  75. package/docs/ideas/backlog.md +447 -97
  76. package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
  77. package/docs/ideas/design-candidates-session-tree-view.md +196 -0
  78. package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
  79. package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
  80. package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
  81. package/package.json +2 -1
  82. package/spec/authoring-spec.json +16 -16
  83. package/spec/shape.schema.json +178 -0
  84. package/spec/workflow-tags.json +232 -47
  85. package/workflows/coding-task-workflow-agentic.json +491 -480
  86. package/workflows/mr-review-workflow.agentic.v2.json +5 -1
  87. package/workflows/wr.shaping.json +182 -0
  88. package/dist/console-ui/assets/index-3oXZ_A9m.js +0 -28
  89. package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
  90. package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
  91. package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
  92. package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
  93. package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
  94. package/dist/infrastructure/session/HttpServer.d.ts +0 -60
  95. package/dist/infrastructure/session/HttpServer.js +0 -912
  96. package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
  97. package/workflows/coding-task-workflow-agentic.v2.json +0 -324
@@ -0,0 +1,284 @@
1
+ # Implementation Plan: POST /api/v2/sessions/:sessionId/steer
2
+
3
+ **Branch:** `feat/session-steer-endpoint`
4
+ **Confidence:** High
5
+ **PR count:** 1
6
+
7
+ ---
8
+
9
+ ## Problem Statement
10
+
11
+ A coordinator script needs to inject text into an agent's next turn during a running daemon session.
12
+ The `steer()` mechanism in `AgentLoop` already delivers injected text. The gap is a bridge between
13
+ an HTTP endpoint and the closure-scoped `pendingSteerText` variable in `runWorkflow()`.
14
+
15
+ Additionally, the existing `pendingSteerText: string | null` is a single-value field that silently
16
+ drops coordinator steers when overwritten by `onAdvance()`. This must be fixed first (R1 finding).
17
+
18
+ ---
19
+
20
+ ## Acceptance Criteria
21
+
22
+ 1. `POST /api/v2/sessions/:sessionId/steer` with body `{ "text": "..." }` returns HTTP 200
23
+ `{ "success": true }` when the sessionId belongs to an active daemon session.
24
+ 2. The injected text is delivered to the agent via `agent.steer()` on the next `turn_end` event,
25
+ concatenated after the step text from `onAdvance()`.
26
+ 3. Returns HTTP 404 `{ "success": false, "error": "Session not found or not a daemon session" }`
27
+ when the sessionId is not in the registry.
28
+ 4. Returns HTTP 503 `{ "success": false, "error": "Steer not available..." }` in standalone console
29
+ mode (no steerRegistry injected).
30
+ 5. Returns HTTP 400 for missing or non-string `text` body.
31
+ 6. Multiple calls to the endpoint between `turn_end` events: all injected texts are delivered in the
32
+ same steer message, joined with `\n\n`.
33
+ 7. After the session completes, calling the endpoint returns 404.
34
+ 8. The existing `pendingSteerText` variable is replaced by `pendingSteerParts: string[]` and
35
+ `onAdvance()` behavior is unchanged from the caller's perspective (step advance still works).
36
+
37
+ ---
38
+
39
+ ## Non-Goals
40
+
41
+ - Auth token on the endpoint (v1 is localhost-only, network binding is the security layer)
42
+ - `waitForCoordinator` blocking gate mechanism (Phase 2B, separate task)
43
+ - `wr.coordinator_signal` artifact schema (Phase A, separate task)
44
+ - MCP-mode injection (deferred to v2)
45
+ - Crash recovery for in-flight steers (in-memory only, v1 known limitation)
46
+ - Structured request body beyond `{ text: string }` (v2 concern)
47
+
48
+ ---
49
+
50
+ ## Philosophy-Driven Constraints
51
+
52
+ - **DI for boundaries**: `SteerRegistry` must be injected into `mountConsoleRoutes()` and
53
+ `runWorkflow()`. No module-level singletons.
54
+ - **Errors as data**: HTTP responses use `{ success: bool, error?: string }` shape. No thrown
55
+ exceptions at the route level.
56
+ - **Validate at boundaries**: 400 for invalid body, 503 for disabled, 404 for not-found -- all
57
+ checked before touching the registry.
58
+ - **YAGNI**: Only what's listed in acceptance criteria. No speculative extension points.
59
+ - **Explicit domain types**: Named type alias `SteerRegistry` (not raw `Map<string, fn>` literal).
60
+
61
+ ---
62
+
63
+ ## Invariants
64
+
65
+ 1. `pendingSteerParts` is only mutated in two places: `onAdvance()` (push step text) and the steer
66
+ callback registered in the `SteerRegistry` (push coordinator text). No other writer.
67
+ 2. `pendingSteerParts` is only read and drained in the `turn_end` subscriber. Single reader.
68
+ 3. JavaScript single-threaded event loop: no race between push and drain.
69
+ 4. The steer callback is registered after `workrailSessionId` is decoded from the continueToken,
70
+ and deregistered in `runWorkflow()`'s `finally` block. No stale entries possible.
71
+ 5. The endpoint is only active when `steerRegistry` is provided to `mountConsoleRoutes()`.
72
+ The standalone console does not provide it.
73
+ 6. The `steerRegistry` param is optional on all functions. No existing callers are broken.
74
+
75
+ ---
76
+
77
+ ## Selected Approach
78
+
79
+ **Hybrid (type alias + parameter injection):**
80
+ - Named type alias `export type SteerRegistry = Map<string, (text: string) => void>` in
81
+ `src/daemon/workflow-runner.ts`.
82
+ - `pendingSteerText: string | null` replaced by `const pendingSteerParts: string[] = []`.
83
+ - `onAdvance()` uses `pendingSteerParts.push(stepText)`.
84
+ - turn_end subscriber drains with `const parts = pendingSteerParts.splice(0)` and calls
85
+ `agent.steer(buildUserMessage(parts.join('\n\n')))` if `parts.length > 0`.
86
+ - `runWorkflow()` gains optional `steerRegistry?: SteerRegistry` param. After workrailSessionId
87
+ is decoded, calls `steerRegistry?.set(workrailSessionId, (text) => pendingSteerParts.push(text))`.
88
+ In `finally`: `steerRegistry?.delete(workrailSessionId)`.
89
+ - `mountConsoleRoutes()` gains optional `steerRegistry?: SteerRegistry` param after `triggerRouter`.
90
+ - `POST /api/v2/sessions/:sessionId/steer` endpoint added in `console-routes.ts`.
91
+ - `TriggerRouter` constructor gains optional `steerRegistry?: SteerRegistry`; passes to
92
+ `runWorkflowFn()` calls in `route()` and `dispatch()`.
93
+ - `RunWorkflowFn` type in `trigger-router.ts` extended with optional 6th param.
94
+
95
+ **Runner-Up:** C2 (SteerRegistry class). Loses only for having a new file for 3 trivial operations.
96
+ Use if the registry gains additional methods or needs isolated unit tests.
97
+
98
+ ---
99
+
100
+ ## Vertical Slices
101
+
102
+ ### Slice 1: Fix `pendingSteerText` -> `pendingSteerParts` (R1 prerequisite)
103
+
104
+ **Files:** `src/daemon/workflow-runner.ts` only.
105
+
106
+ **Change:**
107
+ - Rename `pendingSteerText: string | null` to `const pendingSteerParts: string[] = []`.
108
+ - Update `onAdvance()`: `pendingSteerText = stepText` -> `pendingSteerParts.push(stepText)`.
109
+ - Update turn_end subscriber drain:
110
+ - Before: `if (pendingSteerText !== null && !isComplete) { ... }`
111
+ - After: `if (!isComplete) { const parts = pendingSteerParts.splice(0); if (parts.length > 0) { agent.steer(buildUserMessage(parts.join('\n\n'))); } }`
112
+ - Note: `isComplete` guard moves outside the `splice(0)` -- drain always happens, steer only if not complete and parts non-empty.
113
+
114
+ **Acceptance:** Existing behavior unchanged. Session still receives step text on each advance.
115
+ No coordinator injection yet. Build+type-check passes. Existing tests pass.
116
+
117
+ **Risk:** Low. Pure refactor, no behavior change from external perspective.
118
+
119
+ ---
120
+
121
+ ### Slice 2: SteerRegistry type alias + runWorkflow() registration
122
+
123
+ **Files:** `src/daemon/workflow-runner.ts`.
124
+
125
+ **Change:**
126
+ - Add: `export type SteerRegistry = Map<string, (text: string) => void>;`
127
+ - Add optional param to `runWorkflow()`: `steerRegistry?: SteerRegistry`
128
+ - After `workrailSessionId` is decoded (line ~2190): register callback:
129
+ ```typescript
130
+ if (steerRegistry && workrailSessionId) {
131
+ steerRegistry.set(workrailSessionId, (text: string) => { pendingSteerParts.push(text); });
132
+ }
133
+ ```
134
+ - In `finally` block: `if (steerRegistry && workrailSessionId) { steerRegistry.delete(workrailSessionId); }`
135
+ - Add code comment on the `set()` call documenting the registration gap.
136
+
137
+ **Acceptance:** `runWorkflow()` compiles with new optional param. Existing callers unchanged.
138
+ Manual test: if a steerRegistry Map is passed and a callback is registered/called during a session,
139
+ text is pushed to `pendingSteerParts`.
140
+
141
+ **Risk:** Low. Additive change, no behavior change when `steerRegistry` is undefined.
142
+
143
+ ---
144
+
145
+ ### Slice 3: TriggerRouter wiring
146
+
147
+ **Files:** `src/trigger/trigger-router.ts`.
148
+
149
+ **Change:**
150
+ - Import `SteerRegistry` from `workflow-runner.js`.
151
+ - Extend `RunWorkflowFn` type with optional 6th param:
152
+ `steerRegistry?: SteerRegistry` after `emitter?`.
153
+ - Add `private readonly steerRegistry?: SteerRegistry` to `TriggerRouter`.
154
+ - Add `steerRegistry?: SteerRegistry` to TriggerRouter constructor params.
155
+ - Assign in constructor: `this.steerRegistry = steerRegistry`.
156
+ - Update `route()` call: `this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter, this.steerRegistry)`
157
+ - Update `dispatch()` call: same.
158
+
159
+ **Acceptance:** TriggerRouter compiles. `runWorkflow` in production TriggerRouter path passes
160
+ steerRegistry to the agent loop. Existing trigger tests unaffected (registry is optional).
161
+
162
+ **Risk:** Low. Additive param. All existing test calls to `TriggerRouter` pass `undefined` or
163
+ omit the param.
164
+
165
+ ---
166
+
167
+ ### Slice 4: HTTP endpoint in console-routes.ts
168
+
169
+ **Files:** `src/v2/usecases/console-routes.ts`.
170
+
171
+ **Change:**
172
+ - Import `SteerRegistry` from `../../daemon/workflow-runner.js`.
173
+ - Add `steerRegistry?: SteerRegistry` to `mountConsoleRoutes()` param list (after `triggerRouter`).
174
+ - Add endpoint after the `POST /api/v2/auto/dispatch` block:
175
+
176
+ ```typescript
177
+ // POST /api/v2/sessions/:sessionId/steer
178
+ // Injects text into a running daemon session's next agent turn.
179
+ // Daemon-only: requires steerRegistry to be provided at server startup.
180
+ // Auth: localhost-only (127.0.0.1 binding). No token auth in v1.
181
+ // TODO(v2): Add token auth before any multi-user or remote deployment.
182
+ app.post('/api/v2/sessions/:sessionId/steer', express.json(), (req: Request, res: Response) => {
183
+ if (!steerRegistry) {
184
+ res.status(503).json({ success: false, error: 'Steer not available (not a daemon context).' });
185
+ return;
186
+ }
187
+ const { sessionId } = req.params;
188
+ const body = req.body as { text?: unknown };
189
+ const text = typeof body.text === 'string' ? body.text.trim() : '';
190
+ if (!text) {
191
+ res.status(400).json({ success: false, error: 'text is required and must be a non-empty string.' });
192
+ return;
193
+ }
194
+ const callback = steerRegistry.get(sessionId);
195
+ if (!callback) {
196
+ res.status(404).json({ success: false, error: 'Session not found or not a daemon session.' });
197
+ return;
198
+ }
199
+ callback(text);
200
+ res.json({ success: true });
201
+ });
202
+ ```
203
+
204
+ **Acceptance:** All 5 HTTP response cases work correctly (200, 400, 404, 503). Session receives
205
+ injected text on next turn_end. Standalone console returns 503.
206
+
207
+ **Risk:** Low. New endpoint, no changes to existing routes.
208
+
209
+ ---
210
+
211
+ ### Slice 5: Daemon wiring in daemon-console.ts
212
+
213
+ **Files:** `src/trigger/daemon-console.ts`.
214
+
215
+ **Change:**
216
+ - Import `SteerRegistry` from `../daemon/workflow-runner.js`.
217
+ - Before constructing `TriggerRouter`: `const steerRegistry: SteerRegistry = new Map();`
218
+ - Pass to `TriggerRouter` constructor: `new TriggerRouter(index, ctx, apiKey, runWorkflow, execFn, ..., steerRegistry)`
219
+ - Pass to `mountConsoleRoutes()`: add `steerRegistry` as the last argument (after `triggerRouter`).
220
+ - Also update the direct `runWorkflow()` call in `console-routes.ts` `POST /auto/dispatch` path
221
+ (when `triggerRouter` is absent): pass `steerRegistry` as 6th arg.
222
+
223
+ **Acceptance:** End-to-end: daemon starts, `POST /auto/dispatch` creates a session, coordinator
224
+ calls `POST /sessions/:id/steer`, agent receives injected text on next turn.
225
+
226
+ **Risk:** Medium. This is the wiring step that connects all slices. Most likely source of missed
227
+ call sites.
228
+
229
+ ---
230
+
231
+ ## Test Design
232
+
233
+ ### Unit tests (workflow-runner.ts)
234
+ - Test that `onAdvance()` pushes to `pendingSteerParts`.
235
+ - Test that turn_end subscriber joins and steers when `pendingSteerParts.length > 0`.
236
+ - Test that multiple pushes (simulate both `onAdvance` and steer callback) produce joined text.
237
+ - Test that `steerRegistry.set()` is called after workrailSessionId decoded.
238
+ - Test that `steerRegistry.delete()` is called in finally (mock registry, verify delete).
239
+
240
+ ### Integration test (console-routes.ts)
241
+ - Mock `steerRegistry` with a Map. POST to endpoint with valid body -> 200, callback called.
242
+ - POST with empty body -> 400.
243
+ - POST with unknown sessionId -> 404.
244
+ - POST without steerRegistry injected -> 503.
245
+
246
+ ### Regression: existing tests
247
+ - All existing `runWorkflow()` tests must pass unchanged (optional param, default undefined).
248
+ - All existing `mountConsoleRoutes()` tests must pass unchanged.
249
+ - All existing TriggerRouter tests must pass unchanged.
250
+
251
+ ---
252
+
253
+ ## Risk Register
254
+
255
+ | Risk | Likelihood | Impact | Mitigation |
256
+ |---|---|---|---|
257
+ | Missed call site for steerRegistry in dispatch/route | Medium | High | Search for all `runWorkflowFn(` calls in trigger-router.ts before submitting |
258
+ | `pendingSteerParts.splice(0)` stale closure ref | Low | High | splice(0) mutates in-place; closure over array variable (not array contents) is safe |
259
+ | Registration gap causes 404 for early steers | Very low | Low | Document with code comment; coordinator retries on 404 |
260
+ | `mountConsoleRoutes` callers not updated | Low | Medium | Only 3 callers: daemon-console.ts, standalone-console.ts (no change), console-routes.ts |
261
+
262
+ ---
263
+
264
+ ## PR Packaging Strategy
265
+
266
+ Single PR on branch `feat/session-steer-endpoint`. All 5 slices together. The R1 fix (Slice 1) is
267
+ small enough that it doesn't need its own PR. The endpoint is only usable when all slices are present.
268
+
269
+ PR title: `feat(console): add POST /api/v2/sessions/:sessionId/steer for coordinator injection`
270
+
271
+ ---
272
+
273
+ ## Philosophy Alignment Per Slice
274
+
275
+ | Slice | Principle | Status |
276
+ |---|---|---|
277
+ | S1 pendingSteerParts | Immutability by default | Tension (mutable array) -- acceptable, mutation bounded |
278
+ | S1 pendingSteerParts | Compose with small pure functions | Satisfied -- drain is one expression |
279
+ | S2 SteerRegistry type | Explicit domain types | Satisfied -- named alias |
280
+ | S2 runWorkflow registration | DI for boundaries | Satisfied -- injected, not global |
281
+ | S3 TriggerRouter | Make illegal states unrepresentable | Satisfied -- optional param can't be confused with required |
282
+ | S4 HTTP endpoint | Validate at boundaries | Satisfied -- 400/503/404 before touching registry |
283
+ | S4 HTTP endpoint | Errors as data | Satisfied -- { success: bool } shape |
284
+ | S5 daemon wiring | YAGNI | Satisfied -- single Map, no extra abstraction |