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
1 change: 1 addition & 0 deletions docs/feature-map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ this area is reached from inside it unless stated otherwise.
| Merged edit rows | Back-to-back edits to the same file show as ONE transcript row: the first edit's row carries a chip with the run's combined count ("3 edits -6 +2"), and opening it lists each edit's diff in order under an "Edit N of M" label, with every edit's output in the row's details panel. Rows that draw nothing (completion markers, approvals, thinking) do not break a run; a read, a command, a message or an edit to another file does. An edit that went through an approval prompt, was rejected or blocked, carries a tool-risk badge, or has an oversized or truncated diff or output keeps its own row; an edit you approved at a prompt also shows an "Approved" tag beside its chip, so it does not read as a duplicate of the run above it. File names in the row title, the chip and the diff card headers are shown percent-decoded (`%20` as a space) while the real path is kept for opening the file | Session transcript → consecutive edits to one file | `pages/chat/editRuns.ts` (groups the runs), `pages/chat/ToolCallLine.tsx` (renders the leader and hides followers), `components/DiffBlock.tsx` (stacked diffs), `utils/pathBasename.ts` (display decoding) | client-only (no handler) | none |
| Tool risk badge | A small note under a tool card — **Jev: risky (0.88)** — in a session that approves its own tool calls, saying the Decisions (Jev) seam looked at that call and thought it was worth a second look. An ANNOTATION and nothing else: the permission DECISION is taken without consulting it, so the note changes nothing about what is allowed. It is not a verdict on whether the call RAN either -- a host gate or a PreToolUse hook can still refuse a call that carries a note, and the SEL tool-invocation audit is where that outcome lives. The tooltip says so in words — it states that flagging changed nothing about whether the call was allowed and that the call may still have been refused, never that it ran — and `risky` is drawn in the warning colour rather than the danger colour this transcript uses for a call that WAS blocked, because a coloured word on a tool card otherwise reads as "this was refused". Only the two flagged tiers are drawn, and each of them has to be BELIEVED: a `safe` answer is recorded in the log and leaves the card exactly as it looks without the seam, and so does a `caution` or `risky` answer the seam was under that tier's OWN bar confident about (two separately movable constants, both 0.80 today, and a plain file write or edit never draws `caution` once its write identity is known -- a two-phase call whose diff or kind arrives only on a later refinement is judged by what the initial card had, since the badge is never revised once shown), so the note's presence is the flag. Both bars are measured rather than picked -- the calls a `risky` answer BELOW its bar suppresses were a queued monitor edit and a comment posted on a pull request, and at 0.90 eleven real force-pushes went unflagged; a quarter of the `caution` badges the bar alone still draws are an ordinary workspace write, which the file-write carve-out now suppresses (measured distribution: see `CAUTION_CONFIDENCE_THRESHOLD` in `decisions/points/tool_risk.py`). A session that PROMPTS is never annotated — the person reading the call is the annotation, and a second opinion beside a control they are operating is noise. Carries the same thumbs pair as the decision strip (side `jev`, one shared component), so a reader can say the risk was read wrong. At most twenty notes per turn, and a slow or failed answer draws nothing. The one cost is timing: the note is awaited before this turn's next event is read, so a sampled call's approval can be answered up to the seam's own wait budget later -- bounded, and paid only by a session the switch is on for | Under a tool card in the transcript, when the seam answered for that call and the session auto-approves. Needs BOTH consents: Settings → Developer → Feature Previews → **Decisions (Jev)**, and the second switch under it, **Also send tool-call arguments so Jev can flag risky calls**, which starts off even for an owner who already had the first one on | `pages/chat/ToolRiskBadge.tsx`, `pages/chat/toolRiskRecord.ts` (the pure reader — validates the record and refuses the `safe` tier, no feature of its own), `pages/chat/DecisionVerdictThumbs.tsx` (the verdict control, shared with the decision strip), `pages/chat/ToolCallLine.tsx` (its host) | `decisions/points/tool_risk.py` (the point), `chat_turn/decisions_hooks.py` (`_tool_risk_meta`, which stamps the record on the tool row), `handlers/decisions.py` (the verdict route) | No route of its own. The thumbs post to `POST /api/decisions/feedback` |
| Auto (Jev) send while busy | A third mode on the composer's split send button, beside Steer and Queue: Jev decides per message whether text sent into a RUNNING turn interrupts that turn or waits for the next one (`message.steer`). Both paths are the shipped ones and neither changes — the mode only chooses between them, and EVERY refusal (the seam off, this session unsampled, a timeout, an answer outside the two options) takes the steer path the button has always defaulted to, so the feature can only ever pick the other one. Picking Steer or Queue by hand asks nothing and sends nothing: only `steer: "auto"` is decided, and only for a send the dashboard owner made while a turn is running — an app token, an integration and a cron are never decided for and keep falling through to the fail-closed queue. The mode is drawn only while the gateway reports BOTH the fleet ceiling (`decisions_enabled`) and the owner's consent (`permits`), fail-closed on each, AND a turn is actually running — not merely a busy composer, which also covers a slot whose sub-agents are still working and has no turn to decide about — and a stored `auto` resolves back to Steer while any of those is missing, so a withdrawn consent cannot leave a mode on screen whose send would not be decided. The request carries the new message and, only inside the consented `history_budget_chars` ceiling (default `0`, so nothing by default), a redacted extract of the turn in progress: its own request and the newest assistant text and tool activity, never its private reasoning. The receipt rides the USER row it is about as `meta.decisions_strip` and renders as one line — what Jev CHOSE (the choice, never the delivery, because a chosen steer whose client is gone falls through to the queue), how sure it was, how long it took, and the same `jev` thumbs the decision strip uses. The key is reserved to the gateway: a request's own `meta` cannot carry one, so no caller can stamp a decision nobody made. ⌘↩ still performs the OTHER action for one send, which from this mode is a manual queue that asks nothing. A send nobody decided for carries no record and draws nothing | Composer split send button while a turn runs → **Auto (Jev)** (needs Settings → Developer → Feature Previews → **Decisions (Jev)**) | `components/BusySendButton.tsx`, `components/ChatInput.tsx`, `pages/chat/useJevAutoSend.ts` (the availability read), `pages/chat/SteerDecisionLine.tsx`, `pages/chat/decisionRecord.ts` (`readSteerRecord`), `pages/chat/UserMessage.tsx`, `pages/ChatPage.tsx` (steer routing in `pages/chat/page/busyTurnControls.ts`; its composer in `pages/chat/page/ComposerDockView.tsx`) and `components/ChatPane.tsx` (its two hosts) | `decisions/points/message_steer.py`, `chat_handlers.py` (the busy branch that asks), `chat_delivery.py` (the stamp on each path) | `POST /api/chat` (`steer: "auto"`), `GET /api/dashboard/config` (`decisions_enabled`), `GET /api/decisions/consent` (`permits`). The thumbs post to `POST /api/decisions/feedback` |
| Take back a pending steer | A message sent into a running turn waits until the agent can read it. While it waits, on a kiro-cli session, its "Steering…" line offers **Queue instead** and, one menu deep, **Edit** and **Unsend**. The click asks kiro-cli to drop the steers it holds; what the turn reports next decides the outcome. A dropped steer is taken back: its row stays as a one-line note without the text, an Edit puts the text back in the composer, a Queue instead puts it in the queue, and the other steers the drop took are sent again in order. A steer the turn read first stays steered, and one whose turn ended first never ran (a Queue instead then runs as the requeued message). Offered only on the session's own composer steers, never a peer's, a channel's or an automation's | A pending steer's "Steering…" line, while the turn runs | `pages/chat/UserMessage.tsx`, `hooks/useSteerRetract.ts`, `pages/chat/page/busyTurnControls.ts` | `chat_api/prompt_queue.py` (`api_chat_slot_steer_retract`), `steer_retract.py`, `chat_delivery.py` (marks a retractable row), `chat_turn/steer_queue.py` (the drop, settle and teardown hooks) | `POST /api/chat/slots/{slot}/steers/{mid}/retract` |
| Tool approvals | Approve or deny a tool call the agent proposes | Inline card in the transcript | `components/ApprovalCard.tsx` | `handlers/sessions_parts/live_turn.py` | `GET /api/approvals`, `POST /api/approvals/{id}/{action}` |
| MCP elicitation | Answer a question an MCP server itself asks mid tool call: a small form built from the server's schema, or a link to visit and confirm. No trust setting answers it; the person does, or the wait runs out and the ask is cancelled | Inline card in the transcript | `pages/chat/ElicitationCard.tsx`, `pages/chat/page/transcriptRendering.tsx` | `dashboard/handlers/chat_elicitation.py`, `dashboard/chat_turn/elicitation_prompt.py`, `acp/elicitation.py` | `POST /api/chat/slots/{slot}/elicitation` |
| Question cards | Agent asks a multiple-choice question in chat and waits; the answers return as the tool result and show as a collapsible "N questions answered" card under the tool row | Pending: card above the composer. Answered: \"N questions answered\" card under the `ask_question` tool row in the transcript | `components/PendingQuestionCard.tsx`, `pages/chat/AskAnswersCard.tsx` | `handlers/ask_question.py` | `POST /api/ask-question`, `GET /api/ask-question/pending`, `POST /api/ask-question/dismiss`, `POST /api/ask-question/{ask_id}/answer`, `POST /api/agent-ask/open`, `POST /api/agent-ask/{ask_id}/wait`, `POST /api/agent-ask/{ask_id}/withdraw` |
Expand Down
1 change: 1 addition & 0 deletions docs/system-specs/modules/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ vocabulary.
| `ACP_BACKENDS_STEER` | The `_session/steer` extension. A steer sent to a non-implementer answers `-32601`. Also the deny-notice gate (`supports_refusal_steer`). |
| `ACP_BACKENDS_STEERING_REQUEST` | A user's mid-turn steer sent as a request whose answer is awaited, where an accepted steer can still be dropped with its turn (`steer_needs_loss_recovery`). The `_session/steering` request (codex-acp, claude-agent-acp), whose `injected` / `startedNewTurn` / `failed` / `promptRequired` answer is the delivery evidence, spoken by `AcpSessionHandle` for a shared-runtime host and by `AcpClient` for a per-session one, which also needs the handshake's `_meta.steering.supported` and claude-agent-acp at or above `CLAUDE_ACP_STEERING_MIN_VERSION`; goose's `_goose/unstable/session/steer` (`{runId, messageId}`, then a `user_message_chunk` echo marked `_meta.goose.steer`), spoken by `AcpClient` through `_CLIENT_STEERING_REQUEST_METHODS`; and pi, which takes no request: `AcpClient` writes a sealed steer file Crew's gate extension hands to pi's steering queue, echoed as a pi-acp notify chunk (`acp/pi_steer_turn.py`). Not a deny-notice channel. |
| `ACP_BACKENDS_STEER_CONCURRENT_PROMPT` | A user's mid-turn steer sent as a second `session/prompt` that joins the running loop (opencode); its answer arriving with the turn's own is the delivery evidence. Spoken only by `AcpClient`; not a deny-notice channel, and a member also needs loss recovery. |
| `ACP_BACKENDS_STEER_RETRACT` | `_session/steer/clear` drops the steers the running turn has not read and names exactly those, so a pending steer can be taken back (the dashboard's Unsend, Edit and Queue instead). The clear's answer is no evidence: the drop is the notification that follows it. kiro-cli only, on a live capture; KAS's clear names steers its turn already read. |
| `ACP_BACKENDS_INTERNAL_SANDBOX` | The harness sandboxes itself, so Kiro Crew's own wrapper stands down. Security-relevant: wrong membership hands isolation to a layer that never starts (H7). |
| `ACP_BACKENDS_POD_HOME_REMAP` | A pod-spawned child may have `$HOME` relocated onto the pod tree so home-derived OAuth artifacts remain pod-scoped. Keep this separate from internal-sandbox membership because the two claims have different security effects. |
| `ACP_BACKENDS_ACP_RUNTIME` | Driven through `AcpRuntime` — one process demultiplexing N sessions — rather than its own per-session `AcpClient` spawn branch. Every reader takes the frozenset itself: `AcpProvider.is_acp_runtime_backend` for the FOREGROUND start path, and `session._bg_runtime_backends`, which intersects it with the set below and with selectability. Membership states the TRANSPORT and nothing more — the `cli.json` effort and Tool Search overlay is gated on its readers' own sets (`ACP_BACKENDS_EFFORT_OVERLAY`, `ACP_BACKENDS_TOOL_SEARCH_OVERLAY`) at every site that writes, reads or clears it, so a member reading no such file never collects one. |
Expand Down
2 changes: 1 addition & 1 deletion docs/system-specs/modules/harness-parity.md

Large diffs are not rendered by default.

55 changes: 54 additions & 1 deletion docs/system-specs/modules/learn-cron-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -2891,7 +2891,9 @@ caller re-sends it as a fresh message. The dying turn's teardown
entry meta (set on the entry directly when the teardown requeued it before the
RPC's own stall surfaced), and the drain runs it with
`_run_chat(_steer_possibly_delivered=True)`, which leads only the final model
input with `STEER_POSSIBLY_DELIVERED_NOTE`. The next turn checks before acting
input with `STEER_POSSIBLY_DELIVERED_NOTE`. A partial echo of distinct raw steers
sharing one escaped and redacted key marks the whole collision group possibly delivered.
The next turn checks before acting
on it again, while slash dispatch, the mirror legs and the user's row keep the
text as typed. Such an entry drains
alone (`_dequeue_next_message` never merges it), and the mark is cleared when
Expand Down Expand Up @@ -2923,6 +2925,57 @@ MERGES two requeued steers keeps one `sendId` and the other send keeps the
over-keep default. Pinned by the requeue sendId tests in
`test_steer_requeue_entry_meta.py`.

**Taking back a steer the turn has not read (`dashboard/steer_retract.py`).** A
`written` steer row carries `meta.steerRetract: true` when it can be taken back:
the session's own human typed it into the composer (not a peer, a channel, an
automation or a crewmate's responsive turn), and the slot's live client answers
`supports_steer_retract` (`ACP_BACKENDS_STEER_RETRACT`, kiro-cli). The dashboard
draws Unsend, Edit and Queue instead from that mark alone, and the
`steer_push` frame carries it too. A hard Stop discards the pending steers
without settling their rows, and a gateway restart mid-turn leaves them as saved,
so a row can keep the mark after its turn is over: each top-level turn starts by
withdrawing it (`withdraw_stale_offers`) from every `written` row that still
carries it, unless a steer is pending or a take-back is armed.
`POST /api/chat/slots/{slot}/steers/{mid}/retract`
with `{action}` arms the take-back on the slot (`_steer_retract`, plus
`_steer_clear` for the clear in flight and the steers it can drop) and writes
kiro-cli's `_session/steer/clear`, then answers 202. It refuses (409, changing
nothing) a row that is no longer a pending retractable steer, a second take-back
or a steer RPC still in flight (a clear written beside a steer could land on
either side of it), and an app. The clear's answer is `{cleared: true}` whether
or not anything was held, so it decides nothing: an error answer only means the
build has no such request, and the row's mark is withdrawn. What the turn reports
next decides: `AgentExecutionUserMessageCleared` (`EVENT_STEER_DROPPED`) with the
steer still pending means kiro-cli dropped it unread; a consumption echo settling
it first means the turn read it (the row goes `consumed` as for any steer); the
turn ending, including hard-stop cleanup, finding it still pending means it
never reached the turn. `wrap_user_message` escapes tags inside each steer, so the
echo splits one way, and a possibly-delivered steer is never taken back or sent
again. A
taken-back row keeps its place with `steerState: retracted` and
`steerRetractAction`, but its content is cleared (`_mark_retracted`), so nothing
that later reads the transcript as the conversation presents it as something the
person said, and context replay skips it like a `requeued` one. Regenerate and
the cancelled-turn restore step past a taken-back row to the prompt or steer
before it. Edit
leaves the text to the dashboard, which restores it into the composer from the
row as it stood when the take-back was asked and moves focus there; Queue instead appends it to the slot's queue as a new
composer admission, except when the teardown overtook it, where the ordinary
requeue (head of the queue, row `requeued`) is that outcome. On the dashboard a
row that settles as taken back while on screen collapses from its bubble into a
note in place (straight to the note under reduced motion, and for a row read back
from history); the Edit and Queue instead notes ("You moved this message back to
your message box", "You moved this message to the queue") are buttons that move
focus to the composer, and the Unsend note is plain text. The clear drops
EVERY steer kiro-cli holds, so the others pending when it was written are sent
again in their order, and a steer sent while a clear is unanswered waits for that
answer before it is written (`wait_for_clear`), so the clear cannot drop it. The
wait ends at the clear's answer. The steers the drop took besides the taken-back
one are sent again only when kiro-cli reports the drop, so a steer sent between
the two can reach the turn before them. A
resend that fails leaves its steer pending for the teardown's requeue. Pinned by
`test_steer_retract.py` and `test_steer_clear_handle.py`.

### Wait countdown and early end (`/api/session-keepalive` as a control channel)

While the MCP `wait` tool sleeps, the transcript shows a live countdown and an
Expand Down
4 changes: 3 additions & 1 deletion docs/system-specs/modules/side.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,9 @@ return the id for the frame that announces the row.
`settle_consumed_steers(pending, snapshot)` — pure, and shared with the main chat
(`chat_runner._settle_consumed_steers` delegates to it). Matches by EQUALITY and
is count-aware: containment would false-positive a short steer against a longer
one, and a falsely-settled steer is never requeued, so the question is lost.
one, and a falsely-settled steer is never requeued, so the question is lost. Each
steer is wrapped by `wrap_user_message`, which escapes tags inside the text, so the
echo splits one way and settling compares the escaped text.

### `dashboard/side_prompts.py`

Expand Down
1 change: 1 addition & 0 deletions scripts/check_harness_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ def added_lines(frm: str, path: str) -> set[int]:
"ACP_BACKENDS_STEER": None,
"ACP_BACKENDS_STEERING_REQUEST": None,
"ACP_BACKENDS_STEER_CONCURRENT_PROMPT": None,
"ACP_BACKENDS_STEER_RETRACT": None,
"ACP_BACKENDS_COMPACT": None,
"ACP_BACKENDS_SIDE_READONLY": None,
"ACP_BACKENDS_MEMBER_CAPABILITIES": None,
Expand Down
1 change: 1 addition & 0 deletions src/kiro_crew/acp/_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ def classify_notification(msg: JsonRpcMessage) -> str:
"steering_cleared",
"AgentExecutionUserMessageQueued",
"AgentExecutionSteeringInjected",
"AgentExecutionUserMessageCleared",
):
return "steer"
if msg.is_method(METHOD_SESSION_UPDATE):
Expand Down
Loading
Loading