Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .kilo/agent/e2e-verifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Before testing:
1. Read the learnings — the worktree's `.kilo_workflow/learnings/` plus the main checkout's `~/Projects/cloud/.kilo_workflow/learnings/system/` — then the surface-specific runbook, and follow it exactly for services, device claiming, builds, login, automation drivers, prompts, and cleanup — mobile: `apps/mobile/e2e/AGENTS.md`; extension: `apps/extension/AGENTS.md`; web and services: `DEVELOPMENT.md` and the repository dev runner. Never bypass a helper script's preflight, install unvalidated builds, or guess selectors.
2. Translate the plan's goals and acceptance criteria into observable flows; for user-facing features, cover the happy, retryable-unhappy, non-retryable-unhappy, and empty states.
3. Record pre-existing services, listeners, devices, and tmux sessions so cleanup removes only resources you created. Never use a device claimed by another worktree.
4. Immediately before starting a stack, booting a simulator or emulator, or running a native build — and only then, never while you are still reading or planning — acquire a machine device slot with `.kilo_workflow/e2e-slot.sh acquire <your-tmux-session>`. The owner string is your own tmux session name (`tmux display-message -p '#S'`) — your dispatcher launched you in a dedicated session for exactly this reason; never pass a window name or a shared session name. This is mandatory on every run — the machine is shared and unslotted device work overloads it. The command blocks until a slot frees; blocking is correct behavior, never a wedge to work around, and never a reason to proceed unslotted.
4. Immediately before starting a stack, booting a simulator or emulator, or running a native build — and only then, never while you are still reading or planning — acquire a machine device slot with `.kilo_workflow/e2e-slot.sh acquire <your-tmux-session>`. The owner string is your own tmux session name, resolved through your own pane: `tmux display-message -p -t "$TMUX_PANE" '#S'`. Never the untargeted `tmux display-message -p '#S'` — it answers with the tmux **server's** current session, so you would own a slot under another session's name; that slot is reclaimed when *that* session dies while you are still driving a device, over-subscribing the machine and making every concurrent emulator boot and build time out. If `$TMUX_PANE` is unset you are not in a session that can own a slot: stop and report a test-environment blocker rather than guessing a name. Your dispatcher launched you in a dedicated session for exactly this reason; never pass a window name or a shared session name. This is mandatory on every run — the machine is shared and unslotted device work overloads it. The command blocks until a slot frees; blocking is correct behavior, never a wedge to work around, and never a reason to proceed unslotted.
5. Before any temporary edit, snapshot a baseline outside every repository: `git status --porcelain=v2 -z --untracked-files=all`, binary worktree and index diffs, and the byte hash, file mode, and symlink target of every untracked path. Copy the original bytes and mode of every tracked file you plan to edit. Temporary edits may touch only paths that are clean and tracked at baseline, or brand-new paths — never a pre-existing modified, staged, or untracked path.

Output discipline — long verification runs die when their session payload grows too large. Cap every shell command's output (`| tail -c 1500` or `| tail -5`); write hierarchies, captures, and service logs to files and print only greps or counts; never re-read screenshots into context; keep your final report bounded.
Expand Down
13 changes: 8 additions & 5 deletions .kilo_workflow/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The cwd/worktree is always the **cloud** worktree, even when the slice edits a s

What the script encodes (details in `learnings/`):

- tmux wrapping — harness command timeouts kill bare long runs. The `e2e-verifier` gets its **own tmux session** (E2E slots are owned and auto-reclaimed by session name; a window-named owner leaks or shares slots); other roles run as windows in the dispatcher's session. Names are `<section>-<role>-<label>`, logs `$SCRATCH/<role>-<label>.log`.
- tmux wrapping — harness command timeouts kill bare long runs. The `e2e-verifier` gets its **own tmux session** (E2E slots are owned and auto-reclaimed by session name; a window-named owner leaks or shares slots); other roles run as windows in the dispatcher's session, resolved through `$TMUX_PANE`. A dispatcher that is not itself inside tmux has no such session, so its roles get their own sessions too — never a guessed one. Names are `<section>-<role>-<label>`, logs `$SCRATCH/<role>-<label>.log`.
- Full `KILO_*`/`OPENCODE*` env strip — a dispatcher running inside kilo poisons nested runs otherwise (see `learnings/nested-kilo-run-env-poisoning.md`).
- Output redirected, never piped (`| tee` makes `$?` report the pipe's exit, not kilo's), with `EXITCODE=$?` appended as the log's last line.

Expand Down Expand Up @@ -106,7 +106,7 @@ The session the user invokes the workflow from is the starter, running on the ha
2. Explore the relevant parts of the codebase.
3. Interrogate the requirements — in `hands on` mode by grilling the user one question at a time, in `hands off` mode by grilling itself and answering from repository evidence, recording material assumptions. Always drive toward the simplest solution that achieves the user's goals, and challenge the request itself: "should we even do this?", "why not do this instead?", "we could achieve the same thing simpler, like this".
4. Divide the finalized work into related, **disjoint** sections — no two sections may touch the same files or contracts.
5. For each section: create the dedicated worktree and scratch directory (Ground Rules), write the section brief to `$SCRATCH/brief.md` — the work, the mode, acceptance criteria and constraints gathered so far, the requesting human's GitHub handle for PR assignment, the worktree path, and the scratch path — and launch a planner in a new tmux window on the planner harness and model. `<session>` is the starter's own tmux session (`tmux display-message -p '#S'`), and the `$SCRATCH` value must be expanded by the launching shell (double-quote the tmux command string; the tmux server does not know the variable):
5. For each section: create the dedicated worktree and scratch directory (Ground Rules), write the section brief to `$SCRATCH/brief.md` — the work, the mode, acceptance criteria and constraints gathered so far, the requesting human's GitHub handle for PR assignment, the worktree path, and the scratch path — and launch a planner in a new tmux window on the planner harness and model. `<session>` is the starter's own tmux session, resolved through its own pane — `tmux display-message -p -t "$TMUX_PANE" '#S'`; never the untargeted `tmux display-message -p '#S'`, which answers with the tmux **server's** current session (the most recently active one) and silently files the window under an unrelated section. A starter that is not itself inside tmux has no such session and `$TMUX_PANE` is unset: launch the planner with `tmux new-session -d -s <section>-planner` instead of guessing a target. The `$SCRATCH` value must be expanded by the launching shell (double-quote the tmux command string; the tmux server does not know the variable):

```bash
# kilo planner (the planner agent definition pins permissions; the model is the user's pick):
Expand Down Expand Up @@ -261,16 +261,19 @@ The machine is shared by parallel workflows, and unslotted device or stack work

```bash
.kilo_workflow/e2e-slot.sh acquire <tmux-session> # blocks until a slot frees
.kilo_workflow/e2e-slot.sh status # current holders
.kilo_workflow/e2e-slot.sh status # holders, their worktrees, stack coverage
.kilo_workflow/e2e-slot.sh release <tmux-session> # the moment the device phase ends
.kilo_workflow/e2e-slot.sh stacks [--reap] # stacks running with no slot
```

**A slot and a dev stack are the same resource.** The slot is what entitles a worktree to run a stack, and a stack must never outlive it: `release` stops the releasing worktree's stack, and reclaiming a dead holder's slot stops its stack too. So a later round re-acquires and starts a fresh stack rather than inheriting one — that restart is the price of the cap. A stack up with no slot is a defect, not a shortcut; `stacks` lists them and `stacks --reap` stops the workflow-owned ones (a stack with no section run id in its name was started by hand and is only reported). Five live stacks on this host drove the load average past 300 and made every emulator boot and native build time out, which reads as flaky devices rather than as over-subscription.

- Slot state lives in `$HOME/.cache/kilo-e2e-slots`, machine-global by design: every copy of the script — any worktree, any repository — contends for the same slots, and the script has no overrides by design. When working in a repository without the script (a sibling like `~/Projects/kilocode`), invoke it by absolute path from a cloud worktree.
- This holds on every run, not only when another workflow is visibly active, and a stack that is already up is not an exemption.
- `acquire` blocking is correct behavior, never a wedge to route around and never a reason to start device work unslotted. If an acquire is still blocked after about 45 minutes, the dispatcher inspects `status` for a wedged foreign holder and reports a blocker instead of waiting forever.
- The slot caps load, not data: postgres and redis containers are shared across worktrees. Keep test data keyed to this worktree's accounts (the runbooks' per-worktree defaults) and never wipe shared state.
- Release immediately when the device/stack phase ends. Planning, implementation, review, checks, and CI waits are uncapped; never hold a slot through them.
- Slots are owned by tmux session name and reclaimed automatically when the session dies. A holder that is alive but wedged belongs to its own workflow's monitor — never kill another session to free a slot; if the queue is starved by a foreign wedge, report a blocker to the user instead.
- Release immediately when the device/stack phase ends. Planning, implementation, review, checks, and CI waits are uncapped; never hold a slot through them — and since release takes the stack with it, do not release mid-round while you still need the services.
- Slots are owned by tmux session name, record the worktree that took them, and are reclaimed automatically when the session dies. A holder that is alive but wedged belongs to its own workflow's monitor — never kill another session to free a slot; if the queue is starved by a foreign wedge, report a blocker to the user instead.
- The orchestrator is accountable: every device-phase handoff states the slot rule, and a role agent that reports device work with no acquire gets re-dispatched.

## Feature-State Matrix
Expand Down
22 changes: 18 additions & 4 deletions .kilo_workflow/dispatch-role.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#
# The e2e-verifier gets its own tmux session (device slots are owned and
# auto-reaped by session name); every other role runs as a window in the
# caller's session.
# caller's session, or in its own session when the caller is not inside tmux.
set -euo pipefail

ROLE=${1:?role} SECTION=${2:?section} LABEL=${3:?label} WT=${4:?worktree} SCRATCH=${5:?scratch} MSG=${6:?message}
Expand All @@ -31,13 +31,27 @@ LOG="$SCRATCH/$ROLE-$LABEL.log"
# `|| true` keeps an empty match from failing under the pane's shell.
STRIP='$(env | grep -oE "^(KILO|OPENCODE)[A-Za-z0-9_]*" | sed "s/^/-u /" | tr "\n" " " || true)'

CMD="cd $(printf '%q' "$WT") && env $STRIP kilo run $(printf '%q' "$MSG") --agent $(printf '%q' "$ROLE") --title $(printf '%q' "$NAME")"
# Redirection below means an attached pane shows nothing at all. Say so in the
# pane itself — a blank window reads as a dead agent otherwise. This prints to
# the terminal only, never into the log, so the EXITCODE contract is untouched.
CMD="echo $(printf '%q' "$NAME: output goes to $LOG — this pane stays blank by design; watch with: tail -f $LOG") && cd $(printf '%q' "$WT") && env $STRIP kilo run $(printf '%q' "$MSG") --agent $(printf '%q' "$ROLE") --title $(printf '%q' "$NAME")"
for arg in "$@"; do CMD+=" $(printf '%q' "$arg")"; done
CMD+=" > $(printf '%q' "$LOG") 2>&1; echo EXITCODE=\$? >> $(printf '%q' "$LOG")"

if [ "$ROLE" = "e2e-verifier" ]; then
# Resolve the caller's session through this pane. An untargeted
# `tmux display-message -p '#S'` answers with the SERVER's current session —
# the most recently active one — so a dispatcher running outside tmux (a
# harness shell, a stripped kilo env) silently drops its window into an
# unrelated session. A freshly created `kilo-e2e-android-*` emulator session
# is the usual victim, and it gets killed wholesale on device cleanup.
CALLER_SESSION=""
if [ -n "${TMUX_PANE:-}" ]; then
CALLER_SESSION=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null || true)
fi

if [ "$ROLE" = "e2e-verifier" ] || [ -z "$CALLER_SESSION" ]; then
tmux new-session -d -s "$NAME" "$CMD"
else
tmux new-window -d -t "$(tmux display-message -p '#S')" -n "$NAME" "$CMD"
tmux new-window -d -t "$CALLER_SESSION" -n "$NAME" "$CMD"
fi
echo "$LOG"
Loading
Loading