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
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ describe('feature manifest contract', () => {
'relay skills add',
'relay mcp',
'relay fleet nodes',
'relay fleet spawn',
'relay fleet release',
'relay fleet config',
'relay fleet enable',
'relay fleet disable',
Expand Down
130 changes: 127 additions & 3 deletions .agentworkforce/features/manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version: '1.1'
updated: '2026-07-20'
updated: '2026-07-30'

# Every user-facing feature in this repo, categorized and scored.
#
Expand Down Expand Up @@ -33,6 +33,8 @@ verification:
local-workflows: local-workflow-lifecycle
cloud: cloud-workflows
cloud-workers: cloud-workers
cloud-integrations: integrations-and-webhooks
cloud-rooms: cloud-workflows
fleet: fleet-management
workspace: workspace-management
skills: skills-installation
Expand Down Expand Up @@ -124,6 +126,20 @@ categories:
location: packages/cli/src/cli/commands/agent.ts
verify_tier: 3

- id: agent-me
name: Show Current Agent Identity
cli: relay agent me
description: Show the identity the current agent token resolves to
location: packages/cli/src/cli/commands/agent.ts
verify_tier: 3

- id: agent-presence
name: List Agent Presence
cli: relay agent presence
description: List visible agent presence in the workspace
location: packages/cli/src/cli/commands/agent.ts
verify_tier: 3

- id: agent-remove
name: Remove Agent
cli: relay agent remove <name>
Expand Down Expand Up @@ -554,6 +570,100 @@ categories:
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

cloud-integrations:
name: Cloud Integrations
description: Connect third-party providers to the workspace through Cloud
criticality: standard
features:
- id: cloud-integration-catalog
name: Integration Catalog
cli: relay cloud integration catalog [--static] [--json]
description: Discover static and dynamic (Nango, Composio) Cloud integrations
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-integration-connect
name: Connect Integration
cli: relay cloud integration connect <provider> [--json]
description: Create a Cloud connection session for a provider
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 6

- id: cloud-integration-connections
name: List Connections
cli: relay cloud integration connections [--json]
description: List connected workspace integrations
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-integration-disconnect
name: Disconnect Integration
cli: relay cloud integration disconnect <provider> [--json]
description: Disconnect a provider from the workspace
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

cloud-rooms:
name: Cloud Rooms
description: Workspace-scoped multiplayer rooms and their membership
criticality: standard
features:
- id: cloud-room-session
name: Create Room Session
cli: relay cloud room session
description: Create or resume this device's full room-participant session
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-room-revoke-session
name: Revoke Room Session
cli: relay cloud room revoke-session
description: Revoke this member's scoped session for one device
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-room-members
name: List Room Members
cli: relay cloud room members
description: List workspace room members
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-room-remove-member
name: Remove Room Member
cli: relay cloud room remove-member <member-id>
description: Remove a member and revoke their live room access
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-room-invite
name: Invite Room Participant
cli: relay cloud room invite
description: Invite a full participant to a workspace room
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-room-invites
name: List Room Invitations
cli: relay cloud room invites
description: List workspace room invitations
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

- id: cloud-room-accept
name: Accept Room Invitation
cli: relay cloud room accept
description: Accept an email-bound room invitation
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 6

- id: cloud-room-revoke-invite
name: Revoke Room Invitation
cli: relay cloud room revoke-invite <invite-id>
description: Revoke an unused workspace room invitation
location: packages/cli/src/cli/commands/cloud.ts
verify_tier: 5

cloud-workers:
name: Cloud Worker Nodes
description: Register and manage machines as cloud worker nodes
Expand Down Expand Up @@ -599,6 +709,20 @@ categories:
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-spawn
name: Spawn Fleet Agent
cli: relay fleet spawn <cli> [--name <name>] [--task <text>] [--node <name>] [--persona <persona>] [--model <model>]
description: Spawn a worker agent onto a live fleet node, by explicit node or by persona placement
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-release
name: Release Fleet Agent
cli: relay fleet release <name> [--reason <reason>] [--delete-agent]
description: Release a spawned fleet agent, optionally deleting the agent identity
location: packages/cli/src/cli/commands/fleet.ts
verify_tier: 5

- id: fleet-config
name: Fleet Config
cli: relay fleet config
Expand Down Expand Up @@ -721,8 +845,8 @@ categories:

- id: webhook-create
name: Create Webhook
cli: relay integration webhook create <url> [--event <event>]
description: Register an outbound webhook
cli: relay integration webhook create <channel> [--name <name>]
description: Register an inbound webhook that external services POST to, delivering into a channel
location: packages/cli/src/cli/commands/integration.ts
verify_tier: 5

Expand Down
7 changes: 5 additions & 2 deletions .agentworkforce/features/verify/procedures.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,10 @@ Assert the downloaded skill and delete only this disposable project. Do not run
```bash
: "${CAPTURE_URL:?See Externally provisioned fixtures}"
: "${CAPTURE_FETCH_URL:?GET endpoint returning the current run captured requests}"
HOOK="$(RELAY_AGENT_TOKEN="$TOKEN_A" relay integration webhook create "$CAPTURE_URL" --event message.created)"; HOOK_ID="$(jq -er '.id // .webhookId' <<<"$HOOK")"
# `webhook create` is INBOUND: it takes the channel to deliver into and returns
# the URL external services POST to. It does not take a destination URL.
HOOK="$(RELAY_AGENT_TOKEN="$TOKEN_A" relay integration webhook create "$CHANNEL")"; HOOK_ID="$(jq -er '.webhookId // .id' <<<"$HOOK")"
HOOK_URL="$(jq -er '.url' <<<"$HOOK")"; HOOK_TOKEN="$(jq -er '.token' <<<"$HOOK")"
RELAY_AGENT_TOKEN="$TOKEN_A" relay integration webhook trigger "$HOOK_ID" --payload '{"audit":true}'
DELIVERED=false
for _ in $(seq 1 15); do
Expand All @@ -407,7 +410,7 @@ RELAY_AGENT_TOKEN="$TOKEN_A" relay integration webhook list | jq -e --arg id "$H
RELAY_AGENT_TOKEN="$TOKEN_A" relay integration webhook delete "$HOOK_ID"
```

The capture assertion requires the exact parsed payload `{"audit":true}` and a nonempty header whose lowercased name contains `signature`; it runs before deletion. For inbound, create channel/hook, POST the returned URL with token and documented payload, assert message, delete hook. Create/list/get/delete a unique subscription. For Relayfile, `subscribe --no-input`, assert `subscribe --list`, cause provider event and Relay reply, `unsubscribe` with same provider/resource, assert absent. Localhost cannot receive hosted webhooks.
`webhook create` and `create-inbound` are aliases over the same `POST /v1/webhooks`, whose request is `{ channel, name? }` — there is no outbound registration surface, so a capture receiver is only reachable through `webhook trigger`, not by registering `CAPTURE_URL` as a destination. The capture assertion requires the exact parsed payload `{"audit":true}` and a nonempty header whose lowercased name contains `signature`; it runs before deletion. For the inbound path proper, create the channel and hook, POST `$HOOK_URL` with `$HOOK_TOKEN` and the documented payload, assert the message lands in the channel, then delete the hook. Create/list/get/delete a unique subscription. For Relayfile, `subscribe --no-input`, assert `subscribe --list`, cause provider event and Relay reply, `unsubscribe` with same provider/resource, assert absent. Localhost cannot receive hosted webhooks.

## reflex-history

Expand Down
38 changes: 38 additions & 0 deletions .claude/skills/verify-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@ Use when you need to verify that a specific feature or set of features works cor
.agentworkforce/features/verify/procedures.md # step-by-step verification by tier
```

## Automation

```text
workflows/verify-features.ts # runs tiers 1-6 + critical paths 1-6
workflows/audit-feature-manifest.ts # checks the manifest still matches the CLI
scripts/audit-feature-manifest.mjs # the audit itself; run it directly
```

Before trusting any verification result, check two things:

1. **Which CLI ran.** `verify-features.ts` has a `provenance` step that fails
when `relay version` disagrees with the repo's `package.json`. A run against
a stale globally-installed CLI describes that CLI, not your checkout — this
has already produced a full green run plus one bogus "unknown command"
failure against a real command.
2. **What was skipped.** Every check records `pass`, `fail`, or `skip` with a
reason into `.workflow-artifacts/verify-features/checks.jsonl`, and
`verdict.json` is the authoritative result. A SKIP means _not verified_.
Never read a skip as a pass.

Run the manifest audit before adding checks, so you are not writing coverage
against a stale map:

```bash
node scripts/audit-feature-manifest.mjs # human-readable
node scripts/audit-feature-manifest.mjs --json # for tooling
```

Exit 0 = clean, 1 = drift, 2 = the audit itself could not run. Exit 2 is
deliberately distinct: a broken audit must never be read as a clean manifest.

## How to use it

### 1. Read the manifest to find the feature
Expand Down Expand Up @@ -101,6 +132,13 @@ Add or update entries in `manifest.yaml` when:
- A new harness is supported
- A feature is removed or deprecated

Then run `node scripts/audit-feature-manifest.mjs` and confirm `MANIFEST_CLEAN`.
Note that `manifest-contract.test.ts` cannot catch a missing entry for a _new_
command — a new command is absent from both the manifest and that test's
hardcoded expectation list, so it passes. The audit script derives the surface
from `--help` and `tools/list` instead, which is why it is the check that
matters here. Add new commands to both.

Update `critical-paths.md` when:

- A new path becomes foundational to the product
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ __pycache__/
!/workflows/relayauth-integration/
!/workflows/cloud-connect/
!/workflows/verify-features.ts
!/workflows/audit-feature-manifest.ts

# Eval harness JSON reports (generated per run)
tests/integration/broker/evals-reports/
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ 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]
## [Unreleased - Patch]

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the standard [Unreleased] heading.

Unreleased must not embed a release type; reserve SemVer versions for released sections.

Proposed fix
-## [Unreleased - Patch]
+## [Unreleased]

As per coding guidelines, CHANGELOG.md must “maintain the correctly leveled [Unreleased] heading.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## [Unreleased - Patch]
## [Unreleased]
🤖 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 `@CHANGELOG.md` at line 8, Update the changelog’s top-level heading from
“[Unreleased - Patch]” to the standard “[Unreleased]” format, keeping release
type labels reserved for versioned release sections.

Source: Coding guidelines


### Fixed

- `agent-relay integration webhook create` now works. It took a `<url>` argument and sent `{ url, event }`, but `POST /v1/webhooks` accepts `{ channel, name? }` and returns the URL — so every invocation failed with `channel is required`. It now takes `<channel>` with an optional `--name`, matching `create-inbound`, which posts to the same endpoint.
- `@agent-relay/sdk` `RelayCreateWebhookInput` declared a required `url` and an `event`, neither of which the endpoint accepts. It is now `{ channel, name? }`. Code passing `url`/`event` was already failing at runtime.

## [11.3.0] - 2026-07-30

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Command } from 'commander';
import { describe, expect, it, vi } from 'vitest';

import { registerIntegrationCommands, type IntegrationCommandDependencies } from './integration.js';

/**
* `relay integration webhook create` posts to `POST /v1/webhooks`, whose request
* schema is `{ channel, name? }` — the URL is part of the *response*.
*
* The command previously took a `<url>` argument and sent `{ url, event }`, so
* every invocation was rejected with "channel is required" and the required
* channel was never sent at all. A live verification run caught it. These tests
* pin the corrected contract so the argument order cannot silently regress.
*/
function makeProgram() {
const relay = {
integrations: {
webhooks: {
create: vi.fn(async (input: unknown) => ({ webhookId: 'wh_1', ...(input as object) })),
},
},
};
const log = vi.fn();
const error = vi.fn();
const exit = vi.fn();
const program = new Command();
program.exitOverride();
registerIntegrationCommands(program, {
createAgentRelay: () => relay as never,
resolveLocalRelayOptions: async () => ({ workspaceKey: 'rk_live_test' }),
isInteractive: () => false,
log,
error,
exit: exit as never,
} satisfies Partial<IntegrationCommandDependencies> as IntegrationCommandDependencies);
return { program, relay, log, error, exit };
}

describe('integration webhook create', () => {
it('sends the channel the API requires', async () => {
const { program, relay } = makeProgram();

await program.parseAsync(['integration', 'webhook', 'create', 'deploy-status'], {
from: 'user',
});

expect(relay.integrations.webhooks.create).toHaveBeenCalledWith({
channel: 'deploy-status',
name: undefined,
});
});

it('passes an optional name through', async () => {
const { program, relay } = makeProgram();

await program.parseAsync(
['integration', 'webhook', 'create', 'deploy-status', '--name', 'GitHub Alerts'],
{ from: 'user' }
);

expect(relay.integrations.webhooks.create).toHaveBeenCalledWith({
channel: 'deploy-status',
name: 'GitHub Alerts',
});
});

it('never sends url or event, which the endpoint rejects', async () => {
const { program, relay } = makeProgram();

await program.parseAsync(['integration', 'webhook', 'create', 'ops'], { from: 'user' });

const payload = relay.integrations.webhooks.create.mock.calls[0]?.[0] as Record<string, unknown>;
expect(payload).toBeDefined();
expect(payload).not.toHaveProperty('url');
expect(payload).not.toHaveProperty('event');
});
});
12 changes: 6 additions & 6 deletions packages/cli/src/cli/commands/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1844,17 +1844,17 @@ export function registerIntegrationCommands(
addSdkOptions(
webhook
.command('create')
.description('Register a webhook')
.argument('<url>', 'Webhook URL')
.option('--event <event>', 'Event to deliver')
).action(async (url: string, o: Record<string, unknown>) => {
.description('Register an inbound webhook that delivers into a channel (alias of create-inbound)')
.argument('<channel>', 'Target channel the webhook posts into')
.option('--name <name>', 'Human-readable webhook name (e.g. "GitHub Alerts")')
).action(async (channel: string, o: Record<string, unknown>) => {
await runSdk(deps, async () => {
printJson(
deps,
await runIntegrationOperation(deps, o, (relay) =>
relay.integrations.webhooks.create({
url,
event: o.event as string | undefined,
channel,
name: o.name as string | undefined,
})
)
);
Expand Down
Loading
Loading