feat(sdk): applySiblingLinks — link sibling-repo packages during workflow setup#776
feat(sdk): applySiblingLinks — link sibling-repo packages during workflow setup#776khaliqgant merged 3 commits intomainfrom
Conversation
Adds a generic, language-agnostic helper that workflow authors can
chain into their setup to link sibling-repo packages into the
workflow's working directory. Solves the "agents fabricate interfaces
when the installed package is stale" class of bugs by making the
real, head-of-main interface visible during the workflow run.
Usage:
import { workflow, applySiblingLinks } from '@agent-relay/sdk/workflows';
const wf = applySiblingLinks(workflow('my-feature').pattern('dag'), {
dependsOn: ['install-deps'],
links: [
{
name: '@agent-assistant/proactive',
path: '../agent-assistant/packages/proactive',
expect: ['recordSignal', 'drainSignals'],
},
],
});
Detection auto-dispatches per manifest:
- package.json -> 'npm link' (symlinks, no committed manifest change)
- pyproject.toml / setup.py / setup.cfg -> 'pip install -e' or 'uv pip install -e'
Post-link, when \`expect\` is provided, smoke-tests the linked package
via \`node --input-type=module -e\` (npm) or \`python3 -c\` (python). Any
missing named export fails the step.
MVP scope:
- npm + python (pip / uv). Auto-detect.
- Fail-fast on missing sibling path, unknown manifest, or missing
expected export.
- Other languages (Go, Cargo, Gradle, Bundler, .NET) land as
follow-up dispatch branches behind the same public API.
12 tests covering script generation, manifest branches, quote escaping,
expect-list JSON round-trip, and the builder integration.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aad13197ab
ℹ️ 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".
| ' if command -v uv >/dev/null 2>&1; then', | ||
| ' uv pip install -e "$SIBLING_PATH" --quiet', |
There was a problem hiding this comment.
Fall back to pip when uv cannot install outside a venv
The Python branch always prefers uv when it is present, but uv pip install -e fails in environments without an active virtualenv unless --system is provided; I verified this behavior with uv pip install pip --dry-run (No virtual environment found ... pass --system). Because this code runs under set -e, that failure exits immediately and never reaches the pip/pip3 fallback, so sibling-link setup fails on hosts that have uv installed globally but rely on plain pip workflows.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 8f17750. Added --system to the uv invocation AND wrapped it in if command -v uv && uv pip install --system ...; then : so a uv failure (in any environment) falls through to pip/pip3 via the elif chain rather than exiting under set -e. New test case covers the wrapping pattern.
- Python verify: backslashes inside f-string expressions are a SyntaxError on Python < 3.12. Bind `sep = ","` outside the f-string and reference it from inside (Devin, 🔴 critical — the verifier failed every run previously). - uv dispatch: uv refuses to install outside a venv without --system. Pass --system explicitly AND wrap the uv attempt in an `if ...; then :` so failure falls through to pip/pip3 via the elif chain rather than exiting under `set -e` (Codex, P1 — broken on hosts with uv installed globally but no active venv). - Echo interpolation: the initial script echoed `link.name` / `link.path` into a double-quoted shell string via raw template interpolation, so a name containing `\"`, `$`, or backticks would break quoting or trigger command substitution. Move the echo after the SIBLING_NAME / SIBLING_PATH assignments (which use JSON-encoded safe literals) and reference the shell vars (Devin, 🟡). 3 new tests covering each fix; existing test updated for the --system flag. 15/15 green.
…stitution Follow-up to the earlier shell-injection fix on #776. Moving the echo after the variable assignments was necessary but not sufficient — the assignments themselves used JSON.stringify (shJsonString) which produces DOUBLE-quoted bash literals. Double-quoted strings in bash still expand \$VAR, \$(cmd), and \`cmd\`, so a value like \`../path\$(evil)\` still triggered command substitution on assignment. Switch all three assignment sites (SIBLING_PATH, SIBLING_NAME, EXPECT) to single-quoted bash literals via shSingleQuote, which are literal end-to- end. Embedded single quotes are handled by the existing '\\'' POSIX idiom. The EXPECT JSON payload passes through bash untouched and downstream Node/Python JSON.parse it back. Removes the now-unused shJsonString helper. Updates two tests to assert the single-quoted form; adds one more test covering the embedded-single- quote escape path. 16/16 tests green.
…tion
Nav changes:
- 'Basics' becomes 'Primitives' and groups related pages semantically:
* Message (expandable): Channels, DMs, Threads, Emoji reactions
* File (was 'File sharing')
* Auth (expandable): Permissions
* Schedule (was 'Scheduling')
- 'Spawning an agent' + 'Event handlers' move up into Getting Started
- Workflows promoted from a single bullet under Advanced to a top-level
section with 7 pages: Introduction, Quickstart, Builder API,
Patterns, Setup helpers, Common mistakes, Run from CLI
- CLI's 'Run workflows' folds into the Workflows section
- Advanced now only contains Cloud + Workforce
Nav renderer:
- NavItem gains optional children: NavItem[] for nested sub-items
- DocsNav.tsx renders children as an indented sub-list with left border
- getAllDocSlugs() walks the tree for static-generation coverage
New pages (stubbed with real content, not placeholders):
- workflows-introduction.mdx: landing, mental model, when to use
- workflows-quickstart.mdx: end-to-end 5-min example
- workflows-patterns.mdx: 80-to-100, lead+workers, multi-file edit,
cross-repo, supervisor, parallelism/waves
- workflows-setup-helpers.mdx: per-repo setup helper convention +
applySiblingLinks (landing in relay SDK #776)
- workflows-common-mistakes.mdx: the table every workflow author
needs once, ported from the writing-agent-relay-workflows skill
Content porting is from the two private Claude-Code skills
(writing-agent-relay-workflows and relay-80-100-workflow) so this
content is now public + addressable by URL.
DRAFT PR. Open questions for review:
- Should 'Permissions' be under Auth or its own primitive?
- 'Sending messages' acts as the Message landing — separate page or
umbrella?
- Should there be an 'Agent' primitive (for Spawning + Event handlers)
rather than putting them in Getting Started?
Summary
Adds a generic, language-agnostic helper
applySiblingLinksto@agent-relay/sdk/workflowsthat workflow authors chain into their setup. When a workflow consumes a package that lives in a sibling repo on disk (common in our monorepo-of-repos setup), linking redirects the package at dev-time to the sibling's current build output.Why
In a recent sage wire-up workflow (
AgentWorkforce/sage#105), agents writing code against@agent-assistant/proactivehit a version mismatch:npm installresolved an older published version that didn't have the exports the workflow needed. Rather than stopping, agents reached fordeclare module "@agent-assistant/proactive" { ... }augmentations and ~200 lines of fallback implementations to keep their code compiling. Tests type-checked against the fiction, the 80-to-100 gate passed, the PR shipped code that couldn't integrate with the real package.Root cause: agents only see
node_modulesat workflow-run time, not what the sibling repo actually exposes on disk. Preflight export checks (option A) and prompt rules forbidding augmentation (option B) catch symptoms. Linking removes the wall entirely — agents see the head-of-main interface, no fabrication needed.Public API
Auto-detects the sibling's manifest and dispatches:
package.jsonnpm link(symlinks into node_modules)pyproject.toml/setup.py/setup.cfguv pip install -eorpip install -eWhen
expectis provided, the step runs a post-link smoke import (node --input-type=module -efor npm,python3 -cfor python) and fails if any named export is missing. That's the final guardrail: even if the link command succeeded, if the real interface doesn't match what the workflow expects, we fail before an agent writes a single line.MVP scope
replacedirective, Cargo.cargo/config.toml [patch], GradleincludeBuild, Bundlerbundle config local, .NETProjectReference) land as new branches of the per-link dispatch behind the sameapplySiblingLinksAPI. None require public-API changes.Test plan
bash -c '...'wrapper, expect-list JSON survival through env var round-trip, builder integration, custom step name / dependsOnapplySageRepoSetuponce this SDK version publishes)Follow-ups (not this PR)
writing-agent-relay-workflowsskill rule with a new Rule 10: "workflows consuming a sibling-repo package MUST declare it viaapplySiblingLinks"applySageRepoSetup+applyAgentAssistantRepoSetupto use this