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
2 changes: 1 addition & 1 deletion packages/agents/content/skills/orchestrate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ Include:

After writing the artifact, call `register_artifact` for the run-summary artifact. Present the same summary to the user in the conversation. The conversational output should match the artifact content — do not abbreviate or omit sections.

Call MCP tool `complete_run` with `{ runDir: {run-dir}, status: "completed" | "failed" | "needs_manual_review" }`. This emits a `run_completed` event and stamps `completedAt` on the run-index.json header. The `status` field within the event carries the actual outcome (`completed`, `failed`, or `needs_manual_review`).
Call MCP tool `complete_run` with `{ runDir: {run-dir}, status: "completed" | "failed" | "needs_manual_review", reason?: string }`. When `status` is `"failed"`, this emits a `run_failed` event (the optional `reason` field is included if provided); otherwise it emits a `run_completed` event. Either way, `completedAt` is stamped on the run-index.json header.

## Phase 6: Wrap-up (prompted, conditional)

Expand Down
4 changes: 3 additions & 1 deletion packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ export function createServer(): McpServer {
server.registerTool(
'complete_run',
{
description: 'Complete a run: emit run_completed event and stamp completedAt on run-index.json.',
description:
'Complete a run: emit run_completed (or run_failed when status is failed) event and stamp completedAt on run-index.json.',
inputSchema: {
runDir: z.string(),
status: z.enum(['completed', 'failed', 'needs_manual_review']),
reason: z.string().optional(),
},
},
async (args) => {
Expand Down
30 changes: 27 additions & 3 deletions packages/mcp/src/tools/__tests__/complete-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,39 @@ describe('completeRun', () => {
expect(result.error).toContain('must be one of');
});

it('accepts failed status', async () => {
it('emits run_failed event when status is failed', async () => {
const runDir = await createRunDir();
const result = await completeRun({ runDir, status: 'failed' });

expect(result.success).toBe(true);

const content = await readFile(join(runDir, 'run-log.jsonl'), 'utf8');
const event: unknown = JSON.parse(content.trim());
expect(event).toMatchObject({ status: 'failed' });
expect(event).toMatchObject({ event: 'run_failed', status: 'failed' });
expect(event).not.toHaveProperty('reason');
});

it('includes reason in run_failed event', async () => {
const runDir = await createRunDir();
const result = await completeRun({ runDir, status: 'failed', reason: 'TypeScript compilation errors' });

expect(result.success).toBe(true);

const content = await readFile(join(runDir, 'run-log.jsonl'), 'utf8');
const event: unknown = JSON.parse(content.trim());
expect(event).toMatchObject({ event: 'run_failed', status: 'failed', reason: 'TypeScript compilation errors' });
});

it('ignores reason when status is not failed', async () => {
const runDir = await createRunDir();
const result = await completeRun({ runDir, status: 'completed', reason: 'should be ignored' });

expect(result.success).toBe(true);

const content = await readFile(join(runDir, 'run-log.jsonl'), 'utf8');
const event: unknown = JSON.parse(content.trim());
expect(event).toMatchObject({ event: 'run_completed', status: 'completed' });
expect(event).not.toHaveProperty('reason');
});

it('accepts needs_manual_review status', async () => {
Expand All @@ -73,7 +97,7 @@ describe('completeRun', () => {

const content = await readFile(join(runDir, 'run-log.jsonl'), 'utf8');
const event: unknown = JSON.parse(content.trim());
expect(event).toMatchObject({ status: 'needs_manual_review' });
expect(event).toMatchObject({ event: 'run_completed', status: 'needs_manual_review' });
});

it('rejects when run-index.json is missing', async () => {
Expand Down
21 changes: 16 additions & 5 deletions packages/mcp/src/tools/complete-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type CompletionStatus = z.infer<typeof completionStatusSchema>;
export interface CompleteRunInput {
runDir: string;
status: string;
reason?: string | undefined;
}

export interface CompleteRunResult {
Expand All @@ -21,11 +22,16 @@ export interface CompleteRunResult {
}

/**
* Complete a run: emit a `run_completed` event and stamp `completedAt` on the
* run-index.json header for fast discovery without reading the JSONL log.
* Complete a run: emit a `run_completed` or `run_failed` event (based on the
* status) and stamp `completedAt` on the run-index.json header for fast
* discovery without reading the JSONL log.
*
* When `status` is `'failed'`, a `run_failed` event is emitted instead of
* `run_completed`. The optional `reason` field is included in the `run_failed`
* event; it is ignored for other statuses.
*/
export async function completeRun(input: CompleteRunInput): Promise<CompleteRunResult> {
const { runDir, status } = input;
const { runDir, status, reason } = input;

const statusResult = completionStatusSchema.safeParse(status);
if (!statusResult.success) {
Expand All @@ -40,10 +46,15 @@ export async function completeRun(input: CompleteRunInput): Promise<CompleteRunR
// Capture timestamp once so the event and the index header are consistent
const now = new Date().toISOString();

// Emit run_completed event
// Emit run_failed for failed status, run_completed otherwise
const event =
validStatus === 'failed'
? { event: 'run_failed', status: validStatus, reason }
: { event: 'run_completed', status: validStatus };

const emitResult = await emitEvent({
runDir,
event: { event: 'run_completed', status: validStatus },
event,
timestamp: now,
});

Expand Down