From f65c6adc27ef03b649358b223249f1a8d8269f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 16 Jul 2026 15:29:49 +0200 Subject: [PATCH 1/7] chore(mobile): harden planner workflow --- apps/mobile/.kilo/MOBILE_WORKFLOW.md | 51 +++-- .../mobile/.kilo/agent/mobile-e2e-verifier.md | 24 ++- .../.kilo/agent/mobile-plan-reviewer.md | 38 ++++ dev/local/mobile-workflow.test.ts | 185 +----------------- 4 files changed, 103 insertions(+), 195 deletions(-) create mode 100644 apps/mobile/.kilo/agent/mobile-plan-reviewer.md diff --git a/apps/mobile/.kilo/MOBILE_WORKFLOW.md b/apps/mobile/.kilo/MOBILE_WORKFLOW.md index 24940598f0..6b95c31d5e 100644 --- a/apps/mobile/.kilo/MOBILE_WORKFLOW.md +++ b/apps/mobile/.kilo/MOBILE_WORKFLOW.md @@ -6,14 +6,33 @@ Work must always be done in a dedicated worktree, regardless of the repository. The initial main session is the planner. After plan approval, a fresh session becomes the orchestrator. Role agents use `kilo/kilo-auto/efficient`. The orchestrator retains product judgment, architecture decisions, loop control, final verification, Git integration, and pull-request ownership. Prefer small, logically scoped commits throughout the flow instead of one final catch-all commit. -Role step limits are hard ceilings: `mobile-implementer` has 80 steps, `mobile-reviewer` has 50, and `mobile-e2e-verifier` has 100. Size every handoff below 75% of its role limit; an implementation slice should normally fit in roughly 60 planned steps. Do not raise a global limit to compensate for an oversized task. +Role step limits are hard ceilings: `mobile-plan-reviewer` has 40 steps, `mobile-implementer` has 80, `mobile-reviewer` has 50, and `mobile-e2e-verifier` has 100. Size every handoff below 75% of its role limit; an implementation slice should normally fit in roughly 60 planned steps. Do not raise a global limit to compensate for an oversized task. + +## Planning Mode + +The planner's first interaction must ask the user whether this run is `hands on` or `hands off`. In every mode, always choose the simplest maintainable implementation that completely satisfies the accepted requirements. Reuse existing code and contracts, avoid unnecessary abstraction, and do not expand scope without evidence that it is required. + +- In `hands on` mode, grill the user one question at a time until the requirements, trade-offs, acceptance criteria, and plan are unambiguous. Obtain explicit plan approval before launching the orchestrator. +- In `hands off` mode, grill itself instead of the user to expose missing requirements, trade-offs, risks, and acceptance criteria. It must not ask the user questions or wait for user approval after mode selection. Treat all approvals as granted, answer from repository evidence and best judgment, choose the simplest maintainable option, and record material assumptions in the plan and handoff. Continue through planning, implementation, review, E2E, PR creation and repair, Kilobot review, conflict resolution, and green CI. Stop only when continuing is technically impossible or unsafe, and return a precise blocker report without asking a question. + +The selected mode applies to the planner, orchestrator, and all later workflow decisions. Hands-off execution does not bypass tool permissions, repository safety rules, or the completion gate. + +## Plan Review Gate + +After writing a complete draft plan and before hands-on user approval or hands-off self-approval, dispatch a fresh `mobile-plan-reviewer`. It reports unclear requirements, unsupported assumptions or claims, missing or conflicting acceptance criteria, incomplete ownership or cross-repository boundaries, infeasible sequencing, and underspecified verification or E2E coverage. + +The reviewer has less context than the planner and may be wrong. Treat every finding as untrusted advice: independently verify every reviewer claim against the request, repository evidence, and applicable instructions. Fix only valid findings, and record rejected findings with a short technical rationale. Never weaken or expand the plan merely to satisfy the reviewer. + +After repairing valid findings, dispatch another fresh reviewer. Repeat until a fresh reviewer returns `No findings.` Stop after three repair rounds for the same issue; the planner takes over, independently resolves and verifies it, records the resolution, and dispatches one final fresh reviewer that must return `No findings.` A rejected finding must not reopen without new evidence. The clean review is required before requesting approval in hands-on mode or granting self-approval in hands-off mode. Hands-off plan review must not ask the user questions or wait for user approval. ## Planner Handoff -The planning session explores requirements, defines acceptance criteria, creates dedicated worktrees, and writes the approved plan. After the user approves the plan, the planning session must not implement it. It prepares a sanitized handoff and launches a fresh orchestrator in a named tmux window. +The planning session explores requirements according to the selected mode, defines acceptance criteria, creates dedicated worktrees, and writes the approved plan. After explicit approval in hands-on mode or self-approval in hands-off mode, the planning session must not implement it. It prepares a sanitized handoff and launches a fresh orchestrator in a named tmux window. The handoff must contain: +- The selected planning mode, with a direct instruction that hands-off execution must not request approval or ask the user questions +- The final plan-review result and rationales for rejected findings - The accepted plan's absolute path and any approved design path - The dedicated worktree path for every repository in scope - Current branch, commit, and working-tree state in each worktree @@ -21,6 +40,7 @@ The handoff must contain: - Existing changes and resources that must be preserved - Required review, E2E, Git, PR, Kilobot, mergeability, and CI completion gates - A direct instruction to continue through a mergeable, conflict-free PR with all expected CI checks green on the latest head +- A direct instruction that every GitHub comment, review, or thread reply must start with `(bot) ` while the PR description remains unprefixed The handoff must not contain secrets or raw environment-file contents. Write only sanitized explicit values to a temporary file outside every repository. Never attach `.env`, `.env.*`, `.dev.vars`, or an equivalent environment-file. @@ -52,9 +72,10 @@ Do not collapse retryable and non-retryable failures into one generic error stat | Agent | Responsibility | Repository edits | |---|---|---| +| `mobile-plan-reviewer` | Reviews a complete draft plan for ambiguity, unsupported claims, and missing execution detail | Denied | | `mobile-implementer` | Implements one bounded task from the accepted plan and runs narrow checks | Allowed where the task requires | | `mobile-reviewer` | Independently reviews the full relevant diff and tests | Denied | -| `mobile-e2e-verifier` | Exercises accepted behavior on local services and a simulator/emulator | Denied | +| `mobile-e2e-verifier` | Exercises accepted behavior and may create temporary state-generation fixtures in verify mode | Temporary, verify mode only | Reviewer and verifier invocations must be fresh sessions so earlier conclusions do not anchor later passes. Role agents do not dispatch other agents or perform commits, pushes, or PR operations. @@ -73,21 +94,25 @@ Two slices are parallel-safe only when their write sets do not overlap, neither ## Orchestration -1. Discuss the request with the user. Inspect affected repositories, agree on acceptance criteria for every feature state in the matrix, produce an implementation plan, and create the execution ledger. +1. Follow the selected planning mode. Inspect affected repositories, establish acceptance criteria for every feature state in the matrix, produce an implementation plan, and create the execution ledger. Discuss decisions and obtain approval from the user only in hands-on mode; in hands-off mode, resolve them from evidence and recorded assumptions without asking questions. 2. Dispatch every ready independent slice in a bounded wave, capped at two or three concurrent implementers. Include all other active slices and ownership boundaries in each handoff. Run only ownership-safe narrow checks while the wave is active. 3. Treat completion of every active implementer as a synchronization barrier. Inspect each result, ownership adherence, and the combined diff. Resolve integration and architecture decisions in the main session, then run shared mutating commands and shared checks once. Preserve successful independent slices when another slice fails. 4. Dispatch one fresh `mobile-reviewer` over the coherent wave. Triage findings in the main session and route valid fixes through a bounded repair wave followed by a fresh reviewer. Record rejected findings with a short rationale. 5. After checks and review pass, create concise logical commits at the ledger's intended per-slice boundaries. Never let a role agent commit. If a slice reaches two budget-exhausted invocations, the orchestrator takes over rather than raising role limits. -6. Stop after three repair rounds for the same issue. The orchestrator takes over or asks the user to resolve the underlying ambiguity; never loop indefinitely. -7. When device E2E is likely, dispatch a separate prewarm-only `mobile-e2e-verifier` concurrently with implementation. It may prepare stable services, claim and label a device, install a baseline native build only when unaffected by active slices, connect the exact Metro URL, and establish login state. It must not judge acceptance behavior while implementation is changing. Retain its resource manifest. -8. After the coherent implementation passes review, dispatch a fresh final `mobile-e2e-verifier` with the prewarm resource manifest. It independently revalidates ownership and provenance, relabels the simulator to `verify`, runs acceptance flows, and owns cleanup. Route product failures through a bounded repair wave and fresh reviewer before another fresh final verifier. +6. Stop after three repair rounds for the same issue. The orchestrator takes over; if the underlying ambiguity remains, ask the user only in hands-on mode or use documented best judgment in hands-off mode. Never loop indefinitely. +7. When device E2E is likely, the orchestrator prewarms infrastructure concurrently with implementation. It may prepare stable services, claim and label a device, install a baseline native build only when unaffected by active slices, connect the exact Metro URL, and establish login state. It must not judge acceptance behavior while implementation is changing. Record a resource manifest for the final verifier. +8. After the coherent implementation passes review, dispatch a fresh final `mobile-e2e-verifier` with the prewarm resource manifest. It independently revalidates ownership and provenance, relabels the simulator to `verify`, runs acceptance flows, and owns cleanup. In verify mode it may make temporary E2E state-generation edits limited to backend mocks, fixtures, deterministic state controls, and test harnesses; it must not alter product behavior under test or conceal a product failure. Final Git state in every repository must exactly match the recorded byte-, mode-, index-, worktree-, and untracked-content baseline. Route product failures through a bounded repair wave and fresh reviewer before another fresh final verifier. 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. +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 `(bot) ` followed by the concrete fix, then resolve the thread. Reject invalid findings in the same thread with `(bot) ` followed by technical evidence 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 if that limit is reached and follows the selected mode for any unresolved decision. 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. 14. Inspect the exact latest PR head SHA, mergeability, merge-state status, expected CI checks, and latest-head Kilobot review. If the base branch advances, integrate the current base in the dedicated worktree, resolve conflicts, rerun affected checks and local E2E, obtain fresh review, and push the new head. Wait again for CI and Kilobot on that exact head. +## GitHub Communication + +Every GitHub issue comment, PR comment, review comment, review body, and thread reply written by the workflow must begin exactly with `(bot) `. This includes replies to Kilobot and technical explanations for rejected findings. PR descriptions are the exception and must not receive the `(bot) ` prefix; PR titles are also not comments. + ## Handoff Requirements Every dispatch should include: @@ -103,6 +128,7 @@ Every dispatch should include: - The intended commit boundary for the assigned slice - Priority order, minimum complete outcome, optional work to drop, and a clean stopping rule before budget exhaustion - Required continuation state: completed work, remaining work, failures, files touched, checks run or deferred, and safest next action +- The GitHub comment rule: every comment, review, and thread reply starts with `(bot) `; the PR description is unprefixed - 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. - A prohibition on committing generated E2E fixtures: E2E fixtures must never be committed. Role agents may create them only in a temporary directory for the current run and must clean them up before returning control. @@ -127,12 +153,14 @@ Do not add persistent telemetry or a new data store for these metrics. The orchestrator may call the work complete only when: +In hands-off mode, the orchestrator must not call the work complete while an E2E blocker remains; an E2E exception must not be self-accepted, and an impossible or unsafe blocker produces a blocked result. + - All accepted plan tasks are implemented - Every applicable feature state has automated behavioral coverage, including required CTA presence or complete CTA absence -- Every safely reproducible feature state has passed E2E verification; exceptions have an explicit rationale accepted by the orchestrator +- Every safely reproducible feature state has passed E2E verification; exceptions require explicit user acceptance in hands-on mode - Changes are organized into small logical commits with each commit internally coherent - A fresh reviewer reports no valid actionable findings -- E2E acceptance criteria pass, or a documented environment blocker is explicitly accepted by the user +- E2E acceptance criteria pass, or a documented environment blocker is explicitly accepted by the user in hands-on mode - 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 @@ -140,3 +168,4 @@ The orchestrator may call the work complete only when: - GitHub reports the exact latest head as mergeable with no conflicts - All expected CI checks on the latest head have reached a successful terminal state; failed, cancelled, timed-out, action-required, or pending checks block completion - Generated E2E fixtures have been cleaned up, and final `git status` confirms none are tracked or untracked in the repository +- Every verifier-created temporary edit has been removed, and each repository's final Git state exactly matches its recorded pre-verification baseline diff --git a/apps/mobile/.kilo/agent/mobile-e2e-verifier.md b/apps/mobile/.kilo/agent/mobile-e2e-verifier.md index 2e78181813..bd5b9bfa8b 100644 --- a/apps/mobile/.kilo/agent/mobile-e2e-verifier.md +++ b/apps/mobile/.kilo/agent/mobile-e2e-verifier.md @@ -4,25 +4,25 @@ mode: subagent model: kilo/kilo-auto/efficient steps: 100 permission: - edit: deny + edit: allow external_directory: allow task: deny + background_process: deny bash: "*": allow "socat": deny "socat *": deny - "git commit*": deny - "git push*": deny + "git *commit*": deny + "git *push*": deny + "/usr/bin/git *commit*": deny + "/usr/bin/git *push*": deny "gh pr*": deny maestro_*: allow --- -You are a read-only E2E verifier for an approved mobile-app change. Repository files are immutable during verification, but you may operate worktree-local services, simulators, emulators, Maestro, disposable CLI installs, temporary files, and test data. +You are an independent final E2E verifier for an approved mobile-app change. You may operate worktree-local services, simulators, emulators, Maestro, disposable CLI installs, temporary files, and test data. This role is verify-only; infrastructure-only preparation belongs to the orchestrator. -The orchestrator must assign exactly one mode: - -- `prewarm`: prepare stable infrastructure concurrently with implementation, claim iOS with `pnpm dev:mobile:simulator claim [udid] --phase prewarm`, and return a resource manifest. Do not judge acceptance behavior while implementation is changing. -- `verify`: independently revalidate all resource and bundle provenance, claim or relabel iOS with `pnpm dev:mobile:simulator claim [udid] --phase verify`, exercise acceptance criteria, and own cleanup. This must be a fresh verifier invocation. +Revalidate all resource and bundle provenance, claim or relabel iOS with `pnpm dev:mobile:simulator claim [udid] --phase verify`, exercise acceptance criteria, and own cleanup. This must be a fresh verifier invocation. The 100-step limit is a hard ceiling. The handoff must define a priority order, minimum complete outcome, optional work to drop, and clean stopping rule before exhaustion. @@ -31,6 +31,7 @@ Before testing: 1. Read `apps/mobile/e2e/AGENTS.md` and all instructions it references. 2. Translate the orchestrator's acceptance criteria into observable happy, retryable unhappy, non-retryable unhappy, and empty flows for every new user-facing feature. 3. Record pre-existing services, listeners, simulators, and tmux sessions so cleanup only removes resources you create. Never use a device claimed by another worktree. +4. Before any temporary edit, create a baseline outside every repository containing `git status --porcelain=v2 -z --untracked-files=all`, binary worktree and index diffs, and an inventory with byte hashes, file modes, and symlink targets for every untracked path. Temporary edits may touch only paths that are clean and tracked at baseline or new paths that did not exist; never touch a pre-existing modified, staged, or untracked path. Copy the original bytes and mode of every tracked path you intend to edit to the external baseline. During verification: @@ -46,7 +47,9 @@ During verification: - Confirm non-retryable states show a meaningful message with no CTA at all. - Inspect backend, session-ingest, CLI, or other service logs when the flow crosses those boundaries. - Capture concise evidence such as screenshots, exact visible state, and bounded log excerpts without exposing credentials. -- Do not edit repository files, fix failures, dispatch subagents, commit, push, or create/update a pull request. +- Temporary uncommitted edits may add backend mocks, fixtures, deterministic state controls, or test harnesses when necessary to produce an acceptance state safely. Use the smallest localized change and record every touched file. +- Temporary edits must not change the product behavior under test, bypass provenance or security checks, or fix or conceal a product failure. If producing a state requires changing the behavior being judged, report the state as blocked instead. +- Temporary edits must never be committed, pushed, included in a pull request, or handed back as implementation. Do not dispatch subagents or create/update a pull request. Classify failures as one of: @@ -56,6 +59,8 @@ Classify failures as one of: Attempt one reasonable recovery for a test-environment failure. If exact-URL recovery still points at stale Metro state, return the failure and process/listener evidence to the orchestrator. Never compensate by changing product code or routing around provenance checks. An unmanaged listener invalidates a `prewarm` handoff. +Before returning for any reason, remove every new temporary path and restore each edited tracked path byte-for-byte with its original mode from the external baseline. Compare final porcelain status, binary worktree diff, binary index diff, and untracked-path content hashes, file modes, and symlink targets with the recorded baseline. Any mismatch or cleanup failure is a verification failure: report every affected file and do not claim acceptance behavior passed. + Return a resource manifest containing: - Worktree path and assigned mode @@ -71,4 +76,5 @@ Also return: - In `verify` mode only, pass/fail/skipped result for every feature state and acceptance criterion, with rationale for each skip - Failure classification, exact reproduction steps, and evidence - Cleanup performed and any resources intentionally left running +- Temporary files edited and evidence that final Git state exactly matches the pre-verification baseline - If stopping early: completed work, remaining work, failures, files or resources touched, checks run or deferred, and safest next action diff --git a/apps/mobile/.kilo/agent/mobile-plan-reviewer.md b/apps/mobile/.kilo/agent/mobile-plan-reviewer.md new file mode 100644 index 0000000000..f679a2af41 --- /dev/null +++ b/apps/mobile/.kilo/agent/mobile-plan-reviewer.md @@ -0,0 +1,38 @@ +--- +description: Reviews a drafted mobile implementation plan for ambiguity, unsupported claims, and missing execution detail +mode: subagent +model: kilo/kilo-auto/efficient +steps: 40 +permission: + edit: deny + external_directory: allow + task: deny + background_process: deny + bash: + "*": deny + "true": allow +--- + +You are an independent, read-only reviewer for a drafted mobile implementation plan. Review the plan and relevant repository evidence, but do not use shell commands, edit files, decide product requirements, or fix findings yourself. + +The 40-step limit is a hard ceiling. The handoff must provide the plan path, requirements, planning mode, repositories and dedicated worktrees in scope, priority order, minimum complete review, and a clean stopping rule before exhaustion. + +Identify: + +- Unclear requirements, unsupported assumptions or claims, and missing or conflicting acceptance criteria +- Missing feature states, non-goals, dependencies, ownership boundaries, or cross-repository contracts +- Infeasible or ambiguous sequencing, unsafe parallel work, and underspecified verification or E2E coverage +- Steps that do not choose the simplest maintainable implementation or that introduce unnecessary scope or abstraction +- Handoffs that omit information an implementer, reviewer, verifier, or orchestrator needs to act without guessing + +Inspect repository evidence for claims that materially affect feasibility or correctness. Do not invent requirements beyond the request, and do not assume that uncertainty is a defect when the plan records a reasonable evidence-backed decision. + +Return findings first, ordered by severity. Every finding must include: + +- Severity: critical, high, medium, or low +- Plan section and relevant repository file or instruction reference +- The unclear, unsupported, conflicting, or missing detail +- The concrete implementation, verification, or product decision that could fail +- The required clarification or evidence, without prescribing unnecessary implementation details + +If there are no actionable findings, return exactly `No findings.` followed by any residual risks. Do not praise or summarize the plan before findings. If you must stop early, return the reviewed scope, remaining scope, evidence inspected, and safest next action. diff --git a/dev/local/mobile-workflow.test.ts b/dev/local/mobile-workflow.test.ts index a75ef48c68..43a298e4e2 100644 --- a/dev/local/mobile-workflow.test.ts +++ b/dev/local/mobile-workflow.test.ts @@ -201,11 +201,9 @@ test('Android workflow uses Maestro first and applies resolved tooling to cached test('iOS workflow uses Maestro first with simctl as the low-level fallback', () => { const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - const verifier = fs.readFileSync('apps/mobile/.kilo/agent/mobile-e2e-verifier.md', 'utf8'); assert.match(runbook, /Use Maestro as the primary iOS automation driver/); assert.match(runbook, /Fall back to `xcrun simctl`/); - assert.match(verifier, /Fall back to `xcrun simctl` on iOS/); }); test('Android tooling rejects a non-Java-17 JAVA_HOME', async () => { @@ -252,204 +250,41 @@ test('workflow documents the shared Docker proxy exception without weakening bac assert.match(cli, /Refusing to share occupied worktree service ports/); }); -test('mobile verifier cannot create host-global Metro proxies', () => { - const verifier = fs.readFileSync('apps/mobile/.kilo/agent/mobile-e2e-verifier.md', 'utf8'); +test('mobile runbook prohibits host-global Metro proxies', () => { const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - assert.match(verifier, /"socat": deny/); - assert.match(verifier, /"socat \*": deny/); - assert.match(verifier, /must not create.*proxies.*listeners/is); assert.match(runbook, /must never map.*8081.*worktree.*Metro/is); assert.match(runbook, /sole intentional host-wide proxy exception.*23750/is); assert.match(runbook, /test-environment failure/i); assert.match(runbook, /PID.*parent PID.*bind address.*port/is); }); -test('mobile verifier uses ownership-aware simulator phase labels', () => { - const verifier = fs.readFileSync('apps/mobile/.kilo/agent/mobile-e2e-verifier.md', 'utf8'); +test('mobile runbook uses ownership-aware simulator phase labels', () => { const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - assert.match(verifier, /--phase (?:prewarm|verify)/); assert.match(runbook, /Kilo E2E - - /); assert.match(runbook, /must not call.*simctl rename/i); assert.match(runbook, /restores the original simulator name/i); }); -test('mobile verifier installs validated cached native builds', () => { - const verifier = fs.readFileSync('apps/mobile/.kilo/agent/mobile-e2e-verifier.md', 'utf8'); +test('mobile runbook installs validated cached native builds', () => { const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - for (const document of [verifier, runbook]) { - assert.match(document, /dev:mobile:ios build /); - assert.match(document, /dev:mobile:android build /); - assert.match(document, /validated cached/i); - } + assert.match(runbook, /dev:mobile:ios build /); + assert.match(runbook, /dev:mobile:android build /); + assert.match(runbook, /validated cached/i); assert.doesNotMatch(runbook, /npx expo run:ios --device/); }); -test('mobile verifier handles the exact Safari external-app prompt within the shared budget', () => { - const verifier = fs.readFileSync('apps/mobile/.kilo/agent/mobile-e2e-verifier.md', 'utf8'); +test('mobile runbook handles the exact Safari external-app prompt within the shared budget', () => { const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - for (const document of [verifier, runbook]) { - assert.match(document, /Open this page in [“"]Kilo[”"]\?/); - assert.match(document, /exact.*Open.*accessibility/is); - assert.match(document, /five-second optional-prompt/i); - } + assert.match(runbook, /Open this page in [“"]Kilo[”"]\?/); + assert.match(runbook, /exact.*Open.*accessibility/is); + assert.match(runbook, /five-second optional-prompt/i); assert.match(runbook, /prefer.*simctl openurl.*avoid.*confirmation/is); }); -test('mobile workflow schedules bounded dependency-aware implementation waves', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - const implementer = fs.readFileSync('apps/mobile/.kilo/agent/mobile-implementer.md', 'utf8'); - - assert.match(workflow, /two or three concurrent implementers/i); - assert.match(workflow, /write set/i); - assert.match(workflow, /producer-consumer dependency/i); - assert.match(workflow, /synchronization barrier/i); - assert.match(workflow, /repository-wide formatters.*serialized/is); - assert.match(implementer, /other active slices/i); - assert.match(implementer, /unexpected changes.*owned paths.*stop/is); - assert.match(implementer, /outside.*owned paths.*continue/is); -}); - -test('mobile workflow sizes handoffs below hard role step limits', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - const implementer = fs.readFileSync('apps/mobile/.kilo/agent/mobile-implementer.md', 'utf8'); - const reviewer = fs.readFileSync('apps/mobile/.kilo/agent/mobile-reviewer.md', 'utf8'); - const verifier = fs.readFileSync('apps/mobile/.kilo/agent/mobile-e2e-verifier.md', 'utf8'); - - assert.match(workflow, /80.*50.*100/s); - assert.match(workflow, /75%/); - assert.match(workflow, /roughly 60 planned steps/i); - for (const role of [implementer, reviewer, verifier]) { - assert.match(role, /minimum complete outcome/i); - assert.match(role, /clean stopping rule/i); - assert.match(role, /safest next action/i); - } -}); - -test('mobile workflow overlaps verifier prewarming with implementation', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - - assert.match(workflow, /prewarm-only.*concurrent.*implementation/is); - assert.match(workflow, /fresh.*final.*mobile-e2e-verifier/is); - assert.match(workflow, /resource manifest/i); - assert.match(workflow, /must not.*acceptance.*implementation.*changing/is); -}); - -test('mobile workflow preserves successful slices and bounds repairs', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - - assert.match(workflow, /preserve successful independent slices/i); - assert.match(workflow, /two budget-exhausted invocations.*orchestrator takes over/is); - assert.match(workflow, /bounded repair wave/i); - assert.match(workflow, /fresh reviewer/i); -}); - -test('mobile workflow reports lightweight elapsed-time and contention metrics', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - - assert.match(workflow, /number and width of implementation waves/i); - assert.match(workflow, /budget exhaustion.*collision counts/is); - assert.match(workflow, /unmanaged listener detections/i); - assert.match(workflow, /prewarm reuse.*invalidation/is); - assert.match(workflow, /accepted-plan-to-final-E2E-start time/i); - assert.match(workflow, /accepted-plan-to-merged-PR time/i); - assert.match(workflow, /do not add persistent telemetry/i); -}); - -test('mobile workflow requires a conflict-free CI-green latest PR head', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - - assert.match(workflow, /mergeable.*no conflicts/i); - assert.match(workflow, /latest head/i); - assert.match(workflow, /CI checks.*successful terminal state/i); - assert.match(workflow, /failed.*cancelled.*timed-out.*pending/is); - assert.match(workflow, /base branch advances/i); - assert.match(workflow, /wait again.*CI.*Kilobot/is); -}); - -test('mobile planner hands implementation to a fresh tmux orchestrator', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - const launchShape = workflow.slice( - workflow.indexOf('```bash\ntmux new-window'), - workflow.indexOf('```', workflow.indexOf('```bash\ntmux new-window') + 7) - ); - - assert.match(workflow, /planning session.*must not implement/is); - assert.match(workflow, /fresh orchestrator/i); - assert.match(workflow, /tmux new-window/); - assert.match(workflow, /named.*tmux window/is); - assert.match(workflow, /-c \/apps\/mobile/); - assert.match(workflow, /kilo run --interactive/); - assert.match(workflow, /--model kilo\/kilo-auto\/frontier/); - assert.doesNotMatch(launchShape, /--variant/); - assert.doesNotMatch(launchShape, /(?:--continue|--session)/); -}); - -test('mobile planner handoff is complete and secret-free', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - const handoffSection = workflow.slice( - workflow.indexOf('## Planner Handoff'), - workflow.indexOf('## Feature State Matrix') - ); - - assert.match(handoffSection, /accepted plan.*absolute path/is); - assert.match(handoffSection, /dedicated worktree path.*every repository/is); - assert.match(handoffSection, /current branch.*working-tree state/is); - assert.match(handoffSection, /acceptance criteria.*execution ledger/is); - assert.match(handoffSection, /continue through.*mergeable.*CI/is); - assert.match(handoffSection, /must not contain.*secrets.*environment-file/is); -}); - -test('mobile workflow owns PR assignment and the post-PR Kilobot repair loop', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - - assert.match(workflow, /assign(?:s|ed)? the PR to the requesting human/i); - assert.match(workflow, /Kilobot has reviewed the latest head/i); - assert.match(workflow, /no unresolved actionable Kilobot comments/i); - assert.match(workflow, /plan the smallest coherent repair/i); - assert.match(workflow, /mobile-implementer/); - assert.match(workflow, /fresh `mobile-reviewer`/); - assert.match(workflow, /smallest coherent commit/i); - assert.match(workflow, /reply in the original review thread/i); - assert.match(workflow, /resolve the thread/i); - assert.match(workflow, /local mobile E2E/i); -}); - -test('mobile workflow keeps secret-bearing environment files out of subagent context', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - assert.match(workflow, /must not read .*\.env/i); - assert.match(workflow, /\.dev\.vars/); - assert.match(workflow, /sanitized explicit values/i); - assert.doesNotMatch(runbook, /grep .*\.env/); -}); - -test('mobile workflow keeps generated E2E fixtures out of Git', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - for (const document of [workflow, runbook]) { - assert.match(document, /E2E fixtures must never be committed/i); - assert.match(document, /temporary directory/i); - assert.match(document, /clean(?:ed)? up/i); - } - assert.match(workflow, /git status/i); -}); - -test('mobile workflow requires dedicated worktrees in every repository', () => { - const workflow = fs.readFileSync('apps/mobile/.kilo/MOBILE_WORKFLOW.md', 'utf8'); - - assert.match(workflow, /Work must always be done in a dedicated worktree/i); - assert.match(workflow, /regardless of (?:the )?repository/i); - assert.match(workflow, /sibling repositories/i); - assert.match(workflow, /must not edit the primary checkout/i); - assert.match(workflow, /(?:or|and) the main checkout/i); -}); - test('remote CLI runbook is secret-free and defers credential-bearing setup to the orchestrator', () => { const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); const remoteCliSection = runbook.slice( From a6e1b7f8ea13751100c1fb4443bb7a8da2407667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 16 Jul 2026 16:04:02 +0200 Subject: [PATCH 2/7] fix(mobile): handle iOS external app prompt --- apps/mobile/e2e/flows/open-app.yaml | 23 ++++- apps/mobile/e2e/flows/settle-app.yaml | 21 +++- dev/local/mobile-workflow.test.ts | 135 +++++++++++++++++++++++--- 3 files changed, 161 insertions(+), 18 deletions(-) diff --git a/apps/mobile/e2e/flows/open-app.yaml b/apps/mobile/e2e/flows/open-app.yaml index 0d0c980354..c3f54667d5 100644 --- a/apps/mobile/e2e/flows/open-app.yaml +++ b/apps/mobile/e2e/flows/open-app.yaml @@ -23,31 +23,44 @@ 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.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue' + visible: 'Open this page in "Kilo"\?|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.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications' + visible: 'Open this page in "Kilo"\?|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: + when: + visible: 'Open this page in "Kilo"\?' + commands: + - tapOn: 'Open' +- extendedWaitUntil: + visible: '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: 5000 - runFlow: when: visible: 'Ask App Not to Track' commands: - tapOn: 'Ask App Not to Track' - extendedWaitUntil: - visible: 'This is the developer menu.*' - timeout: 2000 - optional: true + visible: '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: 5000 - runFlow: when: visible: 'This is the developer menu.*' commands: - tapOn: 'Continue' +- extendedWaitUntil: + visible: '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: 5000 - runFlow: when: visible: 'Fast Refresh|Element Inspector' commands: - tapOn: 'Close' +- extendedWaitUntil: + visible: '“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue' + timeout: 5000 - runFlow: when: visible: '“Kilo” Would Like to Send You Notifications' diff --git a/apps/mobile/e2e/flows/settle-app.yaml b/apps/mobile/e2e/flows/settle-app.yaml index d5f75b4207..41720a49cb 100644 --- a/apps/mobile/e2e/flows/settle-app.yaml +++ b/apps/mobile/e2e/flows/settle-app.yaml @@ -1,27 +1,44 @@ 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.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue' + visible: 'Open this page in "Kilo"\?|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.*|Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications' + visible: 'Open this page in "Kilo"\?|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: + when: + visible: 'Open this page in "Kilo"\?' + commands: + - tapOn: 'Open' +- extendedWaitUntil: + visible: '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: 5000 - runFlow: when: visible: 'Ask App Not to Track' commands: - tapOn: 'Ask App Not to Track' +- extendedWaitUntil: + visible: '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: 5000 - runFlow: when: visible: 'This is the developer menu.*' commands: - tapOn: 'Continue' +- extendedWaitUntil: + visible: '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: 5000 - runFlow: when: visible: 'Fast Refresh|Element Inspector' commands: - tapOn: 'Close' +- extendedWaitUntil: + visible: '“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue' + timeout: 5000 - runFlow: when: visible: '“Kilo” Would Like to Send You Notifications' diff --git a/dev/local/mobile-workflow.test.ts b/dev/local/mobile-workflow.test.ts index 43a298e4e2..3749b5f526 100644 --- a/dev/local/mobile-workflow.test.ts +++ b/dev/local/mobile-workflow.test.ts @@ -15,8 +15,8 @@ test('login waits for the delayed Expo developer menu after launching Kilo', () assert.ok(launchIndex >= 0); assert.ok(developerMenuWaitIndex > launchIndex); assert.ok(continueIndex > developerMenuWaitIndex); - assert.match(flow.slice(optionalWaitIndex, continueIndex), /timeout: 2000/); - assert.match(flow.slice(optionalWaitIndex, continueIndex), /optional: true/); + assert.match(flow.slice(optionalWaitIndex, continueIndex), /timeout: 5000/); + assert.doesNotMatch(flow.slice(optionalWaitIndex, continueIndex), /optional: true/); }); test('shared launch flows close the Expo developer menu after its introduction', () => { @@ -98,15 +98,129 @@ test('login polls the local outbox without one-second latency', () => { }); test('shared launch prompt grace periods total at most five seconds', () => { - const flow = fs.readFileSync('apps/mobile/e2e/flows/open-app.yaml', 'utf8'); - const optionalWaits = [...flow.matchAll(/timeout: (\d+)\n\s+optional: true/g)].map(match => - Number(match[1]) - ); + for (const flowPath of [ + 'apps/mobile/e2e/flows/open-app.yaml', + 'apps/mobile/e2e/flows/settle-app.yaml', + ]) { + const flow = fs.readFileSync(flowPath, 'utf8'); + const optionalWaits = [...flow.matchAll(/timeout: (\d+)\n\s+optional: true/g)].map(match => + Number(match[1]) + ); - assert.equal( - optionalWaits.reduce((total, timeout) => total + timeout, 0), - 5000 - ); + assert.deepEqual(optionalWaits, [3000]); + assert.ok(optionalWaits.reduce((total, timeout) => total + timeout, 0) <= 5000); + } +}); + +test('shared launch flows handle the exact iOS external-app prompt within existing waits', () => { + const flowContracts = [ + { + path: 'apps/mobile/e2e/flows/open-app.yaml', + timeouts: [30000, 3000, 5000, 5000, 5000, 5000, 30000], + }, + { + path: 'apps/mobile/e2e/flows/settle-app.yaml', + timeouts: [15000, 3000, 5000, 5000, 5000, 5000, 15000], + }, + ]; + + for (const contract of flowContracts) { + const flow = fs.readFileSync(contract.path, 'utf8'); + const promptGuardIndex = flow.indexOf(`visible: 'Open this page in "Kilo"\\?'`); + const openActionIndex = flow.indexOf("tapOn: 'Open'", promptGuardIndex); + const finalReadyWaitIndex = flow.lastIndexOf('- extendedWaitUntil:'); + const waitBlocks = [...flow.matchAll(/- extendedWaitUntil:\n[\s\S]*?(?=\n- |$)/g)].map( + match => match[0] + ); + const timeouts = [...flow.matchAll(/timeout: (\d+)/g)].map(match => Number(match[1])); + + assert.match( + waitBlocks[0], + /Open this page in "Kilo"\\\?/, + `${contract.path} should recognize the prompt as its initial visible state` + ); + assert.match( + waitBlocks[1], + /Open this page in "Kilo"\\\?/, + `${contract.path} should recognize the prompt inside its optional wait` + ); + assert.match(waitBlocks[1], /timeout: 3000\n\s+optional: true/); + assert.ok( + promptGuardIndex > flow.indexOf(waitBlocks[1]), + `${contract.path} should guard the Open action` + ); + assert.ok( + openActionIndex > promptGuardIndex, + `${contract.path} should tap the exact Open action` + ); + const promptChain = [ + { + action: "tapOn: 'Open'", + nextGuard: 'Ask App Not to Track', + visible: + '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', + }, + { + action: "tapOn: 'Ask App Not to Track'", + nextGuard: 'This is the developer menu.*', + visible: + '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', + }, + { + action: "tapOn: 'Continue'", + nextGuard: 'Fast Refresh|Element Inspector', + visible: + 'Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue', + }, + { + action: "tapOn: 'Close'", + nextGuard: '“Kilo” Would Like to Send You Notifications', + visible: + '“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue', + }, + ]; + + let searchFrom = openActionIndex; + for (const step of promptChain) { + const actionIndex = flow.indexOf(step.action, searchFrom); + const nextGuardIndex = flow.indexOf( + `- runFlow:\n when:\n visible: '${step.nextGuard}'`, + actionIndex + ); + const betweenActionAndGuard = flow.slice(actionIndex, nextGuardIndex); + + assert.ok(actionIndex >= searchFrom, `${contract.path} should include ${step.action}`); + assert.ok( + nextGuardIndex > actionIndex, + `${contract.path} should guard ${step.nextGuard} after ${step.action}` + ); + assert.match( + betweenActionAndGuard, + new RegExp( + `- extendedWaitUntil:\\n visible: '${step.visible.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'\\n timeout: 5000` + ), + `${contract.path} should poll for the next state immediately after ${step.action}` + ); + assert.doesNotMatch( + betweenActionAndGuard, + /timeout: (?:500|1000)\n|optional: true/, + `${contract.path} should use a robust non-optional state gate after ${step.action}` + ); + assert.doesNotMatch( + betweenActionAndGuard.replace(/^- runFlow:[\s\S]*?commands:\n\s+- tapOn: '[^']+'/, ''), + /- runFlow:/, + `${contract.path} should not insert another one-shot guard before polling` + ); + searchFrom = nextGuardIndex; + } + assert.ok( + finalReadyWaitIndex > openActionIndex, + `${contract.path} should still wait for its final ready state` + ); + assert.deepEqual(timeouts, contract.timeouts, `${contract.path} should not add a fixed wait`); + assert.doesNotMatch(flow, /visible: '(?:Allow|Open)'/); + assert.doesNotMatch(flow, /tapOn: '(?:Allow\|Open|Open\|Allow)'/); + } }); test('helper-driven logout settles the app already launched by preflight', () => { @@ -116,7 +230,6 @@ test('helper-driven logout settles the app already launched by preflight', () => assert.match(logout, /settle-app\.yaml/); assert.doesNotMatch(logout, /open-app\.yaml/); assert.doesNotMatch(settle, /stopApp|text: 'Kilo'/); - assert.match(settle, /timeout: 3000/); }); test('logout skips prompt settling for stable signed-in and signed-out states', () => { From 3ada55332369e305caa924c77ab789607adf7038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 16 Jul 2026 16:36:36 +0200 Subject: [PATCH 3/7] fix(mobile): prefer iPhone simulators for E2E --- dev/local/mobile-simulator.test.ts | 171 ++++++++++++++++++++++++++++- dev/local/mobile-simulator.ts | 68 ++++++++++-- 2 files changed, 226 insertions(+), 13 deletions(-) diff --git a/dev/local/mobile-simulator.test.ts b/dev/local/mobile-simulator.test.ts index 54753ef046..4ee26cab44 100644 --- a/dev/local/mobile-simulator.test.ts +++ b/dev/local/mobile-simulator.test.ts @@ -8,6 +8,7 @@ import { bootSimulator, buildSimulatorLabel, claimSimulator, + listIosDevices, parseClaimArgs, releaseSimulator, type SimulatorDevice, @@ -48,10 +49,176 @@ function recordingOutput( } const devices: SimulatorDevice[] = [ - { id: 'A', name: 'Kilo E2E-A', state: 'Booted' }, - { id: 'B', name: 'Kilo E2E-B', state: 'Shutdown' }, + { + id: 'A', + name: 'Kilo E2E-A', + state: 'Booted', + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro', + }, + { + id: 'B', + name: 'Kilo E2E-B', + state: 'Shutdown', + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-16', + }, ]; +test('implicitly claims a shutdown iPhone before a booted iPhone and skips iPads', () => { + const lockRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-simulator-locks-')); + const worktreeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-worktree-')); + const typedDevices = [ + { + id: 'MALFORMED', + name: 'Malformed device', + state: 'Shutdown', + deviceTypeIdentifier: 'invalid.com.apple.CoreSimulator.SimDeviceType.iPhone-16', + }, + { + id: 'IPAD', + name: 'iPad Pro (11-inch) (M4)', + state: 'Shutdown', + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4', + }, + { + id: 'BOOTED-IPHONE', + name: 'iPhone 16 Pro', + state: 'Booted', + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro', + }, + { + id: 'SHUTDOWN-IPHONE', + name: 'iPhone 16', + state: 'Shutdown', + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-16', + }, + ]; + + try { + const claim = claimSimulator({ devices: typedDevices, lockRoot, worktreeRoot }); + + assert.equal(claim.device.id, 'SHUTDOWN-IPHONE'); + } finally { + fs.rmSync(lockRoot, { recursive: true, force: true }); + fs.rmSync(worktreeRoot, { recursive: true, force: true }); + } +}); + +test('explicitly claims a requested iPad', () => { + const lockRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-simulator-locks-')); + const worktreeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-worktree-')); + const typedDevices = [ + { + id: 'IPAD', + name: 'iPad Pro (11-inch) (M4)', + state: 'Shutdown', + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4', + }, + ]; + + try { + const claim = claimSimulator({ + devices: typedDevices, + lockRoot, + worktreeRoot, + requestedId: 'IPAD', + }); + + assert.equal(claim.device.id, 'IPAD'); + } finally { + fs.rmSync(lockRoot, { recursive: true, force: true }); + fs.rmSync(worktreeRoot, { recursive: true, force: true }); + } +}); + +test('explicitly claims a valid requested simulator without family metadata', () => { + const lockRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-simulator-locks-')); + const worktreeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-worktree-')); + + try { + const claim = claimSimulator({ + devices: [{ id: 'UNKNOWN-FAMILY', name: 'Unknown family', state: 'Shutdown' }], + lockRoot, + worktreeRoot, + requestedId: 'UNKNOWN-FAMILY', + }); + + assert.equal(claim.device.id, 'UNKNOWN-FAMILY'); + } finally { + fs.rmSync(lockRoot, { recursive: true, force: true }); + fs.rmSync(worktreeRoot, { recursive: true, force: true }); + } +}); + +test('preserves deviceTypeIdentifier from simctl device listings', () => { + const deviceTypeIdentifier = 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro'; + const exec = () => + JSON.stringify({ + devices: { + 'com.apple.CoreSimulator.SimRuntime.iOS-18-5': [ + { + udid: 'IPHONE', + name: 'iPhone 16 Pro', + state: 'Shutdown', + deviceTypeIdentifier, + }, + ], + }, + }); + + assert.deepEqual(listIosDevices(exec), [ + { + id: 'IPHONE', + name: 'iPhone 16 Pro', + state: 'Shutdown', + deviceTypeIdentifier, + }, + ]); +}); + +for (const [description, deviceTypeIdentifier] of [ + ['missing', undefined], + ['null', null], + ['numeric', 16], +] as const) { + test(`implicit claims skip ${description} simctl device type metadata and continue to an iPhone`, () => { + const lockRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-simulator-locks-')); + const worktreeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-worktree-')); + const malformedDevice = { + udid: `INVALID-${description.toUpperCase()}`, + name: `Invalid ${description}`, + state: 'Shutdown', + ...(deviceTypeIdentifier === undefined ? {} : { deviceTypeIdentifier }), + }; + const exec = () => + JSON.stringify({ + devices: { + 'com.apple.CoreSimulator.SimRuntime.iOS-18-5': [ + malformedDevice, + { + udid: 'VALID-IPHONE', + name: 'iPhone 16 Pro', + state: 'Booted', + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro', + }, + ], + }, + }); + + try { + const claim = claimSimulator({ + devices: listIosDevices(exec), + lockRoot, + worktreeRoot, + }); + + assert.equal(claim.device.id, 'VALID-IPHONE'); + } finally { + fs.rmSync(lockRoot, { recursive: true, force: true }); + fs.rmSync(worktreeRoot, { recursive: true, force: true }); + } + }); +} + test('claims an unowned simulator instead of sharing another worktree simulator', () => { const lockRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-simulator-locks-')); const one = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-worktree-one-')); diff --git a/dev/local/mobile-simulator.ts b/dev/local/mobile-simulator.ts index ce6fc9bae5..856927b0c2 100644 --- a/dev/local/mobile-simulator.ts +++ b/dev/local/mobile-simulator.ts @@ -6,7 +6,12 @@ import path from 'node:path'; import { withProcessLock } from './process-lock'; -type SimulatorDevice = { id: string; name: string; state: string }; +type SimulatorDevice = { + id: string; + name: string; + state: string; + deviceTypeIdentifier?: string; +}; type ExecFn = ( command: string, args: readonly string[], @@ -558,7 +563,13 @@ function claimSimulator(args: ClaimArgs): { device: SimulatorDevice; alreadyOwne fs.mkdirSync(lockRoot, { recursive: true }); const candidates = requestedId ? devices.filter(device => device.id === requestedId) - : [...devices].sort((a, b) => Number(a.state === 'Booted') - Number(b.state === 'Booted')); + : devices + .filter( + device => + typeof device.deviceTypeIdentifier === 'string' && + device.deviceTypeIdentifier.startsWith('com.apple.CoreSimulator.SimDeviceType.iPhone-') + ) + .sort((a, b) => Number(a.state === 'Booted') - Number(b.state === 'Booted')); if (candidates.length === 0) throw new Error(`Simulator ${requestedId ?? ''} is not available`.trim()); @@ -955,16 +966,44 @@ function releaseSimulator(args: { }); } -function listIosDevices(): SimulatorDevice[] { - const raw = JSON.parse( - execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '--json'], { +function listIosDevices(exec: ExecFn = execFileSync): SimulatorDevice[] { + const parsed: unknown = JSON.parse( + exec('xcrun', ['simctl', 'list', 'devices', 'available', '--json'], { encoding: 'utf8', - }) - ) as { devices: Record> }; - return Object.entries(raw.devices) + }).toString() + ); + if (typeof parsed !== 'object' || parsed === null) return []; + const devicesByRuntime = (parsed as Record).devices; + if (typeof devicesByRuntime !== 'object' || devicesByRuntime === null) return []; + + return Object.entries(devicesByRuntime) .filter(([runtime]) => runtime.includes('.iOS-')) - .flatMap(([, devices]) => devices) - .map(device => ({ id: device.udid, name: device.name, state: device.state })); + .flatMap(([, devices]) => (Array.isArray(devices) ? devices : [])) + .flatMap(device => { + if (typeof device !== 'object' || device === null) return []; + const record = device as Record; + if ( + typeof record.udid !== 'string' || + record.udid.length === 0 || + typeof record.name !== 'string' || + record.name.length === 0 || + typeof record.state !== 'string' || + record.state.length === 0 + ) { + return []; + } + return [ + { + id: record.udid, + name: record.name, + state: record.state, + deviceTypeIdentifier: + typeof record.deviceTypeIdentifier === 'string' + ? record.deviceTypeIdentifier + : undefined, + }, + ]; + }); } // Production rename: `xcrun simctl rename `. Throws on @@ -1043,5 +1082,12 @@ if (isMain) { } } -export { bootSimulator, buildSimulatorLabel, claimSimulator, parseClaimArgs, releaseSimulator }; +export { + bootSimulator, + buildSimulatorLabel, + claimSimulator, + listIosDevices, + parseClaimArgs, + releaseSimulator, +}; export type { ClaimRecord, ClaimStatus, RenameFn, SimulatorDevice, SimulatorPhase }; From 06c91fe7f2f437a79f77f8407296f3202336870c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 16 Jul 2026 16:58:54 +0200 Subject: [PATCH 4/7] fix(mobile): skip store IAP on Android --- .../use-store-kilo-pass-purchase.test.tsx | 118 +++++++++++++----- .../kilo-pass/use-store-kilo-pass-purchase.ts | 28 +++++ 2 files changed, 112 insertions(+), 34 deletions(-) diff --git a/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.test.tsx b/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.test.tsx index b1734c59d7..fa52b58d1e 100644 --- a/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.test.tsx +++ b/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.test.tsx @@ -24,8 +24,11 @@ const mockedIap = vi.hoisted(() => ({ } | null, requestPurchase: vi.fn(), restorePurchases: vi.fn(), + useIAP: vi.fn(), })); +const mockedPlatform = vi.hoisted(() => ({ OS: 'ios' })); + const mockedReactQuery = vi.hoisted(() => ({ completeAppStorePurchase: vi.fn(), completeAppStorePurchaseIsPending: false, @@ -34,8 +37,13 @@ const mockedReactQuery = vi.hoisted(() => ({ appAccountToken: '550e8400-e29b-41d4-a716-446655440000', products: [{ appleProductId: 'com.kilo.pass.tier19.monthly' }], }, + useMutation: vi.fn(), + useQuery: vi.fn(), + useQueryClient: vi.fn(), })); +const mockedTrpc = vi.hoisted(() => ({ useTRPC: vi.fn() })); + vi.mock('expo-iap', () => ({ ErrorCode: { AlreadyOwned: 'already-owned', @@ -45,33 +53,42 @@ vi.mock('expo-iap', () => ({ useIAP: (handlers: { onPurchaseError: (error: Error) => void; onPurchaseSuccess: (purchase: Purchase) => void; - }) => ({ - availablePurchases: mockedIap.availablePurchases, - connected: mockedIap.connected, - finishTransaction: mockedIap.finishTransaction, - getAvailablePurchases: mockedIap.getAvailablePurchases, - requestPurchase: mockedIap.requestPurchase, - restorePurchases: mockedIap.restorePurchases, - ...(() => { - mockedIap.handlers = handlers; - return {}; - })(), - }), + }) => { + mockedIap.useIAP(handlers); + mockedIap.handlers = handlers; + return { + availablePurchases: mockedIap.availablePurchases, + connected: mockedIap.connected, + finishTransaction: mockedIap.finishTransaction, + getAvailablePurchases: mockedIap.getAvailablePurchases, + requestPurchase: mockedIap.requestPurchase, + restorePurchases: mockedIap.restorePurchases, + }; + }, })); vi.mock('react-native', () => ({ - Platform: { OS: 'ios' }, + Platform: mockedPlatform, })); vi.mock('@tanstack/react-query', () => ({ - useMutation: () => ({ - isPending: mockedReactQuery.completeAppStorePurchaseIsPending, - mutateAsync: mockedReactQuery.completeAppStorePurchase, - }), - useQuery: () => ({ - data: mockedReactQuery.mobileStoreProductsData, - }), - useQueryClient: () => ({ invalidateQueries: mockedReactQuery.invalidateQueries }), + useMutation: () => { + mockedReactQuery.useMutation(); + return { + isPending: mockedReactQuery.completeAppStorePurchaseIsPending, + mutateAsync: mockedReactQuery.completeAppStorePurchase, + }; + }, + useQuery: () => { + mockedReactQuery.useQuery(); + return { + data: mockedReactQuery.mobileStoreProductsData, + }; + }, + useQueryClient: () => { + mockedReactQuery.useQueryClient(); + return { invalidateQueries: mockedReactQuery.invalidateQueries }; + }, })); vi.mock('sonner-native', () => ({ @@ -86,18 +103,21 @@ vi.mock('@/lib/analytics/posthog', () => ({ })); vi.mock('@/lib/trpc', () => ({ - useTRPC: () => ({ - kiloPass: { - completeAppStorePurchase: { mutationOptions: () => ({}) }, - getCreditHistory: { pathFilter: () => ({ queryKey: ['credit-history'] }) }, - getMobileStoreProducts: { queryOptions: () => ({ queryKey: ['mobile-products'] }) }, - getState: { pathFilter: () => ({ queryKey: ['state'] }) }, - }, - user: { - getContextBalance: { pathFilter: () => ({ queryKey: ['balance'] }) }, - getCreditBlocks: { pathFilter: () => ({ queryKey: ['credits'] }) }, - }, - }), + useTRPC: () => { + mockedTrpc.useTRPC(); + return { + kiloPass: { + completeAppStorePurchase: { mutationOptions: () => ({}) }, + getCreditHistory: { pathFilter: () => ({ queryKey: ['credit-history'] }) }, + getMobileStoreProducts: { queryOptions: () => ({ queryKey: ['mobile-products'] }) }, + getState: { pathFilter: () => ({ queryKey: ['state'] }) }, + }, + user: { + getContextBalance: { pathFilter: () => ({ queryKey: ['balance'] }) }, + getCreditBlocks: { pathFilter: () => ({ queryKey: ['credits'] }) }, + }, + }; + }, })); type StoreKiloPassPurchaseContextValue = { @@ -110,6 +130,7 @@ type StoreKiloPassPurchaseContextValue = { isPending: boolean; isRestoringPurchases: boolean; errorMessage: string | null; + clearError: () => void; }; type ReactInternals = { @@ -186,7 +207,13 @@ function renderStoreKiloPassPurchaseProvider() { try { const renderProviderElement = StoreKiloPassPurchaseProvider; const providerElement = renderProviderElement({ children: null }); - return providerElement.props.value as StoreKiloPassPurchaseContextValue; + const contextProviderElement = + 'value' in providerElement.props + ? providerElement + : (providerElement.type as (props: { children: React.ReactNode }) => React.ReactElement)( + providerElement.props + ); + return (contextProviderElement.props as { value: StoreKiloPassPurchaseContextValue }).value; } finally { reactInternals.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H = previousDispatcher; @@ -307,6 +334,7 @@ beforeEach(() => { vi.clearAllMocks(); resetPurchaseErrorToastDedup(); resetInlinePurchaseErrorOwnership(); + mockedPlatform.OS = 'ios'; mockedIap.availablePurchases = []; mockedIap.connected = false; mockedIap.finishTransaction.mockResolvedValue(undefined); @@ -792,6 +820,28 @@ describe('createAppStoreKiloPassPurchaseActions', () => { }); describe('StoreKiloPassPurchaseProvider', () => { + it('provides inert purchase state without initializing expo-iap on Android', async () => { + mockedPlatform.OS = 'android'; + const provider = renderStoreKiloPassPurchaseProvider(); + + const value = provider.render(); + + expect(mockedIap.useIAP).not.toHaveBeenCalled(); + expect(mockedTrpc.useTRPC).not.toHaveBeenCalled(); + expect(mockedReactQuery.useQueryClient).not.toHaveBeenCalled(); + expect(mockedReactQuery.useMutation).not.toHaveBeenCalled(); + expect(mockedReactQuery.useQuery).not.toHaveBeenCalled(); + expect(value.appStoreOwnershipPreflight).toBeNull(); + expect(value.isPending).toBe(false); + expect(value.isRestoringPurchases).toBe(false); + expect(value.errorMessage).toBeNull(); + await expect(value.purchase(product)).resolves.toBeUndefined(); + await expect(value.restorePurchases()).resolves.toBe('failed'); + expect(() => { + value.clearError(); + }).not.toThrow(); + }); + it('exposes an App Store ownership mismatch preflight from available purchases', () => { mockedIap.availablePurchases = [ createPurchase({ appAccountToken: '550e8400-e29b-41d4-a716-446655440001' }), diff --git a/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts b/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts index 783dd6a31f..b87ddd94ea 100644 --- a/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts +++ b/apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts @@ -96,6 +96,22 @@ type StoreKiloPassPurchaseContextValue = { }; const StoreKiloPassPurchaseContext = createContext(null); + +const androidStoreKiloPassPurchaseValue: StoreKiloPassPurchaseContextValue = { + appStoreOwnershipPreflight: null, + purchase: async () => { + await Promise.resolve(); + }, + restorePurchases: async () => { + await Promise.resolve(); + return 'failed'; + }, + isPending: false, + isRestoringPurchases: false, + errorMessage: null, + clearError: () => undefined, +}; + type PurchaseCompletionResult = | { completed: true; errorMessage?: never } | { completed: false; errorMessage: string | null }; @@ -366,6 +382,18 @@ export function createAppStoreKiloPassPurchaseActions(deps: AppStoreKiloPassPurc } export function StoreKiloPassPurchaseProvider({ children }: { children: ReactNode }) { + if (Platform.OS !== 'ios') { + return createElement( + StoreKiloPassPurchaseContext.Provider, + { value: androidStoreKiloPassPurchaseValue }, + children + ); + } + + return createElement(IosStoreKiloPassPurchaseProvider, null, children); +} + +function IosStoreKiloPassPurchaseProvider({ children }: { children: ReactNode }) { const trpc = useTRPC(); const queryClient = useQueryClient(); const [isRequestingPurchase, setIsRequestingPurchase] = useState(false); From a5853fc254c91ed128ac5b6642a3e66640cd40be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 16 Jul 2026 18:13:19 +0200 Subject: [PATCH 5/7] chore(mobile): skip redundant reruns on clean base sync --- apps/mobile/.kilo/MOBILE_WORKFLOW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/.kilo/MOBILE_WORKFLOW.md b/apps/mobile/.kilo/MOBILE_WORKFLOW.md index 6b95c31d5e..0cd9c58385 100644 --- a/apps/mobile/.kilo/MOBILE_WORKFLOW.md +++ b/apps/mobile/.kilo/MOBILE_WORKFLOW.md @@ -107,7 +107,7 @@ Two slices are parallel-safe only when their write sets do not overlap, neither 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 `(bot) ` followed by the concrete fix, then resolve the thread. Reject invalid findings in the same thread with `(bot) ` followed by technical evidence 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 if that limit is reached and follows the selected mode for any unresolved decision. 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. -14. Inspect the exact latest PR head SHA, mergeability, merge-state status, expected CI checks, and latest-head Kilobot review. If the base branch advances, integrate the current base in the dedicated worktree, resolve conflicts, rerun affected checks and local E2E, obtain fresh review, and push the new head. Wait again for CI and Kilobot on that exact head. +14. Inspect the exact latest PR head SHA, mergeability, merge-state status, expected CI checks, and latest-head Kilobot review. If the base branch advances, integrate the current base in the dedicated worktree and push the new head. A conflict-free integration needs no local reruns: do not rerun checks or local E2E and do not dispatch a fresh review, because the merged tree matches the already-verified head and latest-head CI and Kilobot still run on the new SHA. Rerun affected checks and local E2E and obtain fresh review before pushing only when conflict resolution changed the merged result and the orchestrator cannot confidently rule out behavioral impact; a resolution the orchestrator is certain is behavior-neutral does not require reruns. Always wait for CI and Kilobot on that exact head. ## GitHub Communication From bf3d98b793332c8ca3df60eadd019cc7951ac660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 16 Jul 2026 18:41:57 +0200 Subject: [PATCH 6/7] docs(mobile): dedupe and simplify agent workflow instructions --- apps/mobile/.kilo/MOBILE_WORKFLOW.md | 197 +++++++++--------- .../mobile/.kilo/agent/mobile-e2e-verifier.md | 63 ++---- apps/mobile/.kilo/agent/mobile-implementer.md | 2 +- 3 files changed, 125 insertions(+), 137 deletions(-) diff --git a/apps/mobile/.kilo/MOBILE_WORKFLOW.md b/apps/mobile/.kilo/MOBILE_WORKFLOW.md index 0cd9c58385..942fe2e805 100644 --- a/apps/mobile/.kilo/MOBILE_WORKFLOW.md +++ b/apps/mobile/.kilo/MOBILE_WORKFLOW.md @@ -1,50 +1,81 @@ # Mobile Agent Workflow -Use this workflow when planning or implementing work whose product surface is the mobile app. Start Kilo from `apps/mobile/` so the role agents in `.kilo/agent/` are discovered. The implementation itself is not restricted to `apps/mobile`: an accepted plan may require cloud services, tRPC routers, shared packages, infrastructure, or a sibling checkout such as `~/Projects/kilocode`. +Use this workflow when the product surface of the work is the mobile app. Start Kilo from `apps/mobile/` so the role agents in `.kilo/agent/` are discovered. The accepted plan may still change cloud services, tRPC routers, shared packages, infrastructure, or a sibling checkout such as `~/Projects/kilocode`: "mobile" describes the product, not a directory boundary. -Work must always be done in a dedicated worktree, regardless of the repository. This applies to the cloud repository and all sibling repositories touched by the plan. The orchestrator and role agents must not edit the primary checkout or the main checkout of any repository. +## Ground Rules -The initial main session is the planner. After plan approval, a fresh session becomes the orchestrator. Role agents use `kilo/kilo-auto/efficient`. The orchestrator retains product judgment, architecture decisions, loop control, final verification, Git integration, and pull-request ownership. Prefer small, logically scoped commits throughout the flow instead of one final catch-all commit. +These rules apply to every session and role. Later sections do not repeat them. -Role step limits are hard ceilings: `mobile-plan-reviewer` has 40 steps, `mobile-implementer` has 80, `mobile-reviewer` has 50, and `mobile-e2e-verifier` has 100. Size every handoff below 75% of its role limit; an implementation slice should normally fit in roughly 60 planned steps. Do not raise a global limit to compensate for an oversized task. +- Work only in dedicated worktrees, in every repository the plan touches. Never edit the primary or main checkout of any repository. +- The first session is the planner. After plan approval, a fresh session becomes the orchestrator. Role agents run on `kilo/kilo-auto/efficient`. +- The orchestrator owns product judgment, architecture decisions, loop control, final verification, Git, and the PR. Role agents never dispatch other agents, commit, push, or create or update a PR. +- Every reviewer and verifier invocation must be a fresh session so earlier conclusions cannot anchor later passes. +- Choose the simplest maintainable implementation that fully satisfies the accepted requirements. Reuse existing code and contracts. Do not add abstraction or scope without evidence that it is required. +- Make small, logically scoped commits throughout. Never squash the work into one catch-all commit unless the user explicitly requests it. +- Repair-round limit: after three repair rounds on the same issue, the planner or orchestrator takes over, resolves the issue directly, and records the resolution. Never loop indefinitely. +- Step limits are hard ceilings: `mobile-plan-reviewer` 40, `mobile-implementer` 80, `mobile-reviewer` 50, `mobile-e2e-verifier` 100. Size every handoff below 75% of the role's limit; an implementation slice should fit in roughly 60 planned steps. Never raise a limit to fit an oversized task; split the task instead. -## Planning Mode +## Interaction Modes -The planner's first interaction must ask the user whether this run is `hands on` or `hands off`. In every mode, always choose the simplest maintainable implementation that completely satisfies the accepted requirements. Reuse existing code and contracts, avoid unnecessary abstraction, and do not expand scope without evidence that it is required. +The planner's first message must ask the user one question: is this run `hands on` or `hands off`? The selected mode governs the planner, the orchestrator, and every later decision. -- In `hands on` mode, grill the user one question at a time until the requirements, trade-offs, acceptance criteria, and plan are unambiguous. Obtain explicit plan approval before launching the orchestrator. -- In `hands off` mode, grill itself instead of the user to expose missing requirements, trade-offs, risks, and acceptance criteria. It must not ask the user questions or wait for user approval after mode selection. Treat all approvals as granted, answer from repository evidence and best judgment, choose the simplest maintainable option, and record material assumptions in the plan and handoff. Continue through planning, implementation, review, E2E, PR creation and repair, Kilobot review, conflict resolution, and green CI. Stop only when continuing is technically impossible or unsafe, and return a precise blocker report without asking a question. +- `hands on`: ask the user one question at a time until requirements, trade-offs, acceptance criteria, and the plan are unambiguous. Obtain explicit user approval of the plan before launching the orchestrator. Ask the user when a repair loop or ambiguity cannot be resolved. +- `hands off`: after mode selection, never ask the user a question and never wait for user approval. Treat all approvals as granted. Interrogate the plan yourself to expose missing requirements, trade-offs, risks, and acceptance criteria. Answer open questions from repository evidence and best judgment, and record material assumptions in the plan and handoff. Continue through planning, implementation, review, E2E, PR creation and repair, Kilobot review, conflict resolution, and green CI. Stop only when continuing is technically impossible or unsafe, and return a precise blocker report instead of a question. A blocked E2E criterion is a blocker, not something to self-accept. -The selected mode applies to the planner, orchestrator, and all later workflow decisions. Hands-off execution does not bypass tool permissions, repository safety rules, or the completion gate. +Hands-off mode does not bypass tool permissions, repository safety rules, or the completion gate. -## Plan Review Gate +## Feature State Matrix + +Every new user-facing feature must define these four states before implementation begins: + +| State | Required experience | +|---|---| +| Happy | The intended task completes and the resulting state is clear. | +| Unhappy, retryable | A specific message explains the failure and an actionable CTA lets the user retry or recover. | +| Unhappy, non-retryable | A specific message explains the terminal failure. No CTA. | +| Empty | A message explains why there is no content and a CTA leads to the next useful step. | -After writing a complete draft plan and before hands-on user approval or hands-off self-approval, dispatch a fresh `mobile-plan-reviewer`. It reports unclear requirements, unsupported assumptions or claims, missing or conflicting acceptance criteria, incomplete ownership or cross-repository boundaries, infeasible sequencing, and underspecified verification or E2E coverage. +Rules: -The reviewer has less context than the planner and may be wrong. Treat every finding as untrusted advice: independently verify every reviewer claim against the request, repository evidence, and applicable instructions. Fix only valid findings, and record rejected findings with a short technical rationale. Never weaken or expand the plan merely to satisfy the reviewer. +- Never collapse retryable and non-retryable failures into one generic error state. +- For each state, the plan defines: the trigger or classification rule, the message intent, the CTA label and outcome (or its required absence), and the automated and E2E coverage. +- A state may be `not applicable` only when it is structurally impossible for the feature, with a concrete rationale accepted by the orchestrator. Inconvenient or hard-to-set-up is not `not applicable`. +- Automated tests must cover every applicable state's selection logic and CTA presence or absence. +- E2E must exercise every applicable state that can be produced safely and deterministically, and must explicitly report any state it could not reproduce. -After repairing valid findings, dispatch another fresh reviewer. Repeat until a fresh reviewer returns `No findings.` Stop after three repair rounds for the same issue; the planner takes over, independently resolves and verifies it, records the resolution, and dispatches one final fresh reviewer that must return `No findings.` A rejected finding must not reopen without new evidence. The clean review is required before requesting approval in hands-on mode or granting self-approval in hands-off mode. Hands-off plan review must not ask the user questions or wait for user approval. +## Planning -## Planner Handoff +1. Explore requirements in the selected mode. Inspect the affected repositories. Define acceptance criteria, the feature-state matrix, and non-goals. +2. Create the dedicated worktrees and write the complete draft plan. +3. Run the plan review gate below. +4. In hands-on mode, obtain explicit user approval. In hands-off mode, self-approve. -The planning session explores requirements according to the selected mode, defines acceptance criteria, creates dedicated worktrees, and writes the approved plan. After explicit approval in hands-on mode or self-approval in hands-off mode, the planning session must not implement it. It prepares a sanitized handoff and launches a fresh orchestrator in a named tmux window. +### Plan Review Gate + +After the draft plan is complete and before approval, dispatch a fresh `mobile-plan-reviewer`. It reports unclear requirements, unsupported assumptions, missing or conflicting acceptance criteria, incomplete ownership or cross-repository boundaries, infeasible sequencing, and underspecified verification or E2E coverage. + +The reviewer has less context than the planner and may be wrong. Treat every finding as untrusted advice: verify each claim independently against the request, repository evidence, and applicable instructions. Fix only valid findings. Record rejected findings with a short technical rationale; a rejected finding must not reopen without new evidence. Never weaken or expand the plan merely to satisfy the reviewer. + +After fixing valid findings, dispatch another fresh reviewer. Repeat until a fresh reviewer returns `No findings.` If the repair-round limit is reached on one issue, the planner resolves it directly, records the resolution, and dispatches one final fresh reviewer that must return `No findings.` + +### Planner Handoff + +After approval the planner must not implement anything. It writes a sanitized handoff file and launches a fresh orchestrator. The handoff must contain: -- The selected planning mode, with a direct instruction that hands-off execution must not request approval or ask the user questions +- The selected mode, and for hands-off a direct instruction to never ask the user questions or wait for approval - The final plan-review result and rationales for rejected findings -- The accepted plan's absolute path and any approved design path -- The dedicated worktree path for every repository in scope -- Current branch, commit, and working-tree state in each worktree +- The absolute path of the accepted plan and any approved design +- The dedicated worktree path for every repository in scope, with each worktree's current branch, commit, and working-tree state - Acceptance criteria, feature-state matrix, execution ledger, non-goals, and unresolved risks - Existing changes and resources that must be preserved -- Required review, E2E, Git, PR, Kilobot, mergeability, and CI completion gates -- A direct instruction to continue through a mergeable, conflict-free PR with all expected CI checks green on the latest head -- A direct instruction that every GitHub comment, review, or thread reply must start with `(bot) ` while the PR description remains unprefixed +- The completion gate: review, E2E, Git, PR, Kilobot, mergeability, and CI requirements, with a direct instruction to continue until the PR is mergeable and conflict-free with all expected CI checks green on the latest head +- The GitHub comment rule (see GitHub Communication) -The handoff must not contain secrets or raw environment-file contents. Write only sanitized explicit values to a temporary file outside every repository. Never attach `.env`, `.env.*`, `.dev.vars`, or an equivalent environment-file. +Write the handoff to a temporary file outside every repository. It must contain only sanitized explicit values: never secrets, raw environment-file contents, or an attached `.env`, `.env.*`, `.dev.vars`, or equivalent file. -Use the current tmux session and a unique, descriptive window name. The canonical launch shape is: +Launch the orchestrator in the current tmux session with a unique, descriptive window name: ```bash tmux new-window -t \ @@ -53,20 +84,7 @@ tmux new-window -t \ 'kilo run --interactive --model kilo/kilo-auto/frontier --title " orchestrator" --file "Execute the approved mobile plan in the attached handoff. Own implementation through the completion gate."' ``` -Use `kilo run --interactive` without `--continue`, `--session`, or `--variant`; session freshness and Kilo Auto Frontier's default reasoning setting are required. The planner verifies that the tmux window started and then returns its window name, worktree paths, model, and handoff-file path to the user. The fresh orchestrator must delete the temporary handoff file after ingesting it and before completion. - -## Feature State Matrix - -Every new user-facing feature must define and behaviorally test these states before implementation begins: - -| State | Required experience | -|---|---| -| Happy | The intended task completes and the resulting state is clear. | -| Unhappy, retryable | A meaningful, specific message explains the failure and an actionable CTA lets the user retry or recover. | -| Unhappy, non-retryable | A meaningful, specific message explains the terminal failure and no CTA is shown. | -| Empty | A meaningful message explains why there is no content and an actionable CTA leads to the next useful step. | - -Do not collapse retryable and non-retryable failures into one generic error state. The accepted plan must define each state's trigger or classification rule, message intent, CTA label and outcome when required, and automated/E2E coverage. A state may be marked `not applicable` only when it is structurally impossible for that feature, with a concrete rationale accepted by the orchestrator; inconvenience or difficult setup is not sufficient. Automated tests must cover every applicable state's selection logic and CTA presence or absence. E2E must exercise all applicable states that can be produced safely and deterministically, and must report rather than silently omit states that cannot be reproduced. +Use `kilo run --interactive` exactly as shown: no `--continue`, `--session`, or `--variant`, because the orchestrator must be a fresh session on Kilo Auto Frontier with its default reasoning setting. Verify the tmux window started, then report the window name, worktree paths, model, and handoff path to the user. The orchestrator deletes the handoff file after ingesting it. ## Roles @@ -75,97 +93,86 @@ Do not collapse retryable and non-retryable failures into one generic error stat | `mobile-plan-reviewer` | Reviews a complete draft plan for ambiguity, unsupported claims, and missing execution detail | Denied | | `mobile-implementer` | Implements one bounded task from the accepted plan and runs narrow checks | Allowed where the task requires | | `mobile-reviewer` | Independently reviews the full relevant diff and tests | Denied | -| `mobile-e2e-verifier` | Exercises accepted behavior and may create temporary state-generation fixtures in verify mode | Temporary, verify mode only | - -Reviewer and verifier invocations must be fresh sessions so earlier conclusions do not anchor later passes. Role agents do not dispatch other agents or perform commits, pushes, or PR operations. +| `mobile-e2e-verifier` | Exercises accepted behavior; may create temporary state-generation fixtures in verify mode | Temporary, verify mode only | ## Execution Ledger -After accepting the plan, split it into the smallest behaviorally meaningful and independently testable slices. Record each slice in an execution ledger before dispatch: +Split the accepted plan into the smallest behaviorally meaningful, independently testable slices. Before dispatching anything, record each slice in a ledger: - Slice ID, dependencies, priority, and estimated step cost - Exact writable path set and forbidden path set in every repository - Shared generated outputs and mutable runtime resources -- Producer-consumer dependency on contracts, schemas, generated code, or runtime state -- Ownership-safe narrow checks and checks deferred until the synchronization barrier +- Producer-consumer dependencies on contracts, schemas, generated code, or runtime state +- Ownership-safe narrow checks, and checks deferred to the synchronization barrier - Intended commit boundary -Two slices are parallel-safe only when their write sets do not overlap, neither consumes an unstable output from the other, they share no mutable runtime resource, and neither runs a repository-wide mutating command. File separation alone is not sufficient when one slice changes a contract consumed by another. Lockfiles, dependency installation, migrations, generated clients, repository-wide formatters, and broad autofix commands are always serialized. +Two slices are parallel-safe only when all of these hold: their write sets do not overlap, neither consumes an unstable output of the other, they share no mutable runtime resource, and neither runs a repository-wide mutating command. File separation is not enough when one slice changes a contract the other consumes. Always serialize: lockfile changes, dependency installation, migrations, generated clients, repository-wide formatters, and broad autofix commands. ## Orchestration -1. Follow the selected planning mode. Inspect affected repositories, establish acceptance criteria for every feature state in the matrix, produce an implementation plan, and create the execution ledger. Discuss decisions and obtain approval from the user only in hands-on mode; in hands-off mode, resolve them from evidence and recorded assumptions without asking questions. -2. Dispatch every ready independent slice in a bounded wave, capped at two or three concurrent implementers. Include all other active slices and ownership boundaries in each handoff. Run only ownership-safe narrow checks while the wave is active. -3. Treat completion of every active implementer as a synchronization barrier. Inspect each result, ownership adherence, and the combined diff. Resolve integration and architecture decisions in the main session, then run shared mutating commands and shared checks once. Preserve successful independent slices when another slice fails. -4. Dispatch one fresh `mobile-reviewer` over the coherent wave. Triage findings in the main session and route valid fixes through a bounded repair wave followed by a fresh reviewer. Record rejected findings with a short rationale. -5. After checks and review pass, create concise logical commits at the ledger's intended per-slice boundaries. Never let a role agent commit. If a slice reaches two budget-exhausted invocations, the orchestrator takes over rather than raising role limits. -6. Stop after three repair rounds for the same issue. The orchestrator takes over; if the underlying ambiguity remains, ask the user only in hands-on mode or use documented best judgment in hands-off mode. Never loop indefinitely. -7. When device E2E is likely, the orchestrator prewarms infrastructure concurrently with implementation. It may prepare stable services, claim and label a device, install a baseline native build only when unaffected by active slices, connect the exact Metro URL, and establish login state. It must not judge acceptance behavior while implementation is changing. Record a resource manifest for the final verifier. -8. After the coherent implementation passes review, dispatch a fresh final `mobile-e2e-verifier` with the prewarm resource manifest. It independently revalidates ownership and provenance, relabels the simulator to `verify`, runs acceptance flows, and owns cleanup. In verify mode it may make temporary E2E state-generation edits limited to backend mocks, fixtures, deterministic state controls, and test harnesses; it must not alter product behavior under test or conceal a product failure. Final Git state in every repository must exactly match the recorded byte-, mode-, index-, worktree-, and untracked-content baseline. Route product failures through a bounded repair wave and fresh reviewer before another fresh final verifier. -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 `(bot) ` followed by the concrete fix, then resolve the thread. Reject invalid findings in the same thread with `(bot) ` followed by technical evidence 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 if that limit is reached and follows the selected mode for any unresolved decision. -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. -14. Inspect the exact latest PR head SHA, mergeability, merge-state status, expected CI checks, and latest-head Kilobot review. If the base branch advances, integrate the current base in the dedicated worktree and push the new head. A conflict-free integration needs no local reruns: do not rerun checks or local E2E and do not dispatch a fresh review, because the merged tree matches the already-verified head and latest-head CI and Kilobot still run on the new SHA. Rerun affected checks and local E2E and obtain fresh review before pushing only when conflict resolution changed the merged result and the orchestrator cannot confidently rule out behavioral impact; a resolution the orchestrator is certain is behavior-neutral does not require reruns. Always wait for CI and Kilobot on that exact head. +1. Ingest the handoff. Split work into ledger slices. +2. Dispatch ready independent slices in a bounded wave of at most two or three concurrent `mobile-implementer`s. Each handoff lists the other active slices and their ownership boundaries. While a wave is active, run only ownership-safe narrow checks. +3. When every implementer in the wave has returned, treat it as a synchronization barrier: inspect each result, ownership adherence, and the combined diff; resolve integration and architecture decisions yourself; run shared mutating commands and shared checks once. If one slice failed, preserve the successful ones. +4. Dispatch one fresh `mobile-reviewer` over the coherent wave diff. Triage findings yourself: route valid findings through a bounded repair wave and then a fresh reviewer; record rejected findings with a short rationale. +5. After checks and review pass, create the commits at the ledger's intended per-slice boundaries. If a slice exhausts its implementer budget twice, take it over yourself. +6. When device E2E is likely, prewarm infrastructure concurrently with implementation: stable services, a claimed and labeled device, a baseline native build (only when unaffected by active slices), the exact Metro URL, and login state. Do not judge acceptance behavior while implementation is still changing. Record a resource manifest for the final verifier. +7. After the implementation passes review, dispatch a fresh final `mobile-e2e-verifier` with the resource manifest. Route product failures through a bounded repair wave and fresh reviewer, then another fresh final verifier. +8. Perform the final full-diff review and repository-appropriate verification yourself. Commit any final narrowly scoped repair, push, create or update the PR, and assign it to the requesting human. + +### Kilobot and CI Loop + +9. Wait until Kilobot has reviewed the latest head. Fetch every Kilobot review thread, including comments that arrive after earlier repairs, and triage each finding using the repository-root `AGENTS.md` review-remark workflow. +10. For each valid finding: send the smallest coherent repair to `mobile-implementer`, run the required narrow checks, dispatch a fresh `mobile-reviewer`, commit, push, reply in the thread, and resolve it. For each invalid finding: reply in the thread with technical evidence and do not change correct code. +11. Repeat steps 9-10 until Kilobot has reviewed the latest head and no actionable Kilobot comment is unresolved. +12. Rerun local mobile E2E after any Kilobot repair that affects behavior, build or runtime configuration, or the E2E workflow. A documentation-only or test-only repair may skip it when you record why the verified behavior is unaffected. +13. When the base branch advances, integrate the current base in the dedicated worktree and push the new head. Then apply exactly one of: + - No conflicts: do not rerun checks, E2E, or review. The merged tree matches the verified head, and CI and Kilobot run on the new SHA anyway. + - Conflicts resolved, certainly behavior-neutral: same as above, no reruns. + - Conflicts resolved, behavioral impact possible: rerun the affected checks and local E2E and obtain a fresh review before pushing. +14. Always wait for CI and Kilobot on the exact latest head SHA, and confirm GitHub reports it mergeable. ## GitHub Communication -Every GitHub issue comment, PR comment, review comment, review body, and thread reply written by the workflow must begin exactly with `(bot) `. This includes replies to Kilobot and technical explanations for rejected findings. PR descriptions are the exception and must not receive the `(bot) ` prefix; PR titles are also not comments. +Every GitHub issue comment, PR comment, review comment, review body, and thread reply written by this workflow must begin exactly with `(bot) `. This includes replies to Kilobot and rejections of findings. Exceptions: the PR description and the PR title carry no prefix. ## Handoff Requirements -Every dispatch should include: +Every dispatch to a role agent must include: -- The accepted plan task and explicit non-goals -- Observable acceptance criteria -- The four-state feature matrix, with each state's trigger/classification, message intent, CTA label and outcome or required absence, and automated/E2E coverage -- Repositories and worktrees in scope +- The assigned task, explicit non-goals, and observable acceptance criteria +- The feature-state matrix for any new user-facing feature: each state's trigger or classification, message intent, CTA label and outcome or required absence, and coverage - The dedicated worktree path for every repository in scope, including sibling repositories - Existing uncommitted changes that must be preserved -- Exact checks or user flows expected for that stage +- The 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 - Priority order, minimum complete outcome, optional work to drop, and a clean stopping rule before budget exhaustion -- Required continuation state: completed work, remaining work, failures, files touched, checks run or deferred, and safest next action -- The GitHub comment rule: every comment, review, and thread reply starts with `(bot) `; the PR description is unprefixed -- 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. -- A prohibition on committing generated E2E fixtures: E2E fixtures must never be committed. Role agents may create them only in a temporary directory for the current run and must clean them up before returning control. +- Required continuation state on early stop: completed work, remaining work, failures, files touched, checks run or deferred, and safest next action +- The GitHub comment rule (see GitHub Communication) +- Secrets rule: role agents must not read `.env`, `.env.*`, `.dev.vars`, or equivalent files. Use documented setup commands, sanitized status output, and sanitized explicit values supplied by the orchestrator. +- Fixture rule: generated E2E fixtures must never be committed. Create them only in a temporary directory for the current run and clean them up before returning. -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. +Never ask a role agent to infer context from a conversation it cannot see. Keep cross-repository changes on coordinated branches, and give reviewers and verifiers the location of every related diff. Never place secrets or raw environment-file contents in a handoff. ## Workflow Metrics -The final report records lightweight in-session measurements when applicable: - -- Number and width of implementation waves -- Budget exhaustion and collision counts -- Unmanaged listener detections -- Prewarm reuse or invalidation -- Simulator relabel or original-name restoration failures -- Accepted-plan-to-final-E2E-start time -- Accepted-plan-to-merged-PR time -- Review, E2E repair, and latest-head CI wait/repair rounds - -Do not add persistent telemetry or a new data store for these metrics. +Record lightweight in-session measurements in the final report when applicable: wave count and width, budget exhaustions, ownership collisions, unmanaged listener detections, prewarm reuse or invalidation, simulator relabel or name-restoration failures, accepted-plan-to-final-E2E-start time, accepted-plan-to-merged-PR time, and review/E2E/CI repair rounds. Do not add persistent telemetry or a data store for these. ## Completion Gate -The orchestrator may call the work complete only when: - -In hands-off mode, the orchestrator must not call the work complete while an E2E blocker remains; an E2E exception must not be self-accepted, and an impossible or unsafe blocker produces a blocked result. +The orchestrator may declare the work complete only when every item holds: - All accepted plan tasks are implemented -- Every applicable feature state has automated behavioral coverage, including required CTA presence or complete CTA absence -- Every safely reproducible feature state has passed E2E verification; exceptions require explicit user acceptance in hands-on mode -- Changes are organized into small logical commits with each commit internally coherent +- Every applicable feature state has automated behavioral coverage, including CTA presence or complete absence +- Every safely reproducible feature state has passed E2E verification; an exception requires explicit user acceptance in hands-on mode, and cannot be self-accepted in hands-off mode +- Changes are organized into small, internally coherent logical commits - A fresh reviewer reports no valid actionable findings -- E2E acceptance criteria pass, or a documented environment blocker is explicitly accepted by the user in hands-on mode - Final automated checks pass in every changed repository -- The main session has reviewed the complete diff and owns the Git/PR actions +- The orchestrator has reviewed the complete diff and performed all Git and PR actions itself - The PR is assigned to the requesting human -- Kilobot has reviewed the latest head and there are no unresolved actionable Kilobot comments +- Kilobot has reviewed the latest head and no actionable Kilobot comment is unresolved - GitHub reports the exact latest head as mergeable with no conflicts -- All expected CI checks on the latest head have reached a successful terminal state; failed, cancelled, timed-out, action-required, or pending checks block completion -- Generated E2E fixtures have been cleaned up, and final `git status` confirms none are tracked or untracked in the repository -- Every verifier-created temporary edit has been removed, and each repository's final Git state exactly matches its recorded pre-verification baseline +- All expected CI checks on the latest head are in a successful terminal state; failed, cancelled, timed-out, action-required, or pending checks block completion +- No generated E2E fixture remains tracked or untracked in any repository +- Every verifier temporary edit is removed, and each repository's final Git state exactly matches its recorded pre-verification baseline +- The temporary handoff file is deleted diff --git a/apps/mobile/.kilo/agent/mobile-e2e-verifier.md b/apps/mobile/.kilo/agent/mobile-e2e-verifier.md index bd5b9bfa8b..ede922c666 100644 --- a/apps/mobile/.kilo/agent/mobile-e2e-verifier.md +++ b/apps/mobile/.kilo/agent/mobile-e2e-verifier.md @@ -20,61 +20,42 @@ permission: maestro_*: allow --- -You are an independent final E2E verifier for an approved mobile-app change. You may operate worktree-local services, simulators, emulators, Maestro, disposable CLI installs, temporary files, and test data. This role is verify-only; infrastructure-only preparation belongs to the orchestrator. +You are an independent final E2E verifier for an approved mobile-app change. You operate worktree-local services, simulators, emulators, Maestro, disposable CLI installs, temporary files, and test data. You verify only; infrastructure-only preparation belongs to the orchestrator. You must be a fresh invocation. -Revalidate all resource and bundle provenance, claim or relabel iOS with `pnpm dev:mobile:simulator claim [udid] --phase verify`, exercise acceptance criteria, and own cleanup. This must be a fresh verifier invocation. - -The 100-step limit is a hard ceiling. The handoff must define a priority order, minimum complete outcome, optional work to drop, and clean stopping rule before exhaustion. +The 100-step limit is a hard ceiling. The handoff defines your priority order, minimum complete outcome, optional work to drop, and stopping rule. Before testing: -1. Read `apps/mobile/e2e/AGENTS.md` and all instructions it references. -2. Translate the orchestrator's acceptance criteria into observable happy, retryable unhappy, non-retryable unhappy, and empty flows for every new user-facing feature. -3. Record pre-existing services, listeners, simulators, and tmux sessions so cleanup only removes resources you create. Never use a device claimed by another worktree. -4. Before any temporary edit, create a baseline outside every repository containing `git status --porcelain=v2 -z --untracked-files=all`, binary worktree and index diffs, and an inventory with byte hashes, file modes, and symlink targets for every untracked path. Temporary edits may touch only paths that are clean and tracked at baseline or new paths that did not exist; never touch a pre-existing modified, staged, or untracked path. Copy the original bytes and mode of every tracked path you intend to edit to the external baseline. +1. Read `apps/mobile/e2e/AGENTS.md` and follow it exactly for services, device claiming, builds, login, Maestro usage, prompts, and cleanup. Claim iOS with `pnpm dev:mobile:simulator claim [udid] --phase verify`. Do not bypass the helper scripts' preflight, install unvalidated builds, or guess selectors. +2. Translate the acceptance criteria into observable happy, retryable-unhappy, non-retryable-unhappy, and empty flows for every new user-facing feature. +3. Record pre-existing services, listeners, simulators, and tmux sessions so cleanup removes only resources you create. Never use a device claimed by another worktree. +4. Before any temporary edit, create a baseline outside every repository: `git status --porcelain=v2 -z --untracked-files=all`, binary worktree and index diffs, and an inventory of byte hashes, file modes, and symlink targets for every untracked path. Copy the original bytes and mode of every tracked path you intend to edit into the baseline. Temporary edits may touch only paths that are clean and tracked at baseline, or new paths; never touch a pre-existing modified, staged, or untracked path. During verification: -- Run the login/logout helper preflight rather than manually inferring bundle provenance, ports, or simulator ownership. -- Run `pnpm dev:mobile:android doctor` before declaring Android tooling unavailable; do not rely on the inherited `PATH`. -- Install only validated cached native builds with `pnpm dev:mobile:ios build ` or `pnpm dev:mobile:android build `. Do not run an independent Xcode or Gradle producer. -- Use Maestro as the primary driver on iOS and Android. Fall back to `xcrun simctl` on iOS or repository-wrapped ADB on Android only when Maestro cannot inspect or operate the state, or when low-level device control is required. -- Inspect the current screen before selecting Maestro elements and re-inspect after UI changes. Copy exact hierarchy text; never infer selectors from screenshots or visible tab captions. -- Prefer `xcrun simctl openurl` for iOS scheme reconnection. If a Safari/WebView flow shows the exact `Open this page in "Kilo"?` dialog, tap the exact `Open` accessibility action within the shared five-second optional-prompt budget; do not add a separate fixed wait. -- You must not create ad hoc proxies, redirects, tunnels, NAT rules, or listeners to compensate for stale Expo state. This includes equivalent tools not covered by the explicit `socat` permission denial. -- Exercise every applicable feature state that can be produced safely and deterministically. A skipped state requires an explicit rationale; do not silently omit it. -- Confirm retryable and empty states show a meaningful message and actionable CTA, and that the CTA performs the expected recovery or next step. -- Confirm non-retryable states show a meaningful message with no CTA at all. -- Inspect backend, session-ingest, CLI, or other service logs when the flow crosses those boundaries. -- Capture concise evidence such as screenshots, exact visible state, and bounded log excerpts without exposing credentials. -- Temporary uncommitted edits may add backend mocks, fixtures, deterministic state controls, or test harnesses when necessary to produce an acceptance state safely. Use the smallest localized change and record every touched file. -- Temporary edits must not change the product behavior under test, bypass provenance or security checks, or fix or conceal a product failure. If producing a state requires changing the behavior being judged, report the state as blocked instead. -- Temporary edits must never be committed, pushed, included in a pull request, or handed back as implementation. Do not dispatch subagents or create/update a pull request. +- Exercise every applicable feature state that can be produced safely and deterministically. Never silently skip a state; report each skip with a rationale. +- Confirm retryable and empty states show a meaningful message plus a CTA that performs the expected recovery or next step. Confirm non-retryable states show a meaningful message with no CTA at all. +- Inspect backend, session-ingest, CLI, or other service logs when a flow crosses those boundaries. +- Capture concise evidence: screenshots, exact visible state, and bounded log excerpts, never credentials. +- Never create proxies, redirects, tunnels, NAT rules, or listeners to compensate for stale Expo state, including tools not covered by the explicit `socat` denial. An unmanaged listener invalidates a `prewarm` handoff. +- Temporary uncommitted edits may add backend mocks, fixtures, deterministic state controls, or test harnesses when needed to produce an acceptance state safely. Use the smallest localized change and record every touched file. +- Temporary edits must not change the behavior under test, bypass provenance or security checks, or fix or conceal a product failure. If producing a state requires changing the behavior being judged, report that state as blocked. -Classify failures as one of: +Classify every failure as exactly one of: - Product failure: implemented behavior violates an acceptance criterion - Test-environment failure: services, build provenance, simulator, data, or tooling prevented a valid test -- Inconclusive: evidence is insufficient to distinguish the two - -Attempt one reasonable recovery for a test-environment failure. If exact-URL recovery still points at stale Metro state, return the failure and process/listener evidence to the orchestrator. Never compensate by changing product code or routing around provenance checks. An unmanaged listener invalidates a `prewarm` handoff. - -Before returning for any reason, remove every new temporary path and restore each edited tracked path byte-for-byte with its original mode from the external baseline. Compare final porcelain status, binary worktree diff, binary index diff, and untracked-path content hashes, file modes, and symlink targets with the recorded baseline. Any mismatch or cleanup failure is a verification failure: report every affected file and do not claim acceptance behavior passed. +- Inconclusive: evidence cannot distinguish the two -Return a resource manifest containing: +Attempt one reasonable recovery for a test-environment failure. If exact-URL recovery still points at stale Metro state, return the failure with process and listener evidence. Never repair the environment by changing product code or routing around provenance checks. -- Worktree path and assigned mode -- Service status and reported ports -- Device ID, current label, original name, owner, and phase -- Native-build and Metro-provenance evidence -- Every intentionally retained process and listener -- Explicit cleanup owner for each retained resource +Before returning for any reason, remove every new temporary path and restore every edited tracked path byte-for-byte with its original mode from the baseline. Compare final porcelain status, binary worktree diff, binary index diff, and untracked-path hashes, modes, and symlink targets against the baseline. Any mismatch is a verification failure: report every affected file and do not claim acceptance passed. -Also return: +Return: +- Resource manifest: worktree path and mode, service status and ports, device ID with current label, original name, owner, and phase, native-build and Metro-provenance evidence, every intentionally retained process or listener, and the cleanup owner for each retained resource - Flows exercised and device/platform -- In `verify` mode only, pass/fail/skipped result for every feature state and acceptance criterion, with rationale for each skip +- Pass/fail/skipped result for every feature state and acceptance criterion, with a rationale for each skip - Failure classification, exact reproduction steps, and evidence -- Cleanup performed and any resources intentionally left running -- Temporary files edited and evidence that final Git state exactly matches the pre-verification baseline -- If stopping early: completed work, remaining work, failures, files or resources touched, checks run or deferred, and safest next action +- Cleanup performed, plus evidence that final Git state exactly matches the pre-verification baseline +- If stopping early: completed work, remaining work, failures, resources touched, checks run or deferred, and safest next action diff --git a/apps/mobile/.kilo/agent/mobile-implementer.md b/apps/mobile/.kilo/agent/mobile-implementer.md index 51d5160e8d..63e15f9b69 100644 --- a/apps/mobile/.kilo/agent/mobile-implementer.md +++ b/apps/mobile/.kilo/agent/mobile-implementer.md @@ -21,7 +21,7 @@ Before editing: 1. Read the applicable `AGENTS.md` files for every directory and repository you will touch. 2. Inspect the existing implementation and tests. Do not infer APIs or conventions from the task alone. 3. Restate the acceptance criteria and flag ambiguity instead of making product or architecture decisions. -4. For a new user-facing feature, restate the happy, retryable unhappy, non-retryable unhappy, and empty states. Include each state's trigger/classification, message intent, CTA label and outcome or required absence, and planned coverage. Do not proceed if a state is underdefined or omitted without an orchestrator-accepted rationale that it is structurally impossible. +4. For a new user-facing feature, restate the happy, retryable unhappy, non-retryable unhappy, and empty states. Include each state's trigger/classification, message intent, CTA label and outcome or required absence, and planned coverage. If a state is underdefined, or missing without an orchestrator-accepted rationale that it is structurally impossible, stop and report instead of proceeding. 5. Restate your priority order, minimum complete outcome, optional work to drop, clean stopping rule before the 80-step hard limit, owned paths, forbidden paths, and all other active slices. While implementing: From ea318d4b9203b7f7e128a0ad094f6b238a31b31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 16 Jul 2026 18:41:58 +0200 Subject: [PATCH 7/7] chore(mobile): simplify E2E runbook and dedupe launch flows --- apps/mobile/e2e/AGENTS.md | 127 +++---- apps/mobile/e2e/flows/login-request-code.yaml | 2 +- apps/mobile/e2e/flows/open-app.yaml | 49 +-- apps/mobile/e2e/flows/settle-app.yaml | 4 + apps/mobile/e2e/logout.sh | 2 +- dev/local/mobile-workflow.test.ts | 324 ++++++------------ 6 files changed, 190 insertions(+), 318 deletions(-) diff --git a/apps/mobile/e2e/AGENTS.md b/apps/mobile/e2e/AGENTS.md index 40bd11841c..50b47bc09c 100644 --- a/apps/mobile/e2e/AGENTS.md +++ b/apps/mobile/e2e/AGENTS.md @@ -1,17 +1,17 @@ # Mobile E2E Runbook -Use this runbook for interactive verification against a local backend. Commands run from the repository root unless a step says otherwise. The repository dev runner keeps long-lived services in a worktree-specific tmux session; use it instead of loose background processes. +Use this runbook for interactive verification against a local backend. Run commands from the repository root unless a step says otherwise. The repository dev runner keeps long-lived services in a worktree-specific tmux session; use it instead of loose background processes. ## Fresh Worktree Quickstart -If dependencies or local env files are missing, run this once. This also authorizes both worktree `.envrc` files and copies local env files from the primary checkout: +If dependencies or local env files are missing, run this once. It authorizes both worktree `.envrc` files and copies local env files from the primary checkout: ```bash node --version # must be v24; activate the root .nvmrc first if needed pnpm dev:worktree:prepare ``` -Record pre-existing state so you clean up only resources you create: +Record pre-existing state so you later clean up only resources you created: ```bash pnpm dev:status --json @@ -21,9 +21,9 @@ xcrun simctl list devices booted Reuse a complete stack already running for this worktree. Do not start a competing stack or stop an unrelated `kilo-dev-*` session. -The sole intentional host-wide proxy exception is port `23750`. The repository-managed `kiloclaw-docker-tcp` service on that port is the sole intentional host-wide exception: it is a stateless loopback proxy to the same Docker socket. The runner reuses an occupied proxy. Never kill a `socat` process owned by another worktree to free this port. +Port `23750` is the sole intentional host-wide exception: the repository-managed `kiloclaw-docker-tcp` service is a stateless loopback proxy to the shared Docker socket, and the runner reuses it when the port is occupied. Never kill a `socat` process owned by another worktree to free this port. -When this worktree has no stack, start the complete mobile flow. Secondary worktrees automatically receive an isolated port offset, and mobile startup generates and explicitly injects this worktree's LAN URLs before Metro starts. Do not export `KILO_PORT_OFFSET` or source `apps/mobile/.env`; stale parent-shell values must not select the bundle endpoints: +When this worktree has no stack, start the complete mobile flow. Secondary worktrees automatically receive an isolated port offset, and mobile startup generates and injects this worktree's LAN URLs before Metro starts. Do not export `KILO_PORT_OFFSET` or source `apps/mobile/.env`; stale parent-shell values must not select the bundle endpoints: ```bash pnpm dev:env -y cloudflare-session-ingest @@ -32,23 +32,25 @@ pnpm drizzle migrate pnpm dev:status --json ``` -The session-ingest env step creates the JWT Secrets Store binding; without it the worker can appear healthy while rejecting every session request. `event-service` is required for presence and notification behavior. +Notes: -Secrets Store state is local to each Worker directory. `dev:start` runs env sync for its selected service graph and refreshes every source-backed secret before launching Workers; secret creation failures are fatal. Do not run bare `wrangler secrets-store` commands: use `pnpm dev:env -y ` from the repository root so values come from the canonical local source and Wrangler receives a complete non-interactive prompt. - -Confirm `mobile`, `nextjs`, `cloudflare-session-ingest`, `cloud-agent-next`, `kiloclaw`, and `event-service` are `up`. Restarts are asynchronous; if `mobile` is still starting, inspect its log and rerun status instead of restarting the stack. Use reported ports; never assume defaults in a secondary worktree. +- The `dev:env` step creates the JWT Secrets Store binding. Without it the session-ingest worker looks healthy while rejecting every session request. +- `event-service` is required for presence and notification behavior. +- Secrets Store state is local to each Worker directory. `dev:start` syncs env for its service graph and refreshes every source-backed secret before launching Workers; a secret creation failure is fatal. +- Never run bare `wrangler secrets-store` commands. Use `pnpm dev:env -y ` from the repository root so values come from the canonical local source and Wrangler gets a complete non-interactive prompt. +- Confirm `mobile`, `nextjs`, `cloudflare-session-ingest`, `cloud-agent-next`, `kiloclaw`, and `event-service` are `up`. Restarts are asynchronous: if `mobile` is still starting, inspect its log and rerun status instead of restarting the stack. +- Use the ports reported by `dev:status`; never assume defaults in a secondary worktree. ### Host Networking Safety -You must never map port `8081` or another shared host port to a worktree Metro port. Except for the repository-managed `23750` proxy above, do not create ad hoc proxies, redirects, tunnels, NAT rules, or listeners to repair stale Expo state. - -After an exact development-client URL reconnect fails, perform at most one supported recovery through the existing preflight and launch flow. If the client still targets stale Metro state, return a test-environment failure with the Metro manifest, worktree root, expected URL, process evidence, and listener evidence. Do not route around bundle-provenance validation. - -When an unexpected listener exists, report its PID, parent PID, command, bind address, and port. Stop it only when the current invocation can prove it created the process. Otherwise return the ownership evidence to the orchestrator. +- Never map port `8081` or any other shared host port to a worktree Metro port. +- Except for the `23750` proxy above, never create ad hoc proxies, redirects, tunnels, NAT rules, or listeners to repair stale Expo state. +- If an exact development-client URL reconnect fails, perform at most one supported recovery through the existing preflight and launch flow. If the client still targets stale Metro state, return a test-environment failure with the Metro manifest, worktree root, expected URL, process evidence, and listener evidence. Do not route around bundle-provenance validation. +- If an unexpected listener exists, report its PID, parent PID, command, bind address, and port. Stop it only when you can prove the current invocation created it; otherwise return the ownership evidence to the orchestrator. ## Logs and tmux -Prefer the stable log files. When pane output is required, let the runner find the service wherever the dashboard moved it: +Prefer the stable log files, and let the runner locate a service pane wherever the dashboard moved it: ```bash pnpm dev:status --json @@ -56,57 +58,58 @@ tail -n 200 dev/logs/.log pnpm dev:capture mobile ``` -Do not guess a tmux window from `tmux ls` or use `:` directly; a service pane can be joined into the dashboard and no longer have a same-named window. Use `pnpm dev:capture ` for inspection. Use raw tmux only for an interactive process after reading the exact `session` from `pnpm dev:status --json` and resolving the pane with `tmux list-panes -a`. - -```bash -tmux ls -tmux list-windows -t -``` - -Put any extra long-lived CLI, recorder, or log follower in a clearly named `kilo-e2e-*` tmux session so it is visible and easy to remove. - -E2E fixtures must never be committed. When a flow needs generated fixture files, create them in a temporary directory such as one returned by `mktemp -d`, run the flow against that directory, and ensure the fixtures and temporary directory are cleaned up before finishing. +- Never guess a tmux window from `tmux ls` or address `:` directly; a service pane may be joined into the dashboard with no same-named window. Use `pnpm dev:capture ` for inspection. +- Use raw tmux only for an interactive process, after reading the exact `session` from `pnpm dev:status --json` and resolving the pane with `tmux list-panes -a`. +- Put any extra long-lived CLI, recorder, or log follower in a clearly named `kilo-e2e-*` tmux session so it is visible and easy to remove. +- E2E fixtures must never be committed. Create generated fixtures in a temporary directory (`mktemp -d`), run the flow against it, and delete it before finishing. ## iOS Simulator -Never share a simulator with another worktree. Claim one before any build, install, login, Maestro, or MCP action; the command prefers an unclaimed shutdown device and boots it. A prewarm verifier uses `--phase prewarm`; the fresh acceptance verifier reclaims the same worktree-owned device with `--phase verify`: +Never share a simulator with another worktree. Claim one before any build, install, login, Maestro, or MCP action; the claim command prefers an unclaimed shutdown iPhone and boots it. A prewarm verifier uses `--phase prewarm`; the fresh acceptance verifier reclaims the same worktree-owned device with `--phase verify`: ```bash pnpm dev:mobile:simulator claim [udid] --phase prewarm pnpm dev:mobile:simulator claim [udid] --phase verify ``` -The wrapper visibly renames a claimed device to `Kilo E2E - - `. Verifiers must not call `xcrun simctl rename` directly. Releasing an owned claim restores the original simulator name before removing the claim. +The wrapper visibly renames a claimed device to `Kilo E2E - - ` and restores the original simulator name on release. Never call `xcrun simctl rename` directly. -Install a validated cached native build. A compatible fingerprint avoids rebuilding; a cache miss is serialized through the host-wide native compiler semaphore: +Install a validated cached native build. A compatible fingerprint skips rebuilding; a cache miss is serialized through the host-wide native compiler semaphore. Do not consume an arbitrary DerivedData app or run a separate Expo native build: ```bash pnpm dev:mobile:ios build ``` -Do not consume an arbitrary DerivedData app or run a separate Expo native build. Connect the validated app to the Metro URL shown by this worktree's `mobile` pane: +Connect the app to the Metro URL shown by this worktree's `mobile` pane: ```bash xcrun simctl openurl \ "exp+kilo-app://expo-development-client/?url=http%3A%2F%2F%3A" ``` -Prefer `xcrun simctl openurl` for low-level scheme reconnection to avoid Safari's external-app confirmation. If an acceptance flow intentionally follows the scheme through Safari or a WebView, inspect the hierarchy for the exact message `Open this page in "Kilo"?`, then tap the exact `Open` accessibility action. This is one bounded optional prompt within the existing five-second optional-prompt budget, not permission to add another fixed wait. +Prefer `xcrun simctl openurl` for scheme reconnection: it avoids Safari's external-app confirmation. When an acceptance flow intentionally goes through Safari or a WebView, inspect the hierarchy for the exact message `Open this page in "Kilo"?` and tap the exact `Open` accessibility action. This is one bounded optional prompt inside the existing five-second optional-prompt budget, not permission to add another fixed wait. -Before testing, capture the `mobile` pane and verify `Starting project at /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. +Before testing, capture the `mobile` pane and verify `Starting project at /apps/mobile` plus a fresh `iOS Bundled` line. Seeing the Kilo login screen does not prove bundle provenance. The login preflight additionally reads Metro's development manifest and checks `expoConfig.extra.apiBaseUrl` and `_internal.projectRoot` against this worktree. A dev client gets these extras from the Metro manifest, so after env changes: regenerate env, restart Metro, reconnect the dev client to the exact Metro URL, and reload. Rebuild only when native config or plugins changed. + +The shared launch flows dismiss the clean-install tracking alert, accept the Expo dev-menu introduction with `Continue`, and close the full Expo/React Native developer menu (the one containing Fast Refresh and Element Inspector) with its `Close` accessibility action. ## Sign In and Out -Backend and Metro must be running. These idempotent wrappers first verify simulator ownership, this worktree's required services, generated API port, and Metro project provenance, then reconnect the dev client to that exact Metro URL before launch. Do not bypass their preflight or call the YAML login steps directly: +Backend and Metro must be running. These idempotent wrappers verify simulator ownership, required services, the generated API port, and Metro project provenance, then reconnect the dev client to this worktree's exact Metro URL before Maestro runs. Never bypass their preflight or call the YAML login steps directly: ```bash apps/mobile/e2e/login.sh [email] # default: e2e-mobile@example.com apps/mobile/e2e/logout.sh ``` -Login requests an email OTP, waits up to 30 seconds for the worktree-local outbox, verifies it, accepts first-account consent, and asserts Home. It retries only the known dev-client launch/request boundary once. The wrappers use preflight to open the exact dev-client URL, then `flows/settle-app.yaml` handles late tracking and Expo developer-menu prompts without restarting the app. `flows/open-app.yaml` remains the standalone cold-launch flow. +Login requests an email OTP, waits up to 30 seconds for the worktree-local outbox, verifies the code, accepts first-account consent, and asserts Home. It retries only the known dev-client launch/request boundary once. The wrappers open the exact dev-client URL via preflight, then `flows/settle-app.yaml` handles late tracking and Expo developer-menu prompts without restarting the app. `flows/open-app.yaml` is the standalone cold-launch flow. + +Native prompts are states in the flow, not errors to tap through blindly: -Native prompts are states in the flow, not errors to tap through blindly. The shared launch flow recognizes the iOS tracking prompt (`Allow “Kilo” to track your activity across other companies’ apps and websites?`) and chooses `Ask App Not to Track`; login handles notification permission after authentication. Feature-triggered prompts such as speech recognition and microphone access must be handled only when the acceptance flow reaches that feature: inspect the hierarchy, copy the exact button accessibility text (`Allow` or `Don’t Allow`), and choose the state required by the test. Never use a generic `tapOn: 'Allow'` before identifying which prompt is visible. +- The shared launch flow answers the iOS tracking prompt (`Allow “Kilo” to track your activity across other companies’ apps and websites?`) with `Ask App Not to Track`. +- Login handles the notification permission after authentication. +- Handle feature-triggered prompts (speech recognition, microphone) only when the acceptance flow reaches that feature: inspect the hierarchy, copy the exact button accessibility text (`Allow` or `Don’t Allow`), and choose the state the test requires. +- Never use a generic `tapOn: 'Allow'` before identifying which prompt is visible. Maestro can emit a large interactive transcript. For agent-driven runs, keep successful output out of context and show only a bounded failure tail: @@ -119,8 +122,8 @@ apps/mobile/e2e/login.sh >"$LOGIN_LOG" 2>&1 || \ When editing the flows, preserve these device-tested constraints: - Tap the Kilo home-screen icon; Maestro `launchApp` can bounce the Expo dev client to SpringBoard. -- Pass `EMAIL` and `OTP` with `-e`; flow-level defaults override them in the installed Maestro version. -- Target the email field by `you@example.com`, and tap `Verify code` without trying to dismiss the number pad. +- Pass `EMAIL` and `OTP` with `-e`; flow-level defaults override `-e` values in the installed Maestro version. +- Target the email field by its placeholder `you@example.com`, and tap `Verify code` without trying to dismiss the number pad. - The native sign-out confirmation is the first case-insensitive `Sign Out` match (`index: 0`). Seed only when needed: @@ -132,17 +135,14 @@ pnpm dev:seed app:add-credits ## Maestro -One-time machine setup: - -```bash -brew install maestro -``` - -For MCP use stdio command `maestro mcp`, then restart the agent session so its tools appear. Inspect the screen before selecting elements and re-inspect after UI changes. Never guess a selector from the visible label or screenshot: copy the exact `txt` or `a11y` value from `maestro_inspect_screen`, mapping `a11y` to Maestro `text:`. Maestro text matching is full-string regex, not substring matching. +One-time machine setup: `brew install maestro`. For MCP, use stdio command `maestro mcp`, then restart the agent session so its tools appear. -Use Maestro as the primary iOS automation driver. Fall back to `xcrun simctl` only when Maestro cannot inspect or operate a native state, or when low-level simulator control is required. iOS setup still uses `simctl` for boot, install/uninstall, dev-client URL reconnection, screenshots, shutdown, and cleanup. +Rules: -Tab buttons are exposed through React Navigation's full accessibility labels, not the visible uppercase text. Current iOS labels are `Home, tab, 1 of 4`, `KiloClaw, tab, 2 of 4`, `Agents, tab, 3 of 4`, and `Profile, tab, 4 of 4`. `tapOn: 'Agents'` is wrong. Inspect again before using these examples because tab count or labels can change. +- Use Maestro as the primary automation driver on both iOS and Android. Fall back to `xcrun simctl` (iOS) or repository-wrapped ADB (Android) only when Maestro cannot inspect or operate a native state, or when low-level device control is required. Setup still uses `simctl`/ADB for boot, install, dev-client URL reconnection, screenshots, shutdown, and cleanup. +- Inspect the screen before selecting elements and re-inspect after UI changes. +- Never guess a selector from a visible label or screenshot. Copy the exact `txt` or `a11y` value from `maestro_inspect_screen`, mapping `a11y` to Maestro `text:`. Maestro text matching is full-string regex, not substring matching. +- Tab buttons expose React Navigation's full accessibility labels, not the visible uppercase text. Current iOS labels: `Home, tab, 1 of 4`, `KiloClaw, tab, 2 of 4`, `Agents, tab, 3 of 4`, `Profile, tab, 4 of 4`. `tapOn: 'Agents'` is wrong. Inspect again before relying on these examples; tab count and labels can change. CLI fallback: @@ -156,7 +156,9 @@ 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. 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: +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 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 discovery and mirroring by inspecting its pane and the mobile list: ```bash CLI_SESSION="kilo-e2e-cli-$(basename "$PWD")" @@ -165,17 +167,17 @@ tmux list-windows -t "$CLI_SESSION" tmux capture-pane -p -t "$CLI_SESSION":cli -S -100 ``` -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. +Drive the 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 (about 12 seconds). If no session is prepared for this worktree, stop and ask the orchestrator to install the CLI, mint a token, and start the session. ## Android Emulator -Do not conclude that Android is unavailable from `command -v adb` or the agent's inherited `PATH`. The repository resolves the SDK and JDK 17 from `ANDROID_HOME`, `~/Library/Android/sdk`, and standard Homebrew locations. Run the doctor first; it prints resolved absolute paths and available AVDs: +Do not conclude Android is unavailable from `command -v adb` or the inherited `PATH`. The repository resolves the SDK and JDK 17 from `ANDROID_HOME`, `~/Library/Android/sdk`, and standard Homebrew locations. Run the doctor first; it prints resolved absolute paths and available AVDs: ```bash pnpm dev:mobile:android doctor ``` -Use the wrappers for all Android tooling so the resolved SDK/JDK environment is applied, including the Expo/Gradle build: +Use the wrappers for all Android tooling, including the Expo/Gradle build, so the resolved SDK/JDK environment is applied: ```bash ANDROID_SESSION="kilo-e2e-android-$(basename "$PWD")" @@ -190,7 +192,7 @@ pnpm dev:mobile:android build The build command installs a validated cached APK when the Android native fingerprint and toolchain match. Do not install an APK from another output path or invoke Gradle directly. -Use Maestro as the primary Android automation driver, matching iOS. Fall back to repository-wrapped ADB when Maestro cannot inspect or operate a native prompt, when direct intent/process control is required, or when diagnosing the emulator itself. Android setup still uses ADB for readiness and port reversal. Use the repository wrapper rather than bare `adb`: +ADB fallback commands (Maestro remains the primary driver, as above): ```bash pnpm dev:mobile:android adb devices -l @@ -202,26 +204,29 @@ pnpm dev:mobile:android adb -s shell input text '' pnpm dev:mobile:android adb -s shell input keyevent KEYCODE_BACK ``` -Derive tap coordinates from the current `uiautomator` bounds, not screenshots or remembered positions. Re-dump after every navigation or prompt. Android's `localhost` is the emulator, so restore both `adb reverse` mappings after clearing app data. The dev-client scheme is `exp+kilo-app`. `adb shell pm clear com.kilocode.kiloapp` also forgets the Metro URL, so re-open the dev-client URL afterward with `adb shell am start`. +Android specifics: -The existing login/logout helpers accept either an iOS simulator UDID or an Android ADB serial. Their shared preflight applies platform-specific device ownership and reconnects the dev client to this worktree before Maestro runs. +- Derive tap coordinates from the current `uiautomator` bounds, never from screenshots or remembered positions. Re-dump after every navigation or prompt. +- Android's `localhost` is the emulator itself, so restore both `adb reverse` mappings after clearing app data. +- The dev-client scheme is `exp+kilo-app`. `adb shell pm clear com.kilocode.kiloapp` also forgets the Metro URL; re-open the dev-client URL afterward with `adb shell am start`. +- The login/logout helpers accept either an iOS simulator UDID or an Android ADB serial; their shared preflight applies platform-specific ownership checks and reconnects the dev client to this worktree. ## Cleanup -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: +Clean up only resources you started. The remote CLI session and its disposable install belong to the orchestrator; do not kill `kilo-e2e-cli-*` sessions or remove CLI scratch directories you did not create. ```bash -tmux kill-session -t "$ANDROID_SESSION" # if created -rm -f "$LOGIN_LOG" # if created -pnpm dev:stop # only if you started this worktree's stack -xcrun simctl shutdown # only if you booted it -pnpm dev:mobile:simulator release # release every simulator you claimed +tmux kill-session -t "$ANDROID_SESSION" # if created +rm -f "$LOGIN_LOG" # if created +pnpm dev:stop # only if you started this worktree's stack +xcrun simctl shutdown # only if you booted it +pnpm dev:mobile:simulator release # release every simulator you claimed pnpm dev:mobile:android release # release every Android device you claimed ``` -Also stop recorders, log followers, and emulator processes you created. Never use `tmux kill-server`, kill unrelated `kilo-dev-*` sessions, stop a simulator that was already booted, or use `pnpm dev:stop --force` while sibling worktrees are active. +Also stop recorders, log followers, and emulator processes you created. Never use `tmux kill-server`, kill an unrelated `kilo-dev-*` session, shut down a simulator that was already booted, or use `pnpm dev:stop --force` while sibling worktrees are active. -Verify cleanup: +Verify cleanup, and confirm no generated E2E fixtures remain tracked or untracked: ```bash pnpm dev:status --json @@ -229,5 +234,3 @@ tmux ls xcrun simctl list devices booted git status --short ``` - -Confirm no generated E2E fixtures remain tracked or untracked in the repository. diff --git a/apps/mobile/e2e/flows/login-request-code.yaml b/apps/mobile/e2e/flows/login-request-code.yaml index 0d281fb4a6..abfa9a81e8 100644 --- a/apps/mobile/e2e/flows/login-request-code.yaml +++ b/apps/mobile/e2e/flows/login-request-code.yaml @@ -11,7 +11,7 @@ # default silently overrides the -e value in this Maestro version. # # Prefer the one-shot wrapper e2e/login.sh, which runs both steps and pulls the -# code for you. See e2e/AGENTS.md ("Login / logout helper flows"). +# code for you. See e2e/AGENTS.md ("Sign In and Out"). appId: com.kilocode.kiloapp --- - runFlow: logout.yaml diff --git a/apps/mobile/e2e/flows/open-app.yaml b/apps/mobile/e2e/flows/open-app.yaml index c3f54667d5..cf1c19e894 100644 --- a/apps/mobile/e2e/flows/open-app.yaml +++ b/apps/mobile/e2e/flows/open-app.yaml @@ -1,3 +1,6 @@ +# Standalone cold-launch flow: clears any prompt left on screen, relaunches the +# dev client from SpringBoard, waits for it to render a known state, then +# delegates prompt handling to settle-app.yaml. appId: com.kilocode.kiloapp --- - runFlow: @@ -22,50 +25,8 @@ appId: com.kilocode.kiloapp commands: - tapOn: text: 'Kilo' +# Cold launch and bundling are slow; wait for any known state before settling. - extendedWaitUntil: visible: 'Open this page in "Kilo"\?|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: 'Open this page in "Kilo"\?|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: - when: - visible: 'Open this page in "Kilo"\?' - commands: - - tapOn: 'Open' -- extendedWaitUntil: - visible: '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: 5000 -- runFlow: - when: - visible: 'Ask App Not to Track' - commands: - - tapOn: 'Ask App Not to Track' -- extendedWaitUntil: - visible: '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: 5000 -- runFlow: - when: - visible: 'This is the developer menu.*' - commands: - - tapOn: 'Continue' -- extendedWaitUntil: - visible: '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: 5000 -- runFlow: - when: - visible: 'Fast Refresh|Element Inspector' - commands: - - tapOn: 'Close' -- extendedWaitUntil: - visible: '“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue' - timeout: 5000 -- runFlow: - when: - visible: '“Kilo” Would Like to Send You Notifications' - commands: - - tapOn: 'Allow' -- extendedWaitUntil: - visible: 'HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue' - timeout: 30000 +- runFlow: settle-app.yaml diff --git a/apps/mobile/e2e/flows/settle-app.yaml b/apps/mobile/e2e/flows/settle-app.yaml index 41720a49cb..a6e1d2be6c 100644 --- a/apps/mobile/e2e/flows/settle-app.yaml +++ b/apps/mobile/e2e/flows/settle-app.yaml @@ -1,3 +1,7 @@ +# Settles an already-running app: handles the Safari external-app prompt, +# tracking prompt, Expo developer-menu introduction and menu, and notification +# permission, ending on Home, the login page, or the consent gate. Never +# restarts the app; open-app.yaml is the cold-launch wrapper around this flow. appId: com.kilocode.kiloapp --- - extendedWaitUntil: diff --git a/apps/mobile/e2e/logout.sh b/apps/mobile/e2e/logout.sh index b85a405bed..5d81a971bd 100755 --- a/apps/mobile/e2e/logout.sh +++ b/apps/mobile/e2e/logout.sh @@ -5,7 +5,7 @@ # Usage: # e2e/logout.sh # -# Requires: maestro. See e2e/AGENTS.md ("Login / logout helper flows"). +# Requires: maestro. See e2e/AGENTS.md ("Sign In and Out"). set -euo pipefail DEVICE="${1:?usage: logout.sh }" diff --git a/dev/local/mobile-workflow.test.ts b/dev/local/mobile-workflow.test.ts index 3749b5f526..e08171a7d3 100644 --- a/dev/local/mobile-workflow.test.ts +++ b/dev/local/mobile-workflow.test.ts @@ -2,56 +2,40 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import test from 'node:test'; -test('login waits for the delayed Expo developer menu after launching Kilo', () => { +test('cold launch clears leftover prompts, relaunches, then settles via the shared flow', () => { const flow = fs.readFileSync('apps/mobile/e2e/flows/open-app.yaml', 'utf8'); + const trackingGuardIndex = flow.indexOf("visible: 'Ask App Not to Track'"); + const stopIndex = flow.indexOf('- stopApp'); const launchIndex = flow.indexOf("text: 'Kilo'"); - const developerMenuWaitIndex = flow.indexOf( - "visible: 'This is the developer menu.*'", - launchIndex - ); - const optionalWaitIndex = flow.lastIndexOf('- extendedWaitUntil:', developerMenuWaitIndex); - const continueIndex = flow.indexOf("tapOn: 'Continue'", developerMenuWaitIndex); - - assert.ok(launchIndex >= 0); - assert.ok(developerMenuWaitIndex > launchIndex); - assert.ok(continueIndex > developerMenuWaitIndex); - assert.match(flow.slice(optionalWaitIndex, continueIndex), /timeout: 5000/); - assert.doesNotMatch(flow.slice(optionalWaitIndex, continueIndex), /optional: true/); + const readyWaitIndex = flow.indexOf('- extendedWaitUntil:', launchIndex); + const settleIndex = flow.indexOf('- runFlow: settle-app.yaml'); + + assert.ok(trackingGuardIndex >= 0 && trackingGuardIndex < stopIndex); + assert.ok(stopIndex < launchIndex); + assert.ok(launchIndex < readyWaitIndex); + assert.ok(readyWaitIndex < settleIndex); + assert.match(flow.slice(readyWaitIndex, settleIndex), /timeout: 30000/); + assert.doesNotMatch(flow.slice(readyWaitIndex), /optional: true/); }); -test('shared launch flows close the Expo developer menu after its introduction', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - for (const flowPath of [ - 'apps/mobile/e2e/flows/open-app.yaml', - 'apps/mobile/e2e/flows/settle-app.yaml', - ]) { - const flow = fs.readFileSync(flowPath, 'utf8'); - const continueIndex = flow.indexOf("tapOn: 'Continue'"); - const closeGuardIndex = flow.indexOf( - "visible: 'Fast Refresh|Element Inspector'", - continueIndex - ); - const closeIndex = flow.indexOf("tapOn: 'Close'", closeGuardIndex); +test('settle flow closes the Expo developer menu after its introduction', () => { + const flow = fs.readFileSync('apps/mobile/e2e/flows/settle-app.yaml', 'utf8'); + const continueIndex = flow.indexOf("tapOn: 'Continue'"); + const closeGuardIndex = flow.indexOf("visible: 'Fast Refresh|Element Inspector'", continueIndex); + const closeIndex = flow.indexOf("tapOn: 'Close'", closeGuardIndex); - assert.ok(continueIndex >= 0, `${flowPath} should accept the developer-menu introduction`); - assert.ok(closeGuardIndex > continueIndex, `${flowPath} should detect the opened menu`); - assert.ok(closeIndex > closeGuardIndex, `${flowPath} should close the opened menu`); - assert.doesNotMatch(flow, /when:\n\s+visible: 'Close'/); - } - - assert.match( - runbook, - /developer menu containing Fast Refresh and Element Inspector with its `Close` accessibility action/ - ); + assert.ok(continueIndex >= 0, 'settle-app should accept the developer-menu introduction'); + assert.ok(closeGuardIndex > continueIndex, 'settle-app should detect the opened menu'); + assert.ok(closeIndex > closeGuardIndex, 'settle-app should close the opened menu'); + assert.doesNotMatch(flow, /when:\n\s+visible: 'Close'/); }); -test('login flows never use an unidentified generic Allow selector', () => { +test('launch flows never use an unidentified generic Allow selector', () => { const request = fs.readFileSync('apps/mobile/e2e/flows/login-request-code.yaml', 'utf8'); - const openApp = fs.readFileSync('apps/mobile/e2e/flows/open-app.yaml', 'utf8'); + const settle = fs.readFileSync('apps/mobile/e2e/flows/settle-app.yaml', 'utf8'); assert.doesNotMatch(request, /visible: 'Allow'/); - assert.match(openApp, /“Kilo” Would Like to Send You Notifications/); + assert.match(settle, /“Kilo” Would Like to Send You Notifications/); }); test('login verification does not pay a fixed optional notification wait', () => { @@ -98,129 +82,111 @@ test('login polls the local outbox without one-second latency', () => { }); test('shared launch prompt grace periods total at most five seconds', () => { - for (const flowPath of [ - 'apps/mobile/e2e/flows/open-app.yaml', - 'apps/mobile/e2e/flows/settle-app.yaml', - ]) { - const flow = fs.readFileSync(flowPath, 'utf8'); - const optionalWaits = [...flow.matchAll(/timeout: (\d+)\n\s+optional: true/g)].map(match => - Number(match[1]) - ); + const settle = fs.readFileSync('apps/mobile/e2e/flows/settle-app.yaml', 'utf8'); + const openApp = fs.readFileSync('apps/mobile/e2e/flows/open-app.yaml', 'utf8'); + const optionalWaits = [...settle.matchAll(/timeout: (\d+)\n\s+optional: true/g)].map(match => + Number(match[1]) + ); - assert.deepEqual(optionalWaits, [3000]); - assert.ok(optionalWaits.reduce((total, timeout) => total + timeout, 0) <= 5000); - } + assert.deepEqual(optionalWaits, [3000]); + assert.doesNotMatch(openApp, /optional: true/); }); -test('shared launch flows handle the exact iOS external-app prompt within existing waits', () => { - const flowContracts = [ +test('settle flow handles the exact iOS external-app prompt within existing waits', () => { + const flow = fs.readFileSync('apps/mobile/e2e/flows/settle-app.yaml', 'utf8'); + const promptGuardIndex = flow.indexOf(`visible: 'Open this page in "Kilo"\\?'`); + const openActionIndex = flow.indexOf("tapOn: 'Open'", promptGuardIndex); + const finalReadyWaitIndex = flow.lastIndexOf('- extendedWaitUntil:'); + const waitBlocks = [...flow.matchAll(/- extendedWaitUntil:\n[\s\S]*?(?=\n- |$)/g)].map( + match => match[0] + ); + const timeouts = [...flow.matchAll(/timeout: (\d+)/g)].map(match => Number(match[1])); + + assert.match( + waitBlocks[0], + /Open this page in "Kilo"\\\?/, + 'settle-app should recognize the prompt as its initial visible state' + ); + assert.match( + waitBlocks[1], + /Open this page in "Kilo"\\\?/, + 'settle-app should recognize the prompt inside its optional wait' + ); + assert.match(waitBlocks[1], /timeout: 3000\n\s+optional: true/); + assert.ok(promptGuardIndex > flow.indexOf(waitBlocks[1]), 'settle-app should guard Open'); + assert.ok(openActionIndex > promptGuardIndex, 'settle-app should tap the exact Open action'); + + const promptChain = [ + { + action: "tapOn: 'Open'", + nextGuard: 'Ask App Not to Track', + visible: + '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', + }, + { + action: "tapOn: 'Ask App Not to Track'", + nextGuard: 'This is the developer menu.*', + visible: + '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', + }, { - path: 'apps/mobile/e2e/flows/open-app.yaml', - timeouts: [30000, 3000, 5000, 5000, 5000, 5000, 30000], + action: "tapOn: 'Continue'", + nextGuard: 'Fast Refresh|Element Inspector', + visible: + 'Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue', }, { - path: 'apps/mobile/e2e/flows/settle-app.yaml', - timeouts: [15000, 3000, 5000, 5000, 5000, 5000, 15000], + action: "tapOn: 'Close'", + nextGuard: '“Kilo” Would Like to Send You Notifications', + visible: + '“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue', }, ]; - for (const contract of flowContracts) { - const flow = fs.readFileSync(contract.path, 'utf8'); - const promptGuardIndex = flow.indexOf(`visible: 'Open this page in "Kilo"\\?'`); - const openActionIndex = flow.indexOf("tapOn: 'Open'", promptGuardIndex); - const finalReadyWaitIndex = flow.lastIndexOf('- extendedWaitUntil:'); - const waitBlocks = [...flow.matchAll(/- extendedWaitUntil:\n[\s\S]*?(?=\n- |$)/g)].map( - match => match[0] + let searchFrom = openActionIndex; + for (const step of promptChain) { + const actionIndex = flow.indexOf(step.action, searchFrom); + const nextGuardIndex = flow.indexOf( + `- runFlow:\n when:\n visible: '${step.nextGuard}'`, + actionIndex ); - const timeouts = [...flow.matchAll(/timeout: (\d+)/g)].map(match => Number(match[1])); + const betweenActionAndGuard = flow.slice(actionIndex, nextGuardIndex); - assert.match( - waitBlocks[0], - /Open this page in "Kilo"\\\?/, - `${contract.path} should recognize the prompt as its initial visible state` + assert.ok(actionIndex >= searchFrom, `settle-app should include ${step.action}`); + assert.ok( + nextGuardIndex > actionIndex, + `settle-app should guard ${step.nextGuard} after ${step.action}` ); assert.match( - waitBlocks[1], - /Open this page in "Kilo"\\\?/, - `${contract.path} should recognize the prompt inside its optional wait` + betweenActionAndGuard, + new RegExp( + `- extendedWaitUntil:\\n visible: '${step.visible.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'\\n timeout: 5000` + ), + `settle-app should poll for the next state immediately after ${step.action}` ); - assert.match(waitBlocks[1], /timeout: 3000\n\s+optional: true/); - assert.ok( - promptGuardIndex > flow.indexOf(waitBlocks[1]), - `${contract.path} should guard the Open action` + assert.doesNotMatch( + betweenActionAndGuard, + /timeout: (?:500|1000)\n|optional: true/, + `settle-app should use a robust non-optional state gate after ${step.action}` ); - assert.ok( - openActionIndex > promptGuardIndex, - `${contract.path} should tap the exact Open action` + assert.doesNotMatch( + betweenActionAndGuard.replace(/^- runFlow:[\s\S]*?commands:\n\s+- tapOn: '[^']+'/, ''), + /- runFlow:/, + `settle-app should not insert another one-shot guard before polling` ); - const promptChain = [ - { - action: "tapOn: 'Open'", - nextGuard: 'Ask App Not to Track', - visible: - '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', - }, - { - action: "tapOn: 'Ask App Not to Track'", - nextGuard: 'This is the developer menu.*', - visible: - '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', - }, - { - action: "tapOn: 'Continue'", - nextGuard: 'Fast Refresh|Element Inspector', - visible: - 'Fast Refresh|Element Inspector|“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue', - }, - { - action: "tapOn: 'Close'", - nextGuard: '“Kilo” Would Like to Send You Notifications', - visible: - '“Kilo” Would Like to Send You Notifications|HOME|Home, tab, 1 of 4|Welcome to Kilo Code|Accept and continue', - }, - ]; - - let searchFrom = openActionIndex; - for (const step of promptChain) { - const actionIndex = flow.indexOf(step.action, searchFrom); - const nextGuardIndex = flow.indexOf( - `- runFlow:\n when:\n visible: '${step.nextGuard}'`, - actionIndex - ); - const betweenActionAndGuard = flow.slice(actionIndex, nextGuardIndex); - - assert.ok(actionIndex >= searchFrom, `${contract.path} should include ${step.action}`); - assert.ok( - nextGuardIndex > actionIndex, - `${contract.path} should guard ${step.nextGuard} after ${step.action}` - ); - assert.match( - betweenActionAndGuard, - new RegExp( - `- extendedWaitUntil:\\n visible: '${step.visible.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'\\n timeout: 5000` - ), - `${contract.path} should poll for the next state immediately after ${step.action}` - ); - assert.doesNotMatch( - betweenActionAndGuard, - /timeout: (?:500|1000)\n|optional: true/, - `${contract.path} should use a robust non-optional state gate after ${step.action}` - ); - assert.doesNotMatch( - betweenActionAndGuard.replace(/^- runFlow:[\s\S]*?commands:\n\s+- tapOn: '[^']+'/, ''), - /- runFlow:/, - `${contract.path} should not insert another one-shot guard before polling` - ); - searchFrom = nextGuardIndex; - } - assert.ok( - finalReadyWaitIndex > openActionIndex, - `${contract.path} should still wait for its final ready state` - ); - assert.deepEqual(timeouts, contract.timeouts, `${contract.path} should not add a fixed wait`); - assert.doesNotMatch(flow, /visible: '(?:Allow|Open)'/); - assert.doesNotMatch(flow, /tapOn: '(?:Allow\|Open|Open\|Allow)'/); + searchFrom = nextGuardIndex; } + assert.ok( + finalReadyWaitIndex > openActionIndex, + 'settle-app should still wait for its final ready state' + ); + assert.deepEqual( + timeouts, + [15000, 3000, 5000, 5000, 5000, 5000, 15000], + 'settle-app should not add a fixed wait' + ); + assert.doesNotMatch(flow, /visible: '(?:Allow|Open)'/); + assert.doesNotMatch(flow, /tapOn: '(?:Allow\|Open|Open\|Allow)'/); }); test('helper-driven logout settles the app already launched by preflight', () => { @@ -240,20 +206,6 @@ test('logout skips prompt settling for stable signed-in and signed-out states', assert.ok(logout.indexOf("notVisible: 'HOME|Home, tab, 1 of 4'") < settleIndex); }); -test('shared launch clears an already-visible tracking prompt before tapping the app icon', () => { - const flow = fs.readFileSync('apps/mobile/e2e/flows/open-app.yaml', 'utf8'); - assert.ok(flow.indexOf("visible: 'Ask App Not to Track'") < flow.indexOf("visible: 'Kilo'")); -}); - -test('mobile workflow documents hierarchy-derived tab selectors', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - assert.match(runbook, /Agents, tab, 3 of 4/); - assert.match(runbook, /Never guess a selector from the visible label/); - assert.match(runbook, /pnpm dev:capture mobile/); - assert.match(runbook, /dev:mobile:simulator claim/); -}); - test('tab layout exposes the exact documented accessibility labels', () => { const layout = fs.readFileSync('apps/mobile/src/app/(app)/(tabs)/_layout.tsx', 'utf8'); @@ -299,24 +251,14 @@ test('Android tooling is resolved independently of the agent PATH', async () => assert.match(env.path, /android-commandlinetools\/platform-tools/); }); -test('Android workflow uses Maestro first and applies resolved tooling to cached native builds', () => { +test('Android cached native builds apply resolved tooling', () => { const android = fs.readFileSync('dev/local/mobile-android.ts', 'utf8'); - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); assert.match(android, /'build'/); assert.match(android, /runAndroidBuild/); assert.match(android, /withNativeBuildSemaphore/); assert.match(android, /app:assembleDebug/); assert.match(android, /path\.join\(worktreeRoot, 'apps\/mobile'\)/); - assert.match(runbook, /Use Maestro as the primary Android automation driver/); - assert.match(runbook, /Fall back to repository-wrapped ADB/); -}); - -test('iOS workflow uses Maestro first with simctl as the low-level fallback', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - assert.match(runbook, /Use Maestro as the primary iOS automation driver/); - assert.match(runbook, /Fall back to `xcrun simctl`/); }); test('Android tooling rejects a non-Java-17 JAVA_HOME', async () => { @@ -353,51 +295,13 @@ test('env sync refreshes source-backed Wrangler secrets through completed stdin assert.match(envOutput, /Failed to create Secrets Store secret/); }); -test('workflow documents the shared Docker proxy exception without weakening backend isolation', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); +test('dev CLI shares only the Docker proxy port between worktrees', () => { const cli = fs.readFileSync('dev/local/cli.ts', 'utf8'); - assert.match(runbook, /sole intentional host-wide exception/); - assert.match(runbook, /Never kill a `socat` process owned by another worktree/); assert.match(cli, /name === 'kiloclaw-docker-tcp'/); assert.match(cli, /Refusing to share occupied worktree service ports/); }); -test('mobile runbook prohibits host-global Metro proxies', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - assert.match(runbook, /must never map.*8081.*worktree.*Metro/is); - assert.match(runbook, /sole intentional host-wide proxy exception.*23750/is); - assert.match(runbook, /test-environment failure/i); - assert.match(runbook, /PID.*parent PID.*bind address.*port/is); -}); - -test('mobile runbook uses ownership-aware simulator phase labels', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - assert.match(runbook, /Kilo E2E - - /); - assert.match(runbook, /must not call.*simctl rename/i); - assert.match(runbook, /restores the original simulator name/i); -}); - -test('mobile runbook installs validated cached native builds', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - assert.match(runbook, /dev:mobile:ios build /); - assert.match(runbook, /dev:mobile:android build /); - assert.match(runbook, /validated cached/i); - assert.doesNotMatch(runbook, /npx expo run:ios --device/); -}); - -test('mobile runbook handles the exact Safari external-app prompt within the shared budget', () => { - const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); - - assert.match(runbook, /Open this page in [“"]Kilo[”"]\?/); - assert.match(runbook, /exact.*Open.*accessibility/is); - assert.match(runbook, /five-second optional-prompt/i); - assert.match(runbook, /prefer.*simctl openurl.*avoid.*confirmation/is); -}); - test('remote CLI runbook is secret-free and defers credential-bearing setup to the orchestrator', () => { const runbook = fs.readFileSync('apps/mobile/e2e/AGENTS.md', 'utf8'); const remoteCliSection = runbook.slice(