diff --git a/packages/persona-kit/package.json b/packages/persona-kit/package.json
index bd1e2b88..dfd7a8ee 100644
--- a/packages/persona-kit/package.json
+++ b/packages/persona-kit/package.json
@@ -1,6 +1,6 @@
{
"name": "@agentworkforce/persona-kit",
- "version": "3.0.40",
+ "version": "3.0.41",
"private": false,
"type": "module",
"main": "dist/index.js",
@@ -38,6 +38,7 @@
},
"dependencies": {
"@relayfile/adapter-core": "^0.3.26",
+ "@relayfile/adapter-linear": "^0.3.8",
"@relayfile/local-mount": "^0.7.24"
},
"devDependencies": {
diff --git a/packages/persona-kit/src/define.test.ts b/packages/persona-kit/src/define.test.ts
index 3e3a7fb8..4addb54f 100644
--- a/packages/persona-kit/src/define.test.ts
+++ b/packages/persona-kit/src/define.test.ts
@@ -61,7 +61,12 @@ test('TypedTriggerMap gives per-provider event autocomplete; arbitrary providers
{ on: 'pull_request.opened' },
{ on: 'off_registry.github_event' }
],
- linear: [{ on: 'issue.create' }],
+ linear: [
+ { on: 'issue.create' },
+ { on: 'AgentSessionEvent.created' },
+ { on: 'AgentSessionEvent.prompted' },
+ { on: 'AppUserNotification.issueCommentMention' }
+ ],
slack: [{ on: 'message.created' }],
customProvider: [{ on: 'custom.event' }]
};
diff --git a/packages/persona-kit/src/define.ts b/packages/persona-kit/src/define.ts
index 038c276d..b4ec0ce4 100644
--- a/packages/persona-kit/src/define.ts
+++ b/packages/persona-kit/src/define.ts
@@ -14,9 +14,10 @@ import type {
import type { KnownProviderName, KnownTriggerName } from './triggers.js';
import type { ScopeKey, ScopeKeyProvider } from './scope-keys.js';
import type { KnownPersonaTag } from './constants.js';
+import type { LinearAgentWebhookEvent } from '@relayfile/adapter-linear/types';
export type TriggerNameFor
= P extends KnownProviderName
- ? KnownTriggerName
| (string & {})
+ ? (P extends 'linear' ? KnownTriggerName
| LinearAgentWebhookEvent : KnownTriggerName
) | (string & {})
: string;
export interface TypedTrigger
{
diff --git a/packages/persona-kit/src/triggers.test.ts b/packages/persona-kit/src/triggers.test.ts
index a7043b82..390506cb 100644
--- a/packages/persona-kit/src/triggers.test.ts
+++ b/packages/persona-kit/src/triggers.test.ts
@@ -31,7 +31,12 @@ test('lintTriggers returns no issues for known providers and known triggers', ()
{ on: 'pull_request.opened' },
{ on: 'pull_request_review_comment.created' }
],
- linear: [{ on: 'issue.create' }],
+ linear: [
+ { on: 'issue.create' },
+ { on: 'AgentSessionEvent.created' },
+ { on: 'AgentSessionEvent.prompted' },
+ { on: 'AppUserNotification.issueCommentMention' }
+ ],
slack: [{ on: 'message.created' }]
})
);
diff --git a/packages/persona-kit/src/triggers.ts b/packages/persona-kit/src/triggers.ts
index 0c050af5..3c2b2e3e 100644
--- a/packages/persona-kit/src/triggers.ts
+++ b/packages/persona-kit/src/triggers.ts
@@ -1,5 +1,6 @@
import type { AgentSpec } from './types.js';
import { KNOWN_TRIGGER_CATALOG } from '@relayfile/adapter-core/triggers';
+import { LINEAR_AGENT_WEBHOOK_EVENTS } from '@relayfile/adapter-linear/types';
/**
* Known event names per Relayfile provider, used by the deploy CLI to lint
@@ -24,6 +25,7 @@ const KNOWN_TRIGGER_ALIAS_CATALOG = Object.fromEntries(
export const KNOWN_TRIGGERS = {
...KNOWN_TRIGGER_CATALOG,
+ linear: mergeKnownTriggers(KNOWN_TRIGGER_CATALOG.linear, LINEAR_AGENT_WEBHOOK_EVENTS),
...KNOWN_TRIGGER_ALIAS_CATALOG
};
@@ -137,3 +139,10 @@ export function lintTriggers(agent: AgentSpec): TriggerLintIssue[] {
function knownTriggersForProvider(provider: string): readonly string[] | undefined {
return (KNOWN_TRIGGERS as Record)[provider];
}
+
+function mergeKnownTriggers(
+ base: readonly string[] | undefined,
+ extra: readonly string[]
+): readonly string[] {
+ return [...new Set([...(base ?? []), ...extra])];
+}
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 3677d116..ae11f5f3 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -1,6 +1,6 @@
{
"name": "@agentworkforce/runtime",
- "version": "3.0.40",
+ "version": "3.0.41",
"private": false,
"type": "module",
"main": "dist/index.js",
diff --git a/packages/runtime/src/define-agent.test.ts b/packages/runtime/src/define-agent.test.ts
index a13b345f..ffea0157 100644
--- a/packages/runtime/src/define-agent.test.ts
+++ b/packages/runtime/src/define-agent.test.ts
@@ -1,7 +1,11 @@
import test from 'node:test';
import assert from 'node:assert/strict';
-import { defineAgent, isWorkforceAgent, isWorkforceHandler } from './index.js';
+import { defineAgent, isWorkforceAgent, isWorkforceHandler, unwrapResourceRecord } from './index.js';
+import type {
+ LinearAgentSessionPayload,
+ LinearAppUserNotificationPayload
+} from './types.js';
test('defineAgent brands the object and wraps the handler', () => {
const agent = defineAgent({
@@ -62,3 +66,80 @@ test('defineAgent narrows the handler event type to declared triggers/schedules'
}
});
});
+
+test('defineAgent narrows Linear agent-session payloads for declared triggers', () => {
+ defineAgent({
+ triggers: {
+ linear: [
+ { on: 'AgentSessionEvent.created' },
+ { on: 'AgentSessionEvent.prompted' },
+ { on: 'AppUserNotification.issueCommentMention' }
+ ]
+ },
+ handler: async (_ctx, event) => {
+ if (event.source !== 'linear') return;
+ if (event.type === 'AgentSessionEvent.created') {
+ const record = unwrapResourceRecord(event.payload);
+ if (!isLinearAgentSessionPayload(record)) return;
+ const sessionId: string = record.agentSession.id;
+ const promptContext: string | undefined = record.promptContext;
+ void sessionId;
+ void promptContext;
+ return;
+ }
+ if (event.type === 'AgentSessionEvent.prompted') {
+ const record = unwrapResourceRecord(event.payload);
+ if (!isLinearAgentSessionPayload(record)) return;
+ const body: string | undefined = record.agentActivity?.body;
+ void body;
+ return;
+ }
+ const record = unwrapResourceRecord(event.payload);
+ if (!isLinearAppUserNotificationPayload(record)) return;
+ const issueId: string | undefined =
+ record.notification?.issue?.id ??
+ record.issue?.id;
+ const commentBody: string | undefined =
+ record.notification?.comment?.body ??
+ record.comment?.body;
+ void issueId;
+ void commentBody;
+ }
+ });
+});
+
+test('unwrapResourceRecord unwraps the real relayfile resource payload container', () => {
+ const record = unwrapResourceRecord<{
+ body?: string;
+ issue_id?: string;
+ issue_identifier?: string;
+ }>({
+ resource: {
+ provider: 'linear',
+ objectType: 'comment',
+ payload: {
+ body: '@agentrelay please implement this.',
+ issue_id: '5d6f2e15-0000-4000-8000-000000000000',
+ issue_identifier: 'AR-70'
+ }
+ }
+ });
+
+ assert.equal(isRecord(record), true);
+ if (!isRecord(record)) return;
+ assert.equal(record.body, '@agentrelay please implement this.');
+ assert.equal(record.issue_id, '5d6f2e15-0000-4000-8000-000000000000');
+ assert.equal(record.issue_identifier, 'AR-70');
+});
+
+function isLinearAgentSessionPayload(value: unknown): value is LinearAgentSessionPayload {
+ return isRecord(value) && isRecord(value.agentSession) && typeof value.agentSession.id === 'string';
+}
+
+function isLinearAppUserNotificationPayload(value: unknown): value is LinearAppUserNotificationPayload {
+ return isRecord(value);
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null;
+}
diff --git a/packages/runtime/src/define-agent.ts b/packages/runtime/src/define-agent.ts
index da872f81..9a2a2185 100644
--- a/packages/runtime/src/define-agent.ts
+++ b/packages/runtime/src/define-agent.ts
@@ -10,6 +10,7 @@ import type {
WorkforceEvent,
WorkforceHandler,
WorkforceHandlerExport,
+ LinearAgentSessionEvent,
WorkforceProviderEvent
} from './types.js';
@@ -35,9 +36,27 @@ type OnLiteralsOf = A extends readonly (infer E)[]
type TriggerOnUnion = OnLiteralsOf>;
+type LinearSpecialEvent = Extract;
+
+type ProviderEventFor = P extends 'linear'
+ ? [LinearSpecialEvent] extends [never]
+ ? Omit & { type: O }
+ : LinearSpecialEvent
+ : Omit & { type: O };
+
+type TriggerProviderEvents = {
+ [P in keyof Tr]: P extends string
+ ? OnLiteralsOf> extends infer O
+ ? O extends string
+ ? ProviderEventFor
+ : never
+ : never
+ : never;
+}[keyof Tr];
+
type NarrowedProviderEvent
= [TriggerOnUnion
] extends [never]
? never
- : Omit & { type: TriggerOnUnion };
+ : TriggerProviderEvents
;
/** Distributive union of every schedule `name` literal. */
type ScheduleNameUnion = S extends readonly (infer E)[]
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index 56973229..4a37cda8 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -44,6 +44,23 @@ export type {
WorkforceProviderEvent
} from './types.js';
+// Runtime envelope helpers shared by provider-triggered agents.
+export {
+ unwrapResourceRecord
+} from './types.js';
+
+export type {
+ LinearAgentActivity,
+ LinearAgentActivityType,
+ LinearAgentSession,
+ LinearAgentSessionEvent,
+ LinearAgentSessionEventPayload,
+ LinearAgentSessionPayload,
+ LinearAppUserNotificationEventPayload,
+ LinearAppUserNotificationPayload,
+ LinearIssueReference
+} from './types.js';
+
// VFS-backed transport helpers. All provider interactions go through these
// — no per-provider client code in the runtime. Handlers use listJsonFiles /
// readJsonFile / writeJsonFile directly against the provider path conventions
diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts
index 1a12a8bc..07445f18 100644
--- a/packages/runtime/src/types.ts
+++ b/packages/runtime/src/types.ts
@@ -56,7 +56,112 @@ export interface WorkforceProviderEvent extends WorkforceEventBase {
};
}
-export type WorkforceEvent = WorkforceCronEvent | WorkforceProviderEvent;
+export function unwrapResourceRecord(payload: unknown): T | unknown {
+ const resource = isRecord(payload) && 'resource' in payload ? payload.resource : payload;
+ return isRecord(resource) && 'payload' in resource ? resource.payload : resource;
+}
+
+export interface LinearIssueReference {
+ id: string;
+ identifier?: string;
+ title?: string;
+ url?: string;
+}
+
+export interface LinearAgentSession {
+ id: string;
+ issue?: LinearIssueReference;
+ [key: string]: unknown;
+}
+
+export type LinearAgentActivityType =
+ | 'action'
+ | 'elicitation'
+ | 'error'
+ | 'response'
+ | 'thought';
+
+export interface LinearAgentActivity {
+ type?: LinearAgentActivityType | string;
+ body?: string;
+ action?: string;
+ parameter?: string;
+ result?: string;
+}
+
+export interface LinearAgentSessionPayload {
+ type: 'AgentSessionEvent';
+ action: 'created' | 'prompted' | string;
+ agentSession: LinearAgentSession;
+ agentActivity?: LinearAgentActivity & {
+ id?: string;
+ agentSessionId?: string;
+ content?: LinearAgentActivity;
+ [key: string]: unknown;
+ };
+ issue?: LinearIssueReference;
+ promptContext?: string;
+ notification?: {
+ issue?: LinearIssueReference;
+ comment?: { id?: string; body?: string; [key: string]: unknown };
+ [key: string]: unknown;
+ };
+ [key: string]: unknown;
+}
+
+export interface LinearAppUserNotificationPayload {
+ type: 'AppUserNotification';
+ action: 'issueCommentMention' | string;
+ issue?: LinearIssueReference;
+ comment?: { id?: string; body?: string; [key: string]: unknown };
+ notification?: {
+ issue?: LinearIssueReference;
+ comment?: { id?: string; body?: string; [key: string]: unknown };
+ [key: string]: unknown;
+ };
+ [key: string]: unknown;
+}
+
+export interface LinearWrappedPayload {
+ resource: {
+ payload: TRecord;
+ [key: string]: unknown;
+ };
+ [key: string]: unknown;
+}
+
+export type LinearAgentSessionEventPayload =
+ LinearWrappedPayload & {
+ agentSession?: LinearAgentSession;
+ agentActivity?: LinearAgentSessionPayload['agentActivity'];
+ issue?: LinearIssueReference;
+ promptContext?: string;
+ };
+
+export type LinearAppUserNotificationEventPayload =
+ LinearWrappedPayload & {
+ issue?: LinearIssueReference;
+ comment?: { id?: string; body?: string; [key: string]: unknown };
+ notification?: LinearAppUserNotificationPayload['notification'];
+ };
+
+export type LinearAgentSessionEvent =
+ | (Omit & {
+ source: 'linear';
+ type: 'AgentSessionEvent.created' | 'AgentSessionEvent.prompted';
+ payload: LinearAgentSessionEventPayload;
+ })
+ | (Omit & {
+ source: 'linear';
+ type: 'AppUserNotification.issueCommentMention';
+ payload: LinearAppUserNotificationEventPayload;
+ });
+
+export type WorkforceEvent = WorkforceCronEvent | LinearAgentSessionEvent | WorkforceProviderEvent;
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null;
+}
/**
* Result of a harness invocation. The runtime translates whatever the
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9d145752..c1fbf008 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -92,6 +92,9 @@ importers:
'@relayfile/adapter-core':
specifier: ^0.3.26
version: 0.3.26(@relayfile/sdk@0.7.40)
+ '@relayfile/adapter-linear':
+ specifier: ^0.3.8
+ version: 0.3.9(@relayfile/sdk@0.7.40)
'@relayfile/local-mount':
specifier: ^0.7.24
version: 0.7.24
@@ -860,6 +863,19 @@ packages:
peerDependencies:
'@relayfile/sdk': '>=0.6.0 <1'
+ '@relayfile/adapter-core@0.3.30':
+ resolution: {integrity: sha512-TW03JUsrb8D0lHFfsoP/H4YiglgRev+E+vZg7m2K7khDOtA5rfpU6LIcl9Wl9WrC22oKUrThzbh195yG2XAfyQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ '@relayfile/sdk': '>=0.6.0 <1'
+
+ '@relayfile/adapter-linear@0.3.9':
+ resolution: {integrity: sha512-O3oTPieQNLdhE4nAnbj+WbUO0iMDlCEVO7OKJAzvrZruNjp1MDfmJ8s+ABR5AAEU7LhUzfMtGCnM2rCcP65kcA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@relayfile/sdk': '>=0.6.0 <1'
+
'@relayfile/core@0.7.40':
resolution: {integrity: sha512-vY48SxZgahnvE0CHDyy/iny17ypnfbX5myVPtocZVNpMiz4dS1iabO70WR+uYVgSiIZR7MpKTPFSs3SxEjQWag==}
engines: {node: '>=18'}
@@ -3117,6 +3133,19 @@ snapshots:
minimatch: 10.2.5
yaml: 2.9.0
+ '@relayfile/adapter-core@0.3.30(@relayfile/sdk@0.7.40)':
+ dependencies:
+ '@relayfile/sdk': 0.7.40
+ '@scalar/postman-to-openapi': 0.6.3
+ cheerio: 1.2.0
+ minimatch: 10.2.5
+ yaml: 2.9.0
+
+ '@relayfile/adapter-linear@0.3.9(@relayfile/sdk@0.7.40)':
+ dependencies:
+ '@relayfile/adapter-core': 0.3.30(@relayfile/sdk@0.7.40)
+ '@relayfile/sdk': 0.7.40
+
'@relayfile/core@0.7.40': {}
'@relayfile/local-mount@0.7.24':