Skip to content

Add factory orchestrator and live merge gate#235

Merged
kjgbot merged 3 commits into
mainfrom
factory-sdk/w6-orchestrator
Jun 11, 2026
Merged

Add factory orchestrator and live merge gate#235
kjgbot merged 3 commits into
mainfrom
factory-sdk/w6-orchestrator

Conversation

@kjgbot

@kjgbot kjgbot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add packages/factory-sdk/src/orchestrator/ with createFactory, FactoryLoop, and BatchTracker.
  • Implement W6 reliability paths: batch cap by issue, stable invocationId dedup, sessionRef capture, resume-not-respawn, duplicate exit coalescing, delivery_failed escalation, writeback verification errors, and queue pull-next.
  • Add packages/factory-sdk/src/github/merge-gate.ts live gh pr view merge gate. READY is returned only for positively observed MERGEABLE + CLEAN + expected head SHA + all checks SUCCESS; all uncertainty fails closed.
  • Narrow MountGithubRead.getPr to static mount metadata only and unwrap { provider, payload }; mount state remains advisory and no longer surfaces status/checks/mergeable.
  • Rebased onto W5 1440608 and patched InternalFleetClient.resume to honor input.capability for W6 reviewer resumes (// followups [fleet→W6]).

Verification

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

Non-vacuity proof points for review

  • Start backfill dispatch: packages/factory-sdk/src/orchestrator/factory.test.tsit('start backfills ready issues and dispatches when capacity is available', ...). Reverting #handleChange to queue-only/no-op when capacity is free makes this fail.
  • Start queue path preserved: packages/factory-sdk/src/orchestrator/factory.test.tsit('start queues and emits issue-queued when backfill exceeds batch capacity', ...). Reverting the full-batch queue branch or event emit makes this fail.
  • Concurrent start coalescing: packages/factory-sdk/src/orchestrator/factory.test.tsit('coalesces concurrent starts into one subscription and dispatch pass', ...). Removing #starting makes concurrent start() calls create multiple subscriptions or dispatch passes.
  • Duplicate subscribe guard: packages/factory-sdk/src/orchestrator/factory.test.tsit('dedupes duplicate subscribe events for an already tracked issue', ...). Removing the tracked issue guard causes duplicate subscribe callbacks to double-dispatch.
  • InvocationId guard isolation: packages/factory-sdk/src/orchestrator/factory.test.tsit('BatchTracker blocks duplicate invocation ids within and across issue records', ...). The same-record assertions pin record.invocationIds; the cross-record assertion pins the global #invocationIds set. Reverting shouldSpawn to true or removing the global set makes this fail.
  • InvocationId dispatch path: packages/factory-sdk/src/orchestrator/factory.test.tsit('dedupes dispatch spawns that retry the same invocation id under different agent names', ...). This reaches #spawnAgent without the record.result short-circuit by using two different agent names with the same invocationId; reverting the invocation guard produces an extra spawn and fails.
  • Coarse repeated dispatch idempotency: packages/factory-sdk/src/orchestrator/factory.test.tsit('dedupes repeated dispatch by stable invocation id', ...). This remains a higher-level regression for retrying the same decision after dispatch result capture.
  • Resume-not-respawn + reviewer capability: packages/factory-sdk/src/orchestrator/factory.test.tsit('resumes exited open agents by sessionRef with the original capability', ...). Reverting resume to fresh spawn, omitting sessionRef, or omitting per-agent capability makes the test fail.
  • Duplicate exit coalescing: packages/factory-sdk/src/orchestrator/factory.test.tsit('coalesces duplicate exit callbacks for the same open issue, agent, and sessionRef', ...). Reverting the issue+agent+sessionRef resume dedup makes duplicate exit callbacks double-resume and the test fails.
  • Absent-sessionRef fallback: packages/factory-sdk/src/orchestrator/factory.test.tsit('fresh-spawns on exit only when sessionRef is absent', ...). Removing the explicit fallback or trying to resume without a sessionRef makes this fail.
  • W5 resume capability seam: packages/factory-sdk/src/fleet/internal-fleet-client.test.tsit('resumes with the per-agent capability when provided', ...). Reverting cli: capabilityCli[input.capability ?? this.#resumeCapability] back to the fixed resume capability makes reviewer resume use codex and this fails.
  • delivery_failed escalation: packages/factory-sdk/src/orchestrator/factory.test.tsit('emits an escalation on delivery_failed for an in-flight agent', ...). Removing the onDeliveryFailed handling makes the error/escalation assertion fail.
  • Zombie mount verify failure: packages/factory-sdk/src/orchestrator/factory.test.tsit('emits error and rejects when writeback verification fails', ...). Swallowing confirmWrite failure or skipping error emit makes this fail.
  • GithubMergeGate stale mount vs live contradiction: packages/factory-sdk/src/github/merge-gate.test.tsit('refuses stale mount-clean snapshots when live GitHub contradicts readiness', ...). Defaulting open from stale mount-like data makes this fail.
  • GithubMergeGate isolated head-moved guard: packages/factory-sdk/src/github/merge-gate.test.tsit('refuses when the live head differs from the expected head sha', ...). Reverting only the headRefOid !== expectedHeadSha guard makes this fail.
  • GithubMergeGate advisory checks: packages/factory-sdk/src/github/merge-gate.test.tsit('returns READY for MERGEABLE+CLEAN with neutral, skipped, or expected advisory checks', ...). This proves NEUTRAL, SKIPPED, and EXPECTED are non-blocking under MERGEABLE+CLEAN with matching head.
  • GithubMergeGate check allowlist/fail-closed: packages/factory-sdk/src/github/merge-gate.test.tsit('refuses missing, blocking, pending, or unknown status checks', ...). This proves FAILURE, pending/in-progress, UNKNOWN, empty rollups, and bare status without an allowed conclusion/state refuse.
  • GithubMergeGate fail-closed UNKNOWN/error/partial: packages/factory-sdk/src/github/merge-gate.test.tsit('fails closed when gh returns UNKNOWN, errors, or partial output', ...). Treating UNKNOWN, runner errors, empty/partial JSON, or missing fields as ready makes this fail.

Notes

  • Factory consumes FleetClient only; no concrete fleet implementation is imported by the orchestrator.
  • BatchTracker caps in-flight issues, not agents. The test it('runOnce caps active issues, skips stale state, and pulls queued work after completion', ...) dispatches two issues at batch size 2, queues the third, and pulls it when one issue completes.
  • W6 is currently built against the W1/W5 FleetClient.onAgentExit(listener: (name: string, reason?: string) => void) shape. The implementation intentionally does not key on reason, because W5 can emit duplicate callbacks from typed and broker-event paths with different reason strings.
  • The merge gate is exported and tested but not wired into an auto-merge action tonight; mergePolicy remains never by default.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a complete factory orchestration system with issue triage, agent dispatch, recovery workflows, and GitHub merge-gate evaluation. It extends fleet client capabilities for per-agent resumption, refactors GitHub PR data parsing with new metadata fields, and reorganizes the public API as barrel exports.

Changes

Fleet Resume Capability Enhancement

Layer / File(s) Summary
Fleet Client Resume Contract
packages/factory-sdk/src/ports/fleet.ts
FleetClient.resume method signature updated to accept optional capability?: Capability parameter for capability-specific harness selection during session resumption.
Fleet Client Implementations with Capability Support
packages/factory-sdk/src/fleet/internal-fleet-client.ts, packages/factory-sdk/src/fleet/relay-fleet-client.ts, packages/factory-sdk/src/fleet/internal-fleet-client.test.ts
InternalFleetClient.resume implementation uses the provided capability to select harness cli, RelayFleetClient.resume signature matches the updated port contract, and new test verifies capability parameter handling and continueFrom session reference passing.
Fleet Testing Support with Capability Tracking
packages/factory-sdk/src/testing/fakes.ts
FakeMountClient tracks subscription count; FakeFleetClient records capability in resumes and updates resume() signature to match the port contract.

GitHub Merge Gate Implementation

Layer / File(s) Summary
Merge Gate Contracts and Types
packages/factory-sdk/src/github/merge-gate.ts (lines 1–33)
Defines GithubMergeGate interface, GithubMergeGateInput/GithubMergeGateVerdict evaluation contracts, and GhRunner/GhRunResult abstractions for command execution.
Merge Gate Evaluation Logic
packages/factory-sdk/src/github/merge-gate.ts (lines 35–168)
GhCliGithubMergeGate shells out to gh pr view, evaluateGithubMergeGate validates merge state, head SHA, check rollups, and returns READY or REFUSE verdicts; includes helpers for JSON parsing, check-state normalization, and defensive value extraction.
Merge Gate Test Suite
packages/factory-sdk/src/github/merge-gate.test.ts
Tests verify READY verdict when all validation passes, and REFUSE verdicts for stale snapshots, UNKNOWN merge signals, runner failures, missing fields, and unsuccessful status checks.
GitHub Module Barrel Export
packages/factory-sdk/src/github/index.ts
Module re-exports merge-gate implementation, types, and GithubMergeGatePort type alias for public consumption.

Batch Tracking Infrastructure

Layer / File(s) Summary
Batch Tracker Contracts and Initialization
packages/factory-sdk/src/orchestrator/batch-tracker.ts (lines 1–56)
Defines TrackedAgent, InFlightIssue, QueuedIssue records and BatchTracker class with internal in-flight/queued maps, capacity enforcement, and query accessors.
Batch Tracker Capacity and Lifecycle Operations
packages/factory-sdk/src/orchestrator/batch-tracker.ts (lines 57–131)
Implements issue lookup, capacity gating (canStart), and lifecycle methods (start, queue, complete) that coordinate in-flight/queued transitions and advance queued work when capacity allows.
Batch Tracker Invocation ID and Recording
packages/factory-sdk/src/orchestrator/batch-tracker.ts (lines 133–162)
Adds deterministic invocation ID generation via stable hash, duplicate-invocation suppression with global and per-issue tracking, and recording for real spawns and dry runs with session references.

GitHub PR Data Schema and Parsing

Layer / File(s) Summary
PrSummary Schema Updates
packages/factory-sdk/src/types.ts
PrSummary interface replaces status/checks/mergeable snapshot fields with new metadata (headRef, baseRef, author, filesChanged); FactoryPorts adds optional mergeGate dependency.
GitHub PR Parsing Implementation
packages/factory-sdk/src/writeback/github.ts
MountGithubRead.getPr defensively extracts PR metadata from nested payload structures using new helpers, mapping to simplified output shape; removes prior checksFromPayload helper.
GitHub Parsing Tests
packages/factory-sdk/src/writeback/writeback.test.ts
Test fixture and assertions updated to match new metadata field mappings and defensive extraction behavior.

Factory Orchestrator Loop

Layer / File(s) Summary
Factory Orchestrator Setup and State
packages/factory-sdk/src/orchestrator/factory.ts (lines 1–73)
Initializes FactoryLoop with configuration, port wiring (triage/linear/slack/mergeGate/logger/clock), batch tracking, event listeners, and fleet event handler hookup.
Factory Lifecycle and Main Iteration
packages/factory-sdk/src/orchestrator/factory.ts (lines 75–154)
Implements start/stop lifecycle, mount/subscription management, and runOnce iteration that gathers ready issues, reads, triages, dispatches, and accumulates reporting metrics.
Factory Dispatch and Agent Spawning
packages/factory-sdk/src/orchestrator/factory.ts (lines 156–237)
Implements dispatch with low-confidence escalation, batch capacity gating, implementer/reviewer spawning, Linear state writeback verification, critical reviewer messaging, and event emission.
Factory Change Handling and Issue Reading
packages/factory-sdk/src/orchestrator/factory.ts (lines 251–355)
Implements backfill of existing ready issues, subscription-based change filtering, and defensive issue parsing with error recovery.
Factory Agent Exit Recovery and Resumption
packages/factory-sdk/src/orchestrator/factory.ts (lines 356–433)
handleAgentExit detects completion reasons, resumes session-backed agents using fleet.resume with capability, deduplicates resumes, and respawns agents without sessions using restart invocation IDs.
Factory Delivery Failure Handling and Issue Completion
packages/factory-sdk/src/orchestrator/factory.ts (lines 435–519)
handleDeliveryFailed recovers with injected acknowledgement awaiting; completeIssue sets Linear state to done, posts Slack thread, triggers mergeGate, and dispatches next queued decision.
Factory Issue Parsing and Helpers
packages/factory-sdk/src/orchestrator/factory.ts (lines 521–607)
parseLinearIssue extracts and structures issue data from stored JSON payloads; helpers support issue reference creation, dispatch comment formatting, repo-map building, and agent policies.
Factory Tests Setup and Helpers
packages/factory-sdk/src/orchestrator/factory.test.ts (lines 1–79)
Introduces Vitest configuration, Linear issue/file generators, StaticTriage implementation, async flushing, and changeEvent construction utilities.
Factory Iteration and Capacity Tests
packages/factory-sdk/src/orchestrator/factory.test.ts (lines 81–141)
Tests runOnce iteration with active/stale filtering, queued work pulling, and start backfill with capacity-exceeded queuing and expected spawn/release sequencing.
Factory Dispatch Deduplication Tests
packages/factory-sdk/src/orchestrator/factory.test.ts (lines 142–230)
Tests concurrent start coalescing, duplicate subscribe deduplication, BatchTracker invocation blocking, and dispatch retry deduplication with stable invocation IDs.
Factory Agent Recovery and Error Handling Tests
packages/factory-sdk/src/orchestrator/factory.test.ts (lines 231–341)
Tests dispatch dedup under different agent names, session-backed resumption with capability, duplicate exit coalescing, fresh respawns without sessions, and error handling for delivery failures and writeback verification failures.
Orchestrator Module Barrel Export
packages/factory-sdk/src/orchestrator/index.ts
Module re-exports batch-tracking and factory orchestration symbols for public API consumption.

Public API Consolidation

Layer / File(s) Summary
Root Package Index Barrel Export
packages/factory-sdk/src/index.ts
Converts from inline createFactory implementation to pure barrel exporter, re-exporting orchestrator (factory/batch-tracker/types) and GitHub merge-gate functionality under unified public surface.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant FactoryLoop
  participant LinearMount
  participant TriageEngine
  participant FleetClient
  participant LinearWriteback
  participant MergeGate
  
  Client->>FactoryLoop: start()
  FactoryLoop->>LinearMount: mount issue root
  FactoryLoop->>LinearMount: subscribe to ready issues
  
  LinearMount->>FactoryLoop: issue-ready event
  FactoryLoop->>FactoryLoop: readIssue(path)
  FactoryLoop->>TriageEngine: triageIssue(issue)
  TriageEngine->>FactoryLoop: TriageDecision
  FactoryLoop->>FactoryLoop: dispatch(decision)
  FactoryLoop->>FleetClient: spawn(implementer)
  FactoryLoop->>FleetClient: spawn(reviewer)
  FactoryLoop->>LinearWriteback: set state to "agent implementing"
  FactoryLoop->>FactoryLoop: emit writeback-verified
  
  FleetClient->>FactoryLoop: agent-exit event
  FactoryLoop->>FleetClient: resume(capability, sessionRef)
  FactoryLoop->>FactoryLoop: handleAgentExit
  
  FleetClient->>FactoryLoop: agent-exit with completion
  FactoryLoop->>FactoryLoop: completeIssue
  FactoryLoop->>LinearWriteback: set state to "done"
  FactoryLoop->>MergeGate: check() for PR merge readiness
  MergeGate->>FactoryLoop: GithubMergeGateVerdict
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • AgentWorkforce/pear#232: Introduced the internal fleet client that is now enhanced in this PR to support per-agent capability during session resumption.

Poem

🐰 The factory hops to life with glee,
Triage, dispatch, recovery spree—
Batches bounded, agents resume,
Merge gates guard the PR's bloom,
Issue-to-done in harmonious tune! 🌟

🚥 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 'Add factory orchestrator and live merge gate' accurately summarizes the main changes: introducing the factory orchestrator system with BatchTracker and FactoryLoop, plus the GitHub merge gate implementation.
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.
Description check ✅ Passed The PR description clearly outlines the implementation of factory orchestrator, batch tracking, merge gate functionality, and W6 reliability features with specific code locations and verification methods.

✏️ 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 factory-sdk/w6-orchestrator

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

Implemented two PR-scoped fixes:

  • Ready Linear issue changes now dispatch when capacity is available, instead of only queuing when full: factory.ts.
  • Concurrent FactoryLoop.start() calls now coalesce through an in-flight promise, preventing duplicate subscriptions/backfill passes: factory.ts.

Added regressions for both:

Addressed comments

  • coderabbitai[bot]: review was skipped because the PR was draft; no actionable review finding to validate or fix.
  • GitHub inline review threads: none returned by the connector; no actionable reviewer comments were present.

Verification

Passed locally with final edits in place:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run lint with warnings only, no errors
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web && npx playwright test --config playwright.fidelity.config.ts && npx playwright test --config playwright.redraw.config.ts

I did not run the macOS dist:mac packaged smoke job because this runner is Linux. Remote GitHub Actions/mergeability were not fully available through the connector, so I’m not marking this READY.

@kjgbot kjgbot force-pushed the factory-sdk/w6-orchestrator branch from 1ada2b9 to 96363e8 Compare June 11, 2026 20:01
@kjgbot kjgbot marked this pull request as ready for review June 11, 2026 20:05
@agent-relay-code

Copy link
Copy Markdown
Contributor

Implemented one PR-scoped fix in the GitHub merge gate: it no longer treats a check run with only status: "COMPLETED" as successful without an explicit success conclusion or state. That prevents false READY verdicts from lifecycle status alone.

Changed:

Addressed comments

  • coderabbitai[bot]: review was skipped because the PR was draft; no actionable finding to validate or fix.
  • agent-relay-code[bot]: prior automation summary noted start coalescing and ready-issue backfill dispatch fixes; validated they are already present in the current checkout, no additional change needed.

Advisory Notes

None.

Verification

Local verification passed:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run lint with warnings only, no errors
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

I did not run the macOS-only packaged smoke job (dist:mac) on this Linux runner, and I did not verify remote GitHub mergeability/check status.

@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!

@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!

@agent-relay-code

Copy link
Copy Markdown
Contributor

Implemented one PR-scoped fix: the GitHub merge gate no longer treats a bare status: "COMPLETED" as success. It now requires an explicit success conclusion or state, with a regression covering the false-ready case.

Changed:

Addressed comments

  • coderabbitai[bot]: raised review/tool status only, including an ESLint install network failure; no actionable code finding. Validated locally with npm ci and npm run lint.
  • agent-relay-code[bot] at 2026-06-11T19:58:22Z: noted start coalescing and ready-issue backfill dispatch fixes; stale/already present in current checkout, no change needed.
  • agent-relay-code[bot] at 2026-06-11T20:10:19Z: raised merge-gate false READY from bare status: "COMPLETED"; fixed in merge-gate.ts, regression in merge-gate.test.ts.
  • gemini-code-assist[bot]: quota-limit notice only; no actionable code finding.
  • GitHub inline review threads: none returned by the connector.

Advisory Notes

None.

Verification

Passed locally with final edits:

  • npm ci
  • npx vitest run packages/factory-sdk/src/github/merge-gate.test.ts packages/factory-sdk/src/orchestrator/factory.test.ts packages/factory-sdk/src/fleet/internal-fleet-client.test.ts packages/factory-sdk/src/writeback/writeback.test.ts
  • npm run verify:mcp-resources-drift
  • npm run lint with warnings only, no errors
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web && npx playwright test --config playwright.fidelity.config.ts && npx playwright test --config playwright.redraw.config.ts

Remote GitHub state prevents READY: PR head has moved to d977ba39f0e12cc717941e71e94c867570bfe123, packaged-mcp-smoke is still in progress there, and CodeRabbit status 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: 3

🧹 Nitpick comments (2)
packages/factory-sdk/src/writeback/writeback.test.ts (1)

187-218: ⚡ Quick win

Add a payload.user.login compatibility test case.

This test currently validates only the payload.author path. Add one case with payload.user: { login: ... } to lock in backward-compatible author extraction.

🤖 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/writeback/writeback.test.ts` around lines 187 - 218,
Update the test that uses FakeMountClient + MountGithubRead to also cover the
legacy payload.user.login shape: add a second mounted record (or extend the
existing fixture) for
'/github/repos/AgentWorkforce__cloud/pulls/by-id/2086.json' (or a new pull id)
whose payload includes user: { login: 'legacy-bot' } and assert via
github.getPr(...) that the returned author is 'legacy-bot' (similar to the
existing assertion for payload.author). Ensure the test still asserts the
original payload.author case so both paths (payload.author and
payload.user.login) are validated by the getPr implementation.
packages/factory-sdk/src/orchestrator/factory.ts (1)

67-67: ⚡ Quick win

Remove wasteful GithubRead instantiation.

This line creates a MountGithubRead instance via the nullish coalescing operator but immediately discards it with void. If the intent is to ensure the mount is configured, this side effect is not clear and wastes resources. If GithubRead is not needed by FactoryLoop, remove this line entirely; otherwise, store the result in #github and use it.

♻️ Proposed fix to remove wasteful instantiation
-    void (ports.github ?? MountGithubRead(ports.mount))
🤖 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/orchestrator/factory.ts` at line 67, The line using
void (ports.github ?? MountGithubRead(ports.mount)) causes a throwaway
instantiation of MountGithubRead; either remove the line entirely if FactoryLoop
doesn't require GithubRead, or assign and retain the instance (e.g., store
MountGithubRead(ports.mount) into the orchestrator's github field like `#github`
or into ports.github) and ensure FactoryLoop uses that stored reference; locate
the expression involving ports.github and MountGithubRead in factory.ts and
replace the void usage with a proper assignment or delete it depending on
whether GithubRead is required.
🤖 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/github/merge-gate.ts`:
- Around line 126-128: The gh subprocess call in defaultGhRunner lacks a timeout
and can hang; update the execFileAsync invocation inside defaultGhRunner to
include a timeout option (e.g. { timeout: 30_000, maxBuffer: 1024 * 1024 }) or
wire in a configurable constant (e.g. DEFAULT_GH_TIMEOUT) and pass it as {
timeout: DEFAULT_GH_TIMEOUT, maxBuffer: ... } so the child process is killed
after the timeout; keep the returned shape ({ stdout, stderr }) unchanged and
surface the timeout error as-is.

In `@packages/factory-sdk/src/orchestrator/factory.ts`:
- Line 487: `#completeIssue` currently ends with a no-op "void this.#mergeGate" so
the merge gate is never executed; replace that with a call to the merge gate's
check method (this.#mergeGate.check({...})) using a GithubMergeGateInput object
and ensure the check receives { repo, number, expectedHeadSha } (plumb these
into `#completeIssue`'s parameters or derive them before the call). Update any
callers of `#completeIssue` to pass the repo, number and expectedHeadSha values if
needed, and handle the returned Promise (await or explicitly ignore with void)
so GithubMergeGate.check(...) is actually invoked.

In `@packages/factory-sdk/src/writeback/github.ts`:
- Line 34: The current assignment author: refName(payload.author) ??
stringValue(payload.user) can drop author when payload.user is an object (e.g. {
login: ... }); update it to preserve author by checking for payload.user.login
when payload.user is an object. Replace the fallback with something like:
refName(payload.author) ?? (typeof payload.user === "string" ?
stringValue(payload.user) : stringValue(payload.user?.login)), using the
existing refName and stringValue helpers so both string and object-shaped
payload.user are handled.

---

Nitpick comments:
In `@packages/factory-sdk/src/orchestrator/factory.ts`:
- Line 67: The line using void (ports.github ?? MountGithubRead(ports.mount))
causes a throwaway instantiation of MountGithubRead; either remove the line
entirely if FactoryLoop doesn't require GithubRead, or assign and retain the
instance (e.g., store MountGithubRead(ports.mount) into the orchestrator's
github field like `#github` or into ports.github) and ensure FactoryLoop uses that
stored reference; locate the expression involving ports.github and
MountGithubRead in factory.ts and replace the void usage with a proper
assignment or delete it depending on whether GithubRead is required.

In `@packages/factory-sdk/src/writeback/writeback.test.ts`:
- Around line 187-218: Update the test that uses FakeMountClient +
MountGithubRead to also cover the legacy payload.user.login shape: add a second
mounted record (or extend the existing fixture) for
'/github/repos/AgentWorkforce__cloud/pulls/by-id/2086.json' (or a new pull id)
whose payload includes user: { login: 'legacy-bot' } and assert via
github.getPr(...) that the returned author is 'legacy-bot' (similar to the
existing assertion for payload.author). Ensure the test still asserts the
original payload.author case so both paths (payload.author and
payload.user.login) are validated by the getPr implementation.
🪄 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: aad4f457-34a4-4430-86d6-441100dee24f

📥 Commits

Reviewing files that changed from the base of the PR and between 1440608 and 96363e8.

📒 Files selected for processing (16)
  • packages/factory-sdk/src/fleet/internal-fleet-client.test.ts
  • packages/factory-sdk/src/fleet/internal-fleet-client.ts
  • packages/factory-sdk/src/fleet/relay-fleet-client.ts
  • packages/factory-sdk/src/github/index.ts
  • packages/factory-sdk/src/github/merge-gate.test.ts
  • packages/factory-sdk/src/github/merge-gate.ts
  • packages/factory-sdk/src/index.ts
  • packages/factory-sdk/src/orchestrator/batch-tracker.ts
  • packages/factory-sdk/src/orchestrator/factory.test.ts
  • packages/factory-sdk/src/orchestrator/factory.ts
  • packages/factory-sdk/src/orchestrator/index.ts
  • packages/factory-sdk/src/ports/fleet.ts
  • packages/factory-sdk/src/testing/fakes.ts
  • packages/factory-sdk/src/types.ts
  • packages/factory-sdk/src/writeback/github.ts
  • packages/factory-sdk/src/writeback/writeback.test.ts

Comment on lines +126 to +128
const defaultGhRunner: GhRunner = async (args) => {
const { stdout, stderr } = await execFileAsync('gh', args, { maxBuffer: 1024 * 1024 })
return { stdout, stderr }

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 | 🟠 Major | ⚡ Quick win

Add a timeout to the gh subprocess call.

Line 127 runs an external CLI without a timeout, so a stuck gh process can block merge-gate evaluation indefinitely and stall the loop.

Suggested fix
 const defaultGhRunner: GhRunner = async (args) => {
-  const { stdout, stderr } = await execFileAsync('gh', args, { maxBuffer: 1024 * 1024 })
+  const { stdout, stderr } = await execFileAsync('gh', args, {
+    maxBuffer: 1024 * 1024,
+    timeout: 30_000,
+  })
   return { stdout, stderr }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultGhRunner: GhRunner = async (args) => {
const { stdout, stderr } = await execFileAsync('gh', args, { maxBuffer: 1024 * 1024 })
return { stdout, stderr }
const defaultGhRunner: GhRunner = async (args) => {
const { stdout, stderr } = await execFileAsync('gh', args, {
maxBuffer: 1024 * 1024,
timeout: 30_000,
})
return { stdout, stderr }
}
🤖 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/github/merge-gate.ts` around lines 126 - 128, The gh
subprocess call in defaultGhRunner lacks a timeout and can hang; update the
execFileAsync invocation inside defaultGhRunner to include a timeout option
(e.g. { timeout: 30_000, maxBuffer: 1024 * 1024 }) or wire in a configurable
constant (e.g. DEFAULT_GH_TIMEOUT) and pass it as { timeout: DEFAULT_GH_TIMEOUT,
maxBuffer: ... } so the child process is killed after the timeout; keep the
returned shape ({ stdout, stderr }) unchanged and surface the timeout error
as-is.

})
await this.#slack.reply(root.threadId, `${record.issue.key}: Linear state set to done.`)
}
void this.#mergeGate

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for merge-gate invocations to confirm the bug scope.

# Search for calls to mergeGate.check or mergeGate methods
rg -nP --type=ts '\bmergeGate\s*\.\s*check\s*\(' packages/factory-sdk/src/orchestrator/

# Also check for any usage of the mergeGate field beyond the void statement
rg -nP --type=ts -C2 '\bthis\.#mergeGate\b' packages/factory-sdk/src/orchestrator/factory.ts

Repository: AgentWorkforce/pear

Length of output: 598


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find GithubMergeGate implementation =="
rg -n --hidden --no-ignore-vcs --type=ts 'class\s+GithubMergeGate\b|interface\s+GithubMergeGateInput\b|type\s+GithubMergeGateInput\b' .

echo
echo "== Find calls to any merge-gate methods =="
rg -n --hidden --no-ignore-vcs --type=ts '\bmergeGate\s*.\s*[A-Za-z0-9_]+\s*\(' .

echo
echo "== Find any usage of the private field itself =="
rg -n --hidden --no-ignore-vcs --type=ts '\bthis\.#mergeGate\b' .

echo
echo "== Look for void this.#mergeGate =="
rg -n --hidden --no-ignore-vcs --type=ts 'void\s+this\.#mergeGate\b' packages/factory-sdk/src/orchestrator/factory.ts

echo
echo "== Check what check(...) actually is on GithubMergeGate (if found) =="
# Best-effort: show method definitions around "check"
rg -n --hidden --no-ignore-vcs --type=ts 'GithubMergeGate[\s\S]{0,200}\bcheck\s*\(' -S . || true

Repository: AgentWorkforce/pear

Length of output: 240


🏁 Script executed:

#!/bin/bash
set -u
# Do not use `set -e` so "no matches" doesn't abort the script.

echo "== Find GithubMergeGate implementation (and input type) =="
rg -n --hidden --no-ignore-vcs --type=ts 'class\s+GithubMergeGate\b|export\s+interface\s+GithubMergeGateInput\b|export\s+class\s+GithubMergeGate\b' packages/factory-sdk/src

echo
echo "== Show GithubMergeGateInput definition =="
sed -n '1,200p' packages/factory-sdk/src/github/merge-gate.ts

echo
echo
echo "== Find calls to any merge-gate methods =="
rg -n --hidden --no-ignore-vcs --type=ts '\bmergeGate\s*.\s*[A-Za-z0-9_]+\s*\(' packages/factory-sdk/src || true

echo
echo "== Find any usage of the private field itself =="
rg -n --hidden --no-ignore-vcs --type=ts '\bthis\.#mergeGate\b' packages/factory-sdk/src || true

echo
echo "== Look for void this.#mergeGate =="
rg -n --hidden --no-ignore-vcs --type=ts 'void\s+this\.#mergeGate\b' packages/factory-sdk/src/orchestrator/factory.ts || true

echo
echo "== Find any `.check(` usage on GithubMergeGate (best-effort) =="
rg -n --hidden --no-ignore-vcs --type=ts '\bGithubMergeGate\b|\.check\s*\(' packages/factory-sdk/src || true

Repository: AgentWorkforce/pear

Length of output: 7349


🏁 Script executed:

#!/bin/bash
set -u
echo "== Context around void this.#mergeGate in factory.ts =="
sed -n '430,520p' packages/factory-sdk/src/orchestrator/factory.ts

echo
echo "== Search for any mergeGate usage elsewhere in packages/factory-sdk/src =="
rg -n --hidden --no-ignore-vcs --type=ts '\bmergeGate\b' packages/factory-sdk/src

Repository: AgentWorkforce/pear

Length of output: 3703


🏁 Script executed:

#!/bin/bash
set -u
echo "== Search for expectedHeadSha usage =="
rg -n --hidden --no-ignore-vcs --type=ts '\bexpectedHeadSha\b' packages/factory-sdk/src || true

echo
echo "== Search for head sha / headRefOid / expected head in factory.ts =="
rg -n --hidden --no-ignore-vcs --type=ts '\b(headRefOid|headSha|head\s*sha|expected\s*head)\b' packages/factory-sdk/src/orchestrator/factory.ts || true

echo
echo "== Inspect factory.ts for `#completeIssue-related` data fetching =="
rg -n --hidden --no-ignore-vcs --type=ts '`#completeIssue`\(|`#readIssue`\(|mergePolicy|merge' packages/factory-sdk/src/orchestrator/factory.ts -n

Repository: AgentWorkforce/pear

Length of output: 1436


🏁 Script executed:

#!/bin/bash
set -u
echo "== Find InFlightIssue type in factory.ts =="
rg -n --hidden --no-ignore-vcs --type=ts 'type\s+InFlightIssue\b|interface\s+InFlightIssue\b|class\s+InFlightIssue\b' packages/factory-sdk/src/orchestrator/factory.ts

echo
echo "== Show top portion of factory.ts (imports/types) =="
sed -n '1,220p' packages/factory-sdk/src/orchestrator/factory.ts

echo
echo
echo "== Show InFlightIssue and related record type usage around `#completeIssue` call site =="
sed -n '220,420p' packages/factory-sdk/src/orchestrator/factory.ts

Repository: AgentWorkforce/pear

Length of output: 14043


🏁 Script executed:

#!/bin/bash
set -u
echo "== Locate BatchTracker / InFlightIssue definitions =="
rg -n --hidden --no-ignore-vcs --type=ts 'export\s+type\s+InFlightIssue\b|export\s+interface\s+InFlightIssue\b|export\s+type\s+.*InFlightIssue\b|type\s+InFlightIssue\b' packages/factory-sdk/src/orchestrator

echo
echo "== Show batch-tracker.ts (first 260 lines) =="
wc -l packages/factory-sdk/src/orchestrator/batch-tracker.ts
sed -n '1,260p' packages/factory-sdk/src/orchestrator/batch-tracker.ts

echo
echo "== Search in batch-tracker.ts for GitHub PR metadata =="
rg -n --hidden --no-ignore-vcs --type=ts '\brepo\b|\bnumber\b|\bexpectedHeadSha\b|\bhead\b|GitHub' packages/factory-sdk/src/orchestrator/batch-tracker.ts

Repository: AgentWorkforce/pear

Length of output: 5166


🏁 Script executed:

#!/bin/bash
set -u

echo "== Inspect IssueRef and related types =="
rg -n --hidden --no-ignore-vcs --type=ts 'export\s+interface\s+IssueRef|export\s+type\s+IssueRef|interface\s+IssueRef|type\s+IssueRef' packages/factory-sdk/src/types.ts
sed -n '1,220p' packages/factory-sdk/src/types.ts

echo
echo "== Find fields repo/number/headSha in types.ts =="
rg -n --hidden --no-ignore-vcs --type=ts '\b(repo|number|headSha|headRef|expectedHeadSha|pr)\b' packages/factory-sdk/src/types.ts

echo
echo "== Inspect TriageDecision type =="
rg -n --hidden --no-ignore-vcs --type=ts 'export\s+interface\s+TriageDecision|export\s+type\s+TriageDecision|interface\s+TriageDecision|type\s+TriageDecision' packages/factory-sdk/src/types.ts
rg -n --hidden --no-ignore-vcs --type=ts 'export\s+type\s+TriageDecision|TriageDecision\s*=' packages/factory-sdk/src/types.ts -n
sed -n '220,420p' packages/factory-sdk/src/types.ts

Repository: AgentWorkforce/pear

Length of output: 3362


🏁 Script executed:

#!/bin/bash
set -u

echo "== Search for parseLinearIssue implementation =="
rg -n --hidden --no-ignore-vcs --type=ts 'function\s+parseLinearIssue\b|parseLinearIssue\s*\(' packages/factory-sdk/src

echo
echo "== Search for linear issue raw GitHub/PR metadata keys =="
# Look for likely fields in JSON/raw usage.
rg -n --hidden --no-ignore-vcs --type=ts '\b(pr|pullRequest|merge|mergeable|headSha|headRef|expectedHeadSha|baseRef|repo|number)\b' packages/factory-sdk/src/orchestrator packages/factory-sdk/src/triage packages/factory-sdk/src/writeback packages/factory-sdk/src || true

echo
echo "== Search for mergePolicy usage across the SDK =="
rg -n --hidden --no-ignore-vcs --type=ts '\bmergePolicy\b' packages/factory-sdk/src || true

echo
echo "== Search for GithubMergeGateInput assembly =="
rg -n --hidden --no-ignore-vcs --type=ts 'GithubMergeGateInput|expectedHeadSha|merge-gate' packages/factory-sdk/src || true

Repository: AgentWorkforce/pear

Length of output: 38912


🏁 Script executed:

#!/bin/bash
set -u
rg -n --hidden --no-ignore-vcs --type=ts '\bmergeGate\b' . || true
rg -n --hidden --no-ignore-vcs --type=ts '\bmergeGate\s*.\s*check\s*\(' . || true
rg -n --hidden --no-ignore-vcs --type=ts '\bthis\.#mergeGate\b' . || true

Repository: AgentWorkforce/pear

Length of output: 627


🏁 Script executed:

#!/bin/bash
set -u
echo "== Show parseLinearIssue implementation =="
sed -n '500,720p' packages/factory-sdk/src/orchestrator/factory.ts

echo
echo "== Inspect raw usage of PR metadata around writeback-ready paths =="
rg -n --hidden --no-ignore-vcs --type=ts '\braw\b' packages/factory-sdk/src/orchestrator/factory.ts

echo
echo "== Show LinearIssue parsing test expectations (if any mention PR fields) =="
rg -n --hidden --no-ignore-vcs --type=ts 'parseLinearIssue|LinearIssue|raw|pr|number|head|merge' packages/factory-sdk/src/orchestrator/factory.test.ts

Repository: AgentWorkforce/pear

Length of output: 5732


Critical: merge gate is never executed (packages/factory-sdk/src/orchestrator/factory.ts:487)

#completeIssue ends with void this.#mergeGate, which is a no-op and never calls GithubMergeGate.check(...). A repo-wide search finds no mergeGate.check( usage anywhere, so merge-readiness is never evaluated. Replace the no-op with this.#mergeGate.check({ repo, number, expectedHeadSha }) (i.e., GithubMergeGateInput) and plumb those values into #completeIssue.

🤖 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/orchestrator/factory.ts` at line 487, `#completeIssue`
currently ends with a no-op "void this.#mergeGate" so the merge gate is never
executed; replace that with a call to the merge gate's check method
(this.#mergeGate.check({...})) using a GithubMergeGateInput object and ensure
the check receives { repo, number, expectedHeadSha } (plumb these into
`#completeIssue`'s parameters or derive them before the call). Update any callers
of `#completeIssue` to pass the repo, number and expectedHeadSha values if needed,
and handle the returned Promise (await or explicitly ignore with void) so
GithubMergeGate.check(...) is actually invoked.

state: typeof payload.state === 'string' ? payload.state : undefined,
headRef: refName(payload.headRef) ?? refName(payload.head) ?? stringValue(payload.head_ref),
baseRef: refName(payload.baseRef) ?? refName(payload.base) ?? stringValue(payload.base_ref),
author: refName(payload.author) ?? stringValue(payload.user),

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

Preserve author when payload uses user.login object shape.

Line 34 can lose author metadata when payload.user is an object (e.g. { login: ... }), because the fallback only accepts a string.

Suggested fix
-      author: refName(payload.author) ?? stringValue(payload.user),
+      author: refName(payload.author) ?? refName(payload.user) ?? stringValue(payload.user),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
author: refName(payload.author) ?? stringValue(payload.user),
author: refName(payload.author) ?? refName(payload.user) ?? stringValue(payload.user),
🤖 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/writeback/github.ts` at line 34, The current
assignment author: refName(payload.author) ?? stringValue(payload.user) can drop
author when payload.user is an object (e.g. { login: ... }); update it to
preserve author by checking for payload.user.login when payload.user is an
object. Replace the fallback with something like: refName(payload.author) ??
(typeof payload.user === "string" ? stringValue(payload.user) :
stringValue(payload.user?.login)), using the existing refName and stringValue
helpers so both string and object-shaped payload.user are handled.

@kjgbot kjgbot merged commit 300ac0f into main Jun 11, 2026
5 checks passed
@kjgbot kjgbot deleted the factory-sdk/w6-orchestrator branch June 11, 2026 20:23
@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed one PR-scoped issue in the new GitHub merge gate: EXPECTED status contexts now block readiness instead of being treated as advisory/success-like. That prevents READY when a required status has not reported yet.

Addressed comments

  • No bot/reviewer comment artifacts were present in .workforce; only pr.diff, changed-files.txt, and context.json were available, so there were no external review threads to validate.

Changes

  • Fixed in merge-gate.ts: removed EXPECTED from non-blocking check states.
  • Fixed in merge-gate.test.ts: updated the advisory-check test to cover only neutral/skipped checks.
  • Added regression coverage in merge-gate.test.ts: state: 'EXPECTED' now refuses readiness.

Local validation

  • npm run verify:mcp-resources-drift
  • npm run lint passed with existing warnings only
  • npm run typecheck:web
  • npm run typecheck:node
  • npm test
  • npx vitest run
  • npm run build

I did not check live GitHub mergeability or cloud CI state because the task prohibited gh, so I’m not printing READY.

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