diff --git a/packages/deploy/src/compile-agent.test.ts b/packages/deploy/src/compile-agent.test.ts
index 24a47810..88e04bb9 100644
--- a/packages/deploy/src/compile-agent.test.ts
+++ b/packages/deploy/src/compile-agent.test.ts
@@ -98,6 +98,43 @@ test('unknown capability extensions survive source-to-persistence projection', a
}
});
+test('trigger paths survive source-to-persistence projection for wake scoping', async () => {
+ const dir = await mkdtemp(path.join(tmpdir(), 'compiled-agent-trigger-paths-'));
+ try {
+ const sourcePath = path.join(dir, 'agent.ts');
+ const source = PRESET
+ .replace('integrations: { github: {} },', 'integrations: { github: {}, slack: {} },')
+ .replace(
+ "triggers: { github: [{ on: 'pull_request.opened' }] },",
+ "triggers: {\n" +
+ " github: [{ on: 'pull_request.opened', paths: ['/github/repos/AgentWorkforce/workforce/pulls/**'] }],\n" +
+ " slack: [{ on: 'message.created', paths: ['/slack/channels/C_REVIEW/**'] }]\n" +
+ ' },'
+ );
+ await writeFile(sourcePath, source);
+
+ const compiled = await compileAgentSource(sourcePath);
+ const persisted = projectCompiledAgentForPersistence(compiled);
+
+ assert.deepEqual(persisted.agent.triggers, {
+ github: [
+ {
+ on: 'pull_request.opened',
+ paths: ['/github/repos/AgentWorkforce/workforce/pulls/**']
+ }
+ ],
+ slack: [
+ {
+ on: 'message.created',
+ paths: ['/slack/channels/C_REVIEW/**']
+ }
+ ]
+ });
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+});
+
test('split persona plus agent form remains supported', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'compiled-agent-split-'));
try {
diff --git a/packages/persona-kit/schemas/agent.schema.json b/packages/persona-kit/schemas/agent.schema.json
index 51bb0df6..af6771ba 100644
--- a/packages/persona-kit/schemas/agent.schema.json
+++ b/packages/persona-kit/schemas/agent.schema.json
@@ -49,6 +49,16 @@
"where": {
"type": "string"
},
+ "paths": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^/(?:[^\\r\\n\\u2028\\u2029]*\\S)?$"
+ },
+ "description": "Optional Relayfile path globs used to scope wake routing before an agent is provisioned. Paths are absolute (leading `/`) and provider-specific.",
+ "minItems": 1
+ },
"maxConcurrency": {
"type": "integer",
"description": "Optional delivery backpressure hint for this trigger. Positive integers are preserved by the parser; invalid values are treated as unset so older or malformed specs do not block delivery.",
@@ -58,7 +68,7 @@
"required": [
"on"
],
- "description": "A single event trigger declared by an **agent** (`agent.ts`). `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\nTriggers live on the agent ( {@link AgentSpec.triggers } ), not the persona — the persona only declares which providers it connects to ( {@link PersonaIntegrationConfig } ). The deploy CLI joins the two: events from `agent.triggers[provider]`, connection/scope from `persona.integrations[provider]`.\n\nExamples: { on: \"pull_request.opened\" } { on: \"pull_request_review_comment.created\", match: \"@mention\" } { on: \"check_run.completed\", where: \"conclusion=failure\" } { on: \"message.created\", maxConcurrency: 1 }"
+ "description": "A single event trigger declared by an **agent** (`agent.ts`). `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\nTriggers live on the agent ( {@link AgentSpec.triggers } ), not the persona — the persona only declares which providers it connects to ( {@link PersonaIntegrationConfig } ). The deploy CLI joins the two: events from `agent.triggers[provider]`, connection/scope from `persona.integrations[provider]`.\n\nExamples: { on: \"pull_request.opened\" } { on: \"pull_request_review_comment.created\", match: \"@mention\" } { on: \"check_run.completed\", where: \"conclusion=failure\" } { on: \"message.created\", paths: [\"/slack/channels/C_REVIEW/**\"] } { on: \"message.created\", maxConcurrency: 1 }"
},
"PersonaSchedule": {
"type": "object",
@@ -85,8 +95,11 @@
"paths": {
"type": "array",
"items": {
- "type": "string"
- }
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^/(?:[^\\r\\n\\u2028\\u2029]*\\S)?$"
+ },
+ "minItems": 1
},
"events": {
"type": "array",
diff --git a/packages/persona-kit/scripts/emit-schema.mjs b/packages/persona-kit/scripts/emit-schema.mjs
index a046f28e..aa324c7d 100644
--- a/packages/persona-kit/scripts/emit-schema.mjs
+++ b/packages/persona-kit/scripts/emit-schema.mjs
@@ -146,6 +146,23 @@ if (
};
}
+const absolutePathArrayConstraint = {
+ minItems: 1,
+ items: {
+ type: 'string',
+ minLength: 1,
+ pattern: '^/(?:[^\\r\\n\\u2028\\u2029]*\\S)?$'
+ }
+};
+
+function applyAbsolutePathArrayConstraint(pathsSchema) {
+ if (pathsSchema?.type !== 'array') return;
+ Object.assign(pathsSchema, absolutePathArrayConstraint);
+}
+
+applyAbsolutePathArrayConstraint(agentTrigger?.properties?.paths);
+applyAbsolutePathArrayConstraint(agentSchema.definitions?.WatchRule?.properties?.paths);
+
const agentSerialized = `${JSON.stringify(agentSchema, null, 2)}\n`;
let existingAgent = '';
try {
diff --git a/packages/persona-kit/src/define.test.ts b/packages/persona-kit/src/define.test.ts
index d02b81ad..3d2e80b8 100644
--- a/packages/persona-kit/src/define.test.ts
+++ b/packages/persona-kit/src/define.test.ts
@@ -126,11 +126,18 @@ test('TypedTriggerMap gives per-provider event autocomplete; arbitrary providers
{ on: 'AgentSessionEvent.prompted' },
{ on: 'AppUserNotification.issueCommentMention' }
],
- slack: [{ on: 'message.created', maxConcurrency: 1 }],
+ slack: [
+ {
+ on: 'message.created',
+ paths: ['/slack/channels/C_REVIEW/**'],
+ maxConcurrency: 1
+ }
+ ],
customProvider: [{ on: 'custom.event' }]
};
assert.equal(triggers.github?.[1]?.on, 'off_registry.github_event');
assert.equal(triggers.slack?.[0]?.maxConcurrency, 1);
+ assert.deepEqual(triggers.slack?.[0]?.paths, ['/slack/channels/C_REVIEW/**']);
assert.equal(triggers.customProvider?.[0]?.on, 'custom.event');
});
diff --git a/packages/persona-kit/src/define.ts b/packages/persona-kit/src/define.ts
index c98aab32..06672e6c 100644
--- a/packages/persona-kit/src/define.ts
+++ b/packages/persona-kit/src/define.ts
@@ -24,6 +24,7 @@ export interface TypedTrigger
{
on: TriggerNameFor
;
match?: string;
where?: string;
+ paths?: string[];
maxConcurrency?: number;
}
diff --git a/packages/persona-kit/src/emit-schema.test.ts b/packages/persona-kit/src/emit-schema.test.ts
index 5081183f..8774d4b2 100644
--- a/packages/persona-kit/src/emit-schema.test.ts
+++ b/packages/persona-kit/src/emit-schema.test.ts
@@ -159,7 +159,29 @@ test('agent schema exposes launchedBy/triggers/schedules/watch', async () => {
assert.equal(watchRule.properties?.paths && watchRule.properties.paths !== true
? watchRule.properties.paths.type
: undefined, 'array');
+ const absolutePathItems = {
+ type: 'string',
+ minLength: 1,
+ pattern: '^/(?:[^\\r\\n\\u2028\\u2029]*\\S)?$'
+ };
+ const watchPaths = watchRule.properties?.paths;
+ assert.equal(watchPaths && watchPaths !== true
+ ? watchPaths.minItems
+ : undefined, 1);
+ assert.deepEqual(watchPaths && watchPaths !== true
+ ? watchPaths.items
+ : undefined, absolutePathItems);
const trigger = definitions.PersonaIntegrationTrigger;
+ const triggerPaths = trigger.properties?.paths;
+ assert.equal(triggerPaths && triggerPaths !== true
+ ? triggerPaths.type
+ : undefined, 'array');
+ assert.equal(triggerPaths && triggerPaths !== true
+ ? triggerPaths.minItems
+ : undefined, 1);
+ assert.deepEqual(triggerPaths && triggerPaths !== true
+ ? triggerPaths.items
+ : undefined, absolutePathItems);
const maxConcurrency = trigger.properties?.maxConcurrency;
assert.equal(maxConcurrency && maxConcurrency !== true
? maxConcurrency.type
@@ -168,7 +190,65 @@ test('agent schema exposes launchedBy/triggers/schedules/watch', async () => {
? maxConcurrency.minimum
: undefined, 1);
- assertSchema({ triggers: { slack: [{ on: 'message.created', maxConcurrency: 1 }] } }, schema, schema, 'agent');
+ assertSchema(
+ {
+ triggers: {
+ slack: [
+ {
+ on: 'message.created',
+ paths: ['/ slack/channels/C_REVIEW/**'],
+ maxConcurrency: 1
+ }
+ ]
+ },
+ watch: [{ paths: ['/ github/**'], events: ['created'] }]
+ },
+ schema,
+ schema,
+ 'agent'
+ );
+ assert.throws(
+ () => assertSchema({ triggers: { slack: [{ on: 'message.created', paths: [] }] } }, schema, schema, 'agent'),
+ /agent\.triggers\.slack\[0\]\.paths must have at least 1 item/
+ );
+ assert.throws(
+ () => assertSchema({ triggers: { slack: [{ on: 'message.created', paths: ['slack/**'] }] } }, schema, schema, 'agent'),
+ /agent\.triggers\.slack\[0\]\.paths\[0\] must match/
+ );
+ assert.throws(
+ () => assertSchema({ triggers: { slack: [{ on: 'message.created', paths: ['/slack/** '] }] } }, schema, schema, 'agent'),
+ /agent\.triggers\.slack\[0\]\.paths\[0\] must match/
+ );
+ assert.throws(
+ () => assertSchema({ watch: [{ paths: [], events: ['created'] }] }, schema, schema, 'agent'),
+ /agent\.watch\[0\]\.paths must have at least 1 item/
+ );
+ assert.throws(
+ () => assertSchema({ watch: [{ paths: ['watch/**'], events: ['created'] }] }, schema, schema, 'agent'),
+ /agent\.watch\[0\]\.paths\[0\] must match/
+ );
+ assert.throws(
+ () => assertSchema({ watch: [{ paths: [' /watch/**'], events: ['created'] }] }, schema, schema, 'agent'),
+ /agent\.watch\[0\]\.paths\[0\] must match/
+ );
+ assert.throws(
+ () => assertSchema({ watch: [{ paths: ['/watch/** '], events: ['created'] }] }, schema, schema, 'agent'),
+ /agent\.watch\[0\]\.paths\[0\] must match/
+ );
+ for (const separator of ['\r', '\n', '\u2028', '\u2029']) {
+ assert.throws(
+ () => assertSchema({
+ triggers: { slack: [{ on: 'message.created', paths: [`/slack/${separator}channels/**`] }] }
+ }, schema, schema, 'agent'),
+ /agent\.triggers\.slack\[0\]\.paths\[0\] must match/
+ );
+ assert.throws(
+ () => assertSchema({
+ watch: [{ paths: [`/github/${separator}issues/**`], events: ['created'] }]
+ }, schema, schema, 'agent'),
+ /agent\.watch\[0\]\.paths\[0\] must match/
+ );
+ }
assert.throws(
() => assertSchema({ triggers: { slack: [{ on: 'message.created', maxConcurrency: 0 }] } }, schema, schema, 'agent'),
/agent\.triggers\.slack\[0\]\.maxConcurrency must be >= 1/
@@ -190,6 +270,9 @@ type SchemaNode = Record & {
const?: unknown;
type?: string | string[];
minimum?: number;
+ minItems?: number;
+ minLength?: number;
+ pattern?: string;
properties?: Record;
additionalProperties?: SchemaNode | boolean;
required?: string[];
@@ -232,6 +315,12 @@ function assertSchema(value: unknown, schema: SchemaNode, root: SchemaNode, path
if (typeof schema.minimum === 'number' && typeof value === 'number' && value < schema.minimum) {
throw new Error(`${path} must be >= ${schema.minimum}`);
}
+ if (typeof schema.minLength === 'number' && typeof value === 'string' && value.length < schema.minLength) {
+ throw new Error(`${path} must have at least ${schema.minLength} character(s)`);
+ }
+ if (schema.pattern && typeof value === 'string' && !new RegExp(schema.pattern).test(value)) {
+ throw new Error(`${path} must match ${schema.pattern}`);
+ }
if (schema.type === 'object' || schema.properties || schema.additionalProperties || schema.required) {
if (!isObject(value)) {
throw new Error(`${path} must be an object`);
@@ -256,6 +345,9 @@ function assertSchema(value: unknown, schema: SchemaNode, root: SchemaNode, path
if (!Array.isArray(value)) {
throw new Error(`${path} must be an array`);
}
+ if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
+ throw new Error(`${path} must have at least ${schema.minItems} item(s)`);
+ }
if (schema.items) {
value.forEach((item, index) =>
assertSchema(item, schema.items as SchemaNode, root, `${path}[${index}]`)
diff --git a/packages/persona-kit/src/parse.test.ts b/packages/persona-kit/src/parse.test.ts
index e0decdde..13897f93 100644
--- a/packages/persona-kit/src/parse.test.ts
+++ b/packages/persona-kit/src/parse.test.ts
@@ -774,17 +774,26 @@ test('parseAgentSpec validates launchedBy plus provider-keyed triggers, schedule
triggers: {
github: [
{ on: 'pull_request.opened' },
- { on: 'issue_comment.created', match: '@mention', maxConcurrency: 1 }
+ {
+ on: 'issue_comment.created',
+ match: '@mention',
+ paths: ['/github/repos/AgentWorkforce/workforce/issues/**'],
+ maxConcurrency: 1
+ }
],
- slack: [{ on: 'app_mention' }]
+ slack: [{ on: 'app_mention', paths: ['/slack/channels/C_REVIEW/**'] }]
},
schedules: [{ name: 'nightly', cron: '0 2 * * *', tz: 'UTC' }],
watch: [{ paths: ['/github/x.json'], events: ['created'] }]
});
assert.equal(agent.triggers?.github.length, 2);
assert.equal(agent.triggers?.github[1].match, '@mention');
+ assert.deepEqual(agent.triggers?.github[1].paths, [
+ '/github/repos/AgentWorkforce/workforce/issues/**'
+ ]);
assert.equal(agent.triggers?.github[1].maxConcurrency, 1);
assert.equal(agent.triggers?.slack[0].on, 'app_mention');
+ assert.deepEqual(agent.triggers?.slack[0].paths, ['/slack/channels/C_REVIEW/**']);
assert.equal(agent.schedules?.[0].name, 'nightly');
assert.equal(agent.watch?.[0].paths[0], '/github/x.json');
assert.equal(agent.launchedBy, 'team-dispatcher');
@@ -837,6 +846,38 @@ test('parseAgentSpec rejects malformed triggers maps with precise field paths',
() => parseAgentSpec({ triggers: { github: [{ on: 'issue.opened', where: 1 }] } }),
/triggers\.github\[0\]\.where must be a non-empty string if provided/
);
+ assert.throws(
+ () => parseAgentSpec({ triggers: { github: [{ on: 'issue.opened', paths: '/*' }] } }),
+ /triggers\.github\[0\]\.paths must be a non-empty array/
+ );
+ assert.throws(
+ () => parseAgentSpec({ triggers: { github: [{ on: 'issue.opened', paths: [] }] } }),
+ /triggers\.github\[0\]\.paths must be a non-empty array/
+ );
+ assert.throws(
+ () => parseAgentSpec({ triggers: { github: [{ on: 'issue.opened', paths: [''] }] } }),
+ /triggers\.github\[0\]\.paths\[0\] must be a non-empty string/
+ );
+ assert.throws(
+ () => parseAgentSpec({ triggers: { github: [{ on: 'issue.opened', paths: ['/github/** '] }] } }),
+ /triggers\.github\[0\]\.paths\[0\] must not have leading or trailing whitespace/
+ );
+ assert.throws(
+ () => parseAgentSpec({ triggers: { github: [{ on: 'issue.opened', paths: ['github/**'] }] } }),
+ /triggers\.github\[0\]\.paths\[0\] must start with \//
+ );
+ const triggerWithInternalSpace = parseAgentSpec({
+ triggers: { github: [{ on: 'issue.opened', paths: ['/ github/**'] }] }
+ });
+ assert.deepEqual(triggerWithInternalSpace.triggers?.github[0].paths, ['/ github/**']);
+ for (const separator of ['\r', '\n', '\u2028', '\u2029']) {
+ assert.throws(
+ () => parseAgentSpec({
+ triggers: { github: [{ on: 'issue.opened', paths: [`/github/${separator}issues/**`] }] }
+ }),
+ /triggers\.github\[0\]\.paths\[0\] must not contain line separators/
+ );
+ }
// An empty agent (no listeners) parses to {}; the deploy CLI enforces "at least one".
assert.deepEqual(parseAgentSpec({}), {});
});
@@ -1038,12 +1079,23 @@ test('parseWatch rejects malformed relayfile watch rules with precise field path
assert.throws(() => parseWatch([{ events: ['created'] }], 'watch'), /watch\[0\]\.paths must be a non-empty array/);
assert.throws(() => parseWatch([{ paths: [], events: ['created'] }], 'watch'), /watch\[0\]\.paths must be a non-empty array/);
assert.throws(() => parseWatch([{ paths: [42], events: ['created'] }], 'watch'), /watch\[0\]\.paths\[0\] must be a non-empty string/);
+ assert.throws(() => parseWatch([{ paths: ['/x '], events: ['created'] }], 'watch'), /watch\[0\]\.paths\[0\] must not have leading or trailing whitespace/);
assert.throws(() => parseWatch([{ paths: ['relative/**'], events: ['created'] }], 'watch'), /watch\[0\]\.paths\[0\] must start with \//);
assert.throws(() => parseWatch([{ paths: ['/x'] }], 'watch'), /watch\[0\]\.events must be a non-empty array/);
assert.throws(() => parseWatch([{ paths: ['/x'], events: [] }], 'watch'), /watch\[0\]\.events must be a non-empty array/);
assert.throws(() => parseWatch([{ paths: ['/x'], events: ['changed'] }], 'watch'), /watch\[0\]\.events\[0\] must be one of: created, updated, deleted/);
assert.throws(() => parseWatch([{ paths: ['/x'], events: ['created'], debounceMs: -1 }], 'watch'), /watch\[0\]\.debounceMs must be a non-negative number/);
assert.throws(() => parseWatch([{ paths: ['/x'], events: ['created'], match: '' }], 'watch'), /watch\[0\]\.match must be a non-empty string/);
+ assert.deepEqual(
+ parseWatch([{ paths: ['/ github/**'], events: ['created'] }], 'watch')?.[0].paths,
+ ['/ github/**']
+ );
+ for (const separator of ['\r', '\n', '\u2028', '\u2029']) {
+ assert.throws(
+ () => parseWatch([{ paths: [`/github/${separator}issues/**`], events: ['created'] }], 'watch'),
+ /watch\[0\]\.paths\[0\] must not contain line separators/
+ );
+ }
});
test('parsePersonaSpec round-trips mount.enabled=false (watch now lives on the agent)', () => {
diff --git a/packages/persona-kit/src/parse.ts b/packages/persona-kit/src/parse.ts
index 66d56e41..112af8bd 100644
--- a/packages/persona-kit/src/parse.ts
+++ b/packages/persona-kit/src/parse.ts
@@ -544,6 +544,28 @@ function assertCronExpression(value: string, context: string): void {
}
}
+function parseAbsolutePathList(value: unknown, context: string): string[] {
+ if (!Array.isArray(value) || value.length === 0) {
+ throw new Error(`${context} must be a non-empty array`);
+ }
+ return value.map((path, pathIdx) => {
+ const pathContext = `${context}[${pathIdx}]`;
+ if (typeof path !== 'string' || !path.trim()) {
+ throw new Error(`${pathContext} must be a non-empty string`);
+ }
+ if (path !== path.trim()) {
+ throw new Error(`${pathContext} must not have leading or trailing whitespace`);
+ }
+ if (/[\r\n\u2028\u2029]/u.test(path)) {
+ throw new Error(`${pathContext} must not contain line separators`);
+ }
+ if (!path.startsWith('/')) {
+ throw new Error(`${pathContext} must start with /`);
+ }
+ return path;
+ });
+}
+
export function parseIntegrationTrigger(
value: unknown,
context: string
@@ -551,7 +573,7 @@ export function parseIntegrationTrigger(
if (!isObject(value)) {
throw new Error(`${context} must be an object`);
}
- const { on, match, where, maxConcurrency } = value;
+ const { on, match, where, paths, maxConcurrency } = value;
if (typeof on !== 'string' || !on.trim()) {
throw new Error(`${context}.on must be a non-empty string`);
}
@@ -561,6 +583,9 @@ export function parseIntegrationTrigger(
if (where !== undefined && (typeof where !== 'string' || !where.trim())) {
throw new Error(`${context}.where must be a non-empty string if provided`);
}
+ const parsedPaths = paths === undefined
+ ? undefined
+ : parseAbsolutePathList(paths, `${context}.paths`);
// Intentionally lenient: Cloud derives this as an optional backpressure hint,
// so invalid values mean "unset" rather than a parse failure.
const parsedMaxConcurrency =
@@ -573,6 +598,7 @@ export function parseIntegrationTrigger(
on,
...(typeof match === 'string' ? { match } : {}),
...(typeof where === 'string' ? { where } : {}),
+ ...(parsedPaths ? { paths: parsedPaths } : {}),
...(parsedMaxConcurrency !== undefined
? { maxConcurrency: parsedMaxConcurrency }
: {})
@@ -803,19 +829,7 @@ export function parseWatch(value: unknown, context: string): WatchRule[] | undef
throw new Error(`${entryContext} must be an object`);
}
const { paths, events, debounceMs, match } = entry;
- if (!Array.isArray(paths) || paths.length === 0) {
- throw new Error(`${entryContext}.paths must be a non-empty array`);
- }
- const parsedPaths = paths.map((path, pathIdx) => {
- const pathContext = `${entryContext}.paths[${pathIdx}]`;
- if (typeof path !== 'string' || !path.trim()) {
- throw new Error(`${pathContext} must be a non-empty string`);
- }
- if (!path.startsWith('/')) {
- throw new Error(`${pathContext} must start with /`);
- }
- return path;
- });
+ const parsedPaths = parseAbsolutePathList(paths, `${entryContext}.paths`);
if (!Array.isArray(events) || events.length === 0) {
throw new Error(`${entryContext}.events must be a non-empty array`);
diff --git a/packages/persona-kit/src/types.ts b/packages/persona-kit/src/types.ts
index c773639f..e8d46550 100644
--- a/packages/persona-kit/src/types.ts
+++ b/packages/persona-kit/src/types.ts
@@ -194,12 +194,18 @@ export type McpServerSpec =
* { on: "pull_request.opened" }
* { on: "pull_request_review_comment.created", match: "@mention" }
* { on: "check_run.completed", where: "conclusion=failure" }
+ * { on: "message.created", paths: ["/slack/channels/C_REVIEW/**"] }
* { on: "message.created", maxConcurrency: 1 }
*/
export interface PersonaIntegrationTrigger {
on: string;
match?: string;
where?: string;
+ /**
+ * Optional Relayfile path globs used to scope wake routing before an agent is
+ * provisioned. Paths are absolute (leading `/`) and provider-specific.
+ */
+ paths?: string[];
/**
* Optional delivery backpressure hint for this trigger. Positive integers are
* preserved by the parser; invalid values are treated as unset so older or
diff --git a/packages/runtime/src/define-agent.test.ts b/packages/runtime/src/define-agent.test.ts
index 79f124f2..c4b10b42 100644
--- a/packages/runtime/src/define-agent.test.ts
+++ b/packages/runtime/src/define-agent.test.ts
@@ -11,8 +11,15 @@ test('defineAgent brands the object and wraps the handler', () => {
const agent = defineAgent({
launchedBy: 'team-dispatcher',
triggers: {
- github: [{ on: 'pull_request.opened' }, { on: 'issue_comment.created', match: '@mention' }],
- slack: [{ on: 'app_mention' }]
+ github: [
+ { on: 'pull_request.opened' },
+ {
+ on: 'issue_comment.created',
+ match: '@mention',
+ paths: ['/github/repos/AgentWorkforce/workforce/issues/**']
+ }
+ ],
+ slack: [{ on: 'app_mention', paths: ['/slack/channels/C_REVIEW/**'] }]
},
schedules: [{ name: 'nightly', cron: '0 2 * * *', tz: 'UTC' }],
handler: async () => {
@@ -23,7 +30,11 @@ test('defineAgent brands the object and wraps the handler', () => {
assert.equal(isWorkforceAgent(agent), true);
assert.equal(isWorkforceHandler(agent.handler), true);
assert.equal(agent.triggers?.github?.length, 2);
+ assert.deepEqual(agent.triggers?.github?.[1]?.paths, [
+ '/github/repos/AgentWorkforce/workforce/issues/**'
+ ]);
assert.equal(agent.triggers?.slack?.[0]?.on, 'app_mention');
+ assert.deepEqual(agent.triggers?.slack?.[0]?.paths, ['/slack/channels/C_REVIEW/**']);
assert.equal(agent.schedules?.[0]?.name, 'nightly');
assert.equal(agent.launchedBy, 'team-dispatcher');
// __workforceAgent is non-enumerable so the listener declarations serialize clean.