Skip to content
Closed
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ See [docs/github-app-setup.md](docs/github-app-setup.md) for step-by-step instru
| `GITHUB_APP_WEBHOOK_SECRET` | Yes | GitHub App webhook secret for payload verification |
| `GITHUB_AGENT_USERNAME` | No | GitHub username of the designated coder agent (default: `xmtp-coder-agent`) |
| `PORT` | No | HTTP server port (default: `3000`) |
| `PROMPT` | No | Custom prompt text — issue URL is always appended (`create_task` only) |
| `PROMPT` | No | Custom prompt text — pirate instruction and issue URL are appended (`create_task` only) |

## Running

Expand Down
69 changes: 69 additions & 0 deletions docs/plans/2026-04-03-issue-88-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Issue 88 Design

## Summary

Issue 88 requires the `create_task` flow to instruct the downstream coding agent to speak like a pirate when a task is first created. The change should affect only the initial task input payload built by `CreateTaskHandler`, while preserving the existing optional base prompt and required issue URL context.

## Project Goals & Non-Goals

**Goals**

- Add a deterministic pirate-speaking instruction to new task input created from `issues.assigned`.
- Preserve the configured base prompt when `PROMPT` is set.
- Preserve the issue URL in the task input so task creation still points the agent at the source issue.
- Cover the new behavior with focused unit tests in the existing `create-task` handler test suite.

**Non-Goals**

- Changing comment forwarding behavior for PR comments, issue comments, or failed checks.
- Introducing configurable personas or new environment variables.
- Changing task naming, template selection, permission checks, or GitHub commenting behavior.

## Context

- **Catalyst:** [Issue #88](https://github.com/xmtplabs/coder-action/issues/88)
- **Relevant code:** `src/handlers/create-task.ts`, `src/handlers/create-task.test.ts`, `README.md`
- **Impact area:** Initial task prompt construction for the `create_task` handler

## System Design

`CreateTaskHandler.run()` currently builds the task input as either `PROMPT + issue URL` or only the issue URL. The handler will instead build the input from ordered sections:

1. Optional configured base prompt
2. Fixed pirate-speaking instruction
3. Issue URL

The implementation should keep the formatting simple and deterministic by joining only present sections with blank lines. This keeps existing behavior stable while ensuring every newly created task receives the persona instruction.

## Libraries & Utilities Required

**External dependencies:** None

**Internal modules:**

| Module | Path | Purpose |
|--------|------|---------|
| CreateTaskHandler | `src/handlers/create-task.ts` | Builds task input and creates tasks |
| CreateTaskHandler tests | `src/handlers/create-task.test.ts` | Verifies prompt construction and handler behavior |

## Testing & Validation

### Acceptance Criteria

1. WHEN `CreateTaskHandler` creates a new task THE SYSTEM SHALL include an instruction telling the agent to speak like a pirate in the task input.
2. WHEN a configured base prompt exists THE SYSTEM SHALL preserve that prompt and append the pirate instruction before the issue URL.
3. WHEN no configured base prompt exists THE SYSTEM SHALL still include both the pirate instruction and the issue URL in the task input.
4. THE SYSTEM SHALL NOT change task creation behavior for existing-task reuse, permission checks, template selection, or GitHub issue commenting.

### Edge Cases

- Ensure the new instruction is present exactly once when a base prompt exists.
- Ensure blank-line formatting remains stable so tests can assert exact prompt content.
- Ensure existing task reuse path does not attempt to rebuild or send a new task input.

### Verification Commands

- `bun test src/handlers/create-task.test.ts`
- `bun test`
- `bun run typecheck`
- `bun run lint`
80 changes: 80 additions & 0 deletions docs/plans/2026-04-03-issue-88-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Issue 88 Task Decomposition

> **Source spec:** `docs/plans/2026-04-03-issue-88-design.md`
> **Generated:** 2026-04-03

**Goal:** Add a pirate-speaking instruction to newly created task input without regressing the existing create-task flow.

**Phases:**
1. Prompt construction — update the task input builder and focused tests
2. Verification — run targeted and full project validation

### Task 1: Add pirate instruction to create-task input

**Files:**
- Modify: `src/handlers/create-task.ts`
- Modify: `src/handlers/create-task.test.ts`

- [ ] **Step 1: Write failing test**
```ts
test("includes pirate instruction in task input", async () => {
github.checkActorPermission.mockResolvedValue(true);
coder.getTask.mockResolvedValue(null);
coder.createTask.mockResolvedValue(mockTask);

const handler = new CreateTaskHandler(
coder,
github as unknown as import("../github-client").GitHubClient,
baseInputs,
issueContext,
logger,
);
await handler.run();

const createCall = coder.createTask.mock.calls[0] as unknown as [
string,
{ input: string },
];
expect(createCall[1].input).toContain("speak like a pirate");
});
```

- [ ] **Step 2: Verify test fails**
Run: `bun test src/handlers/create-task.test.ts`
Expected: FAIL because the task input currently contains only the optional prompt and issue URL.

- [ ] **Step 3: Implement minimal code**
```ts
const fullPrompt = [this.inputs.prompt, pirateInstruction, this.context.issueUrl]
.filter((part) => part != null && part.length > 0)
.join("\n\n");
```

- [ ] **Step 4: Verify test passes**
Run: `bun test src/handlers/create-task.test.ts`
Expected: PASS

- [ ] **Step 5: Commit**
`git add src/handlers/create-task.ts src/handlers/create-task.test.ts docs/plans/2026-04-03-issue-88-*.md && git commit -m "feat: add pirate task prompt instruction"`

### Task 2: Verification

**Files:**
- Modify: none

- [ ] **Step 1: Run focused verification**
Run: `bun test src/handlers/create-task.test.ts`
Expected: PASS

- [ ] **Step 2: Run full verification**
Run: `bun test && bun run typecheck && bun run lint`
Expected: PASS

## Requirement Coverage Matrix

| # | EARS Requirement | Task(s) |
|---|-------------------|---------|
| 1 | WHEN `CreateTaskHandler` creates a new task THE SYSTEM SHALL include an instruction telling the agent to speak like a pirate in the task input. | Task 1, Task 2 |
| 2 | WHEN a configured base prompt exists THE SYSTEM SHALL preserve that prompt and append the pirate instruction before the issue URL. | Task 1, Task 2 |
| 3 | WHEN no configured base prompt exists THE SYSTEM SHALL still include both the pirate instruction and the issue URL in the task input. | Task 1, Task 2 |
| 4 | THE SYSTEM SHALL NOT change task creation behavior for existing-task reuse, permission checks, template selection, or GitHub issue commenting. | Task 1, Task 2 |
15 changes: 8 additions & 7 deletions src/handlers/create-task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe("CreateTaskHandler", () => {
});

// AC #4: Issue URL appended to prompt
test("appends issue URL to prompt", async () => {
test("appends pirate instruction and issue URL to prompt", async () => {
github.checkActorPermission.mockResolvedValue(true);
coder.getTask.mockResolvedValue(null);
coder.createTask.mockResolvedValue(mockTask);
Expand All @@ -101,14 +101,13 @@ describe("CreateTaskHandler", () => {
{ input: string },
];
const taskInput = createCall[1].input;
expect(taskInput).toContain("Fix the bug");
expect(taskInput).toEndWith(
"\n\nhttps://github.com/xmtp/libxmtp/issues/42",
expect(taskInput).toBe(
"Fix the bug\n\nSpeak like a pirate in all of your responses.\n\nhttps://github.com/xmtp/libxmtp/issues/42",
);
});

// AC #4: Only issue URL when no prompt provided
test("uses only issue URL when no prompt provided", async () => {
// AC #4: Pirate instruction retained when no prompt provided
test("uses pirate instruction and issue URL when no prompt provided", async () => {
github.checkActorPermission.mockResolvedValue(true);
coder.getTask.mockResolvedValue(null);
coder.createTask.mockResolvedValue(mockTask);
Expand All @@ -126,7 +125,9 @@ describe("CreateTaskHandler", () => {
string,
{ input: string },
];
expect(createCall[1].input).toBe(issueContext.issueUrl);
expect(createCall[1].input).toBe(
"Speak like a pirate in all of your responses.\n\nhttps://github.com/xmtp/libxmtp/issues/42",
);
});

// AC #5: Existing running task
Expand Down
12 changes: 9 additions & 3 deletions src/handlers/create-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface IssueContext {
senderLogin: string;
}

const PIRATE_INSTRUCTION = "Speak like a pirate in all of your responses.";

export class CreateTaskHandler {
constructor(
private readonly coder: CoderClient,
Expand Down Expand Up @@ -85,9 +87,13 @@ export class CreateTaskHandler {
}

// 4. Build prompt
const fullPrompt = this.inputs.prompt
? `${this.inputs.prompt}\n\n${this.context.issueUrl}`
: this.context.issueUrl;
const fullPrompt = [
this.inputs.prompt,
PIRATE_INSTRUCTION,
this.context.issueUrl,
]
.filter((part) => part != null && part.length > 0)
.join("\n\n");

// 5. Get template and create task
const templateName = this.resolveTemplateName();
Expand Down
Loading