From 03dcb1d14d693d76a0790990a7d7d0ca4b0ac067 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 10 Sep 2026 12:16:24 -0400 Subject: [PATCH] Run without asking, and say what ran MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first real session on this harness spent two of its two hours and nineteen minutes parked on approval prompts with nobody sitting in front of them. A prompt that nobody answers is not a safety mechanism, it is a stall, so actions run by default and the transcript reports them: `Ran: ` after the fact rather than a question before it. The interface stays the approver. The decision is not handed down to the engine — every request still arrives here, is still written down, and `/ask on` puts the question back for the rest of the session. What holds a session in is narrower than a prompt was, and worth stating rather than implying: the engine has no OS sandbox, so file tools are confined to the session's own directory, the fetching tools are withheld, and none of the user's settings or servers are in scope. Within that, a command now runs. Wiring verified — the default, the branch that answers, the command that reverses it, and the help that names it. A live approval round trip has not been driven end to end; the next real turn does that. --- README.md | 14 +++++++++++++- docs/local-contract.md | 15 ++++++++++++-- src/tui.mjs | 44 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a2cce21b4e..95488da3ac 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ prompt, orange highlight, magenta handle) and shows the signed-in `@handle` and the piece currently being worked on in the header. Inside the TUI: `/login`, `/logout`, `/whoami`, `/publish [file] [slug]`, -`/piece [name]`, `/runtime [mjs|lisp|processing]`, `/backend [claude|codex]`, +`/autopublish [on|off]`, `/piece [name]`, `/runtime [mjs|lisp|processing]`, +`/backend [claude|codex]`, `/model [name]`, `/qr`, `/live`, `/new`, `/clear`, `/help`, `/quit`. Press `ctrl-c` to interrupt a running turn or exit while idle. @@ -124,12 +125,23 @@ at `https://aesthetic.computer/@handle/slug`, exactly like the web prompt's uploads the source, and reads the live file back before reporting the URL. Writing a file under `disks/` does not publish it, and the engine is told so. +`--autopublish` does that on every save instead, so the session's URL is live +the whole time the piece is being worked on and the last edit is still there +after the terminal closes. It coalesces — a publish runs once the saves stop, +never more than one at a time, and never twice for the same bytes — and it +flushes the pending save on exit. Off by default, since it writes to a public +route under your own handle: turn it on per session with the flag or +`/autopublish`, or for every session with `AESTHETIC_CODE_AUTOPUBLISH=1`, which +`--no-autopublish` overrides. With it on the engine is told the piece is +already live and told not to ask you to publish. + ```sh aesthetic login aesthetic whoami aesthetic publish system/public/aesthetic.computer/disks/smiley.mjs ac --runtime lisp ac --backend codex +ac --autopublish ``` ```sh diff --git a/docs/local-contract.md b/docs/local-contract.md index d044d68c48..5da2479d68 100644 --- a/docs/local-contract.md +++ b/docs/local-contract.md @@ -21,11 +21,22 @@ and the session token to `aesthetic.computer` only when the user runs `/publish`. The engine bridge never receives the token; publishing is an interface action, not an agent tool. +Auto-publish (`--autopublish`, `AESTHETIC_CODE_AUTOPUBLISH=1`, `/autopublish`) +is the one way that becomes repeated rather than per-command: with it on, the +interface publishes the session's piece a couple of seconds after every save, +and the last save is flushed on the way out. It is off by default and has to be +turned on per session or per environment, because it writes to a public route +under the user's own handle. It changes when publishing happens, not who does +it — still the interface, still the same one file, and the token still never +reaches the bridge. The agent cannot turn it on, and the transcript keeps one +line naming the URL each publish went to. + ## Live piece boundary The session's piece is pushed to `aesthetic.computer/run` on a private code -channel every time its file changes, and re-sent unchanged every few seconds so -that a phone scanning the QR code later still receives it. That request carries +channel every time its file changes. The session server retains the last +message a channel received, so a phone that scans the code later still receives +the piece without the interface re-announcing it. That request carries the piece's source and the channel token, and nothing else: no account token, no workspace paths, no conversation. The channel token is random per session and is never reused. diff --git a/src/tui.mjs b/src/tui.mjs index 7a53337f0e..6b30b8b395 100755 --- a/src/tui.mjs +++ b/src/tui.mjs @@ -52,6 +52,12 @@ const state = { // copy of it. `/qr` still brings it back — on a machine with no Slab menu // bar the code in here is the only way onto a phone. showQr: false, + // Actions run without stopping to ask, and are reported once they have. The + // engine has no OS sandbox of its own, so what still holds a session in is + // narrower than a prompt: file tools confined to this directory, the fetching + // tools withheld, and none of the user's own settings or servers in scope. + // `/ask on` trades the speed back for the question. + autoAllow: true, entries: [ { id: "privacy", @@ -412,11 +418,22 @@ function handleRequest(request) { request.method === "item/commandExecution/requestApproval" || request.method === "item/fileChange/requestApproval" ) { - state.approval = { - id: request.id, - method: request.method, - subject: approvalSubject(request.method, request.params || {}), - }; + const subject = approvalSubject(request.method, request.params || {}); + // Allowed without asking, by default. The first real session spent two of + // its two hours and nineteen minutes parked on prompts with nobody sitting + // in front of them, and that is the failure this default answers. + // + // The interface stays the approver rather than handing the decision down to + // the engine, so every action still arrives here and is still written into + // the transcript: you read what ran instead of being asked about it first. + // `/ask on` puts the question back for the rest of the session. + if (state.autoAllow) { + engine.respond(request.id, { decision: "accept" }); + addEntry("notice", `Ran: ${subject}`); + redraw(); + return; + } + state.approval = { id: request.id, method: request.method, subject }; slabSession.awaitingInput( request.method === "item/commandExecution/requestApproval" ? "aesthetic code needs command approval" @@ -637,7 +654,7 @@ async function submitInput() { if (command === "/help") { addEntry( "notice", - "/login · /logout · /whoami · /publish [file] · /autopublish [on|off] · /piece [name] · /runtime [id] · /backend [id] · /model [name] · /open · /qr · /live · /new · /clear · /quit ctrl-c interrupts a running turn", + "/login · /logout · /whoami · /publish [file] · /autopublish [on|off] · /ask [on|off] · /piece [name] · /runtime [id] · /backend [id] · /model [name] · /open · /qr · /live · /new · /clear · /quit ctrl-c interrupts a running turn", ); return redraw(); } @@ -685,6 +702,21 @@ async function submitInput() { } return redraw(); } + // The way back. Auto-allow is the default, so this is the control that + // matters most in here: one word returns the question for the rest of the + // session, and the notice says which way it went rather than assuming the + // reader remembers which way it was. + if (command === "/ask") { + const want = rest.trim().toLowerCase(); + state.autoAllow = want === "on" ? false : want === "off" ? true : !state.autoAllow; + addEntry( + "notice", + state.autoAllow + ? "Running without asking · /ask on to be asked first" + : "Asking before each action · /ask off to stop asking", + ); + return redraw(); + } if (command === "/open") { // The code is for a phone. This is for the machine the session is already // running on: same URL, same channel, same autorun — the piece opens in a -- 2.51.2