@exaudeus/workrail 3.33.0 → 3.34.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-worktrain.js +167 -8
- package/dist/console-ui/assets/{index-BuJFLLfY.js → index-C1JXnwZS.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +1 -0
- package/dist/daemon/agent-loop.js +1 -1
- package/dist/daemon/daemon-events.d.ts +17 -1
- package/dist/daemon/workflow-runner.d.ts +1 -1
- package/dist/daemon/workflow-runner.js +96 -21
- package/dist/manifest.json +43 -67
- package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
- package/dist/mcp/handlers/v2-error-mapping.js +2 -0
- package/dist/mcp/handlers/v2-execution/advance.js +25 -0
- package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
- package/dist/mcp/transports/http-entry.js +0 -7
- package/dist/mcp/transports/stdio-entry.js +0 -8
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +1 -42
- package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
- package/dist/v2/durable-core/domain/observation-builder.js +2 -2
- package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
- package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
- package/dist/v2/usecases/console-service.js +65 -14
- package/dist/v2/usecases/console-types.d.ts +1 -0
- package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
- package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
- package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
- package/docs/discovery/design-candidates.md +180 -0
- package/docs/discovery/design-review-findings.md +110 -0
- package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
- package/docs/ideas/backlog.md +266 -0
- package/package.json +1 -1
- package/workflows/wr.discovery.json +58 -7
- package/dist/mcp/transports/bridge-entry.d.ts +0 -102
- package/dist/mcp/transports/bridge-entry.js +0 -454
- package/dist/mcp/transports/bridge-events.d.ts +0 -55
- package/dist/mcp/transports/bridge-events.js +0 -24
- package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
- package/dist/mcp/transports/primary-tombstone.js +0 -51
package/dist/mcp-server.js
CHANGED
|
@@ -1,60 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.composeServer = exports.
|
|
5
|
-
exports.waitForStdinReadable = waitForStdinReadable;
|
|
4
|
+
exports.composeServer = exports.startHttpServer = exports.startStdioServer = void 0;
|
|
6
5
|
const transport_mode_js_1 = require("./mcp/transports/transport-mode.js");
|
|
7
6
|
const stdio_entry_js_1 = require("./mcp/transports/stdio-entry.js");
|
|
8
7
|
const http_entry_js_1 = require("./mcp/transports/http-entry.js");
|
|
9
|
-
const bridge_entry_js_1 = require("./mcp/transports/bridge-entry.js");
|
|
10
8
|
const assert_never_js_1 = require("./runtime/assert-never.js");
|
|
11
9
|
var stdio_entry_js_2 = require("./mcp/transports/stdio-entry.js");
|
|
12
10
|
Object.defineProperty(exports, "startStdioServer", { enumerable: true, get: function () { return stdio_entry_js_2.startStdioServer; } });
|
|
13
11
|
var http_entry_js_2 = require("./mcp/transports/http-entry.js");
|
|
14
12
|
Object.defineProperty(exports, "startHttpServer", { enumerable: true, get: function () { return http_entry_js_2.startHttpServer; } });
|
|
15
|
-
var bridge_entry_js_2 = require("./mcp/transports/bridge-entry.js");
|
|
16
|
-
Object.defineProperty(exports, "startBridgeServer", { enumerable: true, get: function () { return bridge_entry_js_2.startBridgeServer; } });
|
|
17
|
-
Object.defineProperty(exports, "detectHealthyPrimary", { enumerable: true, get: function () { return bridge_entry_js_2.detectHealthyPrimary; } });
|
|
18
13
|
var server_js_1 = require("./mcp/server.js");
|
|
19
14
|
Object.defineProperty(exports, "composeServer", { enumerable: true, get: function () { return server_js_1.composeServer; } });
|
|
20
|
-
const DEFAULT_MCP_PORT = 3100;
|
|
21
|
-
const STDIO_CLIENT_PROBE_MS = 150;
|
|
22
|
-
function waitForStdinReadable(timeoutMs, stdin = process.stdin) {
|
|
23
|
-
return new Promise((resolve) => {
|
|
24
|
-
stdin.pause();
|
|
25
|
-
const timer = setTimeout(() => {
|
|
26
|
-
stdin.removeListener('readable', onReadable);
|
|
27
|
-
resolve(false);
|
|
28
|
-
}, timeoutMs);
|
|
29
|
-
const onReadable = () => {
|
|
30
|
-
clearTimeout(timer);
|
|
31
|
-
stdin.removeListener('readable', onReadable);
|
|
32
|
-
resolve(true);
|
|
33
|
-
};
|
|
34
|
-
stdin.once('readable', onReadable);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
15
|
async function main() {
|
|
38
16
|
const mode = (0, transport_mode_js_1.resolveTransportMode)(process.env);
|
|
39
|
-
if (mode.kind === 'stdio') {
|
|
40
|
-
const primaryDetected = await (0, bridge_entry_js_1.detectHealthyPrimary)(DEFAULT_MCP_PORT);
|
|
41
|
-
if (primaryDetected != null) {
|
|
42
|
-
const hasClient = await waitForStdinReadable(STDIO_CLIENT_PROBE_MS, process.stdin);
|
|
43
|
-
if (!hasClient) {
|
|
44
|
-
console.error(`[Startup] Primary on :${primaryDetected.port}, no stdio client within ${STDIO_CLIENT_PROBE_MS}ms — exiting`);
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
console.error(`[Startup] Primary detected on :${primaryDetected.port} — starting in bridge mode`);
|
|
48
|
-
try {
|
|
49
|
-
await (0, bridge_entry_js_1.startBridgeServer)(primaryDetected.port, undefined, { originalPrimaryPid: primaryDetected.pid });
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
console.error('[Bridge] Fatal error, falling back to full stdio server:', error);
|
|
53
|
-
await (0, stdio_entry_js_1.startStdioServer)();
|
|
54
|
-
}
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
17
|
switch (mode.kind) {
|
|
59
18
|
case 'stdio':
|
|
60
19
|
await (0, stdio_entry_js_1.startStdioServer)();
|
|
@@ -34,11 +34,11 @@ function anchorsToObservations(anchors) {
|
|
|
34
34
|
});
|
|
35
35
|
break;
|
|
36
36
|
case 'repo_root':
|
|
37
|
-
if (anchor.value.length > constants_js_1.
|
|
37
|
+
if (anchor.value.length > constants_js_1.MAX_OBSERVATION_PATH_LENGTH)
|
|
38
38
|
break;
|
|
39
39
|
observations.push({
|
|
40
40
|
key: 'repo_root',
|
|
41
|
-
value: { type: '
|
|
41
|
+
value: { type: 'path', value: anchor.value },
|
|
42
42
|
confidence: 'high',
|
|
43
43
|
});
|
|
44
44
|
break;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Result } from 'neverthrow';
|
|
2
2
|
import type { Workflow } from '../../../types/workflow.js';
|
|
3
|
-
import type { PromptFragment } from '../../../types/workflow-definition.js';
|
|
3
|
+
import type { AssessmentDefinition, PromptFragment } from '../../../types/workflow-definition.js';
|
|
4
4
|
import type { LoadedSessionTruthV2 } from '../../ports/session-event-log-store.port.js';
|
|
5
5
|
import type { LoopPathFrameV1 } from '../schemas/execution-snapshot/index.js';
|
|
6
6
|
import type { NodeId, RunId } from '../ids/index.js';
|
|
@@ -8,6 +8,7 @@ export type PromptRenderError = {
|
|
|
8
8
|
readonly code: 'RENDER_FAILED';
|
|
9
9
|
readonly message: string;
|
|
10
10
|
};
|
|
11
|
+
export declare function formatAssessmentRequirementsForTest(assessments: readonly Pick<AssessmentDefinition, 'id' | 'purpose' | 'dimensions'>[]): readonly string[];
|
|
11
12
|
export declare function assembleFragmentedPrompt(fragments: readonly PromptFragment[], context: Record<string, unknown>): string;
|
|
12
13
|
export interface StepMetadata {
|
|
13
14
|
readonly stepId: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatAssessmentRequirementsForTest = formatAssessmentRequirementsForTest;
|
|
3
4
|
exports.assembleFragmentedPrompt = assembleFragmentedPrompt;
|
|
4
5
|
exports.renderPendingPrompt = renderPendingPrompt;
|
|
5
6
|
const neverthrow_1 = require("neverthrow");
|
|
@@ -174,6 +175,9 @@ function formatOutputContractRequirements(outputContract) {
|
|
|
174
175
|
];
|
|
175
176
|
}
|
|
176
177
|
}
|
|
178
|
+
function formatAssessmentRequirementsForTest(assessments) {
|
|
179
|
+
return formatAssessmentRequirements(assessments);
|
|
180
|
+
}
|
|
177
181
|
function formatAssessmentRequirements(assessments) {
|
|
178
182
|
if (assessments.length === 0)
|
|
179
183
|
return [];
|
|
@@ -190,6 +194,12 @@ function formatAssessmentRequirements(assessments) {
|
|
|
190
194
|
for (const dimension of assessment.dimensions) {
|
|
191
195
|
requirements.push(` ${dimension.id} (${dimension.levels.join(' | ')}): ${dimension.purpose}`);
|
|
192
196
|
}
|
|
197
|
+
const firstDimension = assessment.dimensions[0];
|
|
198
|
+
const exampleDimValue = firstDimension ? `"${firstDimension.levels[0] ?? 'high'}"` : '"high"';
|
|
199
|
+
const exampleDimKey = firstDimension ? `"${firstDimension.id}"` : '"dimensionId"';
|
|
200
|
+
requirements.push(`Canonical format:\n\`\`\`json\n` +
|
|
201
|
+
`{ "artifacts": [{ "kind": "wr.assessment", "assessmentId": "${assessment.id}", "dimensions": { ${exampleDimKey}: ${exampleDimValue} } }] }\n` +
|
|
202
|
+
`\`\`\``);
|
|
193
203
|
requirements.push('Use only canonical dimension levels. If the engine rejects the artifact, correct the submitted levels instead of inventing new ones.');
|
|
194
204
|
}
|
|
195
205
|
return requirements;
|
|
@@ -60,6 +60,49 @@ const AUTONOMOUS_HEARTBEAT_THRESHOLD_MS = 10 * 60 * 1000;
|
|
|
60
60
|
const LIVE_ACTIVITY_MAX_ENTRIES = 5;
|
|
61
61
|
const DAEMON_EVENT_LOG_READ_LIMIT_BYTES = 100 * 1024;
|
|
62
62
|
const DAEMON_EVENTS_DIR = path.join(os.homedir(), '.workrail', 'events', 'daemon');
|
|
63
|
+
async function isSessionLiveFromEventLog(workrailSessionId) {
|
|
64
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
65
|
+
const filePath = path.join(DAEMON_EVENTS_DIR, `${date}.jsonl`);
|
|
66
|
+
try {
|
|
67
|
+
let raw;
|
|
68
|
+
const stat = await fs.stat(filePath);
|
|
69
|
+
if (stat.size > DAEMON_EVENT_LOG_READ_LIMIT_BYTES) {
|
|
70
|
+
const fd = await fs.open(filePath, 'r');
|
|
71
|
+
const offset = stat.size - DAEMON_EVENT_LOG_READ_LIMIT_BYTES;
|
|
72
|
+
const buf = Buffer.alloc(DAEMON_EVENT_LOG_READ_LIMIT_BYTES);
|
|
73
|
+
try {
|
|
74
|
+
await fd.read(buf, 0, DAEMON_EVENT_LOG_READ_LIMIT_BYTES, offset);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await fd.close();
|
|
78
|
+
}
|
|
79
|
+
raw = buf.toString('utf8');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
raw = await fs.readFile(filePath, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
let hasSeen = false;
|
|
85
|
+
let hasCompleted = false;
|
|
86
|
+
for (const line of raw.split('\n')) {
|
|
87
|
+
if (!line.trim())
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
const event = JSON.parse(line);
|
|
91
|
+
if (event['workrailSessionId'] !== workrailSessionId)
|
|
92
|
+
continue;
|
|
93
|
+
hasSeen = true;
|
|
94
|
+
if (event['kind'] === 'session_completed')
|
|
95
|
+
hasCompleted = true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return hasSeen && !hasCompleted;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
63
106
|
async function readLiveActivity(workrailSessionId, maxEntries) {
|
|
64
107
|
const date = new Date().toISOString().slice(0, 10);
|
|
65
108
|
const filePath = path.join(DAEMON_EVENTS_DIR, `${date}.jsonl`);
|
|
@@ -87,12 +130,24 @@ async function readLiveActivity(workrailSessionId, maxEntries) {
|
|
|
87
130
|
continue;
|
|
88
131
|
try {
|
|
89
132
|
const event = JSON.parse(line);
|
|
90
|
-
|
|
133
|
+
const isToolEvent = event['kind'] === 'tool_called' ||
|
|
134
|
+
event['kind'] === 'tool_call_started' ||
|
|
135
|
+
event['kind'] === 'agent_stuck';
|
|
136
|
+
if (!isToolEvent ||
|
|
91
137
|
event['workrailSessionId'] !== workrailSessionId ||
|
|
92
|
-
typeof event['toolName'] !== 'string' ||
|
|
93
138
|
typeof event['ts'] !== 'number') {
|
|
94
139
|
continue;
|
|
95
140
|
}
|
|
141
|
+
if (event['kind'] === 'agent_stuck') {
|
|
142
|
+
activities.push({
|
|
143
|
+
toolName: 'agent_stuck',
|
|
144
|
+
summary: `STUCK: ${String(event['reason'] ?? '?')} -- ${String(event['detail'] ?? '').slice(0, 80)}`,
|
|
145
|
+
ts: event['ts'],
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (typeof event['toolName'] !== 'string')
|
|
150
|
+
continue;
|
|
96
151
|
activities.push({
|
|
97
152
|
toolName: event['toolName'],
|
|
98
153
|
...(typeof event['summary'] === 'string' ? { summary: event['summary'] } : {}),
|
|
@@ -136,7 +191,6 @@ class ConsoleService {
|
|
|
136
191
|
}
|
|
137
192
|
getSessionDetail(sessionIdStr) {
|
|
138
193
|
const sessionId = (0, index_js_1.asSessionId)(sessionIdStr);
|
|
139
|
-
const nowMs = Date.now();
|
|
140
194
|
return this.ports.sessionStore
|
|
141
195
|
.load(sessionId)
|
|
142
196
|
.mapErr((storeErr) => ({
|
|
@@ -157,17 +211,14 @@ class ConsoleService {
|
|
|
157
211
|
resolveWorkflowNames(dag, this.ports.pinnedWorkflowStore),
|
|
158
212
|
]).map(([completionMap, stepLabels, workflowNames]) => projectSessionDetail(sessionId, truth, completionMap, stepLabels, workflowNames));
|
|
159
213
|
})();
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
...detail,
|
|
169
|
-
liveActivity,
|
|
170
|
-
}));
|
|
214
|
+
const isLiveRA = neverthrow_1.ResultAsync.fromSafePromise(isSessionLiveFromEventLog(sessionIdStr));
|
|
215
|
+
return neverthrow_1.ResultAsync.combine([detailRA, isLiveRA]).andThen(([detail, isLive]) => {
|
|
216
|
+
if (!isLive) {
|
|
217
|
+
return (0, neverthrow_2.okAsync)({ ...detail, isLive: false, liveActivity: null });
|
|
218
|
+
}
|
|
219
|
+
const liveActivityRA = neverthrow_1.ResultAsync.fromSafePromise(readLiveActivity(sessionIdStr, LIVE_ACTIVITY_MAX_ENTRIES));
|
|
220
|
+
return liveActivityRA.map((liveActivity) => ({ ...detail, isLive: true, liveActivity }));
|
|
221
|
+
});
|
|
171
222
|
});
|
|
172
223
|
}
|
|
173
224
|
getNodeDetail(sessionIdStr, nodeId) {
|
|
@@ -87,6 +87,7 @@ export interface ConsoleSessionDetail {
|
|
|
87
87
|
readonly sessionTitle: string | null;
|
|
88
88
|
readonly health: ConsoleSessionHealth;
|
|
89
89
|
readonly runs: readonly ConsoleDagRun[];
|
|
90
|
+
readonly isLive?: boolean;
|
|
90
91
|
readonly liveActivity?: readonly ConsoleToolActivity[] | null;
|
|
91
92
|
}
|
|
92
93
|
export type ConsoleValidationOutcome = 'pass' | 'fail';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# PR-A Bridge Removal -- Design Candidates
|
|
2
|
+
|
|
3
|
+
**Status:** Implementation ready. Single candidate; no genuine alternatives.
|
|
4
|
+
**Date:** 2026-04-17
|
|
5
|
+
**Scope:** PR-A only -- delete bridge-entry.ts, primary-tombstone.ts, bridge-events.ts; remove auto-bridge block from mcp-server.ts; remove tombstone/bridge-events call sites from stdio-entry.ts and http-entry.ts.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem Understanding
|
|
10
|
+
|
|
11
|
+
### Core Tensions
|
|
12
|
+
|
|
13
|
+
**T1: Delete all bridge files vs keep http-entry transport intact**
|
|
14
|
+
|
|
15
|
+
`http-entry.ts` imports from both `primary-tombstone.ts` and `bridge-events.ts`. The task requires keeping `http-entry.ts` transport logic untouched while removing tombstone/bridge-events call sites from it. The seam is at the import level, not the file level.
|
|
16
|
+
|
|
17
|
+
**T2: Self-referential risk**
|
|
18
|
+
|
|
19
|
+
This MCP server is the tool running this workflow. A broken build would kill the active session. All changes must be made, build verified, then committed.
|
|
20
|
+
|
|
21
|
+
**T3: Logging loss**
|
|
22
|
+
|
|
23
|
+
`logBridgeEvent({ kind: 'primary_started', ... })` in both stdio-entry.ts and http-entry.ts records primary server startup for bridge forensics. After bridge removal this logging has no consumer. Removing it is correct but it is the only usage of bridge-events.ts in those files -- confirms the module is entirely removable.
|
|
24
|
+
|
|
25
|
+
### Likely Seam
|
|
26
|
+
|
|
27
|
+
The `import` statements at the tops of `mcp-server.ts`, `stdio-entry.ts`, and `http-entry.ts`. Removing those imports is what makes the TypeScript compiler verify that nothing references the deleted files at compile time. Any missed call site produces a build error rather than silent breakage.
|
|
28
|
+
|
|
29
|
+
### What Makes This Hard
|
|
30
|
+
|
|
31
|
+
Nothing architecturally hard. The main risk is a missed import or call site in a file not in the primary list (e.g. `src/mcp/index.ts` re-exporting from bridge-entry.ts). Must grep for all three module names before deleting. The `mcp-server.test.ts` test reads the source file as a string and checks for specific export patterns -- need to verify no assertion references `startBridgeServer` or `detectHealthyPrimary`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Philosophy Constraints
|
|
36
|
+
|
|
37
|
+
Sources: `/Users/etienneb/CLAUDE.md` and `docs/design/stdio-simplification-design-candidates.md` (philosophy table, lines 68-75).
|
|
38
|
+
|
|
39
|
+
| Principle | Constraint |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Architectural fixes over patches | Delete the root cause (bridge block). No feature flag. |
|
|
42
|
+
| YAGNI with discipline | bridge-entry, primary-tombstone, bridge-events are all YAGNI once auto-bridge block is removed. |
|
|
43
|
+
| Determinism over cleverness | 150ms stdin probe, reconnect backoff, spawn coordinator lock -- all eliminated. |
|
|
44
|
+
| Make illegal states unrepresentable | `waitForStdinReadable` and `STDIO_CLIENT_PROBE_MS` have no callers after removal; deleting them prevents future misuse. |
|
|
45
|
+
|
|
46
|
+
**No conflicts** between CLAUDE.md and design doc for PR-A.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Impact Surface
|
|
51
|
+
|
|
52
|
+
| Surface | Action | Reason |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `src/mcp/transports/http-entry.ts` | Modify (tombstone/bridge-events call sites only) | Transport logic must stay intact |
|
|
55
|
+
| `src/mcp/transports/http-listener.ts` | No change | No bridge imports |
|
|
56
|
+
| `tests/integration/mcp-http-transport.test.ts` | No change | Tests MCP-over-HTTP, not bridge |
|
|
57
|
+
| `tests/unit/mcp/http-listener.test.ts` | No change | Tests createHttpListener lifecycle |
|
|
58
|
+
| `tests/unit/mcp-server.test.ts` | Review only | Checks source file exports as strings; confirmed no assertion for startBridgeServer |
|
|
59
|
+
| `src/mcp/index.ts` | Review before deleting | May re-export from bridge-entry.ts |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Candidates
|
|
64
|
+
|
|
65
|
+
Only one genuine candidate. All alternatives (feature flag, deprecation shim, alias re-exports) contradict the explicit design doc and repo direct-deletion pattern. Noting this honestly.
|
|
66
|
+
|
|
67
|
+
### Candidate 1 (Only Candidate): Direct deletion with targeted import removal
|
|
68
|
+
|
|
69
|
+
**Summary:** Delete the 3 source files and 3 test files outright. Remove their imports and call sites from mcp-server.ts, stdio-entry.ts, and http-entry.ts. Update mcp-server.ts to call `startStdioServer()` unconditionally for stdio mode.
|
|
70
|
+
|
|
71
|
+
**Tensions resolved:** T1 (boundary is at import level, not file level -- transport logic untouched), T2 (minimal blast radius -- 6 files deleted, 3 modified), T3 (logging call sites removed along with the module).
|
|
72
|
+
|
|
73
|
+
**Tensions accepted:** None.
|
|
74
|
+
|
|
75
|
+
**Boundary solved at:** Import statements at the tops of mcp-server.ts, stdio-entry.ts, http-entry.ts.
|
|
76
|
+
|
|
77
|
+
**Why that boundary is best fit:** TypeScript compiler enforces correctness. Any missed call site produces a build error rather than silent breakage. The boundary is compiler-verified, not grep-and-hope.
|
|
78
|
+
|
|
79
|
+
**Failure mode:** A missed import in an unexpected file (e.g. `mcp/index.ts` re-exporting `startBridgeServer`). Mitigation: grep `src/ tests/` for `bridge-entry`, `primary-tombstone`, `bridge-events` before deleting.
|
|
80
|
+
|
|
81
|
+
**Repo-pattern relationship:** Follows direct-deletion pattern exactly. No flags, no shims. Every refactor in the git log (including PR #512) removes code directly.
|
|
82
|
+
|
|
83
|
+
**Gains:** ~1127 lines of dead code deleted. Startup becomes deterministic. No 150ms probe delay. No spawn coordinator lock files. No tombstone files.
|
|
84
|
+
|
|
85
|
+
**Gives up:** Nothing. Bridge functionality is superseded by standalone `worktrain console`.
|
|
86
|
+
|
|
87
|
+
**Scope judgment:** Best-fit. Exactly matches PR-A specification from design doc.
|
|
88
|
+
|
|
89
|
+
**Philosophy:** Honors architectural fixes over patches, YAGNI, determinism, make illegal states unrepresentable. No conflicts.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Comparison and Recommendation
|
|
94
|
+
|
|
95
|
+
**Recommendation: Candidate 1.**
|
|
96
|
+
|
|
97
|
+
Only candidate. All alternative shapes add complexity without benefit and contradict the explicit design doc. Direct deletion is both the simplest and the architecturally correct approach.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Self-Critique
|
|
102
|
+
|
|
103
|
+
**Strongest argument against:** Could keep `bridge-events.ts` as a general-purpose process lifecycle log (it does log `primary_started` which is not bridge-specific). Counter: the file's event vocabulary is overwhelmingly bridge-specific (`reconnected`, `spawn_primary`, `budget_exhausted`, `waiting_for_primary`). Keeping it would require renaming, refactoring the event union, and updating callers. Scope creep. Delete it.
|
|
104
|
+
|
|
105
|
+
**Narrower option:** Keep `primary-tombstone.ts` in place with no callers. Rejected: dead file with no callers is dead code; deleting makes the absence unrepresentable.
|
|
106
|
+
|
|
107
|
+
**Broader option:** Also clean up `worktrain-spawn.ts` and `worktrain-await.ts` which have a `dashboard.lock` fallback that will now be permanently dead code. Evidence from design doc: "No behavioral change, but the fallback is now permanently dead code." The design doc explicitly calls this a follow-up chore PR. Not in PR-A scope.
|
|
108
|
+
|
|
109
|
+
**Assumption that would invalidate:** `src/mcp/index.ts` re-exports `startBridgeServer` as part of public library API. Must verify with grep before deleting.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Open Questions
|
|
114
|
+
|
|
115
|
+
None requiring human decision. All scoped by design doc. The single pre-implementation check (grep for unexpected importers) is a mechanical verification step, not a decision.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# PR-A Bridge Removal -- Design Review Findings
|
|
2
|
+
|
|
3
|
+
**Status:** Review complete. No blocking issues. Ready to implement.
|
|
4
|
+
**Date:** 2026-04-17
|
|
5
|
+
**Reviewed design:** Direct deletion of bridge-entry.ts, primary-tombstone.ts, bridge-events.ts; targeted import removal from mcp-server.ts, stdio-entry.ts, http-entry.ts.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tradeoff Review
|
|
10
|
+
|
|
11
|
+
| Tradeoff | Verdict | Notes |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Losing `logBridgeEvent` lifecycle logging | Acceptable | No consumer reads bridge.log except bridge forensics. Grep confirms no reader in `src/`. |
|
|
14
|
+
| `waitForStdinReadable` removed from public API | Acceptable | Not re-exported from `mcp/index.ts`. Only consumer is `tests/unit/mcp/stdin-probe.test.ts` which is also deleted. |
|
|
15
|
+
| `mcp-server.ts` JSDoc describes removed behavior | Requires fix | Top comment block (lines 11-24) explains auto-bridge mechanism that will no longer exist. Must update. |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Failure Mode Review
|
|
20
|
+
|
|
21
|
+
| Failure Mode | Coverage | Risk |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Missed import in unexpected file | **Fully covered** -- grep of src/ and tests/ returned exactly the expected 7 files. TypeScript compiler provides second verification. | Low |
|
|
24
|
+
| `mcp-server.test.ts` string assertions failing | **Covered** -- test file read in full; no assertion for startBridgeServer, detectHealthyPrimary, or waitForStdinReadable. | Low |
|
|
25
|
+
| http-entry.ts transport logic accidentally broken | **Covered** -- tombstone call sites are isolated lines (32, 35, 111-113); transport registration (73-76, 78), health endpoint (89-91), and port binding (45) are untouched. | Low |
|
|
26
|
+
|
|
27
|
+
No high-risk failure modes remain.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Runner-Up / Simpler Alternative Review
|
|
32
|
+
|
|
33
|
+
No genuine runner-up. All alternatives examined:
|
|
34
|
+
- **Keep bridge-events.ts:** Leaves dead module with bridge-specific event vocabulary. Rejected (YAGNI).
|
|
35
|
+
- **Skip test file deletion:** Would fail `npm test` at import resolution. Not simpler.
|
|
36
|
+
- **Skip source file modification:** Build fails with cannot-find-module errors. Not viable.
|
|
37
|
+
|
|
38
|
+
The selected design is already the minimal change that satisfies acceptance criteria.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Philosophy Alignment
|
|
43
|
+
|
|
44
|
+
| Principle | Status |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Architectural fixes over patches | Satisfied -- root cause deleted, no flag |
|
|
47
|
+
| YAGNI with discipline | Satisfied -- all 3 modules have zero callers post-removal |
|
|
48
|
+
| Determinism over cleverness | Satisfied -- startup becomes one unconditional call |
|
|
49
|
+
| Make illegal states unrepresentable | Satisfied -- waitForStdinReadable deleted |
|
|
50
|
+
| Type safety as first line of defense | Satisfied -- compiler verifies completeness |
|
|
51
|
+
| Document why, not what | Under tension -- JSDoc needs update (see Yellow finding) |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Findings
|
|
56
|
+
|
|
57
|
+
### Yellow -- JSDoc in mcp-server.ts describes removed behavior
|
|
58
|
+
|
|
59
|
+
**File:** `src/mcp-server.ts` lines 11-24 (auto-bridge and zombie-bridge guard comments)
|
|
60
|
+
|
|
61
|
+
**Issue:** After removal, the multi-line JSDoc comment at the top of the file describes behavior (auto-bridge detection, zombie-bridge guard, stdin probe) that no longer exists. Leaving it would mislead future readers.
|
|
62
|
+
|
|
63
|
+
**Recommended fix:** Replace the existing comment block with a simplified description: "Resolves transport mode from environment and starts the appropriate server."
|
|
64
|
+
|
|
65
|
+
**Severity:** Yellow -- not a correctness issue, but violates "Document why, not what" and misleads future readers.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Recommended Revisions
|
|
70
|
+
|
|
71
|
+
1. **Update JSDoc in mcp-server.ts** -- replace the auto-bridge explanation (lines 11-24) with a brief description of the simplified startup path. Required before commit.
|
|
72
|
+
|
|
73
|
+
2. **Verify test passes for `mcp-server.test.ts`** -- after editing, run the test explicitly to confirm the string-match assertions still pass. This is a build-time check, but worth isolating.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Residual Concerns
|
|
78
|
+
|
|
79
|
+
None. Import surface verified. Failure modes covered. Philosophy alignment confirmed. The JSDoc revision is a required edit, not an unresolved concern.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# PR-A Bridge Removal -- Implementation Plan
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-17
|
|
4
|
+
**Branch:** `feat/mcp-simplify-remove-bridge`
|
|
5
|
+
**Confidence:** High
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem Statement
|
|
10
|
+
|
|
11
|
+
The WorkRail MCP server contains a bridge mechanism (bridge-entry.ts, primary-tombstone.ts, bridge-events.ts) that was built to solve port contention between multiple Claude Code windows. PR #512 (merged) introduced `worktrain console` as a standalone process that owns the dashboard UI independently of the MCP server. The bridge mechanism's reason for existence is gone. It adds ~1127 lines of reconnect state machine, spawn coordination, and tombstone logic that make startup non-deterministic (150ms probe delay) and increase operational complexity.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Acceptance Criteria
|
|
16
|
+
|
|
17
|
+
1. `src/mcp/transports/bridge-entry.ts` does not exist
|
|
18
|
+
2. `src/mcp/transports/primary-tombstone.ts` does not exist
|
|
19
|
+
3. `src/mcp/transports/bridge-events.ts` does not exist
|
|
20
|
+
4. `src/mcp-server.ts` no longer imports or re-exports `startBridgeServer`, `detectHealthyPrimary`, or `waitForStdinReadable`
|
|
21
|
+
5. `src/mcp-server.ts` no longer contains the auto-bridge detection block (lines 89-118 in current file)
|
|
22
|
+
6. `src/mcp/transports/stdio-entry.ts` no longer imports from `primary-tombstone.ts` or `bridge-events.ts`
|
|
23
|
+
7. `src/mcp/transports/http-entry.ts` no longer imports from `primary-tombstone.ts` or `bridge-events.ts`
|
|
24
|
+
8. `npm run build` exits 0 with 0 TypeScript errors
|
|
25
|
+
9. `npm test` exits 0
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Non-Goals
|
|
30
|
+
|
|
31
|
+
- HttpServer removal (PR-B)
|
|
32
|
+
- ToolContext changes
|
|
33
|
+
- DI token cleanup
|
|
34
|
+
- Any change to http-entry.ts transport logic (MCP-over-HTTP for bot services)
|
|
35
|
+
- Modifying http-listener.ts or http-listener.test.ts
|
|
36
|
+
- Removing `ctx.httpServer?.stop()` from shutdown hooks (HttpServer still starts in PR-A state)
|
|
37
|
+
- Cleaning up `worktrain-spawn.ts` / `worktrain-await.ts` dashboard.lock fallback (chore PR later)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Philosophy-Driven Constraints
|
|
42
|
+
|
|
43
|
+
- **Architectural fixes over patches:** Delete root cause, not add flags
|
|
44
|
+
- **YAGNI:** Delete all three modules with no callers
|
|
45
|
+
- **Determinism:** Startup path must be a single unconditional call to `startStdioServer()` for stdio mode
|
|
46
|
+
- **Commit type:** `chore(mcp)` -- internal cleanup, no user-visible behavior change, no tool contract change
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Invariants
|
|
51
|
+
|
|
52
|
+
1. `WORKRAIL_TRANSPORT=http` path must still work -- `startHttpServer()` is called unconditionally for http mode
|
|
53
|
+
2. `WORKRAIL_TRANSPORT=stdio` must call `startStdioServer()` directly after removal -- no probe, no bridge detection
|
|
54
|
+
3. `mcp/index.ts` exports must be unchanged (confirmed: no bridge symbols were re-exported)
|
|
55
|
+
4. `tests/integration/mcp-http-transport.test.ts` must pass -- MCP-over-HTTP transport is untouched
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Selected Approach
|
|
60
|
+
|
|
61
|
+
**Direct deletion with targeted import removal.** Delete 3 source files and 3 test files. Remove their imports and call sites from 3 modified source files. TypeScript compiler verifies completeness at the import boundary.
|
|
62
|
+
|
|
63
|
+
**Runner-up:** None. All alternatives were anti-patterns.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Vertical Slices
|
|
68
|
+
|
|
69
|
+
### Slice 1: Create branch and verify baseline
|
|
70
|
+
|
|
71
|
+
- Create git branch `feat/mcp-simplify-remove-bridge` from current HEAD
|
|
72
|
+
- Run `npm run build` to confirm baseline is green
|
|
73
|
+
- Run `npm test` to confirm baseline test suite is green
|
|
74
|
+
- **Done when:** Both commands exit 0
|
|
75
|
+
|
|
76
|
+
### Slice 2: Delete source files
|
|
77
|
+
|
|
78
|
+
- Delete `src/mcp/transports/bridge-entry.ts`
|
|
79
|
+
- Delete `src/mcp/transports/primary-tombstone.ts`
|
|
80
|
+
- Delete `src/mcp/transports/bridge-events.ts`
|
|
81
|
+
- **Done when:** Files do not exist on disk
|
|
82
|
+
|
|
83
|
+
### Slice 3: Delete test files
|
|
84
|
+
|
|
85
|
+
- Delete `tests/unit/mcp/transports/bridge-entry.test.ts`
|
|
86
|
+
- Delete `tests/unit/mcp/transports/primary-tombstone.test.ts`
|
|
87
|
+
- Delete `tests/unit/mcp/stdin-probe.test.ts`
|
|
88
|
+
- **Done when:** Files do not exist on disk
|
|
89
|
+
|
|
90
|
+
### Slice 4: Modify mcp-server.ts
|
|
91
|
+
|
|
92
|
+
Remove the following from `src/mcp-server.ts`:
|
|
93
|
+
- Import of `startBridgeServer`, `detectHealthyPrimary` from `./mcp/transports/bridge-entry.js`
|
|
94
|
+
- Re-export of `startBridgeServer`, `detectHealthyPrimary` from `./mcp/transports/bridge-entry.js`
|
|
95
|
+
- `STDIO_CLIENT_PROBE_MS` constant (line 49)
|
|
96
|
+
- `waitForStdinReadable` function (lines 62-82)
|
|
97
|
+
- Auto-bridge detection block inside `main()` (lines 88-118)
|
|
98
|
+
- Top JSDoc comment block (lines 11-24) describing auto-bridge -- replace with simplified description
|
|
99
|
+
- **Done when:** File compiles with no errors referencing bridge-entry; `main()` calls `startStdioServer()` unconditionally for stdio mode
|
|
100
|
+
|
|
101
|
+
### Slice 5: Modify stdio-entry.ts
|
|
102
|
+
|
|
103
|
+
Remove from `src/mcp/transports/stdio-entry.ts`:
|
|
104
|
+
- Import: `import { writeTombstone, clearTombstone } from './primary-tombstone.js'`
|
|
105
|
+
- Import: `import { logBridgeEvent } from './bridge-events.js'`
|
|
106
|
+
- Call: `logBridgeEvent({ kind: 'primary_started', transport: 'stdio' })` (line 39)
|
|
107
|
+
- Call: `clearTombstone()` (line 44)
|
|
108
|
+
- In `onBeforeTerminate`: remove the tombstone write block (lines 124-127: `const port = ctx.httpServer?.getPort(); if (port != null) { writeTombstone(port, process.pid); }`)
|
|
109
|
+
- **Done when:** File compiles with no imports or references to deleted modules; `onBeforeTerminate` only calls `await ctx.httpServer?.stop()`
|
|
110
|
+
|
|
111
|
+
### Slice 6: Modify http-entry.ts
|
|
112
|
+
|
|
113
|
+
Remove from `src/mcp/transports/http-entry.ts` (tombstone/bridge-events call sites ONLY):
|
|
114
|
+
- Import: `import { clearTombstone, writeTombstone } from './primary-tombstone.js'`
|
|
115
|
+
- Import: `import { logBridgeEvent } from './bridge-events.js'`
|
|
116
|
+
- Call: `logBridgeEvent({ kind: 'primary_started', transport: 'http', port })` (line 32)
|
|
117
|
+
- Call: `clearTombstone()` (line 35)
|
|
118
|
+
- In `onBeforeTerminate`: remove `if (boundPort != null) { writeTombstone(boundPort, process.pid); }` (lines 111-113)
|
|
119
|
+
- **Done when:** File compiles with no imports or references to deleted modules; ALL transport logic (bindWithPortFallback, MCP endpoint registration, health endpoint) is unchanged
|
|
120
|
+
|
|
121
|
+
### Slice 7: Build and test verification
|
|
122
|
+
|
|
123
|
+
- Run `npm run build` -- must exit 0 with 0 errors
|
|
124
|
+
- Run `npm test` -- must exit 0
|
|
125
|
+
- **Done when:** Both commands succeed
|
|
126
|
+
|
|
127
|
+
### Slice 8: Commit and push PR
|
|
128
|
+
|
|
129
|
+
- `git add` the 6 deleted files and 3 modified source files
|
|
130
|
+
- Commit with message: `chore(mcp): remove bridge, tombstone, and bridge-events from MCP server`
|
|
131
|
+
- Push branch and create PR against main
|
|
132
|
+
- **Done when:** PR is open
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Test Design
|
|
137
|
+
|
|
138
|
+
**Tests to delete:**
|
|
139
|
+
- `tests/unit/mcp/transports/bridge-entry.test.ts` -- imports deleted module
|
|
140
|
+
- `tests/unit/mcp/transports/primary-tombstone.test.ts` -- imports deleted module
|
|
141
|
+
- `tests/unit/mcp/stdin-probe.test.ts` -- imports `waitForStdinReadable` from mcp-server.ts (also deleted)
|
|
142
|
+
|
|
143
|
+
**Tests that must stay green:**
|
|
144
|
+
- `tests/unit/mcp-server.test.ts` -- reads mcp-server.ts as string; no assertion for bridge exports
|
|
145
|
+
- `tests/unit/mcp/http-listener.test.ts` -- tests createHttpListener lifecycle; no bridge dependency
|
|
146
|
+
- `tests/integration/mcp-http-transport.test.ts` -- tests MCP-over-HTTP; no bridge dependency
|
|
147
|
+
- All other tests (no bridge imports in non-deleted test files)
|
|
148
|
+
|
|
149
|
+
**No new tests required.** The change is a deletion; the verification is `npm run build` (0 errors) + `npm test`.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Risk Register
|
|
154
|
+
|
|
155
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
156
|
+
|---|---|---|---|
|
|
157
|
+
| Missed import in unexpected file | Low | Build failure | Grep sweep confirmed before implementation: only 7 files (5 src, 2 tests) import from deleted modules |
|
|
158
|
+
| http-entry.ts transport logic broken | Low | Runtime failure | Tombstone call sites are isolated lines; transport logic verified as non-overlapping |
|
|
159
|
+
| mcp-server.test.ts string assertions fail | Low | Test failure | Test file read in full; no assertion for bridge exports |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## PR Packaging Strategy
|
|
164
|
+
|
|
165
|
+
Single PR. Branch: `feat/mcp-simplify-remove-bridge`. All 8 slices in one commit.
|
|
166
|
+
|
|
167
|
+
Commit message: `chore(mcp): remove bridge, tombstone, and bridge-events from MCP server`
|
|
168
|
+
|
|
169
|
+
PR description must include:
|
|
170
|
+
- What was deleted and why (bridge mechanism superseded by standalone worktrain console)
|
|
171
|
+
- What was NOT changed (http-entry.ts transport logic, http-listener.ts, ToolContext)
|
|
172
|
+
- Link to design doc: `docs/design/stdio-simplification-design-candidates.md` PR-A section
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Philosophy Alignment Per Slice
|
|
177
|
+
|
|
178
|
+
| Slice | Principle | Status |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| All slices | Architectural fixes over patches | Satisfied -- root cause deleted, no shim |
|
|
181
|
+
| Slice 2-3 | YAGNI with discipline | Satisfied -- all 3 modules have zero callers post-removal |
|
|
182
|
+
| Slice 4 | Determinism over cleverness | Satisfied -- startup becomes single unconditional call |
|
|
183
|
+
| Slice 4 | Make illegal states unrepresentable | Satisfied -- waitForStdinReadable deleted |
|
|
184
|
+
| Slice 4 | Document why, not what | Satisfied -- JSDoc updated to reflect simplified behavior |
|
|
185
|
+
| Slice 5-6 | Type safety as first line | Satisfied -- compiler verifies no remaining references |
|
|
186
|
+
| Slice 6 | Keep interfaces small | Satisfied -- http-entry.ts shrinks, transport logic unchanged |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Follow-Up Tickets
|
|
191
|
+
|
|
192
|
+
- `worktrain-spawn.ts` and `worktrain-await.ts` `dashboard.lock` fallback is now permanently dead code. Remove in a separate chore PR.
|
|
193
|
+
- PR-B: Remove HttpServer from MCP server startup entirely.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Unresolved Unknown Count
|
|
198
|
+
|
|
199
|
+
**0.** All import surfaces verified, failure modes addressed, acceptance criteria measurable.
|
|
200
|
+
|
|
201
|
+
## Plan Confidence Band
|
|
202
|
+
|
|
203
|
+
**High.**
|