Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `agent-relay skills add` installs the `/orchestrate` skill (from `agentrelay.com/skill.md`) into your coding harnesses. An interactive TUI asks whether to install for the current project or globally and which harnesses to target (Claude Code, Codex, Cursor, Gemini, OpenCode); `--global`/`--local`, `--harness <ids>`, and `--all` flags drive it non-interactively.
- `agent-relay up --verbose` now prints step-by-step startup progress (port resolution, broker process spawn, handshake retries, fleet sidecar, node-delivery wait, agent spawns) and streams the broker's own startup-phase logs and stderr live, instead of only surfacing a terse error if startup fails.
- `agent-relay integration subscribe` now wires a server-side inbound bridge: it provisions a relaycast inbound-target and a relayfile-cloud webhook subscription so real provider messages (Slack/GitHub/Linear) are injected into the target relay channel — and delivered to on-node agents — with no local watcher or client process running.
- `agent-relay cloud enroll --token <ocl_node_enr_…>` redeems a one-time Cloud enrollment token and persists node credentials to `~/.agentworkforce/relay/fleet-enrollments.json` (0600); a later plain `agent-relay node up` then runs as the Cloud-managed node. The token is never printed.
- `agent-relay node up|down|status|metrics|tail`, `node agent …`, and `node workflow run|logs|sync` unify `local up` and `fleet serve` under one command group. `node up [--config <file>]` auto-discovers and serves a `defineNode(...)` file (`agent-relay.{ts,tsx,mts,cts,js,mjs,cjs}`) in the project root and picks up persisted Cloud enrollment credentials; with no config it runs the implicit local node from teams.json.
- `@agent-relay/fleet` now publishes the node runtime — `serveNode(options)` / `startServeNode(options)` returning `RunningNode { stop(), done }`, plus `FleetTriggerSyncClient`, `buildNodeSupervision`, and `readFleetSidecarStatus` — so one package both authors and serves a node. Fleet no longer depends on `@agent-relay/sdk`.
Expand Down
100 changes: 50 additions & 50 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@agent-relay/utils": "9.2.2",
"@modelcontextprotocol/sdk": "^1.0.0",
"@relaycast/sdk": "^5.0.5",
"@relayfile/client": "^0.10.19",
"@relayfile/client": "^0.10.20",
"@relayflows/cli": "^1.0.1",
"@xterm/headless": "^6.0.0",
"commander": "^12.1.0",
Expand Down
42 changes: 36 additions & 6 deletions packages/cli/src/cli/commands/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ vi.mock('../telemetry/index.js', () => ({
track: telemetryMocks.track,
}));

vi.mock('../lib/reflex-capture.js', () => ({
startReflexCapture: vi.fn(() => ({ stop: vi.fn(async () => undefined) })),
}));

vi.mock('@agent-relay/fleet', async (importOriginal) => {
const actual = await importOriginal<typeof import('@agent-relay/fleet')>();
return {
...actual,
startServeNode: vi.fn(() => ({ stop: vi.fn(async () => undefined), done: Promise.resolve() })),
};
});

beforeEach(() => {
sdkStatusClient.getStatus.mockReset();
sdkStatusClient.getStatus.mockResolvedValue({ agent_count: 0, pending_delivery_count: 0 });
Expand Down Expand Up @@ -110,6 +122,7 @@ function createHarness(options?: {
killImpl?: CoreDependencies['killProcess'];
nowImpl?: CoreDependencies['now'];
sleepImpl?: CoreDependencies['sleep'];
holdOpen?: CoreDependencies['holdOpen'];
execPath?: string;
cliScript?: string;
argv?: string[];
Expand All @@ -124,6 +137,8 @@ function createHarness(options?: {
const fs = options?.fs ?? createFsMock();
const relay = options?.relay ?? createRelayMock();
const spawnedProcess = options?.spawnedProcess ?? createSpawnedProcessMock();
const env = options?.env ?? {};
env.AGENT_RELAY_DISABLE_IMPLICIT_FLEET_NODE ??= '1';

const exit = vi.fn((code: number) => {
throw new ExitSignal(code);
Expand All @@ -149,7 +164,7 @@ function createHarness(options?: {
async () => options?.checkForUpdatesResult ?? { updateAvailable: false, latestVersion: '1.2.3' }
) as unknown as CoreDependencies['checkForUpdates'],
getVersion: vi.fn(() => '1.2.3'),
env: options?.env ?? {},
env,
argv: options?.argv ?? ['node', '/tmp/agent-relay.js', 'up'],
execPath: options?.execPath ?? '/usr/bin/node',
cliScript: options?.cliScript ?? '/tmp/agent-relay.js',
Expand All @@ -158,7 +173,7 @@ function createHarness(options?: {
isPortInUse: vi.fn(async () => false),
sleep: options?.sleepImpl ?? vi.fn(async () => undefined),
onSignal: vi.fn(() => undefined),
holdOpen: vi.fn(async () => undefined),
holdOpen: options?.holdOpen ?? vi.fn(async () => undefined),
log: vi.fn(() => undefined),
error: vi.fn(() => undefined),
warn: vi.fn(() => undefined),
Expand Down Expand Up @@ -312,6 +327,7 @@ describe('registerCoreCommands', () => {
});

it('up refuses auto-spawn when node delivery is down', async () => {
let now = 0;
const relay = createRelayMock({
getStatus: vi.fn(async () => ({
agent_count: 0,
Expand All @@ -327,7 +343,11 @@ describe('registerCoreCommands', () => {
autoSpawn: true,
agents: [{ name: 'WorkerA', cli: 'codex', task: 'Ship tests' }],
},
nowImpl: vi.fn().mockReturnValueOnce(0).mockReturnValueOnce(10_000).mockReturnValue(10_000),
nowImpl: vi.fn(() => {
const current = now;
now += 10_000;
return current;
}),
});

const exitCode = await runCommand(program, ['up']);
Expand Down Expand Up @@ -751,9 +771,19 @@ describe('registerCoreCommands', () => {
shutdown: vi.fn(() => new Promise(() => undefined)),
});

const { program, deps } = createHarness({ relay });
const exitCode = await runCommand(program, ['up']);
expect(exitCode).toBeUndefined();
const { program, deps } = createHarness({
relay,
holdOpen: vi.fn(() => new Promise(() => undefined)),
});
void runCommand(program, ['up']);

for (
let i = 0;
i < 10 && (deps.onSignal as unknown as { mock: { calls: unknown[][] } }).mock.calls.length === 0;
i += 1
) {
await Promise.resolve();
}

const onSignalMock = deps.onSignal as unknown as { mock: { calls: unknown[][] } };
const sigintHandler = onSignalMock.mock.calls.find((call) => call[0] === 'SIGINT')?.[1] as
Expand Down
Loading
Loading