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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Relay

The approved agent-integration contract is documented in the [decision record](docs/decisions/0002-agent-integration-contracts.md), [MCP tool reference](docs/mcp-tools.md), [CLI reference](docs/cli-reference.md), and [session semantics](docs/session-semantics.md). These are contract-only artifacts: production MCP and CLI task handlers remain downstream work.

Relay is a local task sidecar for human–AI workflows. The current MVP is usable directly through its local web UI: it stores tasks on this computer and exposes a loopback-only HTTP API behind the UI. Production MCP task tools and companion skills are future work tracked separately under issue #2.

## Prerequisites and setup
Expand Down
67 changes: 53 additions & 14 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
# Relay CLI Contract Reference

This document is reserved by issue #19 and is governed by `docs/decisions/0002-agent-integration-contracts.md` and `docs/superpowers/plans/2026-07-26-agent-integration-contracts.md`.

The source-checkout CLI is implemented by issue #22. Issue #19 defines the stable future-facing command shape:
Issue #19 reserves a deterministic, versioned CLI contract. Production command handlers are implemented later; the stable executable surface is one `relay` command:

```text
relay task capture
relay task list
relay task get <id>
relay task find-similar
relay task edit <id>
relay task triage <id>
relay task start <id>
relay task complete <id>
relay task archive <id>
relay session captures
relay mcp
relay ui
relay doctor
relay task ...
relay session ...
```

Agent-facing commands must support deterministic JSON output with schema version `1`. JSON output is authoritative; diagnostics go to stderr. Do not introduce a generic unrestricted status command.
`relay-mcp` may remain as a compatibility entry point, but new integrations target `relay mcp`.

## JSON protocol

Every agent-facing command accepts `--output json`. JSON mode writes one JSON document followed by a newline to stdout; diagnostics are written to stderr. No caller needs to parse decorative output.

```json
{ "schemaVersion": 1, "ok": true, "data": {}, "warnings": [] }
```

Error details are optional and never expose SQL, stacks, secrets, or local paths.

```json
{
"schemaVersion": 1,
"ok": false,
"error": { "code": "VALIDATION_ERROR", "message": "sessionId has an invalid format" }
}
```

| Exit code | Meaning | Error codes |
| --------- | ------------------------------------ | --------------------------- |
| 0 | Success, warnings, or approved no-op | — |
| 1 | Unexpected internal failure | `INTERNAL_ERROR` |
| 2 | Usage or validation failure | `VALIDATION_ERROR` |
| 3 | Task absent | `NOT_FOUND` |
| 4 | Invalid lifecycle operation | `CONFLICT`, `ARCHIVED_TASK` |
| 5 | Storage failure | `STORAGE_ERROR` |

## Commands

| Command | Required arguments | Optional arguments | Result |
| -------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `relay task capture` | `--title`, `--agent`, `--session` | `--description`, `--priority`, `--workspace`, `--source-context`, `--output json` | `{ task, change: { action: "CREATED" } }` and optional warnings |
| `relay task list` | — | repeatable `--status`, `--workspace`, `--limit 1..100`, `--output json` | `{ tasks, count }` |
| `relay task get <id>` | ID | `--output json` | `{ task }` |
| `relay task find-similar` | `--title` | `--workspace`, `--limit 1..5`, `--output json` | `{ candidates }` |
| `relay session captures` | `--session` | `--limit 1..100`, `--output json` | `{ sessionId, tasks, count }` |
| `relay task edit <id>` | ID and an editable field | clear flags and `--output json` | `{ task, change }` |
| `relay task triage <id>` | ID and `--to INBOX`, `ACTIVE`, or `BACKLOG` | `--output json` | `{ task, change }` |
| `relay task start <id>` | ID | `--output json` | `{ task, change }` |
| `relay task complete <id>` | ID | `--output json` | `{ task, change }` |
| `relay task archive <id>` | ID | `--output json` | `{ task, change }` |

Editing accepts existing editable fields only. Clear nullable fields with explicit flags such as `--clear-description`; empty strings and MCP `null` values are rejected rather than treated as clearing requests. A value and its corresponding clear flag cannot be supplied together. `task triage` excludes `IN_PROGRESS`, `DONE`, and `ARCHIVED`, because those transitions have dedicated intent-specific commands.

CLI commands call application services and reuse `src/database/database-config.ts`; they never access SQLite directly. Database path precedence is explicit command/injected path, non-blank `RELAY_DB_PATH`, then the platform default. The working directory is never production storage configuration.
2 changes: 1 addition & 1 deletion docs/decisions/0002-agent-integration-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Status

Proposed for review under GitHub issue #19.
Accepted for implementation under GitHub issue #19.

## Context

Expand Down
57 changes: 43 additions & 14 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
# Relay MCP Tool Contracts

This document is reserved by issue #19 and is governed by `docs/decisions/0002-agent-integration-contracts.md` and `docs/superpowers/plans/2026-07-26-agent-integration-contracts.md`.
Issue #19 defines this version `1` contract only. It does not implement production MCP task handlers. Every tool returns structured `{ schemaVersion: 1, data, warnings }`; errors use `VALIDATION_ERROR`, `NOT_FOUND`, `CONFLICT`, `ARCHIVED_TASK`, `STORAGE_ERROR`, or `INTERNAL_ERROR` without stack traces, SQLite details, secrets, or local paths. Compact text is a compatibility supplement, never a parsing requirement.

Production MCP tools are implemented by issues #20 and #21. Issue #19 defines the approved names, versioning, error model, task representation, session semantics, and result envelopes before those handlers are written.
## `task_capture`

Canonical tool names:
Input: required `title`, `createdByName`, and `sessionId`; optional `description`, `priority`, `workspace`, and `sourceContext`. The adapter—not the caller—sets `createdByType: AGENT` and `status: INBOX`; caller-supplied provenance or status is invalid. Output: `{ task, change: { action: "CREATED" } }`, with optional advisory `POSSIBLE_DUPLICATE` warnings. Capture always succeeds when a duplicate warning is returned.

- `task_capture`
- `task_list`
- `task_get`
- `task_find_similar`
- `session_captures_list`
- `task_edit`
- `task_triage`
- `task_start`
- `task_complete`
- `task_archive`
## `task_list`

Do not add an unrestricted generic task update or status mutation tool.
Input: optional non-empty `statuses`, `workspace`, and `limit` from 1 through 100. Output: `{ tasks, count }`. This is a bounded read and has no lifecycle side effects.

## `task_get`

Input: required non-empty task ID. Output: `{ task }`. A missing ID maps to `NOT_FOUND`.

## `task_find_similar`

Input: required `title`, optional `workspace`, and `limit` from 1 through 5 (default 5). Output: `{ candidates }`, where each candidate carries a task and stable `matchReason`. Matching is bounded, normalized-title based, non-archived, and advisory; it never merges or changes tasks.

## `session_captures_list`

Input: required valid `sessionId` and `limit` from 1 through 100. Output: `{ sessionId, tasks, count }`. It selects only agent-created tasks with an exact persisted ID, includes completed and archived tasks, and orders by `createdAt ASC, id ASC`.

## `task_edit`

Input: task ID plus one or more editable task fields or explicit clear flags. MCP `null` is rejected; explicit `clear*` flags are the only clear operation, and a value cannot accompany its matching flag. Output: `{ task, change }`, including `NO_CHANGE` for an approved no-op. `sessionId`, provenance, status, and lifecycle timestamps are never editable.

## `task_triage`

Input: task ID and target `INBOX`, `ACTIVE`, or `BACKLOG`. Output: `{ task, change }`. `IN_PROGRESS`, `DONE`, and `ARCHIVED` have their own intent-specific tools.

## `task_start`

Input: task ID. Output: `{ task, change }`. It performs only the focused start lifecycle operation.

## `task_complete`

Input: task ID. Output: `{ task, change }`. It performs only the focused completion lifecycle operation.

## `task_archive`

Input: task ID. Output: `{ task, change }`. It performs only the focused archive lifecycle operation.

## Mutation safety and versioning

Invoke `task_edit`, `task_triage`, `task_start`, `task_complete`, and `task_archive` only after explicit user direction in the active conversation. Relay validates data and lifecycle legality but cannot authenticate conversational intent under the OS-user trust boundary, so it intentionally has no fake `confirmed`, `requestedBy`, or copied-user-text field.

There is no `task_update`, `task_set_status`, generic CRUD mutation, or unrestricted lifecycle command. Tool names are not version-prefixed; a breaking change requires a new integer schema version and an explicit compatibility decision. Later issues implement every handler through shared application services; MCP never reads SQLite directly.
31 changes: 17 additions & 14 deletions docs/session-semantics.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
# Relay Session Semantics

Issue #19 defines `sessionId` as an opaque caller-generated identifier stored as task metadata.
`sessionId` is opaque caller-generated metadata on agent-created tasks. It is not a session table, aggregate, timer, or authentication mechanism.

- No session table is introduced in Epic #2.
- Agent capture requires a valid session ID.
- MCP and CLI callers reuse the same ID for captures and final review.
- Concurrent sessions use distinct IDs.
- Session review selects agent-created tasks whose persisted `sessionId` exactly matches the requested ID.
- Completed and archived tasks remain visible in session review.
- Results are ordered by `createdAt ASC, id ASC`.
- Session completion is never inferred through timers, inactivity, or process lifetime.
## Identifier rules

Validation:
- MCP clients and CLI callers generate IDs in the same namespace.
- Trim surrounding whitespace before validation.
- A valid ID has 1–128 ASCII letters, digits, `.`, `_`, `:`, or `-`.
- Agent capture and session-capture retrieval require a valid ID; malformed or missing input is `VALIDATION_ERROR`.
- Human tasks may have `sessionId: null`.

- trim surrounding whitespace
- length 1–128 characters
- allowed characters: ASCII letters, digits, `.`, `_`, `:`, `-`
An agent reuses the same identifier while capturing and reviewing work in one active session. Different concurrent agents or shells use different IDs, so their capture groups remain isolated. Completion is initiated by the agent or user and is never persisted or inferred from process exit, timers, or inactivity.

The production task-model, migration, repository, and application changes are implemented downstream under issue #20.
## Deterministic capture membership

“Captured during this session” means a task was originally created with `createdByType = AGENT` and its persisted `sessionId` exactly equals the requested identifier. The query uses persisted metadata, not timestamps or process lifetime.

Session review includes captures in every lifecycle state, including `DONE` and `ARCHIVED`, and sorts them by `createdAt ASC, id ASC`.

## Downstream implementation boundary

Issue #19 documents `sessionId` in the external task representation and validates contract input only. Issue #20 adds the nullable domain/persistence field, migration, repository mapping, agent-capture application input, and session-capture query support. No production session storage or query handler is introduced here.
40 changes: 20 additions & 20 deletions docs/superpowers/plans/2026-07-26-agent-integration-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ The contract is versioned independently from the Relay package version using int

### 2.1 Canonical capabilities

| Capability | MCP tool | CLI command |
|---|---|---|
| Capture | `task_capture` | `relay task capture` |
| List | `task_list` | `relay task list` |
| Get | `task_get` | `relay task get <id>` |
| Find similar | `task_find_similar` | `relay task find-similar` |
| Session captures | `session_captures_list` | `relay session captures` |
| Edit | `task_edit` | `relay task edit <id>` |
| Triage | `task_triage` | `relay task triage <id>` |
| Start | `task_start` | `relay task start <id>` |
| Complete | `task_complete` | `relay task complete <id>` |
| Archive | `task_archive` | `relay task archive <id>` |
| Capability | MCP tool | CLI command |
| ---------------- | ----------------------- | -------------------------- |
| Capture | `task_capture` | `relay task capture` |
| List | `task_list` | `relay task list` |
| Get | `task_get` | `relay task get <id>` |
| Find similar | `task_find_similar` | `relay task find-similar` |
| Session captures | `session_captures_list` | `relay session captures` |
| Edit | `task_edit` | `relay task edit <id>` |
| Triage | `task_triage` | `relay task triage <id>` |
| Start | `task_start` | `relay task start <id>` |
| Complete | `task_complete` | `relay task complete <id>` |
| Archive | `task_archive` | `relay task archive <id>` |

Do not expose generic CRUD or unrestricted status mutation.

Expand Down Expand Up @@ -220,14 +220,14 @@ Codes:

CLI exit codes:

| Exit | Meaning |
|---|---|
| `0` | success, including warnings or approved no-op |
| `1` | unexpected internal error |
| `2` | command usage or validation error |
| `3` | task not found |
| `4` | lifecycle conflict or archived-task restriction |
| `5` | database/storage failure |
| Exit | Meaning |
| ---- | ----------------------------------------------- |
| `0` | success, including warnings or approved no-op |
| `1` | unexpected internal error |
| `2` | command usage or validation error |
| `3` | task not found |
| `4` | lifecycle conflict or archived-task restriction |
| `5` | database/storage failure |

Error JSON carries the precise code; do not create an exit code for every domain error.

Expand Down
16 changes: 16 additions & 0 deletions scripts/validate-repository-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,27 @@ export function validateRepositoryAssets(options: ValidateRepositoryAssetsOption
'eslint.config.js',
'package.json',
'README.md',
'docs/decisions/0002-agent-integration-contracts.md',
'docs/mcp-tools.md',
'docs/cli-reference.md',
'docs/session-semantics.md',
'tests/fixtures/contracts/capture-success.json',
'tests/fixtures/contracts/capture-duplicate-warning.json',
'tests/fixtures/contracts/validation-error.json',
'tests/fixtures/contracts/not-found-error.json',
'tests/fixtures/contracts/transition-conflict-error.json',
'tests/fixtures/contracts/storage-error.json',
'tsconfig.base.json',
'src/application/health/get-health.ts',
'src/database/connection.ts',
'src/interfaces/mcp/create-mcp-server.ts',
'src/interfaces/http/create-http-server.ts',
'src/interfaces/contracts/contract-version.ts',
'src/interfaces/contracts/error-contract.ts',
'src/interfaces/contracts/json-value-contract.ts',
'src/interfaces/contracts/session-contract.ts',
'src/interfaces/contracts/task-contract.ts',
'src/interfaces/contracts/warning-contract.ts',
'web/src/App.tsx',
];

Expand Down
1 change: 1 addition & 0 deletions src/interfaces/contracts/contract-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CONTRACT_SCHEMA_VERSION = 1;
34 changes: 34 additions & 0 deletions src/interfaces/contracts/error-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from 'zod';
import { jsonValueSchema } from './json-value-contract.js';

export const CONTRACT_ERROR_CODES = [
'VALIDATION_ERROR',
'NOT_FOUND',
'CONFLICT',
'ARCHIVED_TASK',
'STORAGE_ERROR',
'INTERNAL_ERROR',
] as const;

export type ContractErrorCode = (typeof CONTRACT_ERROR_CODES)[number];

export const contractErrorSchema = z
.object({
code: z.enum(CONTRACT_ERROR_CODES),
message: z.string().min(1),
details: z.record(z.string(), jsonValueSchema).optional(),
})
.strict();

const EXIT_CODES: Record<ContractErrorCode, number> = {
VALIDATION_ERROR: 2,
NOT_FOUND: 3,
CONFLICT: 4,
ARCHIVED_TASK: 4,
STORAGE_ERROR: 5,
INTERNAL_ERROR: 1,
};

export function errorCodeToExitCode(code: ContractErrorCode): number {
return EXIT_CODES[code];
}
15 changes: 15 additions & 0 deletions src/interfaces/contracts/json-value-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from 'zod';

export type JsonValue =
null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };

export const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
z.union([
z.null(),
z.boolean(),
z.number().finite(),
z.string(),
z.array(jsonValueSchema),
z.record(z.string(), jsonValueSchema),
]),
);
21 changes: 21 additions & 0 deletions src/interfaces/contracts/session-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from 'zod';

export const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;

export const sessionIdSchema = z
.string()
.trim()
.min(1, 'sessionId is required')
.max(128, 'sessionId must be at most 128 characters')
.regex(SESSION_ID_PATTERN, 'sessionId has an invalid format');

export const sessionCapturesInputSchema = z
.object({
sessionId: sessionIdSchema,
limit: z.number().int().min(1).max(100).default(100),
})
.strict();

export function parseSessionId(value: unknown): string {
return sessionIdSchema.parse(value);
}
Loading
Loading