Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .agents/skills/orchestrating-agent-relay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,7 @@ available via SDK subscriptions):
#### When you coordinate across nodes rather than only the local broker, capabilities

```bash
# Enable fleet nodes for the workspace FIRST — it is off by default, and a
# node you bring up before enabling will not register/list
agent-relay fleet enable
agent-relay fleet config # inspect workspace fleet config
# Fleet nodes need no per-workspace enablement.
agent-relay fleet status # local broker status + this node's provider attachment

# Bring this node up, serving its node definition (advertises its capabilities).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,6 @@ describe('feature manifest contract', () => {
'relay fleet nodes',
'relay fleet spawn',
'relay fleet release',
'relay fleet config',
'relay fleet enable',
'relay fleet disable',
'relay fleet inherit',
'relay fleet status',
// Mounted product surfaces. Each product's real tree lives in its own SDK
// spec, so the manifest documents the group and the surface owns the rest.
Expand Down
28 changes: 0 additions & 28 deletions .agentworkforce/features/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -731,34 +731,6 @@ categories:
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-config
name: Fleet Config
cli: relay fleet config
description: Show the workspace fleet-node configuration
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-enable
name: Enable Fleet
cli: relay fleet enable
description: Enable fleet nodes for the workspace (modifies cloud state)
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-disable
name: Disable Fleet
cli: relay fleet disable
description: Disable fleet nodes for the workspace (modifies cloud state)
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-inherit
name: Inherit Fleet Config
cli: relay fleet inherit
description: Use deployment defaults for fleet node configuration (modifies cloud state)
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-status
name: Fleet Node Status
cli: relay fleet status
Expand Down
10 changes: 4 additions & 6 deletions .agentworkforce/features/verify/procedures.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,19 +329,17 @@ The CLI persists the daemon PID and log path in its isolated local worker state,

## fleet-management

**Features:** `fleet-nodes`, `fleet-nodes-pretty`, `fleet-config`, `fleet-enable`, `fleet-disable`, `fleet-inherit`, `fleet-status`.
**Features:** `fleet-nodes`, `fleet-nodes-pretty`, `fleet-status`.

**Prerequisites:** disposable workspace; `fleet-status` additionally benefits from a local broker.
**Prerequisites:** workspace credentials; `fleet-status` additionally benefits from a local broker.

```bash
relay fleet nodes | jq -e '.nodes'
relay fleet nodes list --pretty | grep -Eiq 'NODE|No fleet nodes found'
BEFORE="$(relay fleet config)"
relay fleet enable; relay fleet config | jq -e 'type == "object"'
relay fleet disable; relay fleet inherit; relay fleet status | jq -e '.broker'
relay fleet status | jq -e '.broker'
```

Snapshot and restore configuration or discard the workspace. For full two-node dispatch/enrollment coverage, run `npm run test:e2e` with `tests/e2e/fleet/README.md` prerequisites.
For full two-node dispatch/enrollment coverage, run `npm run test:e2e` with `tests/e2e/fleet/README.md` prerequisites.

## workspace-management

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"id": "compact_7kbovjgptr28",
"version": 1,
"type": "compacted",
"compactedAt": "2026-09-22T18:39:39.513Z",
"sourceTrajectories": [
"traj_xjrrec3f7hzx"
],
"dateRange": {
"start": "2026-09-22T18:32:56.391Z",
"end": "2026-09-22T18:39:20.897Z"
},
"summary": {
"totalDecisions": 2,
"totalEvents": 2,
"uniqueAgents": [
"default"
]
},
"decisionGroups": [
{
"category": "api",
"decisions": [
{
"question": "Keep hidden exit-0 compatibility commands and retain their cleanroom coverage",
"chosen": "Keep hidden exit-0 compatibility commands and retain their cleanroom coverage",
"reasoning": "Reviewed plan Option A preserves shipped scripts without invoking the removed API.",
"fromTrajectory": "traj_xjrrec3f7hzx"
}
]
},
{
"category": "testing",
"decisions": [
{
"question": "Regenerate the full CLI inventory and reconcile existing option drift",
"chosen": "Regenerate the full CLI inventory and reconcile existing option drift",
"reasoning": "The live inventory includes new release/spawn options and removes obsolete node-agent options; matrix coverage must match the generated inventory exactly.",
"fromTrajectory": "traj_xjrrec3f7hzx"
}
]
}
],
"keyLearnings": [],
"keyFindings": [],
"filesAffected": [],
"commits": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Trajectory Compaction: Sep 22, 2026 - Sep 22, 2026

## Summary
- Sessions: 1
- Decisions: 2
- Events: 2
- Agents: default
- Files: 0
- Commits: 0

## Api
- Keep hidden exit-0 compatibility commands and retain their cleanroom coverage -> Keep hidden exit-0 compatibility commands and retain their cleanroom coverage (traj_xjrrec3f7hzx)

## Testing
- Regenerate the full CLI inventory and reconcile existing option drift -> Regenerate the full CLI inventory and reconcile existing option drift (traj_xjrrec3f7hzx)

## Key Learnings
- None

## Key Findings
- None
5 changes: 1 addition & 4 deletions .claude/skills/orchestrating-agent-relay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,10 +512,7 @@ When you coordinate across nodes rather than only the local broker, capabilities
and placement come into play:

```bash
# Enable fleet nodes for the workspace FIRST — it is off by default, and a
# node you bring up before enabling will not register/list
agent-relay fleet enable
agent-relay fleet config # inspect workspace fleet config
# Fleet nodes need no per-workspace enablement.
agent-relay fleet status # local broker status + this node's provider attachment

# Bring this node up, serving its node definition (advertises its capabilities).
Expand Down
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,31 @@ All notable changes to Agent Relay will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased - Minor]
## [Unreleased - Major]

### Changed

- Targeted `fleet spawn` now waits for harness readiness; the broker releases workers that miss its 90-second readiness window. Confirmed targeted spawns require `--confirm-timeout` of at least 95000ms.

### Removed

- `@agent-relay/sdk`: removed `workspace.fleetNodes` and `RelayWorkspaceFleetNodesConfig`, whose underlying service API no longer exists.

### Fixed

- `agent-relay fleet config|enable|disable|inherit` now exit successfully as hidden compatibility no-ops instead of failing on the removed workspace rollout API.
- Targeted `fleet spawn` requests explicit readiness proof, preventing healthy launches from being rejected for missing proof; unconfirmed launches report `ready:false` while obsolete handlers remain rejected.
- `@agent-relay/sdk` `placement.spawn` only asks a node to verify readiness when it will wait for the answer, and confirms against the contract it requested, so `confirm` omitted no longer arms a 90-second readiness kill switch and `verifyReady: false` no longer fails a healthy launch.
- Broker `manual_flush` recovery now replays a missing cumulative-ACK predecessor without duplicating an already-completed PTY injection, restores it ahead of parked successors, and reports the head/ACK/received sequence gap plus the reconciliation action in `message flush` and `message auto` results.

### Breaking Changes

- SDK consumers must remove references to `workspace.fleetNodes` and `RelayWorkspaceFleetNodesConfig`.

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.

🟡 Breaking SDK ships as patch

RelayWorkspace loses fleetNodes while [Unreleased - Patch] remains the pending release level. A 12.4.x release breaks consumers that accept patch upgrades.

Learn more

The SDK package is versioned independently as a public package. This change removes members from its exported RelayWorkspace and RelayMessagingClient interfaces, so existing consumers can no longer compile against the new declaration files. The repository rule requires [Unreleased - Major] for a SemVer-major change and forbids lowering the pending level.

Example: A consumer declares "@agent-relay/sdk": "^12.4.1" and calls relay.workspace.fleetNodes.get(). Installing a 12.4.2 release containing this change removes that member despite the compatible patch range.

Recommended fix: Raise the root changelog heading to [Unreleased - Major] and release @agent-relay/sdk with a major version. If a patch release is required, retain a deprecated typed compatibility surface until the next major release.

Devin Review


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


### Migration Guidance

- Fleet nodes need no per-workspace enablement. Remove `fleet enable` from provisioning scripts; `fleet disable` no longer disables nodes.

## [12.4.1] - 2026-09-22

### Added
Expand Down
8 changes: 4 additions & 4 deletions flows/verify/features.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1010,10 +1010,10 @@ skip_check "cloud enroll" "requires interactive browser auth"
gated_check cloud "fleet nodes" "relay fleet nodes" "."
gated_check cloud "fleet nodes list --pretty" \
"relay fleet nodes list --pretty | grep -E '^(NODE|No fleet nodes found\.)'" "."
gated_check cloud "fleet config" "relay fleet config" "."
gated_check cloud "fleet inherit" "relay fleet inherit --help" "Usage"
skip_check "fleet enable" "mutates workspace cloud state"
skip_check "fleet disable" "mutates workspace cloud state"
run_check "fleet config" "relay fleet config" "deprecated"
run_check "fleet inherit" "relay fleet inherit 2>&1" "no-op"
run_check "fleet enable" "relay fleet enable 2>&1" "no-op"
run_check "fleet disable" "relay fleet disable 2>&1" "no-op"

# fleet spawn/release were undocumented in the manifest until this change and
# are still unverified: spawning burns provider credits on a remote node.
Expand Down
4 changes: 2 additions & 2 deletions flows/verify/fleet-daytona.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
* The flag is still accepted on Node 24 and 26, so one invocation works
* everywhere.
*
* Workspace-wide enable/disable/inherit probes are safety-skipped unless the
* active workspace is disposable and VERIFY_FLEET_DISPOSABLE_WORKSPACE=1.
* Legacy fleet config/enable/disable/inherit probes verify deprecated no-ops.
* VERIFY_FLEET_DISPOSABLE_WORKSPACE=1 still requires a clean baseline.
*/

import { randomBytes } from 'node:crypto';
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/cli/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,8 @@ const expectedLeafCommands = [
'flows',
// `session` (singular) is the hidden alias of `sessions`, so it is absent here.
'sessions',
// fleet (serve is a hidden error stub, filtered out below)
// fleet (serve is a hidden error stub; config/enable/disable/inherit are hidden no-ops)
'fleet agent list',
'fleet config',
'fleet disable',
'fleet enable',
'fleet inherit',
'fleet nodes list',
'fleet release',
'fleet spawn',
Expand Down
108 changes: 57 additions & 51 deletions packages/cli/src/cli/commands/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,58 +109,64 @@ describe('spawn lifecycle receipts', () => {
});

describe('fleet command support', () => {
it.each([
['config', 'get', undefined],
['enable', 'set', true],
['disable', 'set', false],
['inherit', 'inherit', undefined],
] as const)('fleet %s delegates to workspace fleet node config API', async (command, method, value) => {
const fleetNodes = {
get: vi.fn(async () => ({ enabled: false, defaultEnabled: false, override: null })),
set: vi.fn(async (enabled: boolean) => ({ enabled, defaultEnabled: false, override: enabled })),
inherit: vi.fn(async () => ({ enabled: false, defaultEnabled: false, override: null })),
};
const createWorkspaceRelay = vi.fn(() => ({ workspace: { fleetNodes } }));
const logs: string[] = [];
const program = new Command();
program.exitOverride();
registerFleetCommands(program, {
resolveSandboxRepository: () => undefined,
sdk: {
createAgentRelay: vi.fn() as never,
createWorkspaceRelay: createWorkspaceRelay as never,
createWorkspace: vi.fn() as never,
log: (message: unknown) => logs.push(String(message)),
error: vi.fn(),
exit: vi.fn(() => {
throw new Error('__exit__');
}) as never,
},
log: () => undefined,
warn: () => undefined,
error: () => undefined,
});

await program.parseAsync(
['fleet', command, '--workspace-key', 'rk_live_test', '--base-url', 'https://relay.example'],
{ from: 'user' }
);

expect(createWorkspaceRelay).toHaveBeenCalledWith({
workspaceKey: 'rk_live_test',
token: undefined,
baseUrl: 'https://relay.example',
});
if (method === 'set') {
expect(fleetNodes.set).toHaveBeenCalledWith(value);
} else {
expect(fleetNodes[method]).toHaveBeenCalledTimes(1);
it.each(['config', 'enable', 'disable', 'inherit'])(
'fleet %s is a hidden no-op without workspace access',
async (command) => {
const createWorkspaceRelay = vi.fn(() => {
throw new Error('workspace access forbidden');
});
const createFleetWorkspaceClient = vi.fn(() => {
throw new Error('fleet access forbidden');
});
const logs: string[] = [];
const errors: string[] = [];
const exit = vi.fn();
const program = new Command();
program.exitOverride();
registerFleetCommands(program, {
createFleetWorkspaceClient: createFleetWorkspaceClient as never,
sdk: {
createAgentRelay: vi.fn() as never,
createWorkspaceRelay: createWorkspaceRelay as never,
createWorkspace: vi.fn() as never,
log: (message: unknown) => logs.push(String(message)),
error: vi.fn(),
exit: exit as never,
},
log: () => undefined,
warn: () => undefined,
error: (message: string) => errors.push(message),
exit: exit as never,
});
// Both credential-free fresh workspaces and older scripts with SDK options work.
for (const options of [[], ['--workspace-key', 'rk_live_test', '--base-url', 'http://127.0.0.1:1']]) {
await program.parseAsync(['fleet', command, ...options], { from: 'user' });
}
expect(createWorkspaceRelay).not.toHaveBeenCalled();
expect(createFleetWorkspaceClient).not.toHaveBeenCalled();
expect(exit).not.toHaveBeenCalled();
expect(errors.join('\n')).toContain(
'Fleet nodes need no per-workspace enablement; this command is a no-op.'
);
expect(errors.join('\n')).not.toContain('requires @relaycast/sdk');
const group = program.commands.find((entry) => entry.name() === 'fleet')!;
expect(
(group.commands.find((entry) => entry.name() === command) as Command & { _hidden: boolean })._hidden
).toBe(true);
expect(group.helpInformation()).not.toMatch(new RegExp(`\\b${command}\\b`));
if (command === 'config') {
expect(logs).toHaveLength(2);
for (const log of logs)
expect(JSON.parse(log)).toEqual({
command: 'fleet config',
status: 'deprecated',
effect: 'none',
message: 'Fleet nodes need no per-workspace enablement; this command is a no-op.',
});
} else expect(logs).toEqual([]);
if (command === 'disable') expect(errors.join('\n')).toContain('Fleet nodes have not been disabled.');
}
expect(JSON.parse(logs[0]!)).toMatchObject({
enabled: method === 'set' ? value : false,
defaultEnabled: false,
});
});
);

it('fleet nodes accepts --wk as an alias for --workspace-key', async () => {
const nodes = { list: vi.fn(async () => []) };
Expand Down
Loading
Loading