@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.
Files changed (38) hide show
  1. package/dist/cli-worktrain.js +167 -8
  2. package/dist/console-ui/assets/{index-BuJFLLfY.js → index-C1JXnwZS.js} +1 -1
  3. package/dist/console-ui/index.html +1 -1
  4. package/dist/daemon/agent-loop.d.ts +1 -0
  5. package/dist/daemon/agent-loop.js +1 -1
  6. package/dist/daemon/daemon-events.d.ts +17 -1
  7. package/dist/daemon/workflow-runner.d.ts +1 -1
  8. package/dist/daemon/workflow-runner.js +96 -21
  9. package/dist/manifest.json +43 -67
  10. package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
  11. package/dist/mcp/handlers/v2-error-mapping.js +2 -0
  12. package/dist/mcp/handlers/v2-execution/advance.js +25 -0
  13. package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
  14. package/dist/mcp/transports/http-entry.js +0 -7
  15. package/dist/mcp/transports/stdio-entry.js +0 -8
  16. package/dist/mcp-server.d.ts +0 -2
  17. package/dist/mcp-server.js +1 -42
  18. package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
  19. package/dist/v2/durable-core/domain/observation-builder.js +2 -2
  20. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
  21. package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
  22. package/dist/v2/usecases/console-service.js +65 -14
  23. package/dist/v2/usecases/console-types.d.ts +1 -0
  24. package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
  25. package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
  26. package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
  27. package/docs/discovery/design-candidates.md +180 -0
  28. package/docs/discovery/design-review-findings.md +110 -0
  29. package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
  30. package/docs/ideas/backlog.md +266 -0
  31. package/package.json +1 -1
  32. package/workflows/wr.discovery.json +58 -7
  33. package/dist/mcp/transports/bridge-entry.d.ts +0 -102
  34. package/dist/mcp/transports/bridge-entry.js +0 -454
  35. package/dist/mcp/transports/bridge-events.d.ts +0 -55
  36. package/dist/mcp/transports/bridge-events.js +0 -24
  37. package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
  38. package/dist/mcp/transports/primary-tombstone.js +0 -51
@@ -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.detectHealthyPrimary = exports.startBridgeServer = exports.startHttpServer = exports.startStdioServer = void 0;
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)();
@@ -10,6 +10,9 @@ export interface ObservationEventData {
10
10
  } | {
11
11
  readonly type: 'sha256';
12
12
  readonly value: string;
13
+ } | {
14
+ readonly type: 'path';
15
+ readonly value: string;
13
16
  };
14
17
  readonly confidence: 'low' | 'med' | 'high';
15
18
  }
@@ -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.MAX_OBSERVATION_SHORT_STRING_LENGTH)
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: 'short_string', value: anchor.value },
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
- if (event['kind'] !== 'tool_called' ||
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 registryEntry = this.ports.daemonRegistry?.snapshot().get(sessionId);
161
- const isLive = registryEntry !== undefined
162
- && (nowMs - registryEntry.lastHeartbeatMs) < AUTONOMOUS_HEARTBEAT_THRESHOLD_MS;
163
- if (!isLive) {
164
- return detailRA.map((detail) => ({ ...detail, liveActivity: null }));
165
- }
166
- const liveActivityRA = neverthrow_1.ResultAsync.fromSafePromise(readLiveActivity(sessionIdStr, LIVE_ACTIVITY_MAX_ENTRIES));
167
- return neverthrow_1.ResultAsync.combine([detailRA, liveActivityRA]).map(([detail, liveActivity]) => ({
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.**