Skip to content

feat(mcp-workforce): MCP server bridging harnesses to workforce primitives#91

Merged
khaliqgant merged 8 commits into
mainfrom
feat/mcp-workforce
May 13, 2026
Merged

feat(mcp-workforce): MCP server bridging harnesses to workforce primitives#91
khaliqgant merged 8 commits into
mainfrom
feat/mcp-workforce

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

Track C of the workforce-deploy-v1 cross-repo plan (see workforce/docs/plans/deploy-v1-workflow-spec.md). Adds @agentworkforce/mcp-workforce — an MCP server that exposes workforce primitives (workflows, memory, integration clients) to a harness running inside a workforce sandbox via stdio MCP.

Stacks on top of #90 (feat/deploy-v1-core) — depends on @agentworkforce/runtime/clients for the GithubClient. Will rebase on main once #90 merges.

Tools shipped

Tool Backed by
workflow.run / workflow.status HTTP against ${WORKFORCE_CLOUD_URL}/api/v1/workspaces/:id/workflows/... with the runtime-injected workspace token
memory.save / memory.recall Supermemory REST (/v3/memories, /v3/search) under a workforce:<workspaceId> container tag
integration.github.{comment, createIssue, upsertIssue, getPr, postReview} @agentworkforce/runtime/clients GithubClient, lazy-constructed from WORKFORCE_INTEGRATION_GITHUB_TOKEN

Design notes

  • Stdio transport via @modelcontextprotocol/sdk (>=1.21.0). npx @agentworkforce/mcp-workforce boots the server and speaks MCP over stdin/stdout.
  • Workspace tag convention (workforce:<workspaceId>) keeps memories written here visible to sage and other agent-assistant consumers using the same shape.
  • Integration tools dispatch through a single switch so the package grows by adding provider files, not by churning the MCP surface.
  • loadConfig enforces WORKFORCE_WORKSPACE_ID at startup; per-tool deps (workspace token, supermemory key, provider tokens) check at first call so partial wiring is debuggable rather than fatal at boot.

Tests

  • 21 tests pass, 0 fail. Coverage:
    • config.test.ts — env normalization, provider-token regex, cloud-url trimming, missing-workspace error
    • tools/workflow.test.ts — POST/GET shape, auth header, 4xx surface, missing token error
    • tools/memory.test.ts — supermemory POST/search, tag dedupe + workspace/scope injection, scope validation, limit-range guard
    • tools/integrations.test.ts — tool-name parsing, unknown-provider rejection, missing-token error, real GithubClient round-trip via globalThis.fetch override, postReview enum validation
    • server.test.ts — full tool-set registration matches the documented surface

Test plan

  • pnpm -r build green
  • pnpm --filter @agentworkforce/mcp-workforce run test — 21/21 pass
  • pnpm run typecheck (and typecheck:examples) clean
  • Manual smoke: npx @agentworkforce/mcp-workforce boots with required env, exits cleanly on stdin close
  • Real memory.save + memory.recall round-trip against staging Supermemory (deferred — gated on SUPERMEMORY_API_KEY in CI)

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Review Change Stack

Warning

Rate limit exceeded

@khaliqgant has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 48 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fcb023a-67a2-4556-925d-bd85b4543958

📥 Commits

Reviewing files that changed from the base of the PR and between a227c47 and 4d26fbb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • packages/mcp-workforce/README.md
  • packages/mcp-workforce/package.json
  • packages/mcp-workforce/src/bin.ts
  • packages/mcp-workforce/src/config.test.ts
  • packages/mcp-workforce/src/config.ts
  • packages/mcp-workforce/src/index.ts
  • packages/mcp-workforce/src/server.test.ts
  • packages/mcp-workforce/src/server.ts
  • packages/mcp-workforce/src/tools/integrations.test.ts
  • packages/mcp-workforce/src/tools/integrations.ts
  • packages/mcp-workforce/src/tools/memory.test.ts
  • packages/mcp-workforce/src/tools/memory.ts
  • packages/mcp-workforce/src/tools/workflow.test.ts
  • packages/mcp-workforce/src/tools/workflow.ts
  • packages/mcp-workforce/tsconfig.json
📝 Walkthrough

Walkthrough

Adds a new @agentworkforce/mcp-workforce package that implements an MCP server (workflow, memory, GitHub integration) runnable over stdio, and refactors runtime integration clients to a Relayfile VFS/writeback transport with new VFS helpers and typed clients for GitHub, Linear, Notion, Jira, and Slack.

Changes

MCP Workforce Server Package

Layer / File(s) Summary
Package metadata and CLI
packages/mcp-workforce/package.json, packages/mcp-workforce/README.md, packages/mcp-workforce/tsconfig.json
New ESM package with CLI bin workforce-mcp, build/test/typecheck scripts, published files, and README documenting usage, env config, and persona wiring.
Config loader and tests
packages/mcp-workforce/src/config.ts, packages/mcp-workforce/src/config.test.ts
WorkforceMcpConfig and loadConfig(env) validate/parse required WORKFORCE_WORKSPACE_ID, normalize WORKFORCE_CLOUD_URL, parse/clamp WORKFORCE_WRITEBACK_TIMEOUT_MS into writebackTimeoutMs, and optionally set relayfileMountRoot (with legacy fallback). Tests cover trimming, defaults, and validation.
Server construction, JSON result, and CLI entry
packages/mcp-workforce/src/server.ts, packages/mcp-workforce/src/server.test.ts, packages/mcp-workforce/src/bin.ts
createWorkforceMcpServer(config) registers workflow.*, memory.*, and integration.github.* tools (Zod-validated), exports jsonResult() (normalizes undefined -> { ok: true }) and runStdioServer(). bin.ts starts stdio server and handles top-level fatal errors. Tests assert json serialization and exact registered tool names.
Workflow tools and tests
packages/mcp-workforce/src/tools/workflow.ts, packages/mcp-workforce/src/tools/workflow.test.ts
Implements workflowRun (POST /workflows/run) and workflowStatus (GET run status) with header construction requiring runtimeToken, response parsing, and non-OK error conversion. Tests verify requests, validation, error messages, and token requirement.
Memory tools and tests (Supermemory)
packages/mcp-workforce/src/tools/memory.ts, packages/mcp-workforce/src/tools/memory.test.ts
Implements memorySave and memoryRecall against Supermemory endpoints, auth via supermemoryApiKey, scope validation, tag deduplication, result normalization (snake_case fallback), and error wrapping. Tests use fakeFetch to assert request shape, validation, and missing-key behavior.
Integration dispatch and GitHub tool wiring
packages/mcp-workforce/src/tools/integrations.ts, packages/mcp-workforce/src/tools/integrations.test.ts
dispatchIntegration(tool, args, deps) parses integration.<provider>.<method>, currently routes github methods (comment/createIssue/upsertIssue/getPr/postReview), validates/coerces args, lazily resolves a cached GitHub client (requires relayfileMountRoot), and exposes _resetIntegrationCache() for tests. Tests cover name validation, provider wiring, relayfile mount requirement, on-disk draft write behavior, enum validation, and field-specific errors.
Public exports barrel
packages/mcp-workforce/src/index.ts
Re-exports loadConfig, WorkforceMcpConfig, server entrypoints, memory/workflow/integration tool functions, types, and integration discovery helpers.

VFS-backed Integration Clients & Request Layer

Layer / File(s) Summary
VFS transport primitives and writeback
packages/runtime/src/clients/request.ts
New IntegrationClientOptions and writeback shapes; resolveMountRoot, encodeSegment, draftFile, atomic writeJsonFile (write+rename), receipt polling (waitForReceipt), and resilient readJsonFile/readTextFile/listJsonFiles/listDirectoryEntries helpers. Errors are wrapped as WorkforceIntegrationError.
WorkforceIntegrationError
packages/runtime/src/errors.ts
Introduces WorkforceIntegrationError class and options interface replacing the previous errors module; stores provider, operation, optional cause, and retryable flag and formats the message.
GitHub client: VFS-backed implementation and tests
packages/runtime/src/clients/github.ts, packages/runtime/src/clients/github.test.ts
Replaces REST GitHub client with a Relayfile-VFS-backed client that writes/reads canonical JSON/text under /github/repos/<owner>/<repo>/.... Methods (comment, createIssue, upsertIssue, getPr, postReview) serialize requests to the mount and derive results from receipts or stored metadata. Tests assert on-disk drafts, updates, PR metadata+diff reads, and review writes.
New typed VFS clients and tests: Linear, Notion, Jira, Slack
packages/runtime/src/clients/{linear,notion,jira,slack}.ts, packages/runtime/src/clients/{linear,notion,jira,slack}.test.ts
Adds createLinearClient, createNotionClient, createJiraClient, createSlackClient implemented over writeJsonFile/read helpers, normalizing responses from receipts/paths. Tests verify expected on-disk draft files and validation (e.g., thread ref parsing for Slack, database id for Notion).
Clients index and runtime exports
packages/runtime/src/clients/index.ts, packages/runtime/src/index.ts, packages/runtime/src/types.ts
Narrows GitHub re-exports to createGithubClient/type, adds Linear/Slack/Notion/Jira client exports and shared request utilities/types (IntegrationClientOptions, WritebackReceipt, WritebackResult, draftFile, encodeSegment, readJsonFile, writeJsonFile, etc.), updates IntegrationClients typings to concrete client types, and re-exports WorkforceIntegrationError.
Cleanup: removed old error/module surfaces
packages/runtime/src/clients/errors.ts (removed)
Removes prior WorkforceIntegrationError and isRetryableStatus from the old location (replaced by new errors.ts and request-layer behavior).
sequenceDiagram
    participant MCP as MCP Server
    participant DISP as dispatchIntegration
    participant CLIENT as Integration Client (e.g., GitHub)
    participant VFS as Relayfile Mount (filesystem)
    participant WB as Writeback Worker

    MCP->>DISP: call integration.github.createIssue(args)
    DISP->>CLIENT: resolve/create client with relayfileMountRoot
    CLIENT->>VFS: write draft JSON via writeJsonFile (atomic write)
    VFS->>WB: (external) writeback worker detects draft -> performs GitHub API call
    WB->>VFS: write receipt file under same path
    CLIENT->>VFS: poll/read receipt (waitForReceipt) and return result to DISP
    DISP->>MCP: return MCP jsonResult(...) to caller
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I wrote a draft to mount and save,
Files bloom where integrations behave,
Workflows run, memories recall,
Relayfile whispers to GitHub’s hall—
A tiny rabbit signs the brave!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.90% 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: adding an MCP server that bridges workforce primitives (workflows, memory, integrations) via stdio MCP.
Description check ✅ Passed The description comprehensively explains the PR's purpose, tools shipped, design decisions, test coverage, and test plan—directly related to the changeset.
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 feat/mcp-workforce

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

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/mcp-workforce/src/server.ts Outdated
content: [
{
type: 'text' as const,
text: JSON.stringify(value)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 JSON.stringify(undefined) produces undefined instead of a string for postReview tool response

The GithubClient.postReview() method returns Promise<void> (packages/runtime/src/clients/github.ts:54), so dispatchIntegration('integration.github.postReview', ...) resolves to undefined. This undefined is passed to jsonResult(), which calls JSON.stringify(undefined). In JavaScript, JSON.stringify(undefined) returns the primitive undefined — not the string "undefined". This means the MCP response content item becomes {type: "text", text: undefined}, which when serialized over the wire drops the text key entirely ({"type":"text"}), violating the MCP protocol's requirement that text content includes a text string.

The effect is that a successful postReview call (review actually posted on GitHub) returns a malformed MCP response, which could cause the MCP client to error or the AI agent to believe the operation failed and retry, posting duplicate reviews.

Suggested change
text: JSON.stringify(value)
text: JSON.stringify(value ?? null)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

khaliqgant added a commit that referenced this pull request May 12, 2026
`integration.github.postReview` is a void-returning tool. The jsonResult
helper called `JSON.stringify(value)` which, when value is undefined,
returns `undefined` (not a string) — the resulting MCP CallToolResult
failed its content-shape check on the wire.

Normalize undefined to a `{ ok: true }` sentinel before stringifying so
void-returning tools still emit parseable JSON. `null` continues to
round-trip as the JSON `null` literal; only `undefined` triggers the
sentinel swap.

`jsonResult` is now exported so the regression test can call it
directly rather than synthesizing a full MCP transport.

Flagged by Devin Review on #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
khaliqgant added a commit that referenced this pull request May 12, 2026
`integration.github.postReview` is a void-returning tool. The jsonResult
helper called `JSON.stringify(value)` which, when value is undefined,
returns `undefined` (not a string) — the resulting MCP CallToolResult
failed its content-shape check on the wire.

Normalize undefined to a `{ ok: true }` sentinel before stringifying so
void-returning tools still emit parseable JSON. `null` continues to
round-trip as the JSON `null` literal; only `undefined` triggers the
sentinel swap.

`jsonResult` is now exported so the regression test can call it
directly rather than synthesizing a full MCP transport.

Flagged by Devin Review on #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/mcp-workforce branch from 7774bbd to fda8995 Compare May 12, 2026 11:02
khaliqgant added a commit that referenced this pull request May 12, 2026
`integration.github.postReview` is a void-returning tool. The jsonResult
helper called `JSON.stringify(value)` which, when value is undefined,
returns `undefined` (not a string) — the resulting MCP CallToolResult
failed its content-shape check on the wire.

Normalize undefined to a `{ ok: true }` sentinel before stringifying so
void-returning tools still emit parseable JSON. `null` continues to
round-trip as the JSON `null` literal; only `undefined` triggers the
sentinel swap.

`jsonResult` is now exported so the regression test can call it
directly rather than synthesizing a full MCP transport.

Flagged by Devin Review on #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/mcp-workforce branch from fda8995 to 2bd3291 Compare May 12, 2026 11:23
Base automatically changed from feat/deploy-v1-core to main May 12, 2026 11:54
khaliqgant added a commit that referenced this pull request May 12, 2026
`integration.github.postReview` is a void-returning tool. The jsonResult
helper called `JSON.stringify(value)` which, when value is undefined,
returns `undefined` (not a string) — the resulting MCP CallToolResult
failed its content-shape check on the wire.

Normalize undefined to a `{ ok: true }` sentinel before stringifying so
void-returning tools still emit parseable JSON. `null` continues to
round-trip as the JSON `null` literal; only `undefined` triggers the
sentinel swap.

`jsonResult` is now exported so the regression test can call it
directly rather than synthesizing a full MCP transport.

Flagged by Devin Review on #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/mcp-workforce branch from 2bd3291 to a227c47 Compare May 12, 2026 11:54
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (4)
packages/mcp-workforce/src/tools/memory.test.ts (1)

130-139: ⚡ Quick win

Consider adding a decimal-limit rejection case

To pin down limit semantics, add a test for non-integer limits (e.g., 1.5) in the same range-validation block.

🤖 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/mcp-workforce/src/tools/memory.test.ts` around lines 130 - 139, Add
a test to reject non-integer limits by asserting memoryRecall({ query: 'q',
limit: 1.5 }, { config: config() }) throws the same validation error; update the
existing test block around memoryRecall to include a decimal-limit case that
expects /"limit" must be 1-50/ so the range/precision semantics are covered.
packages/mcp-workforce/src/tools/memory.ts (1)

131-136: ⚡ Quick win

Sanitize returned scope before assigning PersonaMemoryScope

entry.metadata?.scope comes from external data and is currently trusted as-is. Guard it against unknown values and fallback to 'workspace' to keep the output contract sound.

Suggested patch
   const results = payload.results ?? [];
   const items: MemoryItem[] = results.map((entry) => ({
@@
-    scope: entry.metadata?.scope ?? 'workspace',
+    scope: VALID_SCOPES.has((entry.metadata?.scope ?? 'workspace') as PersonaMemoryScope)
+      ? ((entry.metadata?.scope ?? 'workspace') as PersonaMemoryScope)
+      : 'workspace',
     createdAt: entry.createdAt ?? entry.created_at ?? ''
   }));
🤖 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/mcp-workforce/src/tools/memory.ts` around lines 131 - 136, The scope
assigned in the items mapping (inside the MemoryItem creation) trusts
entry.metadata?.scope directly; validate that value against the allowed
PersonaMemoryScope values (or an equivalent set/enum) before assigning it to
scope and fall back to 'workspace' for unknown/undefined values. Update the
mapping where MemoryItem is constructed (the content block using
entry.metadata?.scope) to check membership (e.g.,
isValidPersonaMemoryScope(scope) or compare against the PersonaMemoryScope enum)
and only use entry.metadata.scope when valid, otherwise use 'workspace'.
packages/mcp-workforce/src/tools/integrations.test.ts (1)

77-104: ⚡ Quick win

Add regression tests for integer-only numeric fields

Given target.number and review comment.line are numeric identifiers, add rejects for decimal/negative values to lock validation behavior.

Example test cases
+test('dispatchIntegration rejects non-integer github numeric fields', async () => {
+  _resetIntegrationCache();
+  await assert.rejects(
+    () =>
+      dispatchIntegration(
+        'integration.github.getPr',
+        { target: { owner: 'o', repo: 'r', number: 1.5 } },
+        { config: config() }
+      ),
+    /target\.number: must be a positive integer/
+  );
+});
🤖 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/mcp-workforce/src/tools/integrations.test.ts` around lines 77 - 104,
Add regression tests to ensure numeric identifier fields reject decimals and
negatives: extend the existing tests around dispatchIntegration (and use
_resetIntegrationCache) to assert.rejects when passing non-integer values for
target.number (e.g., 1.5 and -1) in the 'integration.github.postReview' payload
and when passing non-integer values for review.comment.line (e.g., 2.2 and -3)
in relevant integration payloads; each test should call dispatchIntegration with
{ config: config() } and assert the error message indicates the field must be an
integer (or match the current validation error format) so validation enforces
integer-only numeric IDs.
packages/mcp-workforce/src/index.ts (1)

18-24: ⚡ Quick win

Avoid exporting _resetIntegrationCache from the public barrel.

This looks test-oriented and becomes part of the semver contract once exported publicly. Prefer keeping it internal (or exporting from a clearly internal/testing-only subpath).

Suggested change
 export {
   dispatchIntegration,
   INTEGRATION_TOOL_NAMES,
-  _resetIntegrationCache,
   type IntegrationToolDeps,
   type IntegrationToolName
 } from './tools/integrations.js';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-workforce/src/index.ts` around lines 18 - 24, The public barrel
is exporting a test/internal helper (_resetIntegrationCache) which should not be
part of the public API; remove _resetIntegrationCache from the export list
(leave dispatchIntegration, INTEGRATION_TOOL_NAMES, and the types) so it is not
part of the semver contract, keep the actual _resetIntegrationCache
implementation internal in the integrations module, and if tests need it,
re-export it from a clearly-named internal/test-only entry (or update tests to
import it from the integrations module directly) rather than exposing it from
the public barrel.
🤖 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/mcp-workforce/src/tools/integrations.ts`:
- Around line 151-175: The number and string validators are too permissive:
update asNumber to require a finite integer >= 1 (reject decimals, negatives,
zero) and throw a clear error using the provided label; update asNonEmptyString
to trim the input before validation and return the trimmed string so callers get
normalized values; ensure asTarget continues to call asNonEmptyString for
owner/repo so it receives trimmed values and asNumber for number so it receives
a positive integer (adjust error text if needed to mention "positive integer"
for target.number).

In `@packages/mcp-workforce/src/tools/memory.ts`:
- Around line 99-102: The current validation for args.limit in memory.recall
allows non-integer values like 2.5; update the validation to require an integer
by checking Number.isInteger(limit) (replace or augment Number.isFinite check)
and throw the same or clearer error if it's not an integer or out of range;
reference the local variable limit and the memory.recall usage so you modify the
check around "const limit = args.limit ?? 5" to use Number.isInteger(limit) &&
limit >= 1 && limit <= 50 (otherwise throw).

In `@packages/mcp-workforce/src/tools/workflow.ts`:
- Around line 34-42: Both fetch calls (the local variable fetchImpl used to POST
to url in this file and the other fetch later) can hang indefinitely; wrap each
fetch call with an AbortController-based timeout: create an AbortController,
start a setTimeout that calls controller.abort() after a configurable timeout
(e.g., deps.config.requestTimeoutMs or a default), pass controller.signal to
fetchImpl(...) as the signal option, clear the timeout once the promise
resolves, and handle the AbortError to throw a clear timeout error. Update the
fetch invocation that builds the request body (where fetchImpl is called) and
the later fetch call to use this abort-timeout wrapper so stalled network
requests are aborted cleanly.

---

Nitpick comments:
In `@packages/mcp-workforce/src/index.ts`:
- Around line 18-24: The public barrel is exporting a test/internal helper
(_resetIntegrationCache) which should not be part of the public API; remove
_resetIntegrationCache from the export list (leave dispatchIntegration,
INTEGRATION_TOOL_NAMES, and the types) so it is not part of the semver contract,
keep the actual _resetIntegrationCache implementation internal in the
integrations module, and if tests need it, re-export it from a clearly-named
internal/test-only entry (or update tests to import it from the integrations
module directly) rather than exposing it from the public barrel.

In `@packages/mcp-workforce/src/tools/integrations.test.ts`:
- Around line 77-104: Add regression tests to ensure numeric identifier fields
reject decimals and negatives: extend the existing tests around
dispatchIntegration (and use _resetIntegrationCache) to assert.rejects when
passing non-integer values for target.number (e.g., 1.5 and -1) in the
'integration.github.postReview' payload and when passing non-integer values for
review.comment.line (e.g., 2.2 and -3) in relevant integration payloads; each
test should call dispatchIntegration with { config: config() } and assert the
error message indicates the field must be an integer (or match the current
validation error format) so validation enforces integer-only numeric IDs.

In `@packages/mcp-workforce/src/tools/memory.test.ts`:
- Around line 130-139: Add a test to reject non-integer limits by asserting
memoryRecall({ query: 'q', limit: 1.5 }, { config: config() }) throws the same
validation error; update the existing test block around memoryRecall to include
a decimal-limit case that expects /"limit" must be 1-50/ so the range/precision
semantics are covered.

In `@packages/mcp-workforce/src/tools/memory.ts`:
- Around line 131-136: The scope assigned in the items mapping (inside the
MemoryItem creation) trusts entry.metadata?.scope directly; validate that value
against the allowed PersonaMemoryScope values (or an equivalent set/enum) before
assigning it to scope and fall back to 'workspace' for unknown/undefined values.
Update the mapping where MemoryItem is constructed (the content block using
entry.metadata?.scope) to check membership (e.g.,
isValidPersonaMemoryScope(scope) or compare against the PersonaMemoryScope enum)
and only use entry.metadata.scope when valid, otherwise use 'workspace'.
🪄 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: b039c908-2606-41de-abec-c5774aadcd6a

📥 Commits

Reviewing files that changed from the base of the PR and between a3d8f5f and a227c47.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • packages/mcp-workforce/README.md
  • packages/mcp-workforce/package.json
  • packages/mcp-workforce/src/bin.ts
  • packages/mcp-workforce/src/config.test.ts
  • packages/mcp-workforce/src/config.ts
  • packages/mcp-workforce/src/index.ts
  • packages/mcp-workforce/src/server.test.ts
  • packages/mcp-workforce/src/server.ts
  • packages/mcp-workforce/src/tools/integrations.test.ts
  • packages/mcp-workforce/src/tools/integrations.ts
  • packages/mcp-workforce/src/tools/memory.test.ts
  • packages/mcp-workforce/src/tools/memory.ts
  • packages/mcp-workforce/src/tools/workflow.test.ts
  • packages/mcp-workforce/src/tools/workflow.ts
  • packages/mcp-workforce/tsconfig.json

Comment on lines +151 to +175
function asNonEmptyString(value: unknown, label: string): string {
if (typeof value !== 'string' || !value.trim()) {
throw new Error(`${label}: must be a non-empty string`);
}
return value;
}

function asNumber(value: unknown, label: string): number {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new Error(`${label}: must be a finite number`);
}
return value;
}

function isString(value: unknown): value is string {
return typeof value === 'string';
}

function asTarget(value: unknown): { owner: string; repo: string; number: number } {
const obj = asObject(value, 'target');
return {
owner: asNonEmptyString(obj.owner, 'target.owner'),
repo: asNonEmptyString(obj.repo, 'target.repo'),
number: asNumber(obj.number, 'target.number')
};
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

Harden argument normalization for GitHub identifiers and strings

asNumber currently accepts decimals/negatives (e.g., 1.5, -3), and asNonEmptyString returns untrimmed values after validation. That can generate invalid GitHub endpoints/payloads and harder-to-diagnose API failures.

Suggested patch
 function asNonEmptyString(value: unknown, label: string): string {
-  if (typeof value !== 'string' || !value.trim()) {
+  if (typeof value !== 'string' || !value.trim()) {
     throw new Error(`${label}: must be a non-empty string`);
   }
-  return value;
+  return value.trim();
 }
 
-function asNumber(value: unknown, label: string): number {
-  if (typeof value !== 'number' || !Number.isFinite(value)) {
-    throw new Error(`${label}: must be a finite number`);
+function asPositiveInteger(value: unknown, label: string): number {
+  if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
+    throw new Error(`${label}: must be a positive integer`);
   }
   return value;
 }
@@
                   path: asNonEmptyString(c.path, 'comment.path'),
-                  line: asNumber(c.line, 'comment.line'),
+                  line: asPositiveInteger(c.line, 'comment.line'),
                   body: asNonEmptyString(c.body, 'comment.body')
                 }))
@@
     owner: asNonEmptyString(obj.owner, 'target.owner'),
     repo: asNonEmptyString(obj.repo, 'target.repo'),
-    number: asNumber(obj.number, 'target.number')
+    number: asPositiveInteger(obj.number, 'target.number')
   };
 }
🤖 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/mcp-workforce/src/tools/integrations.ts` around lines 151 - 175, The
number and string validators are too permissive: update asNumber to require a
finite integer >= 1 (reject decimals, negatives, zero) and throw a clear error
using the provided label; update asNonEmptyString to trim the input before
validation and return the trimmed string so callers get normalized values;
ensure asTarget continues to call asNonEmptyString for owner/repo so it receives
trimmed values and asNumber for number so it receives a positive integer (adjust
error text if needed to mention "positive integer" for target.number).

Comment on lines +99 to +102
const limit = args.limit ?? 5;
if (!Number.isFinite(limit) || limit <= 0 || limit > 50) {
throw new Error('memory.recall: "limit" must be 1-50');
}
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

Enforce integer limit in memory.recall

Current check allows decimals (e.g., 2.5). Since limit is a count, enforce integers to avoid downstream API errors or implicit coercion.

Suggested patch
-  if (!Number.isFinite(limit) || limit <= 0 || limit > 50) {
+  if (!Number.isInteger(limit) || limit <= 0 || limit > 50) {
     throw new Error('memory.recall: "limit" must be 1-50');
   }
🤖 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/mcp-workforce/src/tools/memory.ts` around lines 99 - 102, The
current validation for args.limit in memory.recall allows non-integer values
like 2.5; update the validation to require an integer by checking
Number.isInteger(limit) (replace or augment Number.isFinite check) and throw the
same or clearer error if it's not an integer or out of range; reference the
local variable limit and the memory.recall usage so you modify the check around
"const limit = args.limit ?? 5" to use Number.isInteger(limit) && limit >= 1 &&
limit <= 50 (otherwise throw).

Comment on lines +34 to +42
const fetchImpl = deps.fetchImpl ?? fetch;
const url = `${deps.config.cloudUrl}/api/v1/workspaces/${encodeURIComponent(
deps.config.workspaceId
)}/workflows/run`;
const response = await fetchImpl(url, {
method: 'POST',
headers: workflowHeaders(deps.config),
body: JSON.stringify({ name: args.name, args: args.args ?? {} })
});
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "fetchImpl\\(" packages/mcp-workforce/src/tools/workflow.ts
rg -n "signal\\s*:" packages/mcp-workforce/src/tools/workflow.ts

Repository: AgentWorkforce/workforce

Length of output: 158


🏁 Script executed:

cat -n packages/mcp-workforce/src/tools/workflow.ts | head -100

Repository: AgentWorkforce/workforce

Length of output: 3926


🏁 Script executed:

cat packages/mcp-workforce/package.json | jq '.engines'

Repository: AgentWorkforce/workforce

Length of output: 73


🏁 Script executed:

rg -n "AbortController\|AbortSignal\|timeout" packages/mcp-workforce/src/tools/workflow.ts

Repository: AgentWorkforce/workforce

Length of output: 50


🏁 Script executed:

cat packages/mcp-workforce/package.json | head -30

Repository: AgentWorkforce/workforce

Length of output: 690


🏁 Script executed:

cat packages/package.json 2>/dev/null | jq '.engines' || echo "No package.json at root"

Repository: AgentWorkforce/workforce

Length of output: 50


🏁 Script executed:

find . -name "package.json" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat {} | jq ".engines" 2>/dev/null || echo "no engines field"'

Repository: AgentWorkforce/workforce

Length of output: 300


🏁 Script executed:

cat packages/mcp-workforce/tsconfig.json 2>/dev/null | jq '.compilerOptions | {target, lib}' || echo "No tsconfig or no compiler options"

Repository: AgentWorkforce/workforce

Length of output: 104


Add explicit timeouts to workflow HTTP calls.

Both external fetches at lines 38 and 65 can block indefinitely on stalled network conditions. Implement an abort timeout wrapper to prevent hanging requests that tie up harness execution.

Suggested change
+const WORKFLOW_HTTP_TIMEOUT_MS = 30_000;
+
+async function fetchWithTimeout(
+  fetchImpl: typeof fetch,
+  input: string,
+  init: RequestInit
+): Promise<Response> {
+  const controller = new AbortController();
+  const timer = setTimeout(() => controller.abort(), WORKFLOW_HTTP_TIMEOUT_MS);
+  try {
+    return await fetchImpl(input, { ...init, signal: controller.signal });
+  } finally {
+    clearTimeout(timer);
+  }
+}
+
 export async function workflowRun(
   args: { name: string; args?: Record<string, unknown> },
   deps: WorkflowToolDeps
 ): Promise<WorkflowRunResult> {
@@
-  const response = await fetchImpl(url, {
+  const response = await fetchWithTimeout(fetchImpl, url, {
     method: 'POST',
     headers: workflowHeaders(deps.config),
     body: JSON.stringify({ name: args.name, args: args.args ?? {} })
   });
@@
 export async function workflowStatus(
   args: { runId: string },
   deps: WorkflowToolDeps
 ): Promise<WorkflowStatusResult> {
@@
-  const response = await fetchImpl(url, {
+  const response = await fetchWithTimeout(fetchImpl, url, {
     method: 'GET',
     headers: workflowHeaders(deps.config)
   });
🤖 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/mcp-workforce/src/tools/workflow.ts` around lines 34 - 42, Both
fetch calls (the local variable fetchImpl used to POST to url in this file and
the other fetch later) can hang indefinitely; wrap each fetch call with an
AbortController-based timeout: create an AbortController, start a setTimeout
that calls controller.abort() after a configurable timeout (e.g.,
deps.config.requestTimeoutMs or a default), pass controller.signal to
fetchImpl(...) as the signal option, clear the timeout once the promise
resolves, and handle the AbortError to throw a clear timeout error. Update the
fetch invocation that builds the request body (where fetchImpl is called) and
the later fetch call to use this abort-timeout wrapper so stalled network
requests are aborted cleanly.

khaliqgant added a commit that referenced this pull request May 12, 2026
`integration.github.postReview` is a void-returning tool. The jsonResult
helper called `JSON.stringify(value)` which, when value is undefined,
returns `undefined` (not a string) — the resulting MCP CallToolResult
failed its content-shape check on the wire.

Normalize undefined to a `{ ok: true }` sentinel before stringifying so
void-returning tools still emit parseable JSON. `null` continues to
round-trip as the JSON `null` literal; only `undefined` triggers the
sentinel swap.

`jsonResult` is now exported so the regression test can call it
directly rather than synthesizing a full MCP transport.

Flagged by Devin Review on #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/mcp-workforce branch from a227c47 to b509753 Compare May 12, 2026 12:34
@khaliqgant khaliqgant changed the base branch from main to feat/integrations-vfs May 12, 2026 12:34
khaliqgant and others added 2 commits May 12, 2026 20:17
…t style

Switches workforce's integration clients from direct REST calls to the
Relayfile-VFS writeback pattern used by sage + the cloud workflows.
Handler-side surface (ctx.github.upsertIssue, ctx.linear.comment, etc.)
stays identical; the wire underneath flips from "speak HTTP to GitHub"
to "write a JSON draft inside the Relayfile mount and let the writeback
worker do the actual API call." Aligns workforce with the rest of the
org's integration story and inherits writeback durability + retry for
free.

Substrate
  - packages/runtime/src/errors.ts (top-level): WorkforceIntegrationError
    moves here with the { provider, operation, cause, retryable } shape
    sage/cloud already use. Old clients/errors.ts is removed; the public
    surface re-exports it from the same package import path so existing
    consumers (mcp-workforce) keep compiling.
  - packages/runtime/src/clients/request.ts: shared VFS helpers
    (readJsonFile, readTextFile, listJsonFiles, listDirectoryEntries,
    writeJsonFile + atomic write-then-rename) with mount-root path
    validation and optional writeback-receipt polling.

Clients
  - github.ts is rewritten as a VFS client. Same GithubClient interface
    (comment, createIssue, upsertIssue, getPr, postReview); each method
    now reads/writes files at canonical paths under
    `/github/repos/<owner>/<repo>/...`.
  - linear, slack, notion, jira ship as new typed clients with the same
    pattern. IntegrationClients in types.ts now types all five concretely
    instead of leaving four as unknown.

Tests
  - github.test.ts is rewritten end-to-end against a tempdir mount.
  - linear/slack/notion/jira tests run against tempdir mounts too.
  - 29 runtime tests pass (up from 18), 386 across the repo.

Example
  - weekly-digest/agent.ts drops the WORKFORCE_INTEGRATION_GITHUB_TOKEN
    plumbing; the github client picks up RELAYFILE_MOUNT_ROOT instead.
  - weekly-digest/README.md documents the writeback model + Relayfile
    mount env requirement, and drops the GITHUB_TOKEN setup step.

Notes
  - mcp-workforce (PR #91) imports createGithubClient with a different
    construction shape today (`{ token }`); it'll need a follow-up
    commit to switch to IntegrationClientOptions once this lands. The
    MCP package depends on the new shape, not the old.
  - The direct-REST github implementation that shipped in #90 is
    replaced wholesale. No persona today depends on it; weekly-digest
    is updated in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-on to the persona-tier flatten refactor — the openclaw-routing
example still read from selection.runtime.* and selection.tier, which no
longer exist after PersonaSelection lost its per-tier wrapper.
Required to keep the examples typecheck green on top of the flatten.
@khaliqgant khaliqgant force-pushed the feat/integrations-vfs branch from efb115b to 1f8dcdb Compare May 12, 2026 18:20
khaliqgant and others added 4 commits May 12, 2026 20:23
…tives

Ships Track C of the workforce-deploy-v1 cross-repo plan
(workforce/docs/plans/deploy-v1-workflow-spec.md). New package
@agentworkforce/mcp-workforce exposes nine tools over stdio MCP:

  workflow.run, workflow.status
  memory.save, memory.recall
  integration.github.{comment,createIssue,upsertIssue,getPr,postReview}

Design notes
- Stdio transport via @modelcontextprotocol/sdk so the harness's
  built-in MCP client wires up with `npx @agentworkforce/mcp-workforce`.
- Workflow tools POST/GET against the workforce cloud workflows REST
  API using the workspace token the runtime injects via
  WORKFORCE_RUNTIME_TOKEN.
- Memory tools talk directly to the Supermemory REST API
  (workspace-scoped container tag, scope tags) so the package stays
  free of heavy adapter deps. Workspace memories are visible across
  sage and other consumers that use the same container shape.
- Integration tools delegate to @agentworkforce/runtime/clients
  (currently github only); a single dispatcher splits tool names
  like `integration.github.comment` and lazy-constructs the client
  from WORKFORCE_INTEGRATION_GITHUB_TOKEN.
- Config loader (loadConfig) trims env values and enforces
  WORKFORCE_WORKSPACE_ID as the only hard requirement at startup —
  per-tool deps (token, supermemory key) check at call time so
  partial wiring is debuggable.

Tests
- 21 tests across config, workflow, memory, integrations, and server
  registration. Workflow + memory tests use a deterministic fakeFetch;
  integrations test uses a globalThis.fetch override to drive the real
  GithubClient end-to-end.

Persona-side wiring
- Runtime is expected to inject the server automatically when
  ctx.harness.run spawns a harness. Power users can declare it
  manually in persona.mcpServers as documented in the README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`integration.github.postReview` is a void-returning tool. The jsonResult
helper called `JSON.stringify(value)` which, when value is undefined,
returns `undefined` (not a string) — the resulting MCP CallToolResult
failed its content-shape check on the wire.

Normalize undefined to a `{ ok: true }` sentinel before stringifying so
void-returning tools still emit parseable JSON. `null` continues to
round-trip as the JSON `null` literal; only `undefined` triggers the
sentinel swap.

`jsonResult` is now exported so the regression test can call it
directly rather than synthesizing a full MCP transport.

Flagged by Devin Review on #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the MCP server with the runtime's new client shape from #92.
Before: the integration tools called createGithubClient({ token })
using WORKFORCE_INTEGRATION_GITHUB_TOKEN. That signature no longer
exists in the runtime now that github speaks Relayfile-VFS, so the
MCP package would fail to compile once #92 lands.

Config (config.ts)
  - WorkforceMcpConfig drops `providerTokens`. The MCP server is no
    longer the place to hold provider PATs — Relayfile holds the
    credentials and the writeback worker uses them.
  - New `relayfileMountRoot` field, populated from
    RELAYFILE_MOUNT_ROOT (with RELAYFILE_ROOT as a legacy alias).
    The workforce runtime sets these env vars automatically when it
    spawns the harness via ctx.harness.run.
  - New `writebackTimeoutMs` field (default 30s, overridable via
    WORKFORCE_WRITEBACK_TIMEOUT_MS). Passed straight through to
    integration clients so handlers that need a synchronous receipt
    pay the same wait the runtime would.

Integration dispatcher (tools/integrations.ts)
  - resolveGithub() now constructs createGithubClient with
    { relayfileMountRoot, writebackTimeoutMs } from config instead
    of a token. Refuses to construct when the mount root is missing
    with a message pointing at the runtime contract.

Tests
  - config.test exercises RELAYFILE_MOUNT_ROOT (+ RELAYFILE_ROOT
    alias) and WORKFORCE_WRITEBACK_TIMEOUT_MS overrides.
  - integrations.test now writes against a tempdir mount and reads
    the resulting draft JSON file back to assert the canonical
    shape Relayfile expects. Covers happy path, missing-mount-root
    failure, postReview enum validation, and field-pointed errors.
  - server.test loads config with RELAYFILE_MOUNT_ROOT instead of a
    github PAT.
  - workflow.test + memory.test drop the obsolete providerTokens
    fixture key. 23 mcp-workforce tests pass (up from 21).

PR base flips from main to feat/integrations-vfs (PR #92) since the
new client shape lives there. Auto-rebases to main when #92 merges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The README still documented WORKFORCE_INTEGRATION_GITHUB_TOKEN as the
setup step for integration.* tools, but those tools no longer take a
provider token — they write JSON drafts into the Relayfile mount and
Relayfile's writeback worker handles the actual provider call.

Updated the stand-alone setup example and the config table to point
at RELAYFILE_MOUNT_ROOT (with the RELAYFILE_ROOT alias and the new
WORKFORCE_WRITEBACK_TIMEOUT_MS override documented for completeness).
Adds a sentence on the writeback model so readers understand why the
MCP server never sees a provider token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/mcp-workforce branch from c2355fe to d8654e4 Compare May 12, 2026 18:29
khaliqgant added a commit that referenced this pull request May 12, 2026
…t style

Switches workforce's integration clients from direct REST calls to the
Relayfile-VFS writeback pattern used by sage + the cloud workflows.
Handler-side surface (ctx.github.upsertIssue, ctx.linear.comment, etc.)
stays identical; the wire underneath flips from "speak HTTP to GitHub"
to "write a JSON draft inside the Relayfile mount and let the writeback
worker do the actual API call." Aligns workforce with the rest of the
org's integration story and inherits writeback durability + retry for
free.

Substrate
  - packages/runtime/src/errors.ts (top-level): WorkforceIntegrationError
    moves here with the { provider, operation, cause, retryable } shape
    sage/cloud already use. Old clients/errors.ts is removed; the public
    surface re-exports it from the same package import path so existing
    consumers (mcp-workforce) keep compiling.
  - packages/runtime/src/clients/request.ts: shared VFS helpers
    (readJsonFile, readTextFile, listJsonFiles, listDirectoryEntries,
    writeJsonFile + atomic write-then-rename) with mount-root path
    validation and optional writeback-receipt polling.

Clients
  - github.ts is rewritten as a VFS client. Same GithubClient interface
    (comment, createIssue, upsertIssue, getPr, postReview); each method
    now reads/writes files at canonical paths under
    `/github/repos/<owner>/<repo>/...`.
  - linear, slack, notion, jira ship as new typed clients with the same
    pattern. IntegrationClients in types.ts now types all five concretely
    instead of leaving four as unknown.

Tests
  - github.test.ts is rewritten end-to-end against a tempdir mount.
  - linear/slack/notion/jira tests run against tempdir mounts too.
  - 29 runtime tests pass (up from 18), 386 across the repo.

Example
  - weekly-digest/agent.ts drops the WORKFORCE_INTEGRATION_GITHUB_TOKEN
    plumbing; the github client picks up RELAYFILE_MOUNT_ROOT instead.
  - weekly-digest/README.md documents the writeback model + Relayfile
    mount env requirement, and drops the GITHUB_TOKEN setup step.

Notes
  - mcp-workforce (PR #91) imports createGithubClient with a different
    construction shape today (`{ token }`); it'll need a follow-up
    commit to switch to IntegrationClientOptions once this lands. The
    MCP package depends on the new shape, not the old.
  - The direct-REST github implementation that shipped in #90 is
    replaced wholesale. No persona today depends on it; weekly-digest
    is updated in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/integrations-vfs branch from 1f8dcdb to 1225e04 Compare May 12, 2026 22:29
khaliqgant added a commit that referenced this pull request May 12, 2026
…t style

Switches workforce's integration clients from direct REST calls to the
Relayfile-VFS writeback pattern used by sage + the cloud workflows.
Handler-side surface (ctx.github.upsertIssue, ctx.linear.comment, etc.)
stays identical; the wire underneath flips from "speak HTTP to GitHub"
to "write a JSON draft inside the Relayfile mount and let the writeback
worker do the actual API call." Aligns workforce with the rest of the
org's integration story and inherits writeback durability + retry for
free.

Substrate
  - packages/runtime/src/errors.ts (top-level): WorkforceIntegrationError
    moves here with the { provider, operation, cause, retryable } shape
    sage/cloud already use. Old clients/errors.ts is removed; the public
    surface re-exports it from the same package import path so existing
    consumers (mcp-workforce) keep compiling.
  - packages/runtime/src/clients/request.ts: shared VFS helpers
    (readJsonFile, readTextFile, listJsonFiles, listDirectoryEntries,
    writeJsonFile + atomic write-then-rename) with mount-root path
    validation and optional writeback-receipt polling.

Clients
  - github.ts is rewritten as a VFS client. Same GithubClient interface
    (comment, createIssue, upsertIssue, getPr, postReview); each method
    now reads/writes files at canonical paths under
    `/github/repos/<owner>/<repo>/...`.
  - linear, slack, notion, jira ship as new typed clients with the same
    pattern. IntegrationClients in types.ts now types all five concretely
    instead of leaving four as unknown.

Tests
  - github.test.ts is rewritten end-to-end against a tempdir mount.
  - linear/slack/notion/jira tests run against tempdir mounts too.
  - 29 runtime tests pass (up from 18), 386 across the repo.

Example
  - weekly-digest/agent.ts drops the WORKFORCE_INTEGRATION_GITHUB_TOKEN
    plumbing; the github client picks up RELAYFILE_MOUNT_ROOT instead.
  - weekly-digest/README.md documents the writeback model + Relayfile
    mount env requirement, and drops the GITHUB_TOKEN setup step.

Notes
  - mcp-workforce (PR #91) imports createGithubClient with a different
    construction shape today (`{ token }`); it'll need a follow-up
    commit to switch to IntegrationClientOptions once this lands. The
    MCP package depends on the new shape, not the old.
  - The direct-REST github implementation that shipped in #90 is
    replaced wholesale. No persona today depends on it; weekly-digest
    is updated in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/integrations-vfs branch from 1225e04 to af8f4a2 Compare May 12, 2026 22:31
khaliqgant added a commit that referenced this pull request May 12, 2026
…t style

Switches workforce's integration clients from direct REST calls to the
Relayfile-VFS writeback pattern used by sage + the cloud workflows.
Handler-side surface (ctx.github.upsertIssue, ctx.linear.comment, etc.)
stays identical; the wire underneath flips from "speak HTTP to GitHub"
to "write a JSON draft inside the Relayfile mount and let the writeback
worker do the actual API call." Aligns workforce with the rest of the
org's integration story and inherits writeback durability + retry for
free.

Substrate
  - packages/runtime/src/errors.ts (top-level): WorkforceIntegrationError
    moves here with the { provider, operation, cause, retryable } shape
    sage/cloud already use. Old clients/errors.ts is removed; the public
    surface re-exports it from the same package import path so existing
    consumers (mcp-workforce) keep compiling.
  - packages/runtime/src/clients/request.ts: shared VFS helpers
    (readJsonFile, readTextFile, listJsonFiles, listDirectoryEntries,
    writeJsonFile + atomic write-then-rename) with mount-root path
    validation and optional writeback-receipt polling.

Clients
  - github.ts is rewritten as a VFS client. Same GithubClient interface
    (comment, createIssue, upsertIssue, getPr, postReview); each method
    now reads/writes files at canonical paths under
    `/github/repos/<owner>/<repo>/...`.
  - linear, slack, notion, jira ship as new typed clients with the same
    pattern. IntegrationClients in types.ts now types all five concretely
    instead of leaving four as unknown.

Tests
  - github.test.ts is rewritten end-to-end against a tempdir mount.
  - linear/slack/notion/jira tests run against tempdir mounts too.
  - 29 runtime tests pass (up from 18), 386 across the repo.

Example
  - weekly-digest/agent.ts drops the WORKFORCE_INTEGRATION_GITHUB_TOKEN
    plumbing; the github client picks up RELAYFILE_MOUNT_ROOT instead.
  - weekly-digest/README.md documents the writeback model + Relayfile
    mount env requirement, and drops the GITHUB_TOKEN setup step.

Notes
  - mcp-workforce (PR #91) imports createGithubClient with a different
    construction shape today (`{ token }`); it'll need a follow-up
    commit to switch to IntegrationClientOptions once this lands. The
    MCP package depends on the new shape, not the old.
  - The direct-REST github implementation that shipped in #90 is
    replaced wholesale. No persona today depends on it; weekly-digest
    is updated in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/integrations-vfs branch from af8f4a2 to 2fa0ef9 Compare May 12, 2026 22:51
khaliqgant added a commit that referenced this pull request May 12, 2026
…t style

Switches workforce's integration clients from direct REST calls to the
Relayfile-VFS writeback pattern used by sage + the cloud workflows.
Handler-side surface (ctx.github.upsertIssue, ctx.linear.comment, etc.)
stays identical; the wire underneath flips from "speak HTTP to GitHub"
to "write a JSON draft inside the Relayfile mount and let the writeback
worker do the actual API call." Aligns workforce with the rest of the
org's integration story and inherits writeback durability + retry for
free.

Substrate
  - packages/runtime/src/errors.ts (top-level): WorkforceIntegrationError
    moves here with the { provider, operation, cause, retryable } shape
    sage/cloud already use. Old clients/errors.ts is removed; the public
    surface re-exports it from the same package import path so existing
    consumers (mcp-workforce) keep compiling.
  - packages/runtime/src/clients/request.ts: shared VFS helpers
    (readJsonFile, readTextFile, listJsonFiles, listDirectoryEntries,
    writeJsonFile + atomic write-then-rename) with mount-root path
    validation and optional writeback-receipt polling.

Clients
  - github.ts is rewritten as a VFS client. Same GithubClient interface
    (comment, createIssue, upsertIssue, getPr, postReview); each method
    now reads/writes files at canonical paths under
    `/github/repos/<owner>/<repo>/...`.
  - linear, slack, notion, jira ship as new typed clients with the same
    pattern. IntegrationClients in types.ts now types all five concretely
    instead of leaving four as unknown.

Tests
  - github.test.ts is rewritten end-to-end against a tempdir mount.
  - linear/slack/notion/jira tests run against tempdir mounts too.
  - 29 runtime tests pass (up from 18), 386 across the repo.

Example
  - weekly-digest/agent.ts drops the WORKFORCE_INTEGRATION_GITHUB_TOKEN
    plumbing; the github client picks up RELAYFILE_MOUNT_ROOT instead.
  - weekly-digest/README.md documents the writeback model + Relayfile
    mount env requirement, and drops the GITHUB_TOKEN setup step.

Notes
  - mcp-workforce (PR #91) imports createGithubClient with a different
    construction shape today (`{ token }`); it'll need a follow-up
    commit to switch to IntegrationClientOptions once this lands. The
    MCP package depends on the new shape, not the old.
  - The direct-REST github implementation that shipped in #90 is
    replaced wholesale. No persona today depends on it; weekly-digest
    is updated in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant force-pushed the feat/integrations-vfs branch from 2fa0ef9 to 8b2ebe1 Compare May 12, 2026 23:33
khaliqgant added a commit that referenced this pull request May 13, 2026
…t style (#92)

* feat(runtime): adopt Relayfile-VFS as the canonical integration-client style

Switches workforce's integration clients from direct REST calls to the
Relayfile-VFS writeback pattern used by sage + the cloud workflows.
Handler-side surface (ctx.github.upsertIssue, ctx.linear.comment, etc.)
stays identical; the wire underneath flips from "speak HTTP to GitHub"
to "write a JSON draft inside the Relayfile mount and let the writeback
worker do the actual API call." Aligns workforce with the rest of the
org's integration story and inherits writeback durability + retry for
free.

Substrate
  - packages/runtime/src/errors.ts (top-level): WorkforceIntegrationError
    moves here with the { provider, operation, cause, retryable } shape
    sage/cloud already use. Old clients/errors.ts is removed; the public
    surface re-exports it from the same package import path so existing
    consumers (mcp-workforce) keep compiling.
  - packages/runtime/src/clients/request.ts: shared VFS helpers
    (readJsonFile, readTextFile, listJsonFiles, listDirectoryEntries,
    writeJsonFile + atomic write-then-rename) with mount-root path
    validation and optional writeback-receipt polling.

Clients
  - github.ts is rewritten as a VFS client. Same GithubClient interface
    (comment, createIssue, upsertIssue, getPr, postReview); each method
    now reads/writes files at canonical paths under
    `/github/repos/<owner>/<repo>/...`.
  - linear, slack, notion, jira ship as new typed clients with the same
    pattern. IntegrationClients in types.ts now types all five concretely
    instead of leaving four as unknown.

Tests
  - github.test.ts is rewritten end-to-end against a tempdir mount.
  - linear/slack/notion/jira tests run against tempdir mounts too.
  - 29 runtime tests pass (up from 18), 386 across the repo.

Example
  - weekly-digest/agent.ts drops the WORKFORCE_INTEGRATION_GITHUB_TOKEN
    plumbing; the github client picks up RELAYFILE_MOUNT_ROOT instead.
  - weekly-digest/README.md documents the writeback model + Relayfile
    mount env requirement, and drops the GITHUB_TOKEN setup step.

Notes
  - mcp-workforce (PR #91) imports createGithubClient with a different
    construction shape today (`{ token }`); it'll need a follow-up
    commit to switch to IntegrationClientOptions once this lands. The
    MCP package depends on the new shape, not the old.
  - The direct-REST github implementation that shipped in #90 is
    replaced wholesale. No persona today depends on it; weekly-digest
    is updated in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* (rebase PR #92 onto post-Track-D main)

Track E1: Track E1 — rebase #92 (feat/integrations-vfs)

See workforce/docs/plans/deploy-v1-schema-cascade-spec.md

* fix(review): address CodeRabbit comments on PR #92

- github.upsertIssue update: preserve number/state/html_url/url so the
  next call still finds the canonical issue file by number.
- github.getPr: use the discovered pull directory segment verbatim
  instead of re-encoding it (avoids double-escaping slug paths like
  `123__fix%2Fci`).
- request.waitForReceipt: short-circuit in fire-and-forget mode
  (timeoutMs <= 0) before reading the just-written draft, so a draft
  payload carrying top-level id/path/created is never reinterpreted as
  a writeback receipt.
- jira.transition: validate that the transition id is non-empty after
  trimming, throwing a non-retryable WorkforceIntegrationError.
- notion.createPage: throw WorkforceIntegrationError instead of a
  generic Error when parent.database_id is missing, matching the rest
  of the integration error contract.
- weekly-digest README: move RELAYFILE_MOUNT_ROOT in front of `node`
  (was scoped only to `echo`) and add a prerequisite note that real
  GitHub writes require the Relayfile writeback worker to be running.

* feat(examples): add review-agent + linear-shipper (Relayfile-VFS clients) (#93)

* feat(examples): add review-agent + linear-shipper examples (VFS clients)

Ports the two example agents from the closed codex/deploy-v1-pr branch
to the Relayfile-VFS integration-client style introduced in #92.

review-agent
  - GitHub PR opened: pulls the diff via ctx.github.getPr, runs the
    persona's harness on the diff body, posts a review via
    ctx.github.postReview.
  - @mention in an issue/review comment: harness with the comment
    thread as context, posts the reply via ctx.github.comment.
  - check_run.completed (failure): harness with the failed CI logs as
    context, proposes a fix in a comment.
  - Slack app_mention: conversational reply via ctx.slack.

linear-shipper
  - Linear issue created: clones the target repo into the sandbox,
    runs ctx.harness.run on the issue body, opens a draft PR via
    ctx.github, comments back on the Linear issue with the PR link.
  - Headless (no traits in the persona); demonstrates the paraglide
    "Linear issue → ship" pattern.

Both examples adapt to the WorkforceProviderEvent shape — they read
the raw provider payload from event.payload rather than treating the
event as the payload itself.

Tests: typecheck clean across the workspace and against
examples/tsconfig.json (which path-maps @agentworkforce/runtime to
the workspace source).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(examples/linear-shipper): read event.payload, not event itself

Same shape mismatch I fixed in review-agent: the agent was reading
event.issue as if event were the raw Linear webhook body, but
WorkforceProviderEvent.payload is where the provider payload lives.
Without this fix, every linear.issue.created delivery to the
shipper failed at the "Linear event is missing an issue id" guard
because issueRef was always undefined.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(examples/linear-shipper): use a valid PERSONA_INTENT

persona-kit's parser rejects unknown intents. "implementation" is in
PERSONA_TAGS, not PERSONA_INTENTS, so the persona failed at
parsePersonaSpec(...) with `persona[implementation].intent is invalid`
before deploy could do anything. Swap to `implement-frontend` — the
closest valid intent. Not a perfect domain match (the shipper isn't
frontend-specific) but accurate enough to demonstrate the pattern;
users will customize per their own routing taxonomy.

Verified end-to-end: `workforce deploy ./examples/linear-shipper/persona.json --dry-run`
now exits 0 with "persona linear-shipper: 2 integration(s)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(persona-kit): reject removed deploy v1 persona keys

* (rebase PR #93 — strip traits/sandbox from examples)

Track E2: Track E2 — rebase #93 (feat/integrations-vfs-examples)

See workforce/docs/plans/deploy-v1-schema-cascade-spec.md

* fix(examples/linear-shipper): honor env-var overrides in inputDefault

inputDefault previously only returned the static persona JSON default,
silently ignoring REPO_URL / GITHUB_OWNER / GITHUB_REPO env vars that
the README instructs users to set. Mirror the precedence in
resolvePersonaInputs (packages/persona-kit/src/inputs.ts): env var
(spec.env ?? name) wins over spec.default.

Addresses devin-ai-integration review comment on PR #93.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ricky Schema Cascade <ricky@agent-relay.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ricky Schema Cascade <ricky@agent-relay.com>
Base automatically changed from feat/integrations-vfs to main May 13, 2026 08:17
Ricky Schema Cascade added 2 commits May 13, 2026 10:26
# Conflicts:
#	examples/weekly-digest/README.md
#	packages/runtime/src/clients/github.test.ts
#	packages/runtime/src/clients/github.ts
#	packages/runtime/src/clients/jira.ts
#	packages/runtime/src/clients/notion.ts
#	packages/runtime/src/clients/request.ts
#	packages/runtime/src/types.ts
#	pnpm-lock.yaml
@khaliqgant khaliqgant merged commit 6e3678f into main May 13, 2026
0 of 2 checks passed
@khaliqgant khaliqgant deleted the feat/mcp-workforce branch May 13, 2026 08:27
khaliqgant pushed a commit that referenced this pull request May 13, 2026
mcp-workforce was landed in #91 against an older `PersonaMemoryScope`
shape (`session | user | workspace | org | object`). #94 then tightened
the type to `workspace | user | global`. Both PRs passed CI
independently, but main is now broken at build time because the zod enum
in `server.ts` and the runtime `VALID_SCOPES` Set in `tools/memory.ts`
still reference the removed literals.

Aligning both call sites to the canonical persona-kit shape:

  - `MEMORY_SCOPE_ENUM` → z.enum(['workspace', 'user', 'global'])
  - `VALID_SCOPES`      → new Set(['workspace', 'user', 'global'])
  - memory.save tool description updated to match

The default scope stays `workspace`. Callers that previously passed
`'session'`/`'org'`/`'object'` will now get a validation error from the
zod schema before the runtime check — preferable to silently mapping
them to a different scope.

Verified: `pnpm -F @agentworkforce/mcp-workforce typecheck` + `build` +
`test` (23/23) all pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
khaliqgant added a commit that referenced this pull request May 13, 2026
…p-workforce build (#104)

* chore(publish): add runtime + deploy + mcp-workforce to the publish allow-list

The publish.yml allow-list was last updated when the workspace had 5
packages (persona-kit, workload-router, cli, daytona-runner,
agentworkforce). The deploy-v1 cascade shipped 3 more under
@agentworkforce/* that the existing cli already depends on:

  - @agentworkforce/runtime       (consumed by deploy, mcp-workforce)
  - @agentworkforce/deploy        (consumed by cli)
  - @agentworkforce/mcp-workforce (consumed by harness CLIs via MCP)

cli@3.0.1 already declares `@agentworkforce/deploy@0.0.0` as a runtime
dep, but deploy was never published — the 0.0.0 on npm is a placeholder,
so `npm i agentworkforce` today pulls a stub for `workforce deploy`. The
same applies to deploy/mcp-workforce's runtime dep.

This change preserves lockstep umbrella semantics and orders the publish
in topological order (runtime before deploy/mcp-workforce, deploy before
cli, cli before agentworkforce).

personas-core stays on publish-personas.yml as before — not added here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(mcp-workforce): align memory scope enum with PersonaMemoryScope

mcp-workforce was landed in #91 against an older `PersonaMemoryScope`
shape (`session | user | workspace | org | object`). #94 then tightened
the type to `workspace | user | global`. Both PRs passed CI
independently, but main is now broken at build time because the zod enum
in `server.ts` and the runtime `VALID_SCOPES` Set in `tools/memory.ts`
still reference the removed literals.

Aligning both call sites to the canonical persona-kit shape:

  - `MEMORY_SCOPE_ENUM` → z.enum(['workspace', 'user', 'global'])
  - `VALID_SCOPES`      → new Set(['workspace', 'user', 'global'])
  - memory.save tool description updated to match

The default scope stays `workspace`. Callers that previously passed
`'session'`/`'org'`/`'object'` will now get a validation error from the
zod schema before the runtime check — preferable to silently mapping
them to a different scope.

Verified: `pnpm -F @agentworkforce/mcp-workforce typecheck` + `build` +
`test` (23/23) all pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(publish): sync release-notes packageOrder with expanded allow-list

CodeRabbit + Devin both flagged that the `packageOrder` array used to
sort release-note entries was not updated alongside the publish
allow-list, so `runtime` / `deploy` / `mcp-workforce` would have
`indexOf === -1` and sort first (or in an arbitrary order depending on
the sort impl).

Mirror the topological order from "Resolve target packages":
  persona-kit → runtime → workload-router → deploy → mcp-workforce
    → daytona-runner → cli → agentworkforce

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(examples): linear-shipper read spec metadata from inputSpecs

`ctx.persona.inputs` is `Record<string, string>` (resolved values) on
WorkforcePersonaContext; the raw `PersonaInputSpec` with `.env` and
`.default` lives at `ctx.persona.inputSpecs`. The earlier env-var-
precedence patch on PR #93 read .env / .default off `.inputs`, which
only worked while the type was loose. The post-cascade readonly tightening
exposed the bug at typecheck time and broke the examples typecheck job.

Also fold the runtime-resolved value into the fallback chain so we
prefer env > resolved > spec.default — matching `resolvePersonaInputs`.

Verified: `pnpm run typecheck` + `pnpm run typecheck:examples` both
clean after rebuilding @agentworkforce/deploy dist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(persona-kit): make integration-source fixtures pass the schema test

`emit-schema.test` failed on main because PR #97 (IntegrationConfig.source
discriminator) added three new fixtures but they were missing two
schema-required fields (`onEvent` for cloud personas, `skills`) and the
test's hardcoded expected-filename list wasn't updated.

Three small fixes:
  1. `emit-schema.test.ts`: add the three new fixture names to the
     expected-filenames deepEqual.
  2. `integration-source-{deployer,workspace,service-account}.json`:
     add `"onEvent": "./agent.ts"` and `"skills": []` to each, matching
     the pattern used in `full.json` / `cron-only.json`.

The fixtures still exercise their intended IntegrationSource shapes
(no-source default-inject, explicit `workspace`, explicit
`workspace_service_account`) — only the cross-cutting required-for-cloud
fields were added.

Verified: `pnpm -F @agentworkforce/persona-kit test` → 162/162 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Ricky Schema Cascade <ricky@agent-relay.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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