Skip to content

feat(factory-sdk): add internal fleet client#232

Merged
kjgbot merged 3 commits into
mainfrom
factory-sdk/w5-fleet
Jun 11, 2026
Merged

feat(factory-sdk): add internal fleet client#232
kjgbot merged 3 commits into
mainfrom
factory-sdk/w5-fleet

Conversation

@kjgbot

@kjgbot kjgbot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add InternalFleetClient over @agent-relay/harness-driver with the W5 mapping table: capability to CLI, cwd, sessionRef to continueFrom, restartPolicy, release, roster, messaging, delivery failure, and agent exit callbacks.
  • Add createFleet({ backend }) selection with default internal and the existing RelayFleetClient seam for relay.
  • Add dispatch task template rendering with required repo, clone/worktree, Linear issue, PR, reviewer DM, broker DM, no auto-merge, and merge-policy clauses.

Verification

  • npx vitest run packages/factory-sdk
  • npx tsc --noEmit -p tsconfig.node.json

V0 logic checks only; no live V1 surface verification claimed.

@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 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kjgbot, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 3 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 86919834-ce01-44de-856f-a26eff0c30dd

📥 Commits

Reviewing files that changed from the base of the PR and between c44b5ab and 806b022.

📒 Files selected for processing (9)
  • packages/factory-sdk/src/dispatch/templates.test.ts
  • packages/factory-sdk/src/dispatch/templates.ts
  • packages/factory-sdk/src/fleet/create-fleet.test.ts
  • packages/factory-sdk/src/fleet/create-fleet.ts
  • packages/factory-sdk/src/fleet/internal-fleet-client.test.ts
  • packages/factory-sdk/src/fleet/internal-fleet-client.ts
  • packages/factory-sdk/src/index.ts
  • packages/factory-sdk/src/ports/fleet.ts
  • packages/factory-sdk/src/ports/index.ts
📝 Walkthrough

Walkthrough

This PR introduces task instruction templates for agent coordination and a complete InternalFleetClient implementation. The templates render PR task text with repository, issue, and merge-policy details. The fleet client wraps a harness driver, manages PTY spawning/resuming/releasing, handles async broker message delivery with injection timeouts, and deduplicates broker events. A factory selects between internal and relay backends. Public exports are reorganized to reflect the new modules.

Changes

Task Templates and Fleet Client System

Layer / File(s) Summary
Task Template Rendering System
packages/factory-sdk/src/dispatch/templates.ts, packages/factory-sdk/src/dispatch/templates.test.ts
New type-safe template contracts (TemplateIssue, TemplateRoute, RenderAgentTaskInput) and implementation (renderAgentTask, agentSpecWithRenderedTask, mergePolicyLine). Templates render instruction text with normalized repo, optional clone/worktree paths, Linear issue details, reviewer/implementer DM guidance, and merge-policy directives. Tests verify implementer single-repo routes, reviewer team dispatch coordination, and cross-repo merge-policy text.
Internal Fleet Client Core
packages/factory-sdk/src/fleet/internal-fleet-client.ts (types, constructor, public methods)
Introduces InternalFleetClient implementing FleetClient with driver contract HarnessDriverClientLike. Core methods spawn PTYs with capability-to-CLI mapping, resume from session refs, release handles, return local roster, forward broker messages, and implement waitForInjected that subscribes to broker events and returns injected delivery results with timeout handling.
Fleet Client Event and Delivery Handling
packages/factory-sdk/src/fleet/internal-fleet-client.ts (event routing, deduplication, callbacks)
Centralizes broker event handling by event kind to resolve/reject pending injected deliveries, emit agent-exit and delivery-failed callbacks, and deduplicate repeated events using stable event identity or JSON fallback. Maintains bounded in-memory tracking of recent event keys, derives exit reasons from broker signals/codes, and prevents duplicate callback emissions.
Internal Fleet Client Test Suite
packages/factory-sdk/src/fleet/internal-fleet-client.test.ts
Comprehensive tests with FakeHarnessDriverClient test double. Validates spawn capability-to-CLI mapping and per-spawn cwd, resume continueFrom extraction, release/roster agent reporting, message forwarding, waitForInjected resolution with timeout rejection, delivery_failed rejection propagation, event deduplication for broker duplicates, and legacy agent-exit callback non-suppression for distinct session ids.
Fleet Client Factory and Backend Selection
packages/factory-sdk/src/fleet/create-fleet.ts, packages/factory-sdk/src/fleet/create-fleet.test.ts
Introduces createFleet factory with FleetBackend, CreateFleetOptions, and CreateFleetDeps. Selects InternalFleetClient by default or when backend: 'internal', returns RelayFleetClient when backend: 'relay'. Tests verify default internal selection, explicit internal selection, and relay backend error behavior.
Public API Export Refactoring
packages/factory-sdk/src/index.ts, packages/factory-sdk/src/ports/fleet.ts, packages/factory-sdk/src/ports/index.ts
Reorganizes exports: moves createFleet from ports to ./fleet/create-fleet, adds template helpers and types from ./dispatch/templates, exports InternalFleetClient and driver contract types, removes obsolete createFleet stub from ports.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Threads of task and fleet alike,
Template words guide agents right,
Harness wrapped in client's care,
Events deduped, delivered fair!
Factories choose their winding way,
Exports dance to light of day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 summarizes the primary change: adding InternalFleetClient, which is a core component of this PR alongside template rendering and createFleet factory function.
Description check ✅ Passed The description is directly related to the changeset, detailing the new InternalFleetClient implementation, createFleet factory, and dispatch task template rendering with specific feature requirements and verification steps.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch factory-sdk/w5-fleet

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.

agent-relay-code Bot added a commit that referenced this pull request Jun 11, 2026
@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed one validated issue in the PR: legacy agentExited callbacks were deduped only by agent name, which could suppress a real later exit after the same agent respawned. The key now includes the callback payload, and I added a regression test for duplicate-vs-distinct session exits in internal-fleet-client.test.ts. Code fix is in internal-fleet-client.ts.

Addressed comments

  • gemini-code-assist[bot]: quota-limit notice only; no actionable code finding to validate or fix.
  • coderabbitai[bot]: review-rate-limit notice only; no actionable code finding to validate or fix.

Advisory Notes

  • GitHub PR metadata currently reports mergeable: false for remote head 5fe0d49f879be873011f96c938eb6ced05a8adea; I did not use git/gh, so I did not attempt a merge-conflict operation locally.
  • The macOS-only dist:mac packaged smoke job was not run in this Linux sandbox.

Local verification run:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run lint
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web
  • npx playwright install --with-deps chromium
  • npx playwright test --config playwright.redraw.config.ts
  • npx playwright test --config playwright.fidelity.config.ts passed on rerun; an initial run had one timeout, and the timed-out spec passed both isolated and in the full rerun.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Reviewed PR #232 and made one validated fix in the PR scope: legacy agentExited callback dedupe now includes the callback payload, so a later exit for the same agent name but a different session is not suppressed. Fixed in internal-fleet-client.ts, with regression coverage in internal-fleet-client.test.ts.

Addressed comments

  • gemini-code-assist[bot]: quota-limit notice only; no actionable code finding to validate or fix.
  • coderabbitai[bot]: review-rate-limit notice only; no actionable code finding to validate or fix. Current status check is pending on GitHub.
  • agent-relay-code[bot]: raised/fixed legacy agentExited callbacks being deduped only by agent name; reproduced against the local checkout and fixed in packages/factory-sdk/src/fleet/internal-fleet-client.ts:178, with regression test at packages/factory-sdk/src/fleet/internal-fleet-client.test.ts:255.

Advisory Notes

  • GitHub PR metadata still reports mergeable: false for remote head c44b5ab017a7f726c834ca63e1e5fe4795d82fde. There are no conflict markers in this checkout, and I did not use git or gh.
  • The macOS-only packaged smoke job (dist:mac) was not run in this Linux sandbox.

Local verification passed:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run lint
  • npm run typecheck
  • npx vitest run packages/factory-sdk/src/fleet/internal-fleet-client.test.ts packages/factory-sdk/src/fleet/create-fleet.test.ts packages/factory-sdk/src/dispatch/templates.test.ts
  • npm run test:all
  • npm run build
  • npm run build:web
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

I am not printing READY because GitHub still reports the PR as not mergeable and CodeRabbit is pending.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/factory-sdk/src/fleet/create-fleet.test.ts (1)

23-38: ⚡ Quick win

Consider adding test coverage for undefined harnessClient scenario.

All current tests explicitly provide harnessClient in deps. Consider adding a test case that verifies the behavior when createFleet() is called with no arguments (or with deps.harnessClient undefined) to ensure the internal client handles this case gracefully.

🧪 Suggested test case
+  it('handles undefined harnessClient gracefully', () => {
+    const fleet = createFleet()
+    expect(fleet).toBeInstanceOf(InternalFleetClient)
+    // Add assertions to verify that basic operations don't throw
+  })
🤖 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/factory-sdk/src/fleet/create-fleet.test.ts` around lines 23 - 38,
Add a test in create-fleet.test.ts that calls createFleet with no args (or with
deps where harnessClient is undefined) to assert it still returns an
InternalFleetClient and behaves as expected; specifically, create a new it(...)
case that calls createFleet() (or createFleet(undefined, {})) and expects the
result toBeInstanceOf(InternalFleetClient) and any relevant method calls (e.g.,
roster()) to either succeed or reject in the documented/expected way to prove
the internal client handles undefined harnessClient gracefully. Make sure to
reference createFleet and InternalFleetClient in the test so it verifies the
undefined harnessClient scenario.
packages/factory-sdk/src/fleet/internal-fleet-client.ts (2)

336-343: 💤 Low value

Event identity key is redundant when stable ID exists.

When stable (event_id or delivery_id) is present, the key still includes JSON.stringify(event). This means two events with the same stable ID but different content (e.g., different timestamps or metadata) won't deduplicate. If stable IDs are meant to be authoritative for identity, consider using only the stable ID:

♻️ Suggested simplification
 function eventIdentity(event: BrokerEvent): EventIdentity {
   const record = event as BrokerEvent & { event_id?: string; delivery_id?: string }
   const stable = record.event_id ?? record.delivery_id
   return {
-    key: `${event.kind}:${stable ?? ''}:${JSON.stringify(event)}`,
+    key: stable ? `${event.kind}:${stable}` : `${event.kind}::${JSON.stringify(event)}`,
     hasStableId: Boolean(stable),
   }
 }
🤖 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/factory-sdk/src/fleet/internal-fleet-client.ts` around lines 336 -
343, The eventIdentity function currently builds a key that appends
JSON.stringify(event) even when a stable id (event_id or delivery_id) exists,
preventing deduplication by stable id; modify eventIdentity (function name:
eventIdentity, types: BrokerEvent and the record with event_id/delivery_id) so
that if stable is present the key uses only `${event.kind}:${stable}` (or
similar stable-only form) and when stable is absent fall back to the current
`${event.kind}:${''}:${JSON.stringify(event)}` approach; keep hasStableId =
Boolean(stable) unchanged.

168-182: 💤 Low value

Event subscription may process the same event twice when both paths are active.

Both onEvent and addListener('deliveryUpdate') route events to #handleBrokerEvent. If the driver client implements both, delivery-related events (e.g., delivery_injected, delivery_failed) will be processed twice. The deduplication logic in #resolveInjected and #rememberEvent prevents incorrect behavior, but this creates unnecessary overhead.

Consider guarding against double-registration or preferring one subscription path over the other:

♻️ Suggested approach
 `#ensureEventSubscription`(): void {
   if (this.#subscribed) {
     return
   }

   this.#subscribed = true
-  this.#client.onEvent?.((event) => this.#handleBrokerEvent(event))
-  this.#client.addListener?.('deliveryUpdate', (event) => this.#handleBrokerEvent(event))
+  // Prefer onEvent if available; fall back to addListener for delivery updates
+  if (this.#client.onEvent) {
+    this.#client.onEvent((event) => this.#handleBrokerEvent(event))
+  } else {
+    this.#client.addListener?.('deliveryUpdate', (event) => this.#handleBrokerEvent(event))
+  }
   this.#client.addListener?.('agentExited', (agent) =>
🤖 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/factory-sdk/src/fleet/internal-fleet-client.ts` around lines 168 -
182, The current `#ensureEventSubscription` registers two handlers that both call
`#handleBrokerEvent` when the driver supports both this.#client.onEvent and
this.#client.addListener('deliveryUpdate'), causing duplicate processing; change
`#ensureEventSubscription` to prefer one subscription path and avoid
double-registration — e.g., if this.#client.onEvent is present use only that,
otherwise fall back to this.#client.addListener('deliveryUpdate'), or add a
guard boolean to ensure only one of the two registrations is performed; update
references in `#ensureEventSubscription` (and related registration lines) so only
a single route to `#handleBrokerEvent` is created while keeping existing
deduplication in `#resolveInjected` and `#rememberEvent`.
🤖 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/factory-sdk/src/fleet/internal-fleet-client.ts`:
- Around line 315-319: The error message in function assertSelfNode currently
reads "...only supports node 'self' tonight"; update that string to use
"currently" instead of "tonight" so the thrown Error becomes
`InternalFleetClient only supports node 'self' currently; received ${node}`
(keep the same function assertSelfNode and the SpawnInput['node'] check/throw
logic).

---

Nitpick comments:
In `@packages/factory-sdk/src/fleet/create-fleet.test.ts`:
- Around line 23-38: Add a test in create-fleet.test.ts that calls createFleet
with no args (or with deps where harnessClient is undefined) to assert it still
returns an InternalFleetClient and behaves as expected; specifically, create a
new it(...) case that calls createFleet() (or createFleet(undefined, {})) and
expects the result toBeInstanceOf(InternalFleetClient) and any relevant method
calls (e.g., roster()) to either succeed or reject in the documented/expected
way to prove the internal client handles undefined harnessClient gracefully.
Make sure to reference createFleet and InternalFleetClient in the test so it
verifies the undefined harnessClient scenario.

In `@packages/factory-sdk/src/fleet/internal-fleet-client.ts`:
- Around line 336-343: The eventIdentity function currently builds a key that
appends JSON.stringify(event) even when a stable id (event_id or delivery_id)
exists, preventing deduplication by stable id; modify eventIdentity (function
name: eventIdentity, types: BrokerEvent and the record with
event_id/delivery_id) so that if stable is present the key uses only
`${event.kind}:${stable}` (or similar stable-only form) and when stable is
absent fall back to the current `${event.kind}:${''}:${JSON.stringify(event)}`
approach; keep hasStableId = Boolean(stable) unchanged.
- Around line 168-182: The current `#ensureEventSubscription` registers two
handlers that both call `#handleBrokerEvent` when the driver supports both
this.#client.onEvent and this.#client.addListener('deliveryUpdate'), causing
duplicate processing; change `#ensureEventSubscription` to prefer one subscription
path and avoid double-registration — e.g., if this.#client.onEvent is present
use only that, otherwise fall back to
this.#client.addListener('deliveryUpdate'), or add a guard boolean to ensure
only one of the two registrations is performed; update references in
`#ensureEventSubscription` (and related registration lines) so only a single route
to `#handleBrokerEvent` is created while keeping existing deduplication in
`#resolveInjected` and `#rememberEvent`.
🪄 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: 5191d6fb-9641-4f10-9675-0ddc378f6402

📥 Commits

Reviewing files that changed from the base of the PR and between e0637c7 and c44b5ab.

📒 Files selected for processing (9)
  • packages/factory-sdk/src/dispatch/templates.test.ts
  • packages/factory-sdk/src/dispatch/templates.ts
  • packages/factory-sdk/src/fleet/create-fleet.test.ts
  • packages/factory-sdk/src/fleet/create-fleet.ts
  • packages/factory-sdk/src/fleet/internal-fleet-client.test.ts
  • packages/factory-sdk/src/fleet/internal-fleet-client.ts
  • packages/factory-sdk/src/index.ts
  • packages/factory-sdk/src/ports/fleet.ts
  • packages/factory-sdk/src/ports/index.ts
💤 Files with no reviewable changes (2)
  • packages/factory-sdk/src/ports/index.ts
  • packages/factory-sdk/src/ports/fleet.ts

Comment on lines +315 to +319
function assertSelfNode(node: SpawnInput['node']): void {
if (node && node !== 'self') {
throw new Error(`InternalFleetClient only supports node 'self' tonight; received ${node}`)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Typo in error message: "tonight" should be "currently".

The error message contains what appears to be a leftover debug phrase.

✏️ Proposed fix
 function assertSelfNode(node: SpawnInput['node']): void {
   if (node && node !== 'self') {
-    throw new Error(`InternalFleetClient only supports node 'self' tonight; received ${node}`)
+    throw new Error(`InternalFleetClient only supports node 'self'; received ${node}`)
   }
 }
🤖 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/factory-sdk/src/fleet/internal-fleet-client.ts` around lines 315 -
319, The error message in function assertSelfNode currently reads "...only
supports node 'self' tonight"; update that string to use "currently" instead of
"tonight" so the thrown Error becomes `InternalFleetClient only supports node
'self' currently; received ${node}` (keep the same function assertSelfNode and
the SpawnInput['node'] check/throw logic).

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.

1 participant