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: 1 addition & 1 deletion packages/persona-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"lint": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@relayfile/adapter-core": "^0.3.44",
"@relayfile/adapter-core": "^0.3.50",
"@relayfile/local-mount": "^0.7.24"
},
"devDependencies": {
Expand Down
6 changes: 5 additions & 1 deletion packages/persona-kit/schemas/persona.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,13 @@
"additionalProperties": {
"type": "string"
}
},
"config": {
"type": "object",
"additionalProperties": {}
}
},
"description": "Per-provider **connection** configuration for a RelayFile provider. The map key is the provider slug (`github`, `linear`, `slack`, `notion`, `jira`). `scope` is provider-specific filter metadata (e.g. `{ repo: \"org/repo\" }` for github, `{ database: \"<id>\" }` for notion).\n\nThis declares only *that the persona connects to the provider* and how the connection resolves — **not** which events fire it. Event triggers live on the agent ( {@link AgentSpec.triggers } ); the deploy CLI requires every provider in `agent.triggers` to also appear here so the connection is set up.\n\n`source` discriminates the cloud-side resolver between `user_integrations` and `workspace_integrations`; defaults to `{ kind: 'deployer_user' }` when omitted so existing personas keep their pre-discriminator behavior."
"description": "Per-provider **connection** configuration for a RelayFile provider. The map key is the provider slug (`github`, `linear`, `slack`, `notion`, `jira`). `scope` is provider-specific filter metadata (e.g. `{ repo: \"org/repo\" }` for github, `{ database: \"<id>\" }` for notion).\n\nThis declares only *that the persona connects to the provider* and how the connection resolves — **not** which events fire it. Event triggers live on the agent ( {@link AgentSpec.triggers } ); the deploy CLI requires every provider in `agent.triggers` to also appear here so the connection is set up.\n\n`source` discriminates the cloud-side resolver between `user_integrations` and `workspace_integrations`; defaults to `{ kind: 'deployer_user' }` when omitted so existing personas keep their pre-discriminator behavior.\n\n`config` is a forward-compatible adapter passthrough. Persona-kit validates only that it is an object; provider adapters own the nested schema (for example GitHub materialization policy)."
},
"IntegrationSource": {
"anyOf": [
Expand Down
59 changes: 59 additions & 0 deletions packages/persona-kit/src/__fixtures__/personas/full.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,65 @@
"github": {
"scope": {
"repo": "AgentWorkforce/workforce"
},
"config": {
"materialization": {
"default": "lazy",
"webhookWritesForLazyRepos": true,
"rules": [
{
"repos": [
"AgentWorkforce/workforce"
],
"resources": [
"issues",
"pulls"
],
"issues": {
"mode": "eager",
"filter": {
"state": "open",
"labels": [
"bug"
]
}
},
"pulls": "eager"
}
]
}
}
},
"gitlab": {
"scope": {
"projectPath": "AgentWorkforce/workforce"
},
"config": {
"materialization": {
"default": "lazy",
"webhookWritesForLazyProjects": true,
"rules": [
{
"projects": [
"AgentWorkforce/workforce"
],
"resources": [
"issues",
"merge_requests"
],
"issues": {
"mode": "eager",
"filter": {
"state": "opened",
"labels": [
"bug"
]
}
},
"merge_requests": "eager"
}
]
}
}
},
"linear": {},
Expand Down
179 changes: 177 additions & 2 deletions packages/persona-kit/src/define.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
KNOWN_SCOPE_KEY_CATALOG,
definePersona,
parsePersonaSpec,
type GitLabMaterializationPolicy,
type GitHubMaterializationPolicy,
type ScopeKeysFor,
type TypedScopeMap,
type TypedTriggerMap
Expand All @@ -23,9 +25,42 @@ test('definePersona returns authored specs that parse successfully', () => {
}
},
// Personas declare integration *connections* only — event triggers moved
// to the agent (defineAgent). Connection config = source + scope.
// to the agent (defineAgent). Connection config = source + scope + adapter config.
integrations: {
github: { scope: { repo: 'AgentWorkforce/workforce' } },
github: {
scope: { repo: 'AgentWorkforce/workforce' },
config: {
materialization: {
default: 'lazy',
webhookWritesForLazyRepos: true,
rules: [
{
repos: ['AgentWorkforce/workforce'],
resources: ['issues', 'pulls'],
issues: { mode: 'eager', filter: { state: 'open', labels: ['bug'] } },
pulls: 'eager'
}
]
}
}
},
gitlab: {
scope: { projectPath: 'AgentWorkforce/workforce' },
config: {
materialization: {
default: 'lazy',
webhookWritesForLazyProjects: true,
rules: [
{
projects: ['AgentWorkforce/workforce'],
resources: ['issues', 'merge_requests'],
issues: { mode: 'eager', filter: { state: 'opened', labels: ['bug'] } },
merge_requests: 'eager'
}
]
}
}
},
linear: {},
slack: {},
confluence: {},
Expand All @@ -46,7 +81,31 @@ test('definePersona returns authored specs that parse successfully', () => {
assert.equal(parsed.skills.length, 0);
assert.equal(parsed.inputs?.TOPIC.default, 'pull requests');
assert.equal(parsed.integrations?.github.scope?.repo, 'AgentWorkforce/workforce');
assert.deepEqual(parsed.integrations?.github.config?.materialization, {
default: 'lazy',
webhookWritesForLazyRepos: true,
rules: [
{
repos: ['AgentWorkforce/workforce'],
resources: ['issues', 'pulls'],
issues: { mode: 'eager', filter: { state: 'open', labels: ['bug'] } },
pulls: 'eager'
}
]
});
assert.equal(parsed.integrations?.customProvider.source?.kind, 'deployer_user');
assert.deepEqual(parsed.integrations?.gitlab.config?.materialization, {
default: 'lazy',
webhookWritesForLazyProjects: true,
rules: [
{
projects: ['AgentWorkforce/workforce'],
resources: ['issues', 'merge_requests'],
issues: { mode: 'eager', filter: { state: 'opened', labels: ['bug'] } },
merge_requests: 'eager'
}
]
});
assert.deepEqual(parsed.capabilities, {
review: true,
conflictAutofix: { enabled: false }
Expand Down Expand Up @@ -75,6 +134,113 @@ test('TypedTriggerMap gives per-provider event autocomplete; arbitrary providers
assert.equal(triggers.customProvider?.[0]?.on, 'custom.event');
});

test('definePersona types github/gitlab materialization config but keeps unknown provider config generic', () => {
const githubMaterialization: GitHubMaterializationPolicy = {
default: 'lazy',
rules: [
{
repos: ['AgentWorkforce/workforce'],
eager: true,
issues: { mode: 'eager', since: '2026-01-01T00:00:00.000Z' },
pulls: { mode: 'lazy', filter: { state: 'all' } }
}
]
};
const gitlabMaterialization: GitLabMaterializationPolicy = {
default: 'lazy',
rules: [
{
projects: ['AgentWorkforce/workforce'],
resources: ['merge_requests', 'issues', 'pipelines', 'commits'],
merge_requests: { mode: 'eager', filter: { state: 'merged' } },
issues: { mode: 'eager', since: '2026-01-01T00:00:00.000Z' },
pipelines: 'lazy',
commits: { mode: 'eager', incremental: true }
}
]
};

const persona = definePersona({
id: 'adapter-config-author',
intent: 'review',
description: 'Adapter config typing fixture.',
integrations: {
github: {
scope: { repo: 'AgentWorkforce/workforce' },
config: { materialization: githubMaterialization }
},
gitlab: {
scope: { projectPath: 'AgentWorkforce/workforce' },
config: { materialization: gitlabMaterialization }
},
customProvider: {
config: { anyFutureAdapterField: { stays: true } }
}
},
onEvent: './agent.ts',
harnessSettings: { reasoning: 'low', timeoutSeconds: 60 }
});

const issuesPolicy = persona.integrations?.github?.config?.materialization?.rules?.[0]?.issues;
assert.equal(
issuesPolicy && typeof issuesPolicy === 'object' ? issuesPolicy.mode : undefined,
'eager'
);
const mergeRequestPolicy = persona.integrations?.gitlab?.config?.materialization?.rules?.[0]?.merge_requests;
assert.equal(
mergeRequestPolicy && typeof mergeRequestPolicy === 'object'
? mergeRequestPolicy.filter?.state
: undefined,
'merged'
);
assert.deepEqual(persona.integrations?.customProvider?.config, {
anyFutureAdapterField: { stays: true }
});

definePersona({
id: 'bad-github-materialization-mode',
intent: 'review',
description: 'GitHub materialization aliases are adapter-runtime inputs, not typed authoring.',
integrations: {
github: {
config: {
materialization: {
// @ts-expect-error persona-kit authoring exposes canonical lazy/eager modes
default: 'all'
}
}
}
},
onEvent: './agent.ts',
harnessSettings: { reasoning: 'low', timeoutSeconds: 60 }
});

definePersona({
id: 'bad-gitlab-materialization-state',
intent: 'review',
description: 'GitLab materialization states are typed separately from GitHub states.',
integrations: {
gitlab: {
config: {
materialization: {
rules: [
{
merge_requests: {
mode: 'eager',
// @ts-expect-error GitLab uses opened/closed/locked/merged/all, not GitHub's open
filter: { state: 'open' }
}
}
]
}
}
}
},
onEvent: './agent.ts',
harnessSettings: { reasoning: 'low', timeoutSeconds: 60 }
});
});

test('TypedScopeMap gives per-provider scope key autocomplete while allowing future keys', () => {
const githubScope: TypedScopeMap<'github'> = {
owner: 'AgentWorkforce',
Expand All @@ -84,20 +250,29 @@ test('TypedScopeMap gives per-provider scope key autocomplete while allowing fut
const customScope: TypedScopeMap<'customProvider'> = {
anyKey: 'any-value'
};
const gitlabScope: TypedScopeMap<'gitlab'> = {
projectPath: 'AgentWorkforce/workforce'
};

const githubKey: ScopeKeysFor<'github'> = 'repo';
const gitlabKey: ScopeKeysFor<'gitlab'> = 'projectPath';
// @ts-expect-error github has no catalogued "channel" scope key
const badGithubKey: ScopeKeysFor<'github'> = 'channel';
// @ts-expect-error gitlab has no catalogued "repo" scope key
const badGitlabKey: ScopeKeysFor<'gitlab'> = 'repo';

// Providers with no catalogued scope keys (slack today) still accept
// arbitrary keys via TypedScopeMap's index signature — no typing regression.
const slackScope: TypedScopeMap<'slack'> = { channel: 'C123' };

assert.equal(githubScope[githubKey], 'workforce');
assert.deepEqual([...KNOWN_SCOPE_KEY_CATALOG.github], ['owner', 'repo']);
assert.equal(gitlabScope[gitlabKey], 'AgentWorkforce/workforce');
assert.deepEqual([...KNOWN_SCOPE_KEY_CATALOG.gitlab], ['projectPath']);
assert.equal(customScope.anyKey, 'any-value');
assert.equal(slackScope.channel, 'C123');
assert.equal(badGithubKey, 'channel');
assert.equal(badGitlabKey, 'repo');
});

test('definePersona types tags against the closed PersonaTag vocabulary', () => {
Expand Down
Loading
Loading