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
33 changes: 26 additions & 7 deletions examples/review-agent/persona.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
{
"id": "review-agent",
"intent": "review",
"tags": ["review"],
"tags": [
"review"
],
"description": "Reviews opened PRs, responds to @mentions in comments, attempts autofix on red CI.",
"cloud": true,
"useSubscription": true,
"integrations": {
"github": {
"triggers": [
{ "on": "pull_request.opened" },
{ "on": "issue_comment.created", "match": "@mention" },
{ "on": "pull_request_review_comment.created", "match": "@mention" },
{ "on": "check_run.completed", "where": "conclusion=failure" }
{
"on": "pull_request.opened"
},
{
"on": "issue_comment.created",
"match": "@mention"
},
{
"on": "pull_request_review_comment.created",
"match": "@mention"
},
{
"on": "check_run.completed",
"where": "conclusion=failure"
}
]
},
"slack": {
"triggers": [{ "on": "app_mention" }]
"triggers": [
{
"on": "app_mention"
}
Comment on lines +17 to +34

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the JSON example aligned with canonical trigger events.

Line 17 and Line 33 still use non-canonical event names. Update to the canonical catalog names for consistency with the rest of this PR.

Suggested patch
-          "on": "issue_comment.created",
+          "on": "pull_request_review_comment.created",
@@
-          "on": "app_mention"
+          "on": "message.created"
🤖 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 `@examples/review-agent/persona.json` around lines 17 - 34, Replace the
non-canonical event names used in the JSON "on" fields: locate the entries where
"on" equals "issue_comment.created" and where Slack trigger uses "app_mention"
in persona.json and update them to the canonical catalog event names used across
this PR (replace "issue_comment.created" with the catalog canonical name for the
issue comment event, and replace "app_mention" with the catalog canonical Slack
app-mention event name) so the "on" values match the canonical trigger names.

]
}
},
"memory": {
"enabled": true,
"scopes": ["workspace"]
"scopes": [
"workspace"
]
},
"onEvent": "./agent.ts",
"harness": "codex",
Expand Down
39 changes: 39 additions & 0 deletions examples/review-agent/persona.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { definePersona } from '@agentworkforce/persona-kit';

export default definePersona({
id: 'review-agent',
intent: 'review',
tags: ['review'],
description:
'Reviews opened PRs, responds to @mentions in comments, attempts autofix on red CI.',
cloud: true,
useSubscription: true,
integrations: {
github: {
triggers: [
{ on: 'pull_request.opened' },
{ on: 'issue_comment.created', match: '@mention' },

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.

P1: Non-canonical event name issue_comment.created is not in the adapter-core catalog. This trigger will either fail validation or silently never fire. Replace it with the canonical event or remove it if line 15's pull_request_review_comment.created already covers the intended case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/review-agent/persona.ts, line 15:

<comment>Non-canonical event name `issue_comment.created` is not in the adapter-core catalog. This trigger will either fail validation or silently never fire. Replace it with the canonical event or remove it if line 15's `pull_request_review_comment.created` already covers the intended case.</comment>

<file context>
@@ -0,0 +1,39 @@
+    github: {
+      triggers: [
+        { on: 'pull_request.opened' },
+        { on: 'issue_comment.created', match: '@mention' },
+        { on: 'pull_request_review_comment.created', match: '@mention' },
+        { on: 'check_run.completed', where: 'conclusion=failure' }
</file context>

{ on: 'pull_request_review_comment.created', match: '@mention' },
{ on: 'check_run.completed', where: 'conclusion=failure' }
]
},
slack: {
triggers: [{ on: 'app_mention' }]
Comment on lines +15 to +21

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use canonical trigger names in the example persona.

Line 15 and Line 21 still use pre-catalog event names (issue_comment.created, app_mention). This example should match the canonical names used elsewhere in this PR to avoid misleading authors.

Suggested patch
-        { on: 'issue_comment.created', match: '`@mention`' },
+        { on: 'pull_request_review_comment.created', match: '`@mention`' },
@@
-      triggers: [{ on: 'app_mention' }]
+      triggers: [{ on: 'message.created' }]
📝 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
{ on: 'issue_comment.created', match: '@mention' },
{ on: 'pull_request_review_comment.created', match: '@mention' },
{ on: 'check_run.completed', where: 'conclusion=failure' }
]
},
slack: {
triggers: [{ on: 'app_mention' }]
{ on: 'pull_request_review_comment.created', match: '`@mention`' },
{ on: 'pull_request_review_comment.created', match: '`@mention`' },
{ on: 'check_run.completed', where: 'conclusion=failure' }
]
},
slack: {
triggers: [{ on: 'message.created' }]
🤖 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 `@examples/review-agent/persona.ts` around lines 15 - 21, The persona uses
pre-catalog trigger names; update the triggers arrays to use the canonical event
names used elsewhere in this PR: replace "issue_comment.created" with
"issue.commented" and "pull_request_review_comment.created" with
"pull_request_review.commented" in the top-level triggers array, and replace the
slack trigger "app_mention" with the canonical slack trigger name (e.g.,
"slack.app_mention") in the slack.triggers entry so the example matches the
canonical naming used across the codebase.

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.

P1: Non-canonical event name app_mention is not in the Slack adapter catalog. This trigger will either fail validation or silently never fire. Use the canonical event name message.created.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/review-agent/persona.ts, line 21:

<comment>Non-canonical event name `app_mention` is not in the Slack adapter catalog. This trigger will either fail validation or silently never fire. Use the canonical event name `message.created`.</comment>

<file context>
@@ -0,0 +1,39 @@
+      ]
+    },
+    slack: {
+      triggers: [{ on: 'app_mention' }]
+    }
+  },
</file context>

}
},
memory: {
enabled: true,
scopes: ['workspace']
},
onEvent: './agent.ts',
harness: 'codex',
model: 'gpt-5.4',
systemPrompt:
'Review pull requests for correctness, regression risk, security concerns, and missing tests. Be concise and concrete.',
harnessSettings: {
reasoning: 'medium',
timeoutSeconds: 1200,
sandboxMode: 'workspace-write',
workspaceWriteNetworkAccess: true
}
});
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@agentworkforce/workload-router": "workspace:*",
"@relayburn/sdk": "^2.5.2",
"@relayfile/local-mount": "^0.7.24",
"esbuild": "^0.25.0",
"ora": "^9.4.0"
},
"repository": {
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import {
type PersonaSource
} from './local-personas.js';
import { installPersonas, type PersonaInstallResult } from './persona-install.js';
import { runPersonaCompileCommand } from './persona-compile.js';
import { pickPersona, type PickCandidate, type PickResult } from './persona-picker.js';
import { recordRecent, loadRecents, runPersonaPickerTui, type TuiCandidate } from './persona-tui.js';

Expand Down Expand Up @@ -191,6 +192,9 @@ Commands:
including which cascade layer defined it (cwd, user,
dir:<n>, library). Flags:
--json emit the resolved PersonaSpec as JSON
persona compile <path/to/persona.ts>
Compile a typed persona.ts authoring file to sibling
persona.json after validating it with persona-kit.
install [flags] <pkg|path>
Copy persona JSON files from an npm package or local
package directory into
Expand Down Expand Up @@ -4268,6 +4272,15 @@ export async function main(): Promise<void> {
runShow(rest);
}

if (subcommand === 'persona') {
try {
await runPersonaCompileCommand(rest);
return;
} catch (err) {
die((err as Error)?.message ?? String(err), false);
}
}

if (subcommand === 'install') {
runPersonaInstall(rest);
}
Expand Down
93 changes: 93 additions & 0 deletions packages/cli/src/persona-compile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { compilePersonaFile } from './persona-compile.js';

test('compilePersonaFile bundles persona.ts, validates it, and writes persona.json', async () => {
const root = mkdtempSync(join(tmpdir(), 'aw-persona-compile-'));
try {
const inputPath = join(root, 'persona.ts');
const outputPath = join(root, 'persona.json');
writeFileSync(
inputPath,
`import { definePersona } from '@agentworkforce/persona-kit';

export default definePersona({
id: 'compiled-persona',
intent: 'review',
description: 'Compiled persona fixture.',
inputs: {
TARGET: 'repo'
},
integrations: {
github: {
triggers: [
{ on: 'pull_request.opened' },
{ on: 'off_registry.github_event' }
]
},
linear: { triggers: [{ on: 'issue.updated' }] },
slack: { triggers: [{ on: 'message.channels' }] },
notion: { triggers: [{ on: 'page.created' }] },
jira: { triggers: [{ on: 'issue.created' }] },
unknown: { triggers: [{ on: 'whatever.happened' }] }
},
onEvent: './agent.ts',
harnessSettings: {
reasoning: 'medium',
timeoutSeconds: 60
}
});
`,
'utf8'
);

const result = await compilePersonaFile(inputPath);
const compiled = JSON.parse(readFileSync(outputPath, 'utf8')) as {
id: string;
inputs?: Record<string, unknown>;
integrations?: Record<string, { triggers?: Array<{ on: string }> }>;
};

assert.equal(result.personaId, 'compiled-persona');
assert.equal(result.outputPath, outputPath);
assert.equal(compiled.id, 'compiled-persona');
assert.equal(compiled.inputs?.TARGET, 'repo');
assert.equal(
compiled.integrations?.github.triggers?.[1].on,
'off_registry.github_event'
);
assert.equal(compiled.integrations?.unknown.triggers?.[0].on, 'whatever.happened');
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test('compilePersonaFile fails loudly when validation rejects the authored spec', async () => {
const root = mkdtempSync(join(tmpdir(), 'aw-persona-compile-invalid-'));
try {
const inputPath = join(root, 'persona.ts');
writeFileSync(
inputPath,
`export default {
id: 'bad',
intent: 'review',
description: 'Bad persona fixture.',
harnessSettings: { reasoning: 'turbo', timeoutSeconds: 60 },
onEvent: './agent.ts'
};
`,
'utf8'
);

await assert.rejects(
() => compilePersonaFile(inputPath),
/harnessSettings\.reasoning must be low\|medium\|high/
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
117 changes: 117 additions & 0 deletions packages/cli/src/persona-compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { builtinModules } from 'node:module';
import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { isIntent, isObject, parsePersonaSpec } from '@agentworkforce/persona-kit';
import { build } from 'esbuild';

export interface PersonaCompileResult {
inputPath: string;
outputPath: string;
personaId: string;
}

const NODE_EXTERNALS = [
...builtinModules,
...builtinModules.map((name) => `node:${name}`),
'node:*'
];

export async function compilePersonaFile(
inputPath: string,
outputPath?: string
): Promise<PersonaCompileResult> {
const absInput = resolve(inputPath);
await assertReadableFile(absInput, 'persona compile input');

const absOutput = resolve(outputPath ?? join(dirname(absInput), 'persona.json'));
const tempDir = await mkdtemp(join(tmpdir(), 'agentworkforce-persona-compile-'));
const bundledPath = join(tempDir, `${basename(absInput).replace(/\W+/g, '-')}.mjs`);

try {
await build({
entryPoints: [absInput],
outfile: bundledPath,
bundle: true,
format: 'esm',
platform: 'node',
target: 'node20',
sourcemap: 'inline',
logLevel: 'silent',
external: NODE_EXTERNALS,
resolveExtensions: ['.ts', '.mts', '.cts', '.tsx', '.js', '.mjs', '.cjs', '.jsx', '.json'],
nodePaths: packageNodePaths()
});

const mod = await import(pathToFileURL(bundledPath).href);
const spec = mod.default as unknown;
if (!isObject(spec)) {
throw new Error('persona compile: default export must be a persona object');
}
if (typeof spec.intent !== 'string') {
throw new Error('persona compile: default export must include a string intent');
}
if (!isIntent(spec.intent)) {
throw new Error(`persona compile: intent "${spec.intent}" is invalid`);
}
const parsed = parsePersonaSpec(spec, spec.intent);

await mkdir(dirname(absOutput), { recursive: true });
await writeFile(absOutput, JSON.stringify(spec, null, 2) + '\n', 'utf8');

return {
inputPath: absInput,
outputPath: absOutput,
personaId: parsed.id
};
} finally {
await rm(tempDir, { recursive: true, force: true });
}
}

export async function runPersonaCompileCommand(args: string[]): Promise<void> {
const [action, personaPath, ...rest] = args;
if (!action || action === '-h' || action === '--help') {
process.stdout.write('Usage: agentworkforce persona compile <path/to/persona.ts>\n');
process.exit(action ? 0 : 1);
}
if (action !== 'compile') {
throw new Error(`persona: unknown action "${action}". Expected: compile`);
}
if (!personaPath) {
throw new Error('persona compile: missing <path/to/persona.ts>');
}
if (rest.length > 0) {
throw new Error(`persona compile: unexpected argument "${rest[0]}"`);
}

const result = await compilePersonaFile(personaPath);
process.stdout.write(
`Compiled ${result.inputPath} -> ${result.outputPath} (${result.personaId})\n`
);
}

async function assertReadableFile(abs: string, label: string): Promise<void> {
try {
const st = await stat(abs);
if (!st.isFile()) {
throw new Error(`${label}: ${abs} is not a regular file`);
}
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`${label}: file not found at ${abs}`);
}
throw err;
}
}

function packageNodePaths(): string[] {
const here = dirname(fileURLToPath(import.meta.url));
return [
join(here, '..', 'node_modules'),
join(here, '..', '..', 'node_modules'),
join(process.cwd(), 'node_modules')
];
}
1 change: 1 addition & 0 deletions packages/persona-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"lint": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@relayfile/adapter-core": "^0.3.13",
"@relayfile/local-mount": "^0.7.24"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/persona-kit/schemas/persona.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@
"required": [
"on"
],
"description": "A single event trigger declared by an integration. `on` is a Relayfile- adapter-normalized event name (e.g. `pull_request.opened`, `issue.created`, `app_mention`). `match` and `where` are filter sugars the deploy CLI lints against a known registry; unknown values warn but do not fail parse, so the cloud runtime stays the source of truth.\n\nExamples: { on: \"pull_request.opened\" } { on: \"issue_comment.created\", match: \"@mention\" } { on: \"check_run.completed\", where: \"conclusion=failure\" }"
"description": "A single event trigger declared by an integration. `on` is a Relayfile- adapter-normalized event name (e.g. `pull_request.opened`, `issue.create`, `message.created`). `match` and `where` are filter sugars the deploy CLI lints against a known registry; unknown values warn but do not fail parse, so the cloud runtime stays the source of truth.\n\nExamples: { on: \"pull_request.opened\" } { on: \"pull_request_review_comment.created\", match: \"@mention\" } { on: \"check_run.completed\", where: \"conclusion=failure\" }"
},
"PersonaSchedule": {
"type": "object",
Expand Down
9 changes: 4 additions & 5 deletions packages/persona-kit/src/__fixtures__/personas/full.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,14 @@
"scope": { "repo": "AgentWorkforce/workforce" },
"triggers": [
{ "on": "pull_request.opened" },
{ "on": "issue_comment.created", "match": "@mention" },
{ "on": "pull_request_review_comment.created", "match": "@mention" },
{ "on": "check_run.completed", "where": "conclusion=failure" }
]
},
"linear": { "triggers": [{ "on": "issue.created" }] },
"slack": { "triggers": [{ "on": "app_mention" }] },
"linear": { "triggers": [{ "on": "issue.create" }] },
"slack": { "triggers": [{ "on": "message.created" }] },
"notion": {
"scope": { "database": "db_123" },
"triggers": [{ "on": "page.updated" }]
"scope": { "database": "db_123" }
},
"jira": {
"scope": { "project": "ENG" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"integrations": {
"slack": {
"source": { "kind": "workspace" },
"triggers": [{ "on": "app_mention" }]
"triggers": [{ "on": "message.created" }]
}
}
}
Loading
Loading