Skip to content

feat(factory): dispatch relayflows from integration events#47

Merged
khaliqgant merged 3 commits into
mainfrom
feat/relayflow-integration-events
Jun 27, 2026
Merged

feat(factory): dispatch relayflows from integration events#47
khaliqgant merged 3 commits into
mainfrom
feat/relayflow-integration-events

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

  • add a Relayflow policy registry and production dispatch path for Relayfile integration change events
  • wire optional FactoryPorts.relayflows into live subscription handling without polling
  • expose dynamic Relayfile helper clients through relayClient(provider) / providerClient(provider) while keeping named Linear/GitHub/Slack clients
  • preserve live filesystem action semantics so file.created maps to triggers like linear.issue.created while maintaining relayfile.changed compatibility
  • run workflow:run through the embedded @relayflows/core SDK instead of shelling out to a CLI
  • update the Node engine floor to >=20.18.1 to match runtime deps

Verification

  • npm test -- src/subscriptions/__tests__/event-client.test.ts src/dispatch/relayflow-registry.test.ts src/orchestrator/factory.test.ts src/node/factory-node.test.ts src/linear/state-resolver.test.ts
  • npm run build
  • npm test
  • confirmed no Node >=22 engine declarations remain in the relevant runtime tree

Summary by cubic

Dispatch Relayflows from integration events using a registry and the embedded @relayflows/core runner. Live subscriptions now trigger workflows immediately (no polling), with provider clients from @relayfile/relay-helpers.

  • New Features

    • Added RelayflowPolicyRegistry and dispatch from live integration events; workflows run in-process via @relayflows/core.
    • Preserved filesystem semantics: file.created maps to triggers like linear.issue.created, filesystemEventType is forwarded, and relayfile.changed stays compatible.
    • Exposed dynamic clients relayClient(provider) and providerClient(provider) alongside named Linear/GitHub/Slack clients.
    • Added FactoryPorts.relayflows to wire policy dispatch into the factory loop.
    • CLI now mounts the relayfile at each configured clonePath so integration paths resolve from the checkout.
  • Dependencies

    • Added @relayflows/core and @relayfile/relay-helpers.
    • Removed reliance on a relayflows CLI; workflows run via the SDK.
    • Bumped Node engine to >=20.18.1.
    • Added overrides for @scalar/postman-to-openapi and listr2.

Written for commit 92a18ae. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces the workflow:run CLI child-process execution with direct @relayflows/core SDK calls, introduces a RelayflowPolicyRegistry for trigger-driven workflow dispatch, wires dispatch into FactoryLoop via a new relayflows port, adds per-clone-path mount initialization in the fleet CLI, and extends ChangeEvent with a filesystemEventType field.

Changes

Relayflow SDK & Dispatch Integration

Layer / File(s) Summary
ChangeEvent filesystemEventType field
src/subscriptions/event-client.ts, src/subscriptions/__tests__/event-client.test.ts
ChangeEvent gains optional filesystemEventType; filesystemEventToChangeEvent populates it; tests verify the field and trigger derivation from file.created events.
RelayflowPolicyRegistry and dispatch API
src/dispatch/relayflow-registry.ts, src/dispatch/relayflow-registry.test.ts
New RelayflowPolicyRegistry class, trigger types, TriggerMapperContext, dispatchRelayflowForTrigger/ChangeEvent, triggerEventFromChangeEvent, and provider/resource path helpers; fully tested including async mapper context and dynamic client paths.
workflow:run SDK execution in factory-node
src/node/factory-node.ts, src/node/factory-node.test.ts
runRelayflowsWorkflow switches from spawning @relayflows/cli to calling runWorkflow/runScriptWorkflow directly; WorkflowRunnerResult reshaped to runner/status/runId; tests updated with new SDK-only integration test.
FactoryPorts relayflows port and public types
src/types.ts, src/index.ts
FactoryPorts gains optional relayflows port; FactoryRelayflowDispatchPort interface added; registry functions and types re-exported from src/index.ts.
FactoryLoop relayflow dispatch wiring
src/orchestrator/factory.ts, src/orchestrator/factory.test.ts
FactoryLoop stores #relayflows port, calls #dispatchRelayflowEvent in subscription and live-drain paths, increments counters, swallows errors; tested with notion.page.changed subscribe scenario.
Clone-path mount initialization in fleet CLI
src/cli/fleet.ts, src/cli/fleet.test.ts
New ensureClonePathMounts helper iterates config.clonePaths, skips daemon CWD, calls ensureLocalMount per path with warning-on-failure; wired into factory start, run-once, and loop; tests updated.
Package deps, engine constraint, and planning docs
package.json, src/ports/fleet.ts, planning/...
Node engine tightened to >=20.18.1; @relayfile/relay-helpers and @relayflows/core promoted to dependencies; planning docs updated to reflect SDK-over-CLI decision.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/factory#3: Modifies the same src/node/factory-node.ts workflow:run handler and related clone-path checkout behavior.

Poem

🐇 Hop hop, no more shell spawn fuss,
The SDK rides the in-process bus!
Triggers mapped, the registry grows,
Each clone path mounts before loop flows.
From file.created to linear.issue.created neat—
The rabbit's dispatch is now complete! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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 clearly summarizes the main change: dispatching relayflows from integration events in the factory.
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 description accurately summarizes the registry, dispatch, SDK execution, live handling, and engine change in this PR.
✨ 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 feat/relayflow-integration-events

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request integrates a policy-driven relayflow dispatch registry into the factory orchestrator, allowing workflows to be dispatched in response to normalized integration events. It also updates the workflow execution model to run the Relayflows SDK in-process rather than shelling out to a CLI, and ensures that relayfile mounts are running at each configured clone path. Key feedback points include addressing a potential TypeError when config.clonePaths is undefined, avoiding non-concurrency-safe mutations of global process.env during concurrent workflow runs, preventing blocking of the live event drain loop by dispatching relayflows asynchronously, forwarding custom options in the provider client wrappers, and correcting edge cases in the plural-to-singular resource name normalization logic.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/cli/fleet.ts Outdated
const mountFn = deps.ensureLocalMount ?? ensureLocalMount
const mountOpts = { acceptableWorkspaceIds: acceptableMountIds }
const daemonCwd = resolve(process.cwd())
for (const clonePath of new Set(Object.values(config.clonePaths))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If config.clonePaths is undefined or null (which is common if no custom checkouts are configured), calling Object.values(config.clonePaths) will throw a TypeError. Guard this with a nullish coalescing operator to prevent CLI crashes.

Suggested change
for (const clonePath of new Set(Object.values(config.clonePaths))) {
for (const clonePath of new Set(Object.values(config.clonePaths ?? {}))) {

Comment thread src/node/factory-node.ts
Comment on lines +337 to 357
async function withProcessEnv<T>(
values: Record<string, string>,
fn: () => Promise<T>,
): Promise<T> {
const previous = new Map<string, string | undefined>()
for (const [key, value] of Object.entries(values)) {
previous.set(key, process.env[key])
process.env[key] = value
}
try {
const packageJsonPath = requireForResolve.resolve('@relayflows/cli/package.json')
return { command: process.execPath, args: [join(dirname(packageJsonPath), 'dist', 'cli.js'), 'run', workflow] }
} catch {
return { command: 'relayflows', args: ['run', workflow] }
return await fn()
} finally {
for (const [key, value] of previous) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Mutating the global process.env is not concurrency-safe. Since a factory node can run multiple workflows concurrently (as indicated by active_agents and max_agents), concurrent executions of runRelayflowsWorkflow will overwrite each other's environment variables (such as RELAYFLOWS_INPUTS_JSON and RELAY_INVOCATION_ID). Consider passing these variables directly to the execution context or isolating the environment (e.g., by spawning a child process with a custom environment map or using worker threads).

Comment thread src/orchestrator/factory.ts Outdated
const batch = events.splice(0, LIVE_EVENT_DRAIN_BATCH_SIZE)
const paths: string[] = []
for (const event of batch) {
await this.#dispatchRelayflowEvent(event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Awaiting this.#dispatchRelayflowEvent(event) inside the live event drain loop will block the processing of subsequent events in the batch. Since running a relayflow workflow can be a long-running operation (involving network requests or multi-step execution), this can severely degrade the responsiveness of the factory orchestrator. Consider dispatching the relayflow asynchronously (e.g., using void or floating the promise) similar to how it is handled in the subscription callback.

Comment on lines +156 to +157
relayClient: (provider) => relayClient(provider, clientOpts),
providerClient: (provider) => providerClient(provider, clientOpts),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The wrapper functions for relayClient and providerClient currently ignore any custom options passed by the caller (such as a custom mountRoot). Forward the optional arguments to the underlying helper functions to ensure the full signature and behavior are preserved.

Suggested change
relayClient: (provider) => relayClient(provider, clientOpts),
providerClient: (provider) => providerClient(provider, clientOpts),
relayClient: (provider, options) => relayClient(provider, { ...clientOpts, ...options }),
providerClient: (provider, options) => providerClient(provider, { ...clientOpts, ...options }),

Comment on lines +201 to +207
function normalizeResourceName(value: string | undefined): string | undefined {
if (!value) return undefined
if (value === 'pulls' || value === 'pull-requests' || value === 'pull_requests') return 'pull_request'
if (value.endsWith('ies')) return `${value.slice(0, -3)}y`
if (value.endsWith('s')) return value.slice(0, -1)
return value
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The naive plural-to-singular normalization logic has two failure modes:

  1. Words ending in es (like branches or boxes) will be incorrectly normalized to branche or boxe because only the trailing s is stripped.
  2. Singular words ending in s (like status or process) will have their trailing s stripped (e.g., statu or proces).

Update the helper to handle these cases correctly.

Suggested change
function normalizeResourceName(value: string | undefined): string | undefined {
if (!value) return undefined
if (value === 'pulls' || value === 'pull-requests' || value === 'pull_requests') return 'pull_request'
if (value.endsWith('ies')) return `${value.slice(0, -3)}y`
if (value.endsWith('s')) return value.slice(0, -1)
return value
}
function normalizeResourceName(value: string | undefined): string | undefined {
if (!value) return undefined
if (value === 'pulls' || value === 'pull-requests' || value === 'pull_requests') return 'pull_request'
if (value.endsWith('ies')) return `${value.slice(0, -3)}y`
if (value.endsWith('ches') || value.endsWith('shes') || value.endsWith('xes')) return value.slice(0, -2)
if (value.endsWith('s') && !['status', 'process', 'address', 'access', 'analysis'].includes(value)) return value.slice(0, -1)
return value
}

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3df469abec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/orchestrator/factory.ts Outdated
const batch = events.splice(0, LIVE_EVENT_DRAIN_BATCH_SIZE)
const paths: string[] = []
for (const event of batch) {
await this.#dispatchRelayflowEvent(event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Widen the live subscription before dispatching relayflows

When live transport uses subscribe/subscribe-and-poll, this call only sees events enqueued by the subscription registered with [LIVE_ISSUE_GLOB, LIVE_GITHUB_ISSUE_GLOB] at src/orchestrator/factory.ts:529; the real event client checks those globs before invoking onChange, so paths like /slack/... or /notion/... never reach this dispatch. Registered policies for non-issue integration events therefore silently do not run unless poll transport is selected; subscribe to the registered trigger roots or all integration paths before dispatching.

Useful? React with 👍 / 👎.

Comment thread src/orchestrator/factory.ts Outdated
const batch = events.splice(0, LIVE_EVENT_DRAIN_BATCH_SIZE)
const paths: string[] = []
for (const event of batch) {
await this.#dispatchRelayflowEvent(event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress replayed events before dispatching relayflows

This dispatch runs before #prepareLiveEventForDrain applies the high-watermark, replay-skew, and duplicate checks. In live subscribe reconnect/replay or duplicate-delivery scenarios, events that the factory later suppresses for issue processing still invoke registered relayflows, which can repeat writebacks or other side effects; apply the same live-event identity checks before invoking policies.

Useful? React with 👍 / 👎.

Comment thread src/node/factory-node.ts Outdated
const previous = new Map<string, string | undefined>()
for (const [key, value] of Object.entries(values)) {
previous.set(key, process.env[key])
process.env[key] = value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid sharing workflow inputs through process.env

withProcessEnv mutates global process.env across an awaited SDK run; if two workflow:run actions or relayflow dispatches overlap in the node process, the second invocation can overwrite RELAYFLOWS_INPUTS_JSON/RELAY_INVOCATION_ID while the first workflow or its child commands are still reading them, and either finally block can restore/delete the other's values. The previous shell-out passed env per child, so keep inputs in per-run SDK options or serialize/isolated-spawn the env-dependent path.

Useful? React with 👍 / 👎.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Working tree is clean — only the untracked memory/ dir (pre-existing harness artifact, not something I created). I made no edits, so nothing to discard.

Let me make one final review-quality pass on a potential semantic concern worth flagging: the resourceFromPath GitHub branch uses segments[reposIndex + 3] which assumes path shape /github/repos/{owner}/{repo}/{collection}. Let me verify this against the test fixtures to confirm it's correct, not a bug.

The test triggerEventFromChangeEvent uses path /github/repos/AgentWorkforce/factory/pulls/42/meta.json:

  • segments: ['github','repos','AgentWorkforce','factory','pulls','42','meta.json']
  • reposIndex = 1, segments[1+3] = segments[4] = 'pulls' → normalized to 'pull_request'

That matches the test expectation github.pull_request.created. Correct.

Now I have a complete picture. Let me finalize the review.

Review Summary — PR #47: feat/relayflow-integration-events

What the PR does: Replaces the relayflows CLI shell-out with the in-process @relayflows/core SDK for workflow:run, and adds a configurable RelayflowPolicyRegistry that dispatches relayflows from integration change events (Linear/GitHub/Slack/etc.) flowing through the live subscription. Supporting changes: planning-doc updates, filesystemEventType preservation on ChangeEvent, clone-path mount bootstrapping in the fleet CLI, and dependency swaps (@relayflows/cli removed; @relayflows/core + @relayfile/relay-helpers promoted to deps).

Verification (ran the full CI command end-to-end against the current checkout):

  • npm ci — clean
  • npm run build (tsc + tsc-alias) — clean, no type errors
  • npm test (vitest) — 566 passed / 33 files, including the new SDK smoke test that runs a real YAML workflow through @relayflows/core
  • npm pack --dry-run — clean

Tracing results:

  • New WorkflowRunnerResult shape (status/runId replacing command/args/exitCode/stdout/stderr) is consistently consumed in relayflow-registry.ts and re-exported from index.ts; no stale references to the old fields anywhere.
  • No dangling references to the removed @relayflows/cli, RELAYFLOWS_BIN, or resolveRelayflowsInvocation.
  • New relayflowEventsDispatched / relayflowDispatchErrors counters work via the dynamic #increment map and surface through status().counters (asserted by the orchestrator test).
  • #dispatchRelayflowEvent is fire-and-forget in the subscribe handler (matching the existing void this.#handle… pattern) and self-contains its errors in a try/catch, so no unhandled rejections.

Fail-closed safety preserved: runRelayflowsWorkflow throws for pending/running/failed/cancelled and only returns for completed/needs_human. needs_human is a legitimate terminal status in WorkflowRunStatus (verified in @relayflows/core types), so propagating it is correct — not a fail-open conversion.

Mechanical fixes applied: None needed. The one cosmetic item (the rewrapped comment in src/ports/fleet.ts:1-3) is grammatically correct as-is; churning it would be no-value comment noise.

Addressed comments

  • No bot or human review comments were present in the harness context (.workforce/context.json carries only PR metadata; no comments/threads file was provided). Nothing to validate or reconcile.

Advisory Notes

  • None. The PR is internally consistent and self-contained; no out-of-scope reviewer suggestions to record.

Assessment: The PR is coherent and fully green under the canonical CI command (npm ci && npm run build && npm test && npm pack --dry-run) with no edits required. I made no code changes and left the working tree clean. I cannot confirm GitHub's mergeability or the live CI check states from here, and there were no review comments requiring human adjudication — so I'm not marking this READY (that signal is reserved for when every required check is confirmed green/non-pending and a human decision genuinely remains).

@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

🤖 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 `@src/node/factory-node.ts`:
- Around line 295-355: The env-backed workflow execution in factory-node is
mutating process.env inside withProcessEnv, so overlapping workflow:run calls
can clobber each other’s RELAYFLOWS_INPUTS_JSON and RELAY_INVOCATION_ID. Add a
serialization guard around the workflow execution path in the main handler (the
branch that calls runWorkflow and runScriptWorkflowWithCwd) so only one
env-mutating run can happen at a time. Keep the env restore logic in
withProcessEnv, but make sure concurrent requests cannot enter that block
simultaneously.

In `@src/orchestrator/factory.ts`:
- Line 418: The subscribe transport in factory.ts still filters events to only
the Linear/GitHub trees, so non-issue integration paths like Notion never reach
the new dispatch hooks. Update the subscription matching logic around
dispatchRelayflowEvent and the related subscribe path handling so it includes
all Relayfile provider event namespaces, not just issue-based ones, and verify
the same fix is applied at the other referenced hook location as well.
- Around line 740-741: Relayflow dispatch is happening before the live-event
suppression gate, so replayed/stale events can still trigger duplicate external
workflows. Update the flow in the orchestrator factory around
`#dispatchRelayflowEvent` and `#prepareLiveEventForDrain` so relayflow is only
dispatched after the event passes the duplicate/high-watermark filtering, or
otherwise guard `#dispatchRelayflowEvent` with the same suppression result used by
`#prepareLiveEventForDrain`.
🪄 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: 930ec489-f162-4dac-a2e5-c676f7102c24

📥 Commits

Reviewing files that changed from the base of the PR and between 23c0d9e and 3df469a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • package.json
  • planning/factory-cloud-watches-local-node-linear-issue.md
  • planning/factory-unified-node-architecture-linear-issue.md
  • planning/linear-issue-factory-phase-3-fleet-client.md
  • planning/linear-issue-factory-phase-4-node-registration.md
  • src/cli/fleet.test.ts
  • src/cli/fleet.ts
  • src/dispatch/relayflow-registry.test.ts
  • src/dispatch/relayflow-registry.ts
  • src/index.ts
  • src/node/factory-node.test.ts
  • src/node/factory-node.ts
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/ports/fleet.ts
  • src/subscriptions/__tests__/event-client.test.ts
  • src/subscriptions/event-client.ts
  • src/types.ts

Comment thread src/node/factory-node.ts
Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts Outdated
@agent-relay-code

Copy link
Copy Markdown
Contributor

I made no file edits — the diff is clean, has no lint/format tooling, and the full CI command set passes. My findings are behavioral observations for the author. Here is my review.


Review — PR #47: dispatch relayflows from integration events

Summary

This PR moves the workflow:run capability from shelling out to the relayflows CLI to invoking @relayflows/core in-process, and adds a RelayflowPolicyRegistry that dispatches relayflow templates directly from live integration change-events in the orchestrator. I verified the change end-to-end against the current checkout.

CI verification (ran the canonical CI command set)

  • npm ci — clean (lockfile consistent with the new package.json deps + overrides).
  • npm run build (tsc -p tsconfig.build.json && tsc-alias) — passes, typecheck clean.
  • npm test (vitest run) — 567/567 pass on a warm run.
  • npm pack --dry-run — passes.

One transient failure on the first cold run: src/__tests__/dist-entrypoints.test.ts timed out at 5000ms. I reproduced the import standalone (import('./dist/index.js') resolves in ~583ms) and re-ran the test in isolation (passes in 3.35s) and the full suite again (33 files / 567 tests green). It was a cold esbuild-transform timeout under parallel load, not a regression from this PR. Worth noting since it can flake in CI, but not blocking and not caused by these changes.

I made no file edits — the diff is internally consistent, there is no lint/format tooling or lint CI step, and I found no mechanical (typo/import-order/format) issues to apply.

Review comments (for author judgment — left as comments, not changed)

  1. src/orchestrator/factory.ts:419-420 — relayflow dispatch in the legacy start() path is not deduped/watermarked. In the subscribe-transport path, dispatch is gated through #prepareLiveEventForDrain (watermark, time-cutoff, and dedupe suppression all return dispatchRelayflow: false). In the legacy path at line 420, void this.#dispatchRelayflowEvent(event) fires on every raw event with none of those guards. With '/**' subscription breadth, this path can dispatch the same workflow on replayed/duplicate events. Confirm this asymmetry is intended; if not, the legacy path likely wants the same suppression gating. I did not change it — it's live-event/dispatch flow and the correct behavior is a design decision.

  2. src/orchestrator/factory.ts:419 / #subscriptionGlobs — configuring relayflows widens ALL subscriptions to '/**'. When the relayflows port is present, both subscription sites switch from the narrow factory globs (/linear/issues/**, /github/repos/**) to '/**'. That's the intent (relayflow policies can match any provider), but it materially increases event volume through the factory's live handlers even for workspaces with a single narrow policy registered. Consider whether the subscription scope should be the union of factory globs + the registered policy triggers rather than a blanket '/**'. Behavioral/architectural call — left for you.

  3. src/node/factory-node.ts:294-367runRelayflowsWorkflow mutates global process.env. withProcessEnv sets RELAYFLOWS_INPUTS_JSON / FACTORY_WORKFLOW_INPUTS_JSON / RELAY_INVOCATION_ID on the shared process.env, runs the SDK, then restores. The new workflowEnvLock serializes concurrent workflow runs to prevent interleaved env clobbering, which addresses the obvious race. Flagging for awareness: any unrelated code reading these env vars during a run still sees the mutated values, and the serialization means workflow runs on a node can no longer overlap. Both look acceptable for a node-side runner, but worth a conscious sign-off.

Addressed comments

No bot or human review comments were provided in the PR metadata (.workforce/context.json contains no comment threads, and no comments file was supplied). Nothing to reconcile. If there are review threads on GitHub not included in the harness context, they were not visible to this run.

Advisory Notes

  • factory-build/run-factory-build.sh and factory-build/README.md still describe the CLI runner. The PR removed @relayflows/cli from devDependencies and switched the runtime to @relayflows/core, but these files still reference node_modules/@relayflows/cli/dist/cli.js and "the relayflows run runner". This does not break the build — @relayflows/cli remains installed transitively (another dependency requires @relayflows/cli@^1.0.1, and npm ci still places it in node_modules), so the script resolves. factory-build/ is outside this PR's changed-file set and unrelated to the dispatch feature, so I left it unchanged rather than fold a doc/script cleanup into this PR. Recommend a follow-up to align that runner doc with the SDK model.
  • The planning/*.md doc updates in this PR correctly re-document the shift from "shell out to relayflows run" to the embedded SDK and are consistent with the code.

CI is green across the full command set, the change is well-covered by new and updated tests, and the remaining items are author-judgment behavioral questions rather than defects. Not printing READY: the three review comments above are genuine open questions for the author (dedup asymmetry, subscription breadth, global-env mutation) that should be resolved or consciously accepted before merge, so this is not yet a clean hand-off requiring only human sign-off.

@khaliqgant khaliqgant merged commit f6ed2fc into main Jun 27, 2026
3 checks passed
@khaliqgant khaliqgant deleted the feat/relayflow-integration-events branch June 27, 2026 20:07
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