Skip to content

refactor: decompose the three largest TypeScript god files into single-responsibility modules#1183

Open
willwashburn wants to merge 6 commits into
mainfrom
claude/architecture-refactor-bftmcr
Open

refactor: decompose the three largest TypeScript god files into single-responsibility modules#1183
willwashburn wants to merge 6 commits into
mainfrom
claude/architecture-refactor-bftmcr

Conversation

@willwashburn

@willwashburn willwashburn commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

Continues the god-file decomposition pattern (prior e7fd375 did the CLI) on the three largest TypeScript files — the system's most central client/orchestration surfaces. Each change is a behavior-preserving pure move behind an unchanged public API.

File Before → After New modules
sdk/messaging/relaycast.ts 1654 → 1126 relaycast-translate.ts, relaycast-placement.ts, relaycast-client.ts
harness-driver/client.ts 1199 → 931 spawn-request.ts, broker-process.ts
cloud/workflows.ts 1129 → 893 workflow-paths.ts

The common thread: separate pure logic (wire↔domain translation, spawn-request shaping, workflow-path parsing) from the stateful/IO code (messaging client, broker-process client, cloud S3/API orchestrator). Public surfaces are preserved via re-exports, so consumers and tests are unaffected.

Per-file detail

  • relaycast.tsrelaycast-translate.ts (pure wire↔domain translation + record readers), relaycast-placement.ts (RelayPlacementError, placement payload helpers), relaycast-client.ts (structural relaycast adapter types, options, createRelaycastClient). Re-exports RelayPlacementError and RelaycastMessagingOptions. A follow-up commit also unexports the translate readers that don't cross a module boundary (review feedback).
  • harness-driver/client.tsspawn-request.ts (/api/spawn body builders + patch applier), broker-process.ts (managed broker child-process lifecycle: API-URL wait, stdio drain, recent-output buffering, startup-error formatting, shutdown). BrokerExitInfo moved and re-exported so the package barrel is unchanged.
  • cloud/workflows.tsworkflow-paths.ts (YAML path parser + TS literal scanner + parseWorkflowPaths/inferWorkflowFileType/shouldSyncCodeByDefault). Validation (which shells out), the runtime PATH_NAME_RE guard, GitHub-remote helpers, and all execution/API/S3 code stay behind; the three parser entry points are re-exported so the barrel and tests still import from ./workflows.js.

Verification (every step)

  • Build: npm run build:core green, 0 TS errors.
  • Tests: full vitest run959 passed / 5 skipped, identical to baseline throughout.
  • Live smoke tests: exercised the moved code through the built packages (SDK public surface + agents.list; harness-driver isProcessRunning/buffer-cap/spawn-body builders; cloud YAML+TS path parsing + re-export identity).
  • Independent autoreview on each diff (finder + verifier agents): all returned APPROVE / empty bug list — verbatim-move integrity, import hygiene, public-surface preservation, and no-import-cycle confirmed.

Notes

  • One reviewer flag was deliberately not acted on: merging the translate.ts record readers with normalize.ts's. They have genuine semantic divergence (normalize.readString coerces numbers→strings; readRecord clones) — consolidating would be a silent behavior change, not a refactor.
  • No CHANGELOG.md entry: these are internal module reorganizations with no user-visible effect.

🤖 Generated with Claude Code


Generated by Claude Code

Review in cubic

claude added 5 commits June 21, 2026 15:02
…ules

Decompose the 1654-line RelaycastMessagingClient file into single-
responsibility siblings behind an unchanged public surface:

- relaycast-translate.ts: pure wire<->domain translation and record readers
- relaycast-placement.ts: RelayPlacementError, placement payload helpers
- relaycast-client.ts: structural relaycast adapter types, options, factory

relaycast.ts (now 1126 lines) keeps the stateful client and re-exports
RelayPlacementError and RelaycastMessagingOptions so consumers are unaffected.
Pure move: no behavior change. Verified with build, full test suite (959
pass), and an SDK public-surface smoke test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPFXDfGA74hrgEBah6KTT2
…modules

Drop the export keyword on readRecord/readNumber/readBoolean/readStringArray/
readMention in relaycast-translate.ts — they are only used within the module.
asRecord, readStr, and definedOptions stay exported since relaycast.ts and
relaycast-client.ts consume them. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPFXDfGA74hrgEBah6KTT2
…rocess

Decompose the 1199-line HarnessDriverClient file into two single-responsibility
siblings behind an unchanged public surface (client.ts now 931 lines):

- spawn-request.ts: spawn input -> broker /api/spawn request-body builders and
  the beforeAgentSpawn patch applier
- broker-process.ts: managed broker child-process lifecycle (API-URL wait,
  stdio drain, recent-output buffering, startup-error formatting, shutdown)

BrokerExitInfo moved into broker-process.ts and is re-exported from client.ts so
the package barrel surface is unchanged. Pure move: no behavior change. Verified
with build, full test suite (959 pass), and a harness-driver smoke test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPFXDfGA74hrgEBah6KTT2
Move the pure, side-effect-free workflow-path parsers out of workflows.ts
(1129 -> 893 lines) into a focused workflow-paths.ts module: the YAML path
parser, the TypeScript literal scanner, the parseWorkflowPaths dispatcher, and
inferWorkflowFileType / shouldSyncCodeByDefault.

Validation (validateYamlWorkflow / validateTypeScriptWorkflow, which shells out),
the runtime PATH_NAME_RE guard, GitHub-remote helpers, and the cloud
execution/API/S3 code stay in workflows.ts, which re-exports the three parser
entry points so the package barrel and tests still import them from
'./workflows.js'. Pure move: no behavior change. Verified with build, full test
suite (959 pass), and a parser smoke test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPFXDfGA74hrgEBah6KTT2
Move the trajectory from active/ to completed/ after wrapping up the SDK/
harness-driver/cloud god-file decompositions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPFXDfGA74hrgEBah6KTT2
@willwashburn willwashburn requested a review from khaliqgant as a code owner June 22, 2026 02:46
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Three large TypeScript modules (cloud/workflows.ts, harness-driver/client.ts, sdk/messaging/relaycast.ts) are decomposed into dedicated single-responsibility sibling modules. Extracted code is re-imported and re-exported to preserve public API surfaces. Agent-workforce trajectory artifacts documenting the refactor are also added.

Changes

God-file decomposition across cloud, harness-driver, and sdk packages

Layer / File(s) Summary
cloud: workflow-paths extraction and delegation
packages/cloud/src/workflow-paths.ts, packages/cloud/src/workflows.ts
New workflow-paths.ts implements WorkflowPathDefinition type, YAML line-scanner, TypeScript literal-scanner (bracket/brace matching), parseWorkflowPaths dispatcher, inferWorkflowFileType, and shouldSyncCodeByDefault. workflows.ts imports and re-exports those symbols, removing ~245 lines of inline parsing code.
harness-driver: broker-process module extraction
packages/harness-driver/src/broker-process.ts
New broker-process.ts provides BrokerExitInfo/BrokerStartupDebugContext types, isProcessRunning, waitForApiUrl (readline-based URL detection with timeout and SIGTERM), drainBrokerStdioAfterStartup, pushBufferedLine, cloneBrokerExitInfo, formatBrokerStartupError, and waitForExit (force-kills with SIGKILL on timeout).
harness-driver: spawn-request module extraction
packages/harness-driver/src/spawn-request.ts
New spawn-request.ts exports isBundledHeadlessCli, resolveSpawnTransport, buildSpawnPtyBody, buildSpawnCliBody (broker /api/spawn JSON body builders with agentResultSchema coercion via actionSchemaToJsonSchema), and applySpawnPatch for selective in-place input mutation.
harness-driver: client.ts wiring after extraction
packages/harness-driver/src/client.ts
Removes the @agent-relay/sdk/actions import and inline BrokerExitInfo definition. Imports all helpers from broker-process.js and spawn-request.js, re-exports BrokerExitInfo as a type, and updates __clientTestInternals to source drainBrokerStdioAfterStartup from the extracted module.
sdk: relaycast-client and relaycast-placement new modules
packages/sdk/src/messaging/relaycast-client.ts, packages/sdk/src/messaging/relaycast-placement.ts
relaycast-client.ts defines RelaycastWorkspaceLike, RelaycastAgentLike, RelaycastMessagingOptions, delivery status/surface types, and createRelaycastClient (reuses injected client or constructs RelayCast with telemetry/retry). relaycast-placement.ts defines PlacementReconcileReason, PlacementSelection, RelayPlacementError, placementActionInput (with capability_mismatch enforcement), and delay.
sdk: relaycast-translate stateless translation layer
packages/sdk/src/messaging/relaycast-translate.ts
New relaycast-translate.ts adds record-reader primitives (asRecord, readStr, boolean/number readers), relay→relaycast request mappers (register action, trigger, invocation completion), relaycast wire→relay domain converters (capabilities, nodes, triggers, fleet config, invocation acks, webhooks), and option/attachment serialization utilities.
sdk: relaycast.ts wiring and re-exports after extraction
packages/sdk/src/messaging/relaycast.ts
Rewrites imports to pull createRelaycastClient, placement types/errors, and all translation helpers from sibling modules; removes ~566 lines of inline implementations; re-exports RelayPlacementError and RelaycastMessagingOptions to preserve the consumer-facing API surface.

Agent trajectory artifacts

Layer / File(s) Summary
Trajectory trace, summary, and decision log
.agentworkforce/trajectories/completed/2026-06/traj_q768ixcuyzh7.*
Adds traj_q768ixcuyzh7.trace.json, summary.md, and trajectory.json recording the completed refactor: task status, timestamps, AI contributor line ranges, key decisions (module-private record readers, workflow-path extraction), chapter bullets, commit SHAs, and total files changed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

  • AgentWorkforce/relay#1082: Overlaps directly with the new spawn-request.ts module — both touch agentResultSchema coercion logic and spawn body construction in harness-driver.
  • AgentWorkforce/relay#1108: Both modify packages/sdk/src/messaging/relaycast.ts — that PR added node/trigger APIs while this PR restructures the file into sibling modules.
  • AgentWorkforce/relay#1141: Directly overlaps with relaycast-placement.ts — that PR extends placement/spawn orchestration in the same messaging layer being refactored here.

Suggested reviewers

  • khaliqgant

Poem

🐇 Hop hop, the god-files grow too wide,
So rabbit split them, module by module inside!
workflow-paths hops free, broker-process takes flight,
relaycast-translate glows with pure, stateless light.
Re-exports keep the old paths intact and true —
One warren, many burrows, all fresh and new! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: decomposing three large TypeScript files into smaller modules with single responsibilities, which is the core objective of this refactoring PR.
Description check ✅ Passed The description provides a comprehensive summary of changes, includes a detailed breakdown table of file size reductions and new modules, explains the refactoring pattern, provides per-file details, documents verification steps with concrete test results, and addresses design decisions. All key information is present and well-structured.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/architecture-refactor-bftmcr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cloud/src/workflow-paths.ts`:
- Around line 126-201: The functions extractPathArrayLiterals,
extractObjectLiterals, and readStringProperty scan raw source code without
skipping comments, which causes commented-out paths to be incorrectly extracted
as active workflow paths. Add comment-skipping logic to the scanner by creating
a helper function that identifies and skips single-line comments (starting with
//) and block comments (/* ... */), then integrate this helper into the
extraction functions propertyPattern and methodPattern execution loops in
extractPathArrayLiterals, and in the character-by-character scanning loops of
extractObjectLiterals and readStringProperty to ensure commented content is
never parsed as active declarations.
- Around line 52-67: The parser in the switch statement handling the key-value
pairs does not properly process YAML block scalars (indicated by | or >) for the
pushPrBody field. When a block scalar indicator like pushPrBody: | is
encountered, the code currently stores only the marker character instead of
collecting the subsequent indented lines. Modify the handling for the pushPrBody
case to detect when the value starts with | or > (block scalar indicators) and
if so, collect all following indented lines as the actual multi-line content
rather than storing just the marker. Apply the same fix to any other cases
mentioned at lines 117-119 that also handle multi-line content.

In `@packages/harness-driver/src/broker-process.ts`:
- Around line 176-189: The fast-path check in the waitForExit function only
checks if child.exitCode is not null to determine if the process has already
exited, but this misses processes that exited via signal where exitCode is null
and signalCode is not null. Update the condition to also check for
child.signalCode !== null so that processes that have already exited via signal
are detected immediately and the function can resolve without waiting for the
timeout, avoiding unnecessary SIGKILL attempts and shutdown latency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46dc6c62-0bb3-4a0e-9463-a9862e05be94

📥 Commits

Reviewing files that changed from the base of the PR and between c7380ef and a3e1427.

📒 Files selected for processing (12)
  • .agentworkforce/trajectories/completed/2026-06/traj_q768ixcuyzh7.trace.json
  • .agentworkforce/trajectories/completed/2026-06/traj_q768ixcuyzh7/summary.md
  • .agentworkforce/trajectories/completed/2026-06/traj_q768ixcuyzh7/trajectory.json
  • packages/cloud/src/workflow-paths.ts
  • packages/cloud/src/workflows.ts
  • packages/harness-driver/src/broker-process.ts
  • packages/harness-driver/src/client.ts
  • packages/harness-driver/src/spawn-request.ts
  • packages/sdk/src/messaging/relaycast-client.ts
  • packages/sdk/src/messaging/relaycast-placement.ts
  • packages/sdk/src/messaging/relaycast-translate.ts
  • packages/sdk/src/messaging/relaycast.ts

Comment on lines +52 to +67
const value = stripYamlScalar(text.slice(colonIndex + 1));
switch (key) {
case 'name':
target.name = value;
break;
case 'path':
target.path = value;
break;
case 'pushBranch':
target.pushBranch = value;
break;
case 'pushBase':
target.pushBase = value;
break;
case 'pushPrBody':
target.pushPrBody = value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Handle YAML block scalars before accepting pushPrBody.

pushPrBody: | or pushPrBody: > is valid YAML and the natural way to write a multi-line PR body, but this parser stores only the marker (|/>) and ignores the following indented lines. That submits the wrong PR body instead of the declared text.

Also applies to: 117-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cloud/src/workflow-paths.ts` around lines 52 - 67, The parser in the
switch statement handling the key-value pairs does not properly process YAML
block scalars (indicated by | or >) for the pushPrBody field. When a block
scalar indicator like pushPrBody: | is encountered, the code currently stores
only the marker character instead of collecting the subsequent indented lines.
Modify the handling for the pushPrBody case to detect when the value starts with
| or > (block scalar indicators) and if so, collect all following indented lines
as the actual multi-line content rather than storing just the marker. Apply the
same fix to any other cases mentioned at lines 117-119 that also handle
multi-line content.

Comment on lines +126 to +201
function findMatchingBracket(source: string, startIndex: number, open: string, close: string): number {
let depth = 0;
let quote: '"' | "'" | '`' | null = null;
let escaped = false;

for (let i = startIndex; i < source.length; i += 1) {
const ch = source[i] as '"' | "'" | '`' | string;
if (quote) {
if (escaped) {
escaped = false;
} else if (ch === '\\') {
escaped = true;
} else if (ch === quote) {
quote = null;
}
continue;
}
if (ch === '"' || ch === "'" || ch === '`') {
quote = ch;
continue;
}
if (ch === open) {
depth += 1;
} else if (ch === close) {
depth -= 1;
if (depth === 0) return i;
}
}

return -1;
}

function extractPathArrayLiterals(source: string): string[] {
const literals: string[] = [];

const propertyPattern = /\bpaths\s*:/g;
let propertyMatch: RegExpExecArray | null;
while ((propertyMatch = propertyPattern.exec(source)) !== null) {
const arrayStart = source.indexOf('[', propertyPattern.lastIndex);
if (arrayStart === -1) continue;
const arrayEnd = findMatchingBracket(source, arrayStart, '[', ']');
if (arrayEnd !== -1) {
literals.push(source.slice(arrayStart, arrayEnd + 1));
propertyPattern.lastIndex = arrayEnd + 1;
}
}

const methodPattern = /\.paths\s*\(/g;
let methodMatch: RegExpExecArray | null;
while ((methodMatch = methodPattern.exec(source)) !== null) {
const arrayStart = source.indexOf('[', methodPattern.lastIndex);
if (arrayStart === -1) continue;
const arrayEnd = findMatchingBracket(source, arrayStart, '[', ']');
if (arrayEnd !== -1) {
literals.push(source.slice(arrayStart, arrayEnd + 1));
methodPattern.lastIndex = arrayEnd + 1;
}
}

return literals;
}

function extractObjectLiterals(arrayLiteral: string): string[] {
const objects: string[] = [];
for (let i = 0; i < arrayLiteral.length; i += 1) {
if (arrayLiteral[i] !== '{') continue;
const end = findMatchingBracket(arrayLiteral, i, '{', '}');
if (end === -1) break;
objects.push(arrayLiteral.slice(i, end + 1));
i = end;
}
return objects;
}

function readStringProperty(objectLiteral: string, propertyName: string): string | null {
const pattern = new RegExp(`\\b${propertyName}\\s*:\\s*(['"])(.*?)\\1`, 's');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the TypeScript literal scanner comment-aware.

The scanner currently reads raw text, so commented-out declarations such as // { name: 'old', path: '../old' } inside a paths array, or // paths: [...], can still be extracted as active workflow paths. Since runWorkflow tarballs/uploads every parsed path, disabled paths can still be synced. Please skip line/block comments while locating paths, object literals, and properties, or switch this path to a TypeScript AST parse.

🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 200-200: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\b${propertyName}\\s*:\\s*(['"])(.*?)\\1, 's')
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🪛 OpenGrep (1.22.0)

[ERROR] 163-163: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 175-175: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cloud/src/workflow-paths.ts` around lines 126 - 201, The functions
extractPathArrayLiterals, extractObjectLiterals, and readStringProperty scan raw
source code without skipping comments, which causes commented-out paths to be
incorrectly extracted as active workflow paths. Add comment-skipping logic to
the scanner by creating a helper function that identifies and skips single-line
comments (starting with //) and block comments (/* ... */), then integrate this
helper into the extraction functions propertyPattern and methodPattern execution
loops in extractPathArrayLiterals, and in the character-by-character scanning
loops of extractObjectLiterals and readStringProperty to ensure commented
content is never parsed as active declarations.

Comment on lines +176 to +189
export function waitForExit(child: ChildProcess, timeoutMs: number): Promise<void> {
return new Promise((resolve) => {
if (child.exitCode !== null) {
resolve();
return;
}
const timer = setTimeout(() => {
child.kill('SIGKILL');
resolve();
}, timeoutMs);
child.on('exit', () => {
clearTimeout(timer);
resolve();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle signaled exits in the waitForExit fast-path.

Line 178 only checks exitCode, but a process that already exited via signal has exitCode === null and signalCode !== null. In that case, this code can wait the full timeout and then attempt SIGKILL, which can add avoidable shutdown latency downstream (e.g., HarnessDriverClient.shutdown()).

Suggested fix
 export function waitForExit(child: ChildProcess, timeoutMs: number): Promise<void> {
   return new Promise((resolve) => {
-    if (child.exitCode !== null) {
+    if (child.exitCode !== null || child.signalCode !== null) {
       resolve();
       return;
     }
     const timer = setTimeout(() => {
       child.kill('SIGKILL');
       resolve();
     }, timeoutMs);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/harness-driver/src/broker-process.ts` around lines 176 - 189, The
fast-path check in the waitForExit function only checks if child.exitCode is not
null to determine if the process has already exited, but this misses processes
that exited via signal where exitCode is null and signalCode is not null. Update
the condition to also check for child.signalCode !== null so that processes that
have already exited via signal are detected immediately and the function can
resolve without waiting for the timeout, avoiding unnecessary SIGKILL attempts
and shutdown latency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants