Skip to content
Open
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
11 changes: 9 additions & 2 deletions apps/mobile/.kilo/MOBILE_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ Reviewer and verifier invocations must be fresh sessions so earlier conclusions
6. Stop after three repair rounds if findings remain. The main session takes over or asks the user to resolve the underlying ambiguity; never loop indefinitely.
7. Once review has no valid findings, dispatch `mobile-e2e-verifier` with observable acceptance criteria and the intended worktree/service context.
8. Route product failures through implementer and reviewer again. Let the verifier attempt one recovery for environment failures. The main session classifies inconclusive results before deciding whether code should change.
9. The main session performs the final full-diff review and repository-appropriate verification, commits any final narrowly scoped repair, then pushes and creates or updates the PR. Do not squash the work into a catch-all commit unless the user explicitly requests it.
9. The main session performs the final full-diff review and repository-appropriate verification, commits any final narrowly scoped repair, then pushes and creates or updates the PR. Assign the PR to the requesting human. Do not squash the work into a catch-all commit unless the user explicitly requests it.
10. Wait until Kilobot has reviewed the latest head. Fetch every Kilobot review thread, including comments that arrive after earlier repairs, and triage each finding in the main session using the repository-root `AGENTS.md` review-remark workflow.
11. For each valid finding, plan the smallest coherent repair and send that bounded task to `mobile-implementer`. Run the required narrow checks, dispatch a fresh `mobile-reviewer`, and create the smallest coherent commit before pushing. Reply in the original review thread with the concrete fix, then resolve the thread. Reject invalid findings with technical evidence in the same thread instead of changing correct code.
12. Repeat the Kilobot triage, implementer, fresh reviewer, commit, push, reply, and resolution cycle until Kilobot has reviewed the latest head and there are no unresolved actionable Kilobot comments. Preserve the three-repair-round limit for any one finding; the main session takes over or asks the user if that limit is reached.
13. Run local mobile E2E again after Kilobot repairs that affect behavior, build/runtime configuration, or the E2E workflow. Documentation-only or test-only repairs may skip repeated device E2E when the orchestrator records why the previously verified behavior is unaffected.

## Handoff Requirements

Expand All @@ -51,8 +55,9 @@ Every dispatch should include:
- Exact checks or user flows expected for that stage
- Prior findings being addressed, including rejected findings that must not be reopened without new evidence
- The intended commit boundary for the assigned slice
- A prohibition on reading secret-bearing environment files: role agents must not read `.env`, `.env.*`, `.dev.vars`, or equivalent files. Use documented setup commands, sanitized status or manifest output, and sanitized explicit values supplied by the orchestrator instead.

Do not ask a role agent to infer context from the conversation it cannot see. Keep cross-repository changes on coordinated branches or working trees, and give the reviewer and verifier the location of every related diff.
Do not ask a role agent to infer context from the conversation it cannot see. Keep cross-repository changes on coordinated branches or working trees, and give the reviewer and verifier the location of every related diff. Never place secrets or raw environment-file contents in a handoff; provide only the minimum sanitized explicit values required for the task.

## Completion Gate

Expand All @@ -66,3 +71,5 @@ The orchestrator may call the work complete only when:
- E2E acceptance criteria pass, or a documented environment blocker is explicitly accepted by the user
- Final automated checks pass in every changed repository
- The main session has reviewed the complete diff and owns the Git/PR actions
- The PR is assigned to the requesting human
- Kilobot has reviewed the latest head and there are no unresolved actionable Kilobot comments
45 changes: 9 additions & 36 deletions apps/mobile/e2e/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ xcrun simctl openurl <udid> \
"exp+kilo-app://expo-development-client/?url=http%3A%2F%2F<lan-ip>%3A<metro-port>"
```

Before testing, capture the `mobile` pane and verify `Starting project at <this-worktree>/apps/mobile` plus a fresh `iOS Bundled` line. Seeing the Kilo login screen alone does not prove bundle provenance. The login preflight also reads Metro's development manifest and verifies `expoConfig.extra.apiBaseUrl` and `_internal.projectRoot` against this worktree. These endpoint extras come from the Metro manifest in a dev client; after env changes, regenerate env, restart Metro, reconnect the dev client to the exact Metro URL, and reload. Rebuild only when native config/plugins changed. The login helper dismisses the clean-install tracking alert and Expo dev-menu introduction when present.
Before testing, capture the `mobile` pane and verify `Starting project at <this-worktree>/apps/mobile` plus a fresh `iOS Bundled` line. Seeing the Kilo login screen alone does not prove bundle provenance. The login preflight also reads Metro's development manifest and verifies `expoConfig.extra.apiBaseUrl` and `_internal.projectRoot` against this worktree. These endpoint extras come from the Metro manifest in a dev client; after env changes, regenerate env, restart Metro, reconnect the dev client to the exact Metro URL, and reload. Rebuild only when native config/plugins changed. The shared launch flows dismiss the clean-install tracking alert, accept the Expo dev-menu introduction with `Continue`, and then close the full Expo/React Native developer menu containing Fast Refresh and Element Inspector with its `Close` accessibility action.

## Sign In and Out

Expand Down Expand Up @@ -148,41 +148,16 @@ Attach a screenshot of the changed flow to the PR when it helps review. For tran

## Remote CLI Session Flows

Use this only when testing session discovery, mirroring, or mobile-to-CLI messaging. Install the CLI in a disposable directory, never globally:

```bash
CLI_SCRATCH=$(mktemp -d /tmp/kilo-cli.XXXXXX)
npm install --prefix "$CLI_SCRATCH" @kilocode/cli
E2E_EMAIL=${E2E_EMAIL:-e2e-mobile@example.com}
USER_ID=$(pnpm -s dev:seed app:user-id "$E2E_EMAIL" --json | jq -r .userId)
TOKEN=$(NEXTAUTH_SECRET=$(grep '^NEXTAUTH_SECRET=' .env.local | cut -d= -f2- | tr -d '"') \
USER_ID="$USER_ID" node -e '
const crypto = require("crypto");
const b64 = value => Buffer.from(JSON.stringify(value)).toString("base64url");
const header = b64({ alg: "HS256", typ: "JWT" });
const payload = b64({ kiloUserId: process.env.USER_ID, apiTokenPepper: null, version: 3 });
const signature = crypto.createHmac("sha256", process.env.NEXTAUTH_SECRET)
.update(`${header}.${payload}`).digest("base64url");
process.stdout.write(`${header}.${payload}.${signature}`);
')
```

Do not print or log the token. Read the actual Next.js and session-ingest ports from `pnpm dev:status --json`, then run the CLI in its own tmux session:

```bash
CLI_SESSION="kilo-e2e-$(basename "$PWD")"
tmux new-session -d -s "$CLI_SESSION" -c "$PWD"
tmux set-environment -t "$CLI_SESSION" KILO_API_URL http://localhost:<nextjs-port>
tmux set-environment -t "$CLI_SESSION" KILO_SESSION_INGEST_URL http://localhost:<session-ingest-port>
tmux set-environment -t "$CLI_SESSION" KILO_AUTH_CONTENT \
"$(printf '{"kilo":{"type":"api","key":"%s"}}' "$TOKEN")"
tmux set-environment -t "$CLI_SESSION" KILO_REMOTE 1
tmux set-environment -t "$CLI_SESSION" KILO_CLI_BIN "$CLI_SCRATCH/node_modules/.bin/kilo"
tmux new-window -t "$CLI_SESSION" -n cli -c "$PWD" '"$KILO_CLI_BIN"'
Use this only when testing session discovery, mirroring, or mobile-to-CLI messaging. The orchestrator mints the user's local auth token, installs the CLI in a disposable directory, and starts it in a `kilo-e2e-cli-$(basename "$PWD")` tmux session with the required API URLs and bearer-token environment already set. Role agents must not read environment files, accept a bearer token, install the CLI, or run `wrangler` commands. Reuse the orchestrator-prepared session and verify session discovery and mirroring by inspecting its pane and the mobile list:

```bash
CLI_SESSION="kilo-e2e-cli-$(basename "$PWD")"
tmux ls
tmux list-windows -t "$CLI_SESSION"
tmux capture-pane -p -t "$CLI_SESSION":cli -S -100
```

The mobile list updates after the CLI WebSocket connects and its first heartbeat (usually about 12 seconds). Use `tmux send-keys` for automation; slash commands need one Enter for autocomplete and another to submit.
Drive the orchestrator-prepared session with `tmux send-keys`; slash commands need one Enter for autocomplete and another to submit. The mobile list updates after the CLI WebSocket connects and its first heartbeat (usually about 12 seconds). If the orchestrator has not prepared a session for this worktree, stop and ask the orchestrator to install the CLI, mint a token, and start the session before exercising CLI flows.

## Android Emulator

Expand Down Expand Up @@ -224,13 +199,11 @@ The existing login/logout helpers accept either an iOS simulator UDID or an Andr

## Cleanup

Clean up only resources you started:
Clean up only resources you started. The remote CLI session and its disposable install are owned by the orchestrator; do not kill `kilo-e2e-cli-*` sessions or remove CLI scratch directories you did not create:

```bash
tmux kill-session -t "$CLI_SESSION" # if created
tmux kill-session -t "$IOS_BUILD_SESSION" # if created
tmux kill-session -t "$ANDROID_SESSION" # if created
rm -rf "$CLI_SCRATCH" # if created
rm -f "$LOGIN_LOG" # if created
pnpm dev:stop # only if you started this worktree's stack
xcrun simctl shutdown <udid> # only if you booted it
Expand Down
7 changes: 6 additions & 1 deletion apps/mobile/e2e/flows/logout.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
# See e2e/AGENTS.md for the full workflow.
appId: com.kilocode.kiloapp
---
- runFlow:
when:
visible: 'Verify code'
commands:
- tapOn: 'Back'
- runFlow:
when:
notVisible: 'Welcome to Kilo Code'
Expand Down Expand Up @@ -43,4 +48,4 @@ appId: com.kilocode.kiloapp
- tapOn:
text: 'Sign out'
index: 0
- assertVisible: 'Welcome to Kilo Code'
- assertVisible: 'you@example.com'
14 changes: 12 additions & 2 deletions apps/mobile/e2e/flows/open-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ appId: com.kilocode.kiloapp
visible: 'This is the developer menu.*'
commands:
- tapOn: 'Continue'
- runFlow:
when:
visible: 'Fast Refresh|Element Inspector'
commands:
- tapOn: 'Close'
- stopApp
- runFlow:
when:
Expand All @@ -18,10 +23,10 @@ appId: com.kilocode.kiloapp
- tapOn:
text: 'Kilo'
- extendedWaitUntil:
visible: 'Allow “Kilo” to track your activity across other companies’ apps and websites\?|Ask App Not to Track|This is the developer menu.*|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue'
visible: 'Allow “Kilo” to track your activity across other companies’ apps and websites\?|Ask App Not to Track|This is the developer menu.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue'
timeout: 30000
- extendedWaitUntil:
visible: 'Ask App Not to Track|This is the developer menu.*|“Kilo” Would Like to Send You Notifications'
visible: 'Ask App Not to Track|This is the developer menu.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications'
timeout: 3000
optional: true
- runFlow:
Expand All @@ -38,6 +43,11 @@ appId: com.kilocode.kiloapp
visible: 'This is the developer menu.*'
commands:
- tapOn: 'Continue'
- runFlow:
when:
visible: 'Fast Refresh|Element Inspector'
commands:
- tapOn: 'Close'
- runFlow:
when:
visible: '“Kilo” Would Like to Send You Notifications'
Expand Down
9 changes: 7 additions & 2 deletions apps/mobile/e2e/flows/settle-app.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
appId: com.kilocode.kiloapp
---
- extendedWaitUntil:
visible: 'Allow “Kilo” to track your activity across other companies’ apps and websites\?|Ask App Not to Track|This is the developer menu.*|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue'
visible: 'Allow “Kilo” to track your activity across other companies’ apps and websites\?|Ask App Not to Track|This is the developer menu.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue'
timeout: 15000
- extendedWaitUntil:
visible: 'Ask App Not to Track|This is the developer menu.*|“Kilo” Would Like to Send You Notifications'
visible: 'Ask App Not to Track|This is the developer menu.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications'
timeout: 3000
optional: true
- runFlow:
Expand All @@ -17,6 +17,11 @@ appId: com.kilocode.kiloapp
visible: 'This is the developer menu.*'
commands:
- tapOn: 'Continue'
- runFlow:
when:
visible: 'Fast Refresh|Element Inspector'
commands:
- tapOn: 'Close'
- runFlow:
when:
visible: '“Kilo” Would Like to Send You Notifications'
Expand Down
4 changes: 3 additions & 1 deletion dev/local/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
captureServicePane,
} from './tmux';
import { detectLanIp, prepareMobileEnvironment } from './mobile-env';
import { probeDockerApi } from './docker-api-probe';
import {
findRepoRoot,
startServiceInTmux,
Expand Down Expand Up @@ -189,7 +190,8 @@ async function cmdUp(args: string[], repoRoot: string): Promise<void> {
const service = getService(name);
if (service.type !== 'infra' && service.port > 0 && (await probePort(service.port))) {
if (name === 'kiloclaw-docker-tcp') {
reusedHostServices.add(name);
if (await probeDockerApi(service.port)) reusedHostServices.add(name);
else conflictingPorts.push(`${name}:${service.port}`);
} else {
conflictingPorts.push(`${name}:${service.port}`);
}
Expand Down
49 changes: 49 additions & 0 deletions dev/local/docker-api-probe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from 'node:assert/strict';
import * as http from 'node:http';
import test from 'node:test';

import { probeDockerApi } from './docker-api-probe';

async function listen(server: http.Server): Promise<number> {
await new Promise<void>((resolve, reject) => {
server.listen(0, '127.0.0.1', resolve);
server.once('error', reject);
});
const address = server.address();
assert.ok(address && typeof address !== 'string');
return address.port;
}

async function close(server: http.Server): Promise<void> {
await new Promise<void>((resolve, reject) => {
server.close(error => (error ? reject(error) : resolve()));
});
}

test('does not identify an arbitrary TCP listener as the Docker API proxy', async () => {
const server = http.createServer((_request, response) => {
response.end('not docker');
});
const port = await listen(server);

try {
assert.equal(await probeDockerApi(port), false);
} finally {
await close(server);
}
});

test('identifies a ready Docker API listener by its ping response', async () => {
const server = http.createServer((request, response) => {
assert.equal(request.url, '/_ping');
response.setHeader('Api-Version', '1.48');
response.end('OK');
});
const port = await listen(server);

try {
assert.equal(await probeDockerApi(port), true);
} finally {
await close(server);
}
});
14 changes: 14 additions & 0 deletions dev/local/docker-api-probe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export async function probeDockerApi(port: number, timeoutMs = 500): Promise<boolean> {
try {
const response = await fetch(`http://127.0.0.1:${port}/_ping`, {
signal: AbortSignal.timeout(timeoutMs),
});
return (
response.status === 200 &&
response.headers.has('api-version') &&
(await response.text()).trim() === 'OK'
);
} catch {
return false;
}
}
Loading
Loading