Skip to main content

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:

export KOSUKE_API_KEY=kosuke_...
jq --version

Authentication says how to create a key.

1. Find the project​

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:

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. Kosuke creates the branch and the sandbox for it.

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:

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 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:

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:

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 explains the flags the preview can answer with.

5. Ask for one more change​

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 or Kimi 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.

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. Open PR lists every flag.

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}'
{ "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 watch replaces the poll loop:

kosuke sessions create "$PROJECT_ID" --content "Add rate limiting to the public search endpoint." > session.json
SESSION_ID=$(jq -r '.data.session.id' session.json)
kosuke sessions watch "$PROJECT_ID" "$SESSION_ID" "$(jq -r '.data.chatJobId' session.json)"
kosuke sessions send "$PROJECT_ID" "$SESSION_ID" --content "/kosuke:open-pr" | 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 or Kimi session it goes by your Default Model's provider instead and sends the message on that model, and it refuses when that is GLM or Kimi too.

Next steps​