@exaudeus/workrail 3.40.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.
- package/dist/cli/commands/init.js +0 -3
- package/dist/cli-worktrain.js +8 -0
- package/dist/cli.js +0 -18
- package/dist/config/app-config.d.ts +0 -16
- package/dist/config/app-config.js +0 -14
- package/dist/config/config-file.js +0 -3
- package/dist/console-ui/assets/index-CQt4UhPB.js +28 -0
- package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
- package/dist/console-ui/index.html +2 -2
- package/dist/coordinators/pr-review.d.ts +17 -0
- package/dist/coordinators/pr-review.js +164 -0
- package/dist/daemon/daemon-events.d.ts +9 -1
- package/dist/daemon/soul-template.d.ts +2 -2
- package/dist/daemon/soul-template.js +11 -1
- package/dist/daemon/workflow-runner.d.ts +14 -1
- package/dist/daemon/workflow-runner.js +395 -25
- package/dist/di/container.js +1 -25
- package/dist/di/tokens.d.ts +0 -3
- package/dist/di/tokens.js +0 -3
- package/dist/engine/engine-factory.js +0 -1
- package/dist/infrastructure/console-defaults.d.ts +1 -0
- package/dist/infrastructure/console-defaults.js +4 -0
- package/dist/infrastructure/session/index.d.ts +0 -1
- package/dist/infrastructure/session/index.js +1 -3
- package/dist/manifest.json +87 -103
- package/dist/mcp/handlers/session.d.ts +1 -0
- package/dist/mcp/handlers/session.js +61 -13
- package/dist/mcp/server.js +1 -18
- package/dist/mcp/transports/http-entry.js +0 -2
- package/dist/mcp/transports/stdio-entry.js +1 -2
- package/dist/mcp/types.d.ts +0 -2
- package/dist/trigger/daemon-console.d.ts +2 -0
- package/dist/trigger/daemon-console.js +1 -1
- package/dist/trigger/trigger-listener.d.ts +2 -0
- package/dist/trigger/trigger-listener.js +3 -1
- package/dist/trigger/trigger-router.d.ts +4 -3
- package/dist/trigger/trigger-router.js +4 -3
- package/dist/trigger/trigger-store.js +17 -4
- package/dist/v2/usecases/console-routes.d.ts +2 -1
- package/dist/v2/usecases/console-routes.js +29 -5
- package/dist/v2/usecases/console-service.js +14 -0
- package/dist/v2/usecases/console-types.d.ts +1 -0
- package/docs/authoring.md +16 -16
- package/docs/design/coordinator-message-queue-drain-plan.md +241 -0
- package/docs/design/coordinator-message-queue-drain-review.md +120 -0
- package/docs/design/coordinator-message-queue-drain.md +289 -0
- package/docs/design/shaping-workflow-external-research.md +119 -0
- package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
- package/docs/discovery/late-bound-goals-review.md +82 -0
- package/docs/discovery/late-bound-goals.md +118 -0
- package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
- package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
- package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
- package/docs/ideas/backlog.md +292 -0
- package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
- package/docs/ideas/design-candidates-session-tree-view.md +196 -0
- package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
- package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
- package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
- package/package.json +2 -1
- package/spec/authoring-spec.json +16 -16
- package/spec/shape.schema.json +178 -0
- package/spec/workflow-tags.json +232 -47
- package/workflows/coding-task-workflow-agentic.json +491 -480
- package/workflows/wr.shaping.json +182 -0
- package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
- package/dist/console-ui/assets/index-CXWCAonr.js +0 -28
- package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
- package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
- package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
- package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
- package/dist/infrastructure/session/HttpServer.d.ts +0 -60
- package/dist/infrastructure/session/HttpServer.js +0 -912
- package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
- package/workflows/coding-task-workflow-agentic.v2.json +0 -324
|
@@ -3,6 +3,7 @@ import express from 'express';
|
|
|
3
3
|
import type { V2ToolContext } from '../mcp/types.js';
|
|
4
4
|
import type { TriggerStoreError } from './trigger-store.js';
|
|
5
5
|
import { TriggerRouter, type RunWorkflowFn } from './trigger-router.js';
|
|
6
|
+
import type { SteerRegistry } from '../daemon/workflow-runner.js';
|
|
6
7
|
import type { WorkspaceConfig } from './types.js';
|
|
7
8
|
import type { DaemonEventEmitter } from '../daemon/daemon-events.js';
|
|
8
9
|
import type { FetchFn } from './adapters/gitlab-poller.js';
|
|
@@ -17,6 +18,7 @@ export type TriggerListenerError = TriggerStoreError | {
|
|
|
17
18
|
export interface TriggerListenerHandle {
|
|
18
19
|
readonly port: number;
|
|
19
20
|
readonly router: TriggerRouter;
|
|
21
|
+
readonly steerRegistry: SteerRegistry;
|
|
20
22
|
stop(): Promise<void>;
|
|
21
23
|
}
|
|
22
24
|
export interface StartTriggerListenerOptions {
|
|
@@ -183,8 +183,9 @@ async function startTriggerListener(ctx, options) {
|
|
|
183
183
|
const notificationService = (notifyMacOs || (notifyWebhook !== undefined && notifyWebhook !== ''))
|
|
184
184
|
? new notification_service_js_1.NotificationService({ macOs: notifyMacOs, webhookUrl: notifyWebhook })
|
|
185
185
|
: undefined;
|
|
186
|
+
const steerRegistry = new Map();
|
|
186
187
|
const runWorkflowFn = options.runWorkflowFn ?? workflow_runner_js_1.runWorkflow;
|
|
187
|
-
const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter, notificationService);
|
|
188
|
+
const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter, notificationService, steerRegistry);
|
|
188
189
|
const app = createTriggerApp(router);
|
|
189
190
|
const allTriggers = [...triggerIndex.values()];
|
|
190
191
|
const polledEventStore = new polled_event_store_js_1.PolledEventStore(env);
|
|
@@ -219,6 +220,7 @@ async function startTriggerListener(ctx, options) {
|
|
|
219
220
|
resolve({
|
|
220
221
|
port: actualPort,
|
|
221
222
|
router,
|
|
223
|
+
steerRegistry,
|
|
222
224
|
stop: async () => {
|
|
223
225
|
pollingScheduler.stop();
|
|
224
226
|
return new Promise((res, rej) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkflowTrigger, WorkflowRunResult } from '../daemon/workflow-runner.js';
|
|
1
|
+
import type { WorkflowTrigger, WorkflowRunResult, SteerRegistry } from '../daemon/workflow-runner.js';
|
|
2
2
|
import type { V2ToolContext } from '../mcp/types.js';
|
|
3
3
|
import type { TriggerDefinition, WebhookEvent } from './types.js';
|
|
4
4
|
import type { ExecFn } from './delivery-action.js';
|
|
@@ -20,7 +20,7 @@ export type RouteResult = {
|
|
|
20
20
|
readonly _tag: 'error';
|
|
21
21
|
readonly error: RouteError;
|
|
22
22
|
};
|
|
23
|
-
export type RunWorkflowFn = (trigger: WorkflowTrigger, ctx: V2ToolContext, apiKey: string, daemonRegistry?: import('../v2/infra/in-memory/daemon-registry/index.js').DaemonRegistry, emitter?: DaemonEventEmitter) => Promise<WorkflowRunResult>;
|
|
23
|
+
export type RunWorkflowFn = (trigger: WorkflowTrigger, ctx: V2ToolContext, apiKey: string, daemonRegistry?: import('../v2/infra/in-memory/daemon-registry/index.js').DaemonRegistry, emitter?: DaemonEventEmitter, steerRegistry?: SteerRegistry) => Promise<WorkflowRunResult>;
|
|
24
24
|
export declare function interpolateGoalTemplate(template: string, staticGoal: string, payload: Readonly<Record<string, unknown>>, triggerId: string): string;
|
|
25
25
|
export declare class TriggerRouter {
|
|
26
26
|
private readonly index;
|
|
@@ -33,7 +33,8 @@ export declare class TriggerRouter {
|
|
|
33
33
|
private readonly _maxConcurrentSessions;
|
|
34
34
|
private readonly emitter;
|
|
35
35
|
private readonly notificationService;
|
|
36
|
-
|
|
36
|
+
private readonly steerRegistry;
|
|
37
|
+
constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn, execFn?: ExecFn, maxConcurrentSessions?: number, emitter?: DaemonEventEmitter, notificationService?: NotificationService, steerRegistry?: SteerRegistry);
|
|
37
38
|
get activeSessions(): number;
|
|
38
39
|
get maxConcurrentSessions(): number;
|
|
39
40
|
route(event: WebhookEvent): RouteResult;
|
|
@@ -183,7 +183,7 @@ class Semaphore {
|
|
|
183
183
|
}
|
|
184
184
|
const DEFAULT_MAX_CONCURRENT_SESSIONS = 3;
|
|
185
185
|
class TriggerRouter {
|
|
186
|
-
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService) {
|
|
186
|
+
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService, steerRegistry) {
|
|
187
187
|
this.index = index;
|
|
188
188
|
this.ctx = ctx;
|
|
189
189
|
this.apiKey = apiKey;
|
|
@@ -192,6 +192,7 @@ class TriggerRouter {
|
|
|
192
192
|
this.execFn = execFn ?? execFileAsync;
|
|
193
193
|
this.emitter = emitter;
|
|
194
194
|
this.notificationService = notificationService;
|
|
195
|
+
this.steerRegistry = steerRegistry;
|
|
195
196
|
const requested = maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
|
|
196
197
|
const cap = Number.isNaN(requested) ? DEFAULT_MAX_CONCURRENT_SESSIONS : requested;
|
|
197
198
|
if (cap < 1) {
|
|
@@ -260,7 +261,7 @@ class TriggerRouter {
|
|
|
260
261
|
await this.semaphore.acquire();
|
|
261
262
|
let result;
|
|
262
263
|
try {
|
|
263
|
-
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter);
|
|
264
|
+
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter, this.steerRegistry);
|
|
264
265
|
}
|
|
265
266
|
finally {
|
|
266
267
|
this.semaphore.release();
|
|
@@ -321,7 +322,7 @@ class TriggerRouter {
|
|
|
321
322
|
await this.semaphore.acquire();
|
|
322
323
|
let result;
|
|
323
324
|
try {
|
|
324
|
-
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter);
|
|
325
|
+
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter, this.steerRegistry);
|
|
325
326
|
}
|
|
326
327
|
finally {
|
|
327
328
|
this.semaphore.release();
|
|
@@ -426,7 +426,6 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
426
426
|
const requiredStringFields = [
|
|
427
427
|
'provider',
|
|
428
428
|
'workflowId',
|
|
429
|
-
'goal',
|
|
430
429
|
];
|
|
431
430
|
for (const field of requiredStringFields) {
|
|
432
431
|
const v = raw[field];
|
|
@@ -491,7 +490,21 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
491
490
|
return secretResult;
|
|
492
491
|
hmacSecret = secretResult.value;
|
|
493
492
|
}
|
|
494
|
-
const
|
|
493
|
+
const LATE_BOUND_GOAL_SENTINEL = 'Autonomous task';
|
|
494
|
+
let resolvedGoal;
|
|
495
|
+
let resolvedGoalTemplate = raw.goalTemplate?.trim();
|
|
496
|
+
if (!raw.goal?.trim()) {
|
|
497
|
+
resolvedGoal = LATE_BOUND_GOAL_SENTINEL;
|
|
498
|
+
if (!resolvedGoalTemplate) {
|
|
499
|
+
resolvedGoalTemplate = '{{$.goal}}';
|
|
500
|
+
console.log(`[TriggerStore] Trigger "${rawId}" has no static goal or goalTemplate -- ` +
|
|
501
|
+
`defaulting to goalTemplate: "{{$.goal}}" (goal taken from webhook payload). ` +
|
|
502
|
+
`Fallback goal if payload has no goal field: "${LATE_BOUND_GOAL_SENTINEL}".`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
resolvedGoal = raw.goal.trim();
|
|
507
|
+
}
|
|
495
508
|
const referenceUrlsRaw = raw.referenceUrls?.trim();
|
|
496
509
|
const referenceUrls = referenceUrlsRaw
|
|
497
510
|
? referenceUrlsRaw.split(/\s+/).filter(Boolean)
|
|
@@ -712,13 +725,13 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
712
725
|
provider,
|
|
713
726
|
workflowId: raw.workflowId.trim(),
|
|
714
727
|
workspacePath: resolvedWorkspacePath,
|
|
715
|
-
goal:
|
|
728
|
+
goal: resolvedGoal,
|
|
716
729
|
concurrencyMode,
|
|
717
730
|
...(hmacSecret !== undefined ? { hmacSecret } : {}),
|
|
718
731
|
...(raw.contextMapping !== undefined
|
|
719
732
|
? { contextMapping: assembleContextMapping(raw.contextMapping) }
|
|
720
733
|
: {}),
|
|
721
|
-
...(
|
|
734
|
+
...(resolvedGoalTemplate ? { goalTemplate: resolvedGoalTemplate } : {}),
|
|
722
735
|
...(referenceUrls !== undefined && referenceUrls.length > 0 ? { referenceUrls } : {}),
|
|
723
736
|
...(agentConfig !== undefined ? { agentConfig } : {}),
|
|
724
737
|
...(callbackUrl !== undefined ? { callbackUrl } : {}),
|
|
@@ -4,4 +4,5 @@ import type { WorkflowService } from '../../application/services/workflow-servic
|
|
|
4
4
|
import type { ToolCallTimingRingBuffer } from '../../mcp/tool-call-timing.js';
|
|
5
5
|
import type { TriggerRouter } from '../../trigger/trigger-router.js';
|
|
6
6
|
import type { V2ToolContext } from '../../mcp/types.js';
|
|
7
|
-
|
|
7
|
+
import type { SteerRegistry } from '../../daemon/workflow-runner.js';
|
|
8
|
+
export declare function mountConsoleRoutes(app: Application, consoleService: ConsoleService, workflowService?: WorkflowService, timingRingBuffer?: ToolCallTimingRingBuffer, toolCallsPerfFile?: string, serverVersion?: string, v2ToolContext?: V2ToolContext, triggerRouter?: TriggerRouter, steerRegistry?: SteerRegistry): () => void;
|
|
@@ -45,6 +45,7 @@ const worktree_service_js_1 = require("./worktree-service.js");
|
|
|
45
45
|
const workflow_js_1 = require("../../types/workflow.js");
|
|
46
46
|
const dev_mode_js_1 = require("../../mcp/dev-mode.js");
|
|
47
47
|
const workflow_runner_js_1 = require("../../daemon/workflow-runner.js");
|
|
48
|
+
const assert_never_js_1 = require("../../runtime/assert-never.js");
|
|
48
49
|
const start_js_1 = require("../../mcp/handlers/v2-execution/start.js");
|
|
49
50
|
const v2_token_ops_js_1 = require("../../mcp/handlers/v2-token-ops.js");
|
|
50
51
|
function watchSessionsDir(sessionsDir, onChanged) {
|
|
@@ -90,7 +91,7 @@ function loadWorkflowTags() {
|
|
|
90
91
|
return { version: 0, tags: [], workflows: {} };
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuffer, toolCallsPerfFile, serverVersion, v2ToolContext, triggerRouter) {
|
|
94
|
+
function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuffer, toolCallsPerfFile, serverVersion, v2ToolContext, triggerRouter, steerRegistry) {
|
|
94
95
|
const sseClients = new Set();
|
|
95
96
|
let sseDebounceTimer = null;
|
|
96
97
|
function broadcastChange() {
|
|
@@ -590,18 +591,21 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
590
591
|
triggerRouter.dispatch(trigger);
|
|
591
592
|
}
|
|
592
593
|
else {
|
|
593
|
-
void (0, workflow_runner_js_1.runWorkflow)(trigger, v2ToolContext, apiKey ?? '').then((result) => {
|
|
594
|
+
void (0, workflow_runner_js_1.runWorkflow)(trigger, v2ToolContext, apiKey ?? '', undefined, undefined, steerRegistry).then((result) => {
|
|
594
595
|
if (result._tag === 'success') {
|
|
595
596
|
console.log(`[ConsoleRoutes] Auto dispatch completed: workflowId=${workflowId} stopReason=${result.stopReason}`);
|
|
596
597
|
}
|
|
598
|
+
else if (result._tag === 'delivery_failed') {
|
|
599
|
+
console.log(`[ConsoleRoutes] Auto dispatch delivery failed: workflowId=${workflowId}`);
|
|
600
|
+
}
|
|
597
601
|
else if (result._tag === 'timeout') {
|
|
598
602
|
console.log(`[ConsoleRoutes] Auto dispatch timed out: workflowId=${workflowId}`);
|
|
599
603
|
}
|
|
600
|
-
else if (result._tag === '
|
|
601
|
-
console.log(`[ConsoleRoutes] Auto dispatch
|
|
604
|
+
else if (result._tag === 'error') {
|
|
605
|
+
console.log(`[ConsoleRoutes] Auto dispatch failed: workflowId=${workflowId} error=${result.message}`);
|
|
602
606
|
}
|
|
603
607
|
else {
|
|
604
|
-
|
|
608
|
+
(0, assert_never_js_1.assertNever)(result);
|
|
605
609
|
}
|
|
606
610
|
});
|
|
607
611
|
}
|
|
@@ -621,6 +625,26 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
621
625
|
}));
|
|
622
626
|
res.json({ success: true, data: { triggers } });
|
|
623
627
|
});
|
|
628
|
+
app.post('/api/v2/sessions/:sessionId/steer', express_1.default.json(), (req, res) => {
|
|
629
|
+
if (!steerRegistry) {
|
|
630
|
+
res.status(503).json({ success: false, error: 'Steer not available (not a daemon context).' });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const { sessionId } = req.params;
|
|
634
|
+
const body = req.body;
|
|
635
|
+
const text = typeof body.text === 'string' ? body.text.trim() : '';
|
|
636
|
+
if (!text) {
|
|
637
|
+
res.status(400).json({ success: false, error: 'text is required and must be a non-empty string.' });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const callback = steerRegistry.get(sessionId);
|
|
641
|
+
if (!callback) {
|
|
642
|
+
res.status(404).json({ success: false, error: 'Session not found or not a daemon session.' });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
callback(text);
|
|
646
|
+
res.json({ success: true });
|
|
647
|
+
});
|
|
624
648
|
const consoleDist = resolveConsoleDist();
|
|
625
649
|
if (consoleDist) {
|
|
626
650
|
app.use('/console', express_1.default.static(consoleDist, {
|
|
@@ -585,6 +585,17 @@ function extractRepoRoot(events) {
|
|
|
585
585
|
}
|
|
586
586
|
return workspacePathFallback;
|
|
587
587
|
}
|
|
588
|
+
function extractParentSessionId(events) {
|
|
589
|
+
for (const e of events) {
|
|
590
|
+
if (e.kind === constants_js_1.EVENT_KIND.SESSION_CREATED) {
|
|
591
|
+
const parentId = e.data.parentSessionId;
|
|
592
|
+
if (typeof parentId === 'string' && parentId.length > 0)
|
|
593
|
+
return parentId;
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
588
599
|
function truncateTitle(text, maxLen = 120) {
|
|
589
600
|
if (text.length <= maxLen)
|
|
590
601
|
return text;
|
|
@@ -612,6 +623,7 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
|
|
|
612
623
|
const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
|
|
613
624
|
const gitBranch = extractGitBranch(events);
|
|
614
625
|
const repoRoot = extractRepoRoot(events);
|
|
626
|
+
const parentSessionId = extractParentSessionId(events);
|
|
615
627
|
const isAutonomous = (() => {
|
|
616
628
|
if (!sortedEventsRes.isOk())
|
|
617
629
|
return false;
|
|
@@ -643,6 +655,7 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
|
|
|
643
655
|
lastModifiedMs,
|
|
644
656
|
isAutonomous,
|
|
645
657
|
isLive,
|
|
658
|
+
parentSessionId,
|
|
646
659
|
};
|
|
647
660
|
}
|
|
648
661
|
const workflow = run.workflow;
|
|
@@ -688,6 +701,7 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
|
|
|
688
701
|
lastModifiedMs,
|
|
689
702
|
isAutonomous,
|
|
690
703
|
isLive,
|
|
704
|
+
parentSessionId,
|
|
691
705
|
};
|
|
692
706
|
}
|
|
693
707
|
function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, workflowNames, skippedStepsMap = {}) {
|
|
@@ -20,6 +20,7 @@ export interface ConsoleSessionSummary {
|
|
|
20
20
|
readonly lastModifiedMs: number;
|
|
21
21
|
readonly isAutonomous: boolean;
|
|
22
22
|
readonly isLive: boolean;
|
|
23
|
+
readonly parentSessionId: string | null;
|
|
23
24
|
}
|
|
24
25
|
export interface ConsoleSessionListResponse {
|
|
25
26
|
readonly sessions: readonly ConsoleSessionSummary[];
|
package/docs/authoring.md
CHANGED
|
@@ -42,7 +42,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
42
42
|
**Source refs**
|
|
43
43
|
- `spec/workflow.schema.json` (schema) — Legal structure and supported fields.
|
|
44
44
|
- `src/application/services/validation-engine.ts` (runtime) — Validator-enforced authoring rules.
|
|
45
|
-
- `workflows/coding-task-workflow-agentic.
|
|
45
|
+
- `workflows/coding-task-workflow-agentic.json` (example) — Current modern example.
|
|
46
46
|
|
|
47
47
|
### validate-early-and-often
|
|
48
48
|
- **Level**: required
|
|
@@ -141,7 +141,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
141
141
|
- Part A / Part B / Rules: ... when the structure adds ceremony rather than clarity
|
|
142
142
|
|
|
143
143
|
**Example refs**
|
|
144
|
-
- `workflows/coding-task-workflow-agentic.
|
|
144
|
+
- `workflows/coding-task-workflow-agentic.json` — See the sharpened user-voiced prompts in the current lean coding workflow.
|
|
145
145
|
|
|
146
146
|
### protocol-footers-stay-explicit
|
|
147
147
|
- **Level**: required
|
|
@@ -160,10 +160,10 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
160
160
|
- Replacing exact capture requirements with vague summary prose
|
|
161
161
|
|
|
162
162
|
**Example refs**
|
|
163
|
-
- `workflows/coding-task-workflow-agentic.
|
|
163
|
+
- `workflows/coding-task-workflow-agentic.json` — Uses compact Capture footers and explicit loop-control wording.
|
|
164
164
|
|
|
165
165
|
**Source refs**
|
|
166
|
-
- `workflows/coding-task-workflow-agentic.
|
|
166
|
+
- `workflows/coding-task-workflow-agentic.json` (example) — Uses explicit capture footers and shape-preserving loop outputs.
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
## Prompt composition
|
|
@@ -185,11 +185,11 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
185
185
|
- Encoding runtime logic in prose when promptFragments or templates are the right mechanism
|
|
186
186
|
|
|
187
187
|
**Example refs**
|
|
188
|
-
- `workflows/coding-task-workflow-agentic.
|
|
188
|
+
- `workflows/coding-task-workflow-agentic.json` — Uses prompt fragments and context templates to keep prompts slimmer at render time.
|
|
189
189
|
|
|
190
190
|
**Source refs**
|
|
191
191
|
- `docs/authoring.md` (documentation) — Documents context templates and prompt fragments.
|
|
192
|
-
- `workflows/coding-task-workflow-agentic.
|
|
192
|
+
- `workflows/coding-task-workflow-agentic.json` (example) — Uses prompt fragments to slim mode-specific prompt branches.
|
|
193
193
|
|
|
194
194
|
### templates-are-for-simple-substitution
|
|
195
195
|
- **Level**: recommended
|
|
@@ -405,11 +405,11 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
405
405
|
- Prompt text says to stop, but the example output only permits continue
|
|
406
406
|
|
|
407
407
|
**Example refs**
|
|
408
|
-
- `workflows/coding-task-workflow-agentic.
|
|
408
|
+
- `workflows/coding-task-workflow-agentic.json` — Current loop decision steps show shape-only output examples.
|
|
409
409
|
|
|
410
410
|
**Source refs**
|
|
411
411
|
- `scripts/validate-workflows-registry.ts` (validator) — Registry validation should preserve semantically correct discoverable workflows.
|
|
412
|
-
- `workflows/coding-task-workflow-agentic.
|
|
412
|
+
- `workflows/coding-task-workflow-agentic.json` (example) — Current loop decision prompts show shape-only output examples.
|
|
413
413
|
|
|
414
414
|
### loops-need-real-exit-rules
|
|
415
415
|
- **Level**: required
|
|
@@ -445,7 +445,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
445
445
|
- `contextAuditNeeded = true|false` without an explicit rubric
|
|
446
446
|
|
|
447
447
|
**Example refs**
|
|
448
|
-
- `workflows/coding-task-workflow-agentic.
|
|
448
|
+
- `workflows/coding-task-workflow-agentic.json` — Phase 0 uses a context-clarity rubric instead of a vibes-only confidence flag.
|
|
449
449
|
|
|
450
450
|
|
|
451
451
|
## Confirmation discipline
|
|
@@ -466,7 +466,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
466
466
|
- Using requireConfirmation as a substitute for clear loop or rigor policy
|
|
467
467
|
|
|
468
468
|
**Source refs**
|
|
469
|
-
- `workflows/coding-task-workflow-agentic.
|
|
469
|
+
- `workflows/coding-task-workflow-agentic.json` (example) — Uses confirmation for real review barriers like MultiPR checkpoints.
|
|
470
470
|
|
|
471
471
|
|
|
472
472
|
## Assessment gates
|
|
@@ -544,7 +544,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
544
544
|
- Treating named builder or researcher roles as alternate owners
|
|
545
545
|
|
|
546
546
|
**Source refs**
|
|
547
|
-
- `workflows/coding-task-workflow-agentic.
|
|
547
|
+
- `workflows/coding-task-workflow-agentic.json` (example) — Delegation checkpoints keep the main agent as the synthesizer and decision-maker.
|
|
548
548
|
|
|
549
549
|
### batched-checkpoints-over-ad-hoc-optionality
|
|
550
550
|
- **Level**: recommended
|
|
@@ -563,7 +563,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
563
563
|
- Optional challenge wording at high-value decision points
|
|
564
564
|
|
|
565
565
|
**Example refs**
|
|
566
|
-
- `workflows/coding-task-workflow-agentic.
|
|
566
|
+
- `workflows/coding-task-workflow-agentic.json` — Uses explicit challenge, audit, and verification barriers.
|
|
567
567
|
|
|
568
568
|
|
|
569
569
|
## Subagent synthesis and claim adoption
|
|
@@ -601,10 +601,10 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
601
601
|
- Using delegated findings as blockers or green lights without verification
|
|
602
602
|
|
|
603
603
|
**Example refs**
|
|
604
|
-
- `workflows/coding-task-workflow-agentic.
|
|
604
|
+
- `workflows/coding-task-workflow-agentic.json` — Major synthesis checkpoints use Confirmed / Plausible / Rejected for decision-driving findings.
|
|
605
605
|
|
|
606
606
|
**Source refs**
|
|
607
|
-
- `workflows/coding-task-workflow-agentic.
|
|
607
|
+
- `workflows/coding-task-workflow-agentic.json` (example) — Major synthesis checkpoints use Confirmed / Plausible / Rejected for adopted claims.
|
|
608
608
|
|
|
609
609
|
|
|
610
610
|
## Discouraged legacy patterns
|
|
@@ -670,7 +670,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
670
670
|
- Keeping a canonical example ref after the workflow has drifted into a legacy style
|
|
671
671
|
|
|
672
672
|
**Example refs**
|
|
673
|
-
- `workflows/coding-task-workflow-agentic.
|
|
673
|
+
- `workflows/coding-task-workflow-agentic.json` — Current example of modern prompt composition, delegation barriers, and loop semantics.
|
|
674
674
|
|
|
675
675
|
|
|
676
676
|
## Validation
|
|
@@ -728,7 +728,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
|
|
|
728
728
|
- Verification steps check one artifact while planning updates a different one
|
|
729
729
|
|
|
730
730
|
**Example refs**
|
|
731
|
-
- `workflows/coding-task-workflow-agentic.
|
|
731
|
+
- `workflows/coding-task-workflow-agentic.json` — Uses explicit spec vs implementation-plan ownership.
|
|
732
732
|
|
|
733
733
|
|
|
734
734
|
## Planned guidance
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Implementation Plan: Coordinator Message Queue Drain
|
|
2
|
+
|
|
3
|
+
## 1. Problem Statement
|
|
4
|
+
|
|
5
|
+
`worktrain tell "<message>"` appends to `~/.workrail/message-queue.jsonl` but the PR review
|
|
6
|
+
coordinator (`runPrReviewCoordinator`) never reads this file. Messages sent from a phone,
|
|
7
|
+
terminal, or automation (e.g., "stop", "skip-pr 42") are silently ignored. The coordinator
|
|
8
|
+
must drain this queue at the start of each cycle and act on actionable messages before spawning
|
|
9
|
+
any agent.
|
|
10
|
+
|
|
11
|
+
## 2. Acceptance Criteria
|
|
12
|
+
|
|
13
|
+
AC1. When `stop` appears as the first meaningful word in a queued message (matched by
|
|
14
|
+
`/^\s*stop\b/i`), the coordinator exits cleanly without reviewing any PR, and appends an
|
|
15
|
+
outbox notification that includes the full triggering message text and timestamp.
|
|
16
|
+
|
|
17
|
+
AC2. When `skip-pr N` appears in a queued message (matched by `/\bskip[- ]pr[\s#]+(\d+)/i`),
|
|
18
|
+
PR #N is removed from the list before Stage 1 review dispatch. An outbox notification is
|
|
19
|
+
appended confirming the skip.
|
|
20
|
+
|
|
21
|
+
AC3. When `add-pr N` appears in a queued message (matched by `/\badd[- ]pr[\s#]+(\d+)/i`),
|
|
22
|
+
PR #N is added to the list (with Set dedup to prevent duplicates). An outbox notification
|
|
23
|
+
is appended confirming the addition.
|
|
24
|
+
|
|
25
|
+
AC4. Messages that match no recognized pattern are skipped silently (treated as notes).
|
|
26
|
+
|
|
27
|
+
AC5. After draining, the cursor in `~/.workrail/message-queue-cursor.json` is updated so
|
|
28
|
+
processed messages are not re-processed on the next coordinator invocation.
|
|
29
|
+
|
|
30
|
+
AC6. If `~/.workrail/message-queue.jsonl` does not exist (ENOENT), the drain returns a no-op
|
|
31
|
+
result and the coordinator proceeds normally.
|
|
32
|
+
|
|
33
|
+
AC7. Malformed JSONL lines (unparseable JSON) are skipped without crashing the coordinator.
|
|
34
|
+
A stderr warning is emitted for each skipped malformed line.
|
|
35
|
+
|
|
36
|
+
AC8. All drain I/O (readFile, appendFile, homedir, joinPath, now, generateId) is injected via
|
|
37
|
+
`CoordinatorDeps`. No direct `fs` imports are added to `pr-review.ts`.
|
|
38
|
+
|
|
39
|
+
AC9. Unit tests for `drainMessageQueue()` use fake deps (in-memory file map). No real filesystem
|
|
40
|
+
access in tests.
|
|
41
|
+
|
|
42
|
+
## 3. Non-Goals
|
|
43
|
+
|
|
44
|
+
- No `reprioritize` message kind in this PR
|
|
45
|
+
- No workspace routing (workspaceHint matching) -- all messages are consumed regardless of hint
|
|
46
|
+
- No structured `kind` field on `QueuedMessage` (Candidate C) -- that is a follow-up issue
|
|
47
|
+
- No truncation or compaction of consumed messages (queue remains append-only)
|
|
48
|
+
- No real-time / `--watch` mode
|
|
49
|
+
- No multi-coordinator fan-out (single coordinator consumes the queue)
|
|
50
|
+
- No integration test (unit tests with fakes are sufficient)
|
|
51
|
+
|
|
52
|
+
## 4. Philosophy-Driven Constraints
|
|
53
|
+
|
|
54
|
+
- Errors as data: `drainMessageQueue` returns `DrainResult`, never throws
|
|
55
|
+
- All I/O injected: `CoordinatorDeps` gains `readFile` and `appendFile`; zero direct fs imports
|
|
56
|
+
- Immutability: `DrainResult` and all new interfaces are fully readonly
|
|
57
|
+
- Prefer fakes over mocks: tests use in-memory fake deps
|
|
58
|
+
- Validate at boundaries: JSONL parsing, ENOENT, cursor desync handled at the read boundary
|
|
59
|
+
- Document WHY: function header explains the cursor pattern and text-matching tradeoff
|
|
60
|
+
|
|
61
|
+
## 5. Invariants
|
|
62
|
+
|
|
63
|
+
I1. `message-queue.jsonl` is never written or truncated by the coordinator (append-only)
|
|
64
|
+
I2. The coordinator drains the queue BEFORE Stage 1 (PR discovery) -- never mid-agent-run
|
|
65
|
+
I3. `stop: true` in `DrainResult` takes absolute precedence; coordinator must check stop before
|
|
66
|
+
acting on `skipPrNumbers` or `addPrNumbers`
|
|
67
|
+
I4. The cursor advances only AFTER successful outbox writes (best-effort; cursor write failure
|
|
68
|
+
does not block drain -- same pattern as worktrain-inbox.ts)
|
|
69
|
+
I5. ENOENT on message-queue.jsonl = no messages = coordinator proceeds normally (not an error)
|
|
70
|
+
I6. Cursor desync guard: if `cursor > totalLines`, reset to 0 (queue was wiped)
|
|
71
|
+
|
|
72
|
+
## 6. Selected Approach & Rationale
|
|
73
|
+
|
|
74
|
+
**Selected: Candidate B** -- `drainMessageQueue()` pure function with cursor + text parsing.
|
|
75
|
+
|
|
76
|
+
**Rationale:** Direct adaptation of the `worktrain-inbox.ts` cursor pattern (already tested, same
|
|
77
|
+
`InboxCursor` shape `{ lastReadCount: number }`). Additive to `CoordinatorDeps`. Text parsing is
|
|
78
|
+
narrow (`^\\s*stop\\b`) and consistent with how `parseFindingsFromNotes()` works in the same file.
|
|
79
|
+
|
|
80
|
+
**Runner-up: Candidate C** (structured `kind` field on `QueuedMessage`). Loses because it
|
|
81
|
+
requires a schema change to the public CLI interface (`worktrain tell`), which is out of scope.
|
|
82
|
+
Filed as a follow-up.
|
|
83
|
+
|
|
84
|
+
## 7. Vertical Slices
|
|
85
|
+
|
|
86
|
+
### Slice 1: Extend `CoordinatorDeps` and add `DrainResult` type
|
|
87
|
+
|
|
88
|
+
**Files:** `src/coordinators/pr-review.ts`
|
|
89
|
+
|
|
90
|
+
**Work:**
|
|
91
|
+
- Add `readFile: (path: string) => Promise<string>` to `CoordinatorDeps`
|
|
92
|
+
- Add `appendFile: (path: string, content: string) => Promise<void>` to `CoordinatorDeps`
|
|
93
|
+
- Add `mkdir: (path: string, options: { recursive: boolean }) => Promise<string | undefined>` to `CoordinatorDeps`
|
|
94
|
+
- Define `DrainResult` interface (readonly: stop, stopReason, skipPrNumbers, addPrNumbers, messagesProcessed)
|
|
95
|
+
|
|
96
|
+
**Done when:** TypeScript compiles with new interface fields. No runtime behavior change yet.
|
|
97
|
+
|
|
98
|
+
**Note:** Updating fake deps in `coordinator-pr-review.test.ts` is part of this slice (compile-
|
|
99
|
+
time requirement).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### Slice 2: Implement `drainMessageQueue()`
|
|
104
|
+
|
|
105
|
+
**Files:** `src/coordinators/pr-review.ts`
|
|
106
|
+
|
|
107
|
+
**Work:**
|
|
108
|
+
- New exported function `drainMessageQueue(deps, workrailDir)` -- deps is the coordinator deps
|
|
109
|
+
subset; workrailDir defaults to `deps.joinPath(deps.homedir(), '.workrail')`
|
|
110
|
+
- Reads `message-queue.jsonl` (ENOENT -> return empty result)
|
|
111
|
+
- Reads cursor from `message-queue-cursor.json` (missing/corrupt -> 0)
|
|
112
|
+
- Applies cursor desync guard (cursor > totalLines -> reset to 0)
|
|
113
|
+
- Parses new lines (slice from cursor), skips malformed with stderr warning
|
|
114
|
+
- For each parsed `QueuedMessage`:
|
|
115
|
+
- `^\\s*stop\\b/i` match -> set stop=true, record stopReason=message.message
|
|
116
|
+
- `/\\bskip[- ]pr[\\s#]+([0-9]+)/i` match -> add to skipSet
|
|
117
|
+
- `/\\badd[- ]pr[\\s#]+([0-9]+)/i` match -> add to addSet
|
|
118
|
+
- Otherwise: skip (informational note)
|
|
119
|
+
- After processing all new messages:
|
|
120
|
+
- For each actionable message: appendFile to outbox.jsonl with confirmation text
|
|
121
|
+
- Append stderr `[INFO coord:drain kind=... message="..." ts=...]` per actionable message
|
|
122
|
+
- Update cursor file (non-fatal on failure)
|
|
123
|
+
- Return `DrainResult`
|
|
124
|
+
|
|
125
|
+
**Done when:** Function exists, TypeScript compiles, unit tests pass.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Slice 3: Integrate drain into `runPrReviewCoordinator()`
|
|
130
|
+
|
|
131
|
+
**Files:** `src/coordinators/pr-review.ts`
|
|
132
|
+
|
|
133
|
+
**Work:**
|
|
134
|
+
- Call `drainMessageQueue(deps)` at the top of `runPrReviewCoordinator()` (before Stage 1 log)
|
|
135
|
+
- Check `drainResult.stop` immediately:
|
|
136
|
+
- If true: log stop reason, write report (empty/aborted), return early with all zeros
|
|
137
|
+
- Apply `drainResult.skipPrNumbers` to remove PRs from the discovered list (after Stage 1)
|
|
138
|
+
- Apply `drainResult.addPrNumbers` to add PRs to the list (with Set dedup, before Stage 1)
|
|
139
|
+
- Log drain activity: `[drain] processed N messages, skip=[...], add=[...]` if messagesProcessed > 0
|
|
140
|
+
|
|
141
|
+
**Done when:** Integration passes existing coordinator unit tests + new drain integration test.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### Slice 4: Wire new deps in `cli-worktrain.ts`
|
|
146
|
+
|
|
147
|
+
**Files:** `src/cli-worktrain.ts`
|
|
148
|
+
|
|
149
|
+
**Work:**
|
|
150
|
+
- Add `readFile: (p: string) => fs.promises.readFile(p, 'utf-8')` to CoordinatorDeps wiring
|
|
151
|
+
- Add `appendFile: (p: string, content: string) => fs.promises.appendFile(p, content, 'utf-8')`
|
|
152
|
+
to CoordinatorDeps wiring
|
|
153
|
+
- Add `mkdir: (p: string, opts: { recursive: boolean }) => fs.promises.mkdir(p, opts)` to
|
|
154
|
+
CoordinatorDeps wiring
|
|
155
|
+
|
|
156
|
+
**Done when:** `worktrain run pr-review --dry-run` compiles and runs without error.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Slice 5: Unit tests for `drainMessageQueue()`
|
|
161
|
+
|
|
162
|
+
**Files:** `tests/unit/coordinator-pr-review.test.ts`
|
|
163
|
+
|
|
164
|
+
**Work:**
|
|
165
|
+
- Add `readFile` and `appendFile` to the existing fake CoordinatorDeps helper
|
|
166
|
+
- New `describe('drainMessageQueue')` block covering:
|
|
167
|
+
- ENOENT -> returns empty DrainResult (messagesProcessed=0, stop=false)
|
|
168
|
+
- Stop message at start of message text -> stop=true, stopReason set
|
|
169
|
+
- Stop NOT triggered when 'stop' appears mid-sentence ("please stop overthinking" -- note: this
|
|
170
|
+
still fires with `^\\s*stop` since it doesn't start the message; test confirms this is the
|
|
171
|
+
designed behavior)
|
|
172
|
+
- skip-pr with PR number -> skipPrNumbers contains the number
|
|
173
|
+
- add-pr with PR number -> addPrNumbers contains the number
|
|
174
|
+
- Malformed JSONL lines skipped, messagesProcessed counts only valid lines
|
|
175
|
+
- Cursor advances after drain
|
|
176
|
+
- Cursor desync guard resets to 0 when cursor > totalLines
|
|
177
|
+
- Multiple messages: stop takes precedence regardless of order in queue
|
|
178
|
+
- Note-only messages: no action, cursor advances, messagesProcessed = N
|
|
179
|
+
|
|
180
|
+
**Done when:** All new tests pass; no existing tests broken.
|
|
181
|
+
|
|
182
|
+
## 8. Test Design
|
|
183
|
+
|
|
184
|
+
**Strategy:** Fake deps only (in-memory Map for files, Set for dirs). No real filesystem.
|
|
185
|
+
|
|
186
|
+
**Key test helpers:**
|
|
187
|
+
```ts
|
|
188
|
+
interface FakeDrainFs {
|
|
189
|
+
files: Map<string, string>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function makeDrainDeps(fs: FakeDrainFs): Pick<CoordinatorDeps, 'readFile' | 'appendFile' | 'mkdir' | 'homedir' | 'joinPath' | 'now' | 'generateId' | 'stderr'>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Critical test cases:**
|
|
196
|
+
- `stop` as sole message: stop=true, outbox has triggering text
|
|
197
|
+
- `skip-pr 42` after a note: skipPrNumbers=[42], messagesProcessed=2
|
|
198
|
+
- Two `skip-pr` for same PR: deduplicated in Set (skipPrNumbers=[42] not [42, 42])
|
|
199
|
+
- Cursor = 5, file has 5 lines: messagesProcessed=0 (all previously read)
|
|
200
|
+
- Cursor = 10, file has 5 lines: cursor reset to 0, all 5 processed
|
|
201
|
+
|
|
202
|
+
## 9. Risk Register
|
|
203
|
+
|
|
204
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
205
|
+
|---|---|---|---|
|
|
206
|
+
| `stop` false positive on note message | Low | Medium | `^\\s*stop\\b` anchor; outbox shows triggering text |
|
|
207
|
+
| Cursor file write failure | Very Low | Low | Non-fatal; next run re-reads from 0 (desync reset) |
|
|
208
|
+
| Outbox write failure during stop | Very Low | Low | Non-fatal; stderr log is backup |
|
|
209
|
+
| `readFile`/`appendFile` not wired in cli-worktrain.ts | Low | High | Slice 4 is explicit; TypeScript will catch missing fields at compile time |
|
|
210
|
+
|
|
211
|
+
## 10. PR Packaging Strategy
|
|
212
|
+
|
|
213
|
+
Single PR on branch `feat/coordinator-message-queue`. All 5 slices in one PR -- they are
|
|
214
|
+
tightly coupled (type change -> function -> integration -> wiring -> tests). Separating them
|
|
215
|
+
would create a non-compiling intermediate state.
|
|
216
|
+
|
|
217
|
+
## 11. Philosophy Alignment Per Slice
|
|
218
|
+
|
|
219
|
+
| Slice | Principle | Status |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| 1 | Immutability by default | Satisfied -- all new fields are readonly |
|
|
222
|
+
| 1 | Explicit domain types | Tension -- DrainResult uses boolean stop not a discriminated union; documented |
|
|
223
|
+
| 2 | Errors are data | Satisfied -- DrainResult is a value; ENOENT returns empty result |
|
|
224
|
+
| 2 | Dependency injection | Satisfied -- all I/O via injected deps |
|
|
225
|
+
| 2 | Validate at boundaries | Satisfied -- malformed JSONL skipped at parse boundary |
|
|
226
|
+
| 3 | Determinism over cleverness | Satisfied -- same queue + cursor = same result |
|
|
227
|
+
| 4 | Compose with small pure functions | Satisfied -- drainMessageQueue is pure at logic level |
|
|
228
|
+
| 5 | Prefer fakes over mocks | Satisfied -- fake deps, no vi.mock() |
|
|
229
|
+
|
|
230
|
+
## 12. Follow-Up Tickets
|
|
231
|
+
|
|
232
|
+
1. **Add `kind` field to `QueuedMessage` for structured dispatch** (Candidate C) -- unblocks
|
|
233
|
+
automated tooling writing to the message queue without text fragility.
|
|
234
|
+
2. **`worktrain tell --help` should list recognized coordinator command patterns** -- discovery
|
|
235
|
+
for users who don't know what command words the coordinator recognizes.
|
|
236
|
+
|
|
237
|
+
## Summary
|
|
238
|
+
|
|
239
|
+
- `estimatedPRCount`: 1
|
|
240
|
+
- `unresolvedUnknownCount`: 0
|
|
241
|
+
- `planConfidenceBand`: High
|