-
Notifications
You must be signed in to change notification settings - Fork 1
Vendor Relayfile trigger catalog in persona-kit #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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' }, | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Non-canonical event name Prompt for AI agents |
||||||||||||||||||||||||||||||
| { 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use canonical trigger names in the example persona. Line 15 and Line 21 still use pre-catalog event names ( 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
Suggested change
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Non-canonical event name Prompt for AI agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| 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 }); | ||
| } | ||
| }); |
| 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') | ||
| ]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents