-
Notifications
You must be signed in to change notification settings - Fork 0
feat(personas): minimal Mode 2 E2E hello persona #160
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
base: main
Are you sure you want to change the base?
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,99 @@ | ||
| import { handler } from '@agentworkforce/runtime'; | ||
|
|
||
| /** | ||
| * Minimal Mode 2 handler. One trigger -> one action. | ||
| * | ||
| * Listens for GitHub `issues.opened` / `issues.labeled` events on | ||
| * AgentWorkforce/cloud. If the issue is open and carries the `hello` | ||
| * label, posts a single confirmation comment via ctx.github.comment. | ||
| * Anything else is ignored. No clone, no shell, no workflow. | ||
| */ | ||
|
|
||
| const REPO_OWNER = 'AgentWorkforce'; | ||
| const REPO_NAME = 'cloud'; | ||
| const REPO_FULL_NAME = `${REPO_OWNER}/${REPO_NAME}`; | ||
| const LABEL = 'hello'; | ||
| const COMMENT_BODY = | ||
| 'hello from e2e-mode2-hello persona — Mode 2 E2E reached this handler.'; | ||
|
|
||
| export default handler(async (ctx, event) => { | ||
| if (event.source !== 'github') { | ||
| ctx.log('info', 'ignoring unsupported event source', { source: event.source }); | ||
| return; | ||
| } | ||
| if (event.type !== 'issues.opened' && event.type !== 'issues.labeled') { | ||
| ctx.log('info', 'ignoring non-issue event', { type: event.type }); | ||
| return; | ||
| } | ||
|
|
||
| const resource = asRecord(event.payload); | ||
| const issue = maybeRecord(resource.issue) ?? resource; | ||
| const repo = asRecord(resource.repository); | ||
| const fullName = stringValue(repo?.full_name) ?? REPO_FULL_NAME; | ||
|
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: Fail closed when Prompt for AI agents |
||
| if (fullName !== REPO_FULL_NAME) { | ||
| ctx.log('info', 'ignoring event for different repo', { fullName }); | ||
| return; | ||
|
Comment on lines
+32
to
+35
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. Fail closed when Line 32 currently defaults missing repo metadata to Proposed fix- const fullName = stringValue(repo?.full_name) ?? REPO_FULL_NAME;
- if (fullName !== REPO_FULL_NAME) {
+ const fullName = stringValue(repo?.full_name);
+ if (!fullName) {
+ ctx.log('warn', 'missing repository.full_name', { eventId: event.id });
+ return;
+ }
+ if (fullName !== REPO_FULL_NAME) {
ctx.log('info', 'ignoring event for different repo', { fullName });
return;
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| const issueState = stringValue(issue.state ?? resource.state)?.toLowerCase(); | ||
| if (issueState !== 'open') { | ||
| ctx.log('info', 'skipping non-open issue', { issueState, eventId: event.id }); | ||
| return; | ||
| } | ||
|
|
||
| const labels = readLabels(issue.labels ?? resource.labels); | ||
| if (!labels.includes(LABEL)) { | ||
|
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. P2: Prompt for AI agents |
||
| ctx.log('info', 'skipping issue without hello label', { labels, eventId: event.id }); | ||
| return; | ||
| } | ||
|
|
||
| const issueNumber = numberValue(issue.number ?? resource.number); | ||
| if (!issueNumber) { | ||
| ctx.log('warn', 'missing issue number', { eventId: event.id }); | ||
| return; | ||
| } | ||
|
|
||
| if (!ctx.github?.comment) { | ||
| // Fail loud: a missing github.comment binding silently hides every effect | ||
| // this persona produces. Surface it in cloud-web tail rather than appear | ||
| // green-but-no-op. | ||
| ctx.log('warn', 'ctx.github.comment unavailable; comment dropped', { | ||
| issueNumber, | ||
| bodyPreview: COMMENT_BODY.slice(0, 120) | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| await ctx.github.comment( | ||
| { owner: REPO_OWNER, repo: REPO_NAME, number: issueNumber }, | ||
| COMMENT_BODY | ||
| ); | ||
| ctx.log('info', 'posted hello comment', { issueNumber }); | ||
| }); | ||
|
|
||
| function asRecord(value: unknown): Record<string, any> { | ||
| return value && typeof value === 'object' && !Array.isArray(value) | ||
| ? (value as Record<string, any>) | ||
| : {}; | ||
| } | ||
|
|
||
| function maybeRecord(value: unknown): Record<string, any> | null { | ||
| return value && typeof value === 'object' && !Array.isArray(value) | ||
| ? (value as Record<string, any>) | ||
| : null; | ||
| } | ||
|
|
||
| function stringValue(value: unknown): string | null { | ||
| return typeof value === 'string' && value.trim() ? value.trim() : null; | ||
| } | ||
|
|
||
| function numberValue(value: unknown): number | null { | ||
| const number = typeof value === 'number' ? value : Number(value); | ||
| return Number.isFinite(number) && number > 0 ? number : null; | ||
| } | ||
|
|
||
| function readLabels(value: unknown): string[] { | ||
| return Array.isArray(value) | ||
| ? value.map((entry) => String(asRecord(entry).name ?? entry).toLowerCase()) | ||
| : []; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "e2e-mode2-hello", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "dependencies": { | ||
| "@agentworkforce/runtime": "^3.0.30" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| "id": "e2e-mode2-hello", | ||
| "intent": "review", | ||
| "tags": [ | ||
| "review" | ||
| ], | ||
| "description": "Minimal Mode 2 E2E probe: replies to AgentWorkforce/cloud issues labeled `hello` with a single confirmation comment, to prove the single-agent handler path runs end-to-end.", | ||
| "cloud": true, | ||
| "onEvent": "./agent.ts", | ||
| "harness": "codex", | ||
| "model": "gpt-5", | ||
| "systemPrompt": "Handle the proactive event.", | ||
| "harnessSettings": { | ||
| "reasoning": "low", | ||
| "timeoutSeconds": 60 | ||
| }, | ||
| "integrations": { | ||
| "github": { | ||
| "source": { | ||
| "kind": "workspace" | ||
| }, | ||
| "triggers": [ | ||
| { | ||
| "on": "issues.opened" | ||
| }, | ||
| { | ||
| "on": "issues.labeled" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { definePersona } from '@agentworkforce/persona-kit'; | ||
|
|
||
| /** | ||
| * Minimal Mode 2 persona (single-agent handler). | ||
| * | ||
| * Trigger: a GitHub issue is opened or labeled on AgentWorkforce/cloud. | ||
| * Action: if the issue carries the `hello` label, the handler posts a | ||
| * single comment back acknowledging the Mode 2 handler ran. | ||
| * | ||
| * Exists to prove the Mode 2 path (proactive trigger -> single agent.ts | ||
| * handler -> back-channel write) end-to-end with the smallest possible | ||
| * persona — no clone, no shell, no workflow, no runtime work. | ||
| */ | ||
| export default definePersona({ | ||
| id: 'e2e-mode2-hello', | ||
| intent: 'review', | ||
| tags: ['review'], | ||
| description: | ||
| 'Minimal Mode 2 E2E probe: replies to AgentWorkforce/cloud issues labeled `hello` with a single confirmation comment, to prove the single-agent handler path runs end-to-end.', | ||
| cloud: true, | ||
| onEvent: './agent.ts', | ||
| harness: 'codex', | ||
| model: 'gpt-5', | ||
| systemPrompt: 'Handle the proactive event.', | ||
| harnessSettings: { reasoning: 'low', timeoutSeconds: 60 }, | ||
| integrations: { | ||
| github: { | ||
| source: { kind: 'workspace' }, | ||
| triggers: [ | ||
| { on: 'issues.opened' }, | ||
| { on: 'issues.labeled' } | ||
| ] | ||
| } | ||
| } | ||
| }); |
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.
The current label gate can post duplicate comments for unrelated label events.
Once an issue has
hello, every futureissues.labeledevent still satisfies Line 45 and can post another confirmation comment, which breaks the intended single-comment probe behavior.Proposed fix
const labels = readLabels(issue.labels ?? resource.labels); if (!labels.includes(LABEL)) { ctx.log('info', 'skipping issue without hello label', { labels, eventId: event.id }); return; } + if (event.type === 'issues.labeled') { + const addedLabel = stringValue(asRecord(resource.label).name)?.toLowerCase(); + if (addedLabel !== LABEL) { + ctx.log('info', 'skipping labeled event for non-hello label', { addedLabel, eventId: event.id }); + return; + } + }Also applies to: 44-47, 67-71
🤖 Prompt for AI Agents