# Ship a change end to end

Take one change from prompt to pull request with `curl` and `jq`. You start a chat session, wait for the agent's turn, read the reply and the diff, ask for one more change, and open the pull request. Each step sets the variables the next one uses, so run them in one shell.

## Before you start

Set your API key and check that `jq` is installed:

```bash
export KOSUKE_API_KEY=kosuke_...
jq --version
```

[Authentication](https://kosuke.ai/docs/api/authentication.md) says how to create a key.

## 1. Find the project

```bash
curl -s https://app.kosuke.ai/api/projects \
  -H "Authorization: Bearer $KOSUKE_API_KEY" | jq -r '.data[] | "\(.id)  \(.name)"'
```

Set the id of the project you want:

```bash
PROJECT_ID=$(curl -s https://app.kosuke.ai/api/projects \
  -H "Authorization: Bearer $KOSUKE_API_KEY" | jq -r '.data[] | select(.name == "storefront") | .id')
echo "$PROJECT_ID"
```

## 2. Start a session

The first message starts a turn at once. Sensei creates the branch and the sandbox for it.

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Add rate limiting to the public search endpoint, 60 requests per minute per IP."}' \
  > session.json

SESSION_ID=$(jq -r '.data.session.id' session.json)
CHAT_JOB_ID=$(jq -r '.data.chatJobId' session.json)
echo "https://app.kosuke.ai/projects/$PROJECT_ID/chat-sessions?session=$SESSION_ID"
```

The last line prints the session's link, so a person can watch it in the web app.

## 3. Wait for the turn

Define a function that polls a job until it ends, then run it:

```bash
wait_for_turn() {
  while true; do
    JOB=$(curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat/$1" \
      -H "Authorization: Bearer $KOSUKE_API_KEY")
    STATUS=$(echo "$JOB" | jq -r '.data.job.status')
    case "$STATUS" in
      pending|running) sleep 3 ;;
      completed) echo "completed"; return 0 ;;
      awaiting_response) echo "The agent asked a Clarification. Answer it before you go on."; return 2 ;;
      cancelled) echo "A person stopped this turn. Stop here."; return 1 ;;
      failed) echo "Failed: $(echo "$JOB" | jq -r '.data.job.error')"; return 1 ;;
      *) echo "Unexpected response: $JOB"; return 1 ;;
    esac
  done
}

wait_for_turn "$CHAT_JOB_ID"
```

If the turn ends `awaiting_response`, read the questions and send your answers as a message, as [Follow a turn](https://kosuke.ai/docs/api/turns.md) shows. Then wait for that turn too.

## 4. Read the reply and the diff

The agent's reply is in the text blocks of its last message:

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/messages" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  | jq -r '[.data.messages[] | select(.role == "assistant")] | last | .blocks[] | select(.type == "text") | .content'
```

Reading the diff needs a running sandbox. Request the preview to start the wake. A paused sandbox answers `provisioning: true` at once. Then list the changed files. If the answer carries `unavailable`, wait a few seconds and ask again:

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/preview?sessionId=$SESSION_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" > /dev/null

curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/preview/diff/files" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" | jq '.data.files[] | {path, status, additions, deletions}'
```

[Sandboxes and previews](https://kosuke.ai/docs/api/sandboxes.md) explains the flags the preview can answer with.

## 5. Ask for one more change

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Use a sliding window instead of a fixed one."}' \
  > send.json

jq '.data' send.json
CHAT_JOB_ID=$(jq -r '.data.chatJobId // empty' send.json)
[ -n "$CHAT_JOB_ID" ] && wait_for_turn "$CHAT_JOB_ID"
```

`delivery` is `job` when the message started a turn. It is `queued` when another turn was running, and the message then waits Up next. In that case, wait for the running turn, then read the session's `latestChatJob` for the new job. If the running turn was stopped, failed or ended on a Clarification, Up next pauses instead. No waiting message starts a job until a member resumes Up next or sends a new message, which starts a turn at once and lets Up next carry on after it.

## 6. Open the pull request

Send the `kosuke:open-pr` skill as a message. A Claude, GLM, Kimi or DeepSeek session takes `/kosuke:open-pr`, and a Codex session takes `$kosuke:open-pr`. When the project sets a Target Branch, pass it on with `--target-branch`.

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" > detail.json

TRIGGER=$(jq -r 'if (.data.session.model // "" | startswith("openai:")) then "$" else "/" end' detail.json)
TARGET=$(jq -r '.data.session.prTargetBranch // empty' detail.json)
COMMAND="${TRIGGER}open-pr${TARGET:+ --target-branch $TARGET}"

jq -n --arg content "$COMMAND" '{content: $content}' > open-pr.json

curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @open-pr.json > send.json

CHAT_JOB_ID=$(jq -r '.data.chatJobId // empty' send.json)
[ -n "$CHAT_JOB_ID" ] && wait_for_turn "$CHAT_JOB_ID"
```

The skill commits and pushes the work, writes the title and body, and opens the pull request. Add `-d` to the command to open it as a draft. [/kosuke:open-pr](https://kosuke.ai/docs/skills/open-pr.md) lists every flag.

## 7. Read the pull request link

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" | jq '{prUrl: .data.prUrl, status: .data.session.status}'
```

```json
{ "prUrl": "https://github.com/acme/storefront/pull/482", "status": "under-review" }
```

A pull request opened ready for review moves the session to `under-review`. A draft keeps it `active`.

## With the CLI or the MCP server

The CLI runs the same flow, and `kosuke sessions jobs watch` replaces the poll loop:

```bash
kosuke sessions create "$PROJECT_ID" --content "Add rate limiting to the public search endpoint." --json > session.json
SESSION_ID=$(jq -r '.data.session.id' session.json)
kosuke sessions jobs watch "$PROJECT_ID" "$SESSION_ID" "$(jq -r '.data.chatJobId' session.json)"
kosuke sessions send "$PROJECT_ID" "$SESSION_ID" --content "/kosuke:open-pr" --json | jq '.data'   # $kosuke:open-pr in a Codex session
```

With the MCP server, an agent calls `ship` with the project and the prompt, then `watch`, then `submit`, then `watch` again. `submit` picks `/` or `$` for a Claude or Codex session. For a GLM, Kimi or DeepSeek session it goes by your Default Model's provider instead and sends the message on that model, and it refuses when that is GLM, Kimi or DeepSeek too.

## Next steps

- [Follow a turn](https://kosuke.ai/docs/api/turns.md)
- [Send review comments to the agent](https://kosuke.ai/docs/recipes/send-review-comments-to-the-agent.md)
- [Build an agent on Sensei](https://kosuke.ai/docs/api/build-an-agent.md)
