Automation reference
It is written in Lua, a small scripting language, but a few lines is all you need.
This document is both an explanation for humans and the specification that the “let an AI write it” button in the settings screen hands to the AI.
Translations live next to this file as docs/AUTOMATION.<code>.md (for example
docs/AUTOMATION.ja.md) and are picked automatically from your language setting.
1. When it runs (events)
Section titled “1. When it runs (events)”Put a file named after an event into the automation folder and it runs at that moment. Only add the ones you need.
| File name | When it runs |
|---|---|
on_start.lua |
Once the tab has started and settled (see below) |
on_done.lua |
When the AI has finished answering something it was asked |
on_question.lua |
When the AI asks something or offers choices |
on_exit.lua |
When the session ends (including disconnects and crashes) |
on_busy.lua |
When an answer starts (advanced). Set Checking on a tab that keeps working in Settings and it runs again, at that interval, for as long as the tab is still working |
on_notify.lua |
When the program rings the terminal — a bell, an OSC notification, even over ssh. The second variable holds the text. Forward it with shikisha.notify(...), route it, or log it; the on-screen toast still shows |
_shared.lua |
Loaded before all of the above. Put shared helper functions here |
on_done.lua and on_busy.lua only run once something has been sent to the tab. Every
program prints something as it starts, which makes the screen move and then stop — the same
shape as an answer — so without this a banner would be forwarded as if it were a reply.
on_start.lua does not run the instant the tab appears. An AI CLI ignores input until
it has drawn its own prompt, so it runs once the program has produced output and the
screen has stopped changing — usually a second or two. You do not have to wait yourself.
Write only the body of the work in the file. No function ... end wrapper.
-- example of on_done.luashikisha.send_to_tab(2, "Please review this code:\n" .. tab.output)2. Variables you can use
Section titled “2. Variables you can use”tab is available in every event.
| Variable | Contents |
|---|---|
tab.index |
Tab number (starting at 1) |
tab.name |
Tab name |
tab.id |
The automation name from the settings, if it has one. The one handle that survives a rename — branch on this, not the number or the display name. nil when unset |
tab.output |
The latest response text (no earlier history) |
tab.state |
"BUSY" / "DONE" / "QUESTION" / "WAIT" / "EXIT" |
tab.profile |
Name of the profile in effect |
tab.chain_depth |
How many times this was handed on automatically. 0 means a human started it |
tab.locked |
Whether input is locked |
tab.is_model |
Whether this tab talks to a model over an API rather than running a CLI |
tab.reply |
A model tab’s reply, exactly as it came back (only on such a tab). tab.output is the same text as the screen drew it, wrapped |
on_question.lua gets a second variable screen holding the whole screen text; on_notify.lua gets a second variable holding the notification text.
3. Commands you can use
Section titled “3. Commands you can use”How to point at a tab
Section titled “How to point at a tab”A tab is addressed by its name used by automation – the field on the tab’s
settings page, written as id in config.json.
{ "name": "Review", "id": "reviewer", "command": "codex" }shikisha.send_to_tab("reviewer", "please review") -- recommendedshikisha.send_to_tab(2, "please review") -- by number too (changes on reorder)The name shown on the tab will not reach it. That name is a heading: you can write anything there, including the same thing on two tabs. Two tabs called “Review” would leave nobody able to say which one received the work, so the address is the automation name only. It is unique within the desk and does not change when you rename the tab.
Every tab has one even if you never chose it. A tab added from the screen is given
two short words (calm-otter, brave-finch); one written into config.json without
an id gets one made from its name, or from its command when it has no name. The
tab’s settings page shows it and lets you change it.
The name is an address, and goes back to be drawn again when its tab closes. What
the app keeps about a tab – its conversations, the jobs it leads and the work it was
handed, its mail, its place in a split – is kept under who the tab is: a uid the
app writes on the tab’s line in the settings when the tab is made, and never hands
to another tab. A new tab that is given a closed tab’s name starts with none of it.
| Command | Description |
|---|---|
shikisha.send_to_tab(tab, "text") |
Give a tab an instruction and run it. Works on this tab too (automatic chain +1) |
shikisha.ask_tab(tab_id, "line", "text", {timeout_ms=…}) |
Ask another tab’s AI and get its reply back. When a message contains <@ID>, that is a SHIKISHA tab: call this with that ID to hand it work (a review, a question) and wait. line is one short line for the chat (AIConfer shows it; one line, no longer than the settings allow, 80 characters unless changed); text is everything the tab is sent. Waits while the tab is busy, sends, waits for it to finish, and returns {state, reply, round, max_rounds, same_folder, note}. state is DONE with the reply, QUESTION when it waits for approval, or PENDING when it is still working at the timeout (50 min by default) – its reply is then typed into your tab when it finishes. Stop when round reaches max_rounds. same_folder: false means it cannot see your uncommitted changes. Through the pipe or MCP only |
shikisha.send(tab, "text") |
Send raw keystrokes (newline is \r). For answering prompts, not for instructions |
shikisha.tab_run(tab_id, "command", {timeout_ms=…}) |
Run a command in a terminal tab and get its output. Only in a terminal the person named with @ in what they last sent you. Waits while the tab is busy, types the command, waits for it to finish, and returns {state, reply} with the output in reply (PENDING at the timeout, the output then typed into your tab). Through the pipe or MCP only |
shikisha.browser_do(tab_id, "goal", {timeout_ms=…}) |
Have a web page driven toward a goal and get what it found. Only a page the person named with @. Runs the page’s 🗣 words run and returns {state, reply, note}: DONE with what the page shows about the goal, STUCK with why, STOPPED when someone stopped it, PENDING at the timeout. One page at a time. Through the pipe or MCP only |
shikisha.tab_list() |
The tabs of this desk, as {id, name, kind (ai / terminal / page), folder, you, named}. named is a tab the person named in what they last sent the caller. Through the pipe or MCP only |
shikisha.tab_conversation(tab_id, {want=6, before=…}) |
Another tab’s conversation, as the phone’s reader shows it. Read from the CLI’s own record – on this PC, or on the server or MicroVM the tab runs on – so a long answer comes back whole, not cut to the screen. Returns {turns = {{who = "you" / "ai", text}, …}, from, more, source}, oldest first: the last want things said (1 to 40). For older ones pass the from you got as before; more is false once the start is reached. A tab whose CLI keeps no record answers source = "none" – use tab_screen. Through the pipe or MCP only |
shikisha.note(tab, "text") |
Write a line on that tab’s screen for the person watching. Nothing is sent to what runs there and no answer is expected |
shikisha.wait(tab, "pattern", ms) |
Wait until the text appears on screen; true if it did |
shikisha.sleep(ms) |
Wait (other tabs keep running while you wait) |
shikisha.state(tab) |
Read the state right now (use this as a loop condition) |
shikisha.wait_state(tab, "DONE", ms) |
Wait until it reaches that state |
shikisha.notify("target", "text") |
Notify Slack / Discord / Telegram, this PC’s own notification area, or a phone that registered itself (only configured targets) |
shikisha.restart(tab) |
Restart that tab, carrying its conversation over. shikisha.restart(tab, "fresh") starts a new one |
shikisha.log("text") |
Record in logs/hooks.log |
shikisha.set_session("id") |
Say which conversation THIS tab’s CLI is running, so a restart can pick it up. No tab argument: the caller is the tab |
shikisha.report_prompt("text") |
Say what a person just asked THIS tab’s CLI. A folder with Auto on writes its name and summary from these. Claude Code’s and Codex’s own hooks report through here, which also catches what was typed straight into the terminal |
shikisha.set_state("BUSY") |
Say what THIS tab is doing, instead of leaving it to be read off the screen: BUSY, QUESTION, DONE or WAIT. This is how an AI CLI’s own hooks drive the state dot. A second argument is the sender’s clock in milliseconds, so reports that overtake each other still apply in the order they were said |
shikisha.set_helper("a1f2", true) |
Say that a helper THIS tab’s program runs on the side (a subagent, by the program’s own id for it) began (true) or ended (false). While one runs, a tab whose turn is over reads BACKGROUND rather than done. An AI CLI’s own hooks report through this |
shikisha.set_running({"a1f2"}, false) |
Say everything THIS tab’s program still has running beside its conversation: the helpers by id, and whether anything that is not a helper (a command left running) is still going. Replaces what was said before; set_running({}, false) says nothing runs. What is not heard of again for three hours is let go of |
shikisha.set_status("key", "text", tab) |
Say what a tab is doing, in its own words, under its name in the tab bar. key lets several sources speak without overwriting each other; an empty text removes that one. Leave tab out and it is THIS tab |
shikisha.set_progress(0.4, "label", tab) |
How far along, 0..1, shown beside the status. nil removes it. Leave tab out and it is THIS tab |
A CLI that has never heard of this app can say it too. The notification escapes every terminal understands land in the same place, with nothing to set up — useful over ssh, or inside a container, where nothing of ours is installed:
printf '\e]777;notify;Build;3 tests failed\a' # title and bodyprintf '\e]9;build finished\a' # body onlyIt appears under that tab’s name and, if you are looking at a different tab, as a
one-line toast. Looking at the tab already is not news, so the toast is held back.
| shikisha.get_var("key") / shikisha.set_var("key", value) | Remembered variables, shared inside the desk |
If on_question.lua returns a string, that string is sent automatically.
Returning nil (or nothing) leaves the decision to the human.
Instructing an AI: use send_to_tab
Section titled “Instructing an AI: use send_to_tab”An AI CLI takes a pasted instruction and the Enter that runs it as two separate events,
and drops the Enter if it arrives before the paste has been taken in. send_to_tab
handles that for you.
-- Right. One call: the text is entered and runshikisha.send_to_tab(tab, "You are on Bianca's side. Argue your case.")
-- Wrong. The text lands in the input box and stays thereshikisha.send(tab, "You are on Bianca's side. Argue your case.")shikisha.send(tab, "\r")Do not paper over it with sleep. A fixed wait is a guess about how long the other
program takes to be ready, and that changes with the machine, the model and the length of
the prompt — it will hold until the day it does not. send_to_tab waits on the actual
event instead of on the clock.
send remains the right tool for keystrokes an AI is already waiting for — answering a
confirmation with "1\r", or driving a shell.
4. Common examples
Section titled “4. Common examples”Give a tab its opening instruction (on_start.lua)
Section titled “Give a tab its opening instruction (on_start.lua)”shikisha.send_to_tab(tab, "Summarise what changed in this project yesterday.")Nothing else is needed — the hook already waits until the program is ready to be typed at.
Resume yesterday’s work just by starting (on_start.lua)
Section titled “Resume yesterday’s work just by starting (on_start.lua)”if not shikisha.wait(tab, "%$ $", 15000) then return endshikisha.send(tab, "cd /srv/myproj\r")shikisha.wait(tab, "%$ $", 5000)shikisha.send(tab, "claude --continue\r") -- pick the previous conversation back upTo choose which past conversation to resume, use claude --resume. It shows a list,
and picking from that list can be automated too:
shikisha.send(tab, "claude --resume\r")if shikisha.wait(tab, "[Ss]elect", 8000) then shikisha.send(tab, "\r") -- choose the topmost sessionendApprove automatically, but hand risky questions to a human (on_question.lua)
Section titled “Approve automatically, but hand risky questions to a human (on_question.lua)”if screen:match("delete") or screen:match("rm %-rf") then return nil -- leave it to the humanendreturn "1\r" -- pick choice 1Bounce a review between A and B, stopping after 5 rounds (on_done.lua)
Section titled “Bounce a review between A and B, stopping after 5 rounds (on_done.lua)”-- do nothing when a human gave the instruction directlyif tab.chain_depth == 0 then return end
local rounds = shikisha.get_var("rounds") or 0if tab.output:match("LGTM") or rounds >= 5 then shikisha.notify("slack", "Review finished (" .. rounds .. " rounds)") return -- doing nothing = the loop endsendshikisha.set_var("rounds", rounds + 1)shikisha.send_to_tab(1, "Please fix these points:\n" .. tab.output)Reconnect automatically after a disconnect (on_exit.lua)
Section titled “Reconnect automatically after a disconnect (on_exit.lua)”local n = (shikisha.get_var("retry") or 0) + 1if n > 5 then shikisha.notify("slack", tab.name .. " keeps dying") returnendshikisha.set_var("retry", n)shikisha.sleep(2000)shikisha.restart(tab) -- after the restart, on_start runs againCheck in periodically (on_busy.lua)
Section titled “Check in periodically (on_busy.lua)”The screen and the other tabs keep running while you sleep. You choose the interval.
-- while it is working, record every 30 secondswhile shikisha.state(tab) == "BUSY" do shikisha.sleep(30000) shikisha.log(tab.name .. " is still working")endtab.state is the state at the moment you were called, so use
shikisha.state(tab) (the state right now) as the loop condition.
When a tab exits or restarts, waiting loops are discarded automatically.
Without the loop: set Checking on a tab that keeps working in Settings, and
on_busy.lua is simply run again at that interval while the tab keeps working.
Each run is told the state and the screen as they are now, so a watchdog can be
written as one if instead of a loop:
-- a turn that has run this long has stopped answering, not started thinkinglocal since = shikisha.epoch_ms() - (shikisha.get_var("since_" .. tab.index) or 0)if shikisha.get_var("since_" .. tab.index) == nil then shikisha.set_var("since_" .. tab.index, shikisha.epoch_ms())elseif since > 900000 then shikisha.notify(tab.name .. " has been working for 15 minutes without a word") shikisha.set_var("since_" .. tab.index, shikisha.epoch_ms())endYou are only asked again about a tab you were told about in the first place, and never about one waiting on a person.
Just notify Slack when it is done (on_done.lua)
Section titled “Just notify Slack when it is done (on_done.lua)”shikisha.notify("slack", tab.name .. " finished:\n" .. tab.output)Stop before it starts, and leave a note (on_done.lua)
Section titled “Stop before it starts, and leave a note (on_done.lua)”if shikisha.state(2) ~= "WAIT" then shikisha.skip("tab 2 is still working") -- nothing below this line runsendshikisha.send_to_tab(2, tab.output)skip ends this run where it is called and puts one line on the tab’s screen and
in logs/hooks.log. Use it whenever an automation decides not to act: a hand-over
that quietly does nothing looks exactly like a hand-over that is broken.
What gets passed on
Section titled “What gets passed on”Only the reply is forwarded — not the terminal furniture around it. Startup banners,
input-box borders, and the hint and status lines a CLI keeps at the bottom
(? for shortcuts, the model and directory readout) are dropped.
They are found by position and by change, never by matching their text. Rows below the cursor belong to the input box whatever they say; everything else is compared against a snapshot taken the instant the prompt was submitted, so anything already on screen before the reply existed is not part of the reply. A CLI can reword or translate its status line and this keeps working, and a reply can contain any wording at all without risk of being eaten.
The instruction itself is not sent back either. A reply begins on the line after the one that was submitted, which matters when the instruction was long enough to wrap: the second half of it used to arrive at the top of the answer, on its own, looking like something the other side had said.
One thing is beyond reach: narrowing the window while a reply is arriving truncates
it, because the terminal clips every stored row to the new width and the discarded
text is gone. Widening and height changes are harmless. A narrowing mid-reply is
recorded in logs/hooks.log so a short answer is not a mystery.
Watching it happen
Section titled “Watching it happen”Handing work to a tab does not move the screen. Say so when you want to be watched:
shikisha.show("reviewer") -- put this tab on screenshikisha.send_to_tab("reviewer", msg) -- ...and hand it the workTwo lines, in that order, and nothing moves behind your back. shikisha.show(0) goes
back to the board.
The person always outranks the script. show does nothing if they turned Auto-switch
off in the general settings, if they moved the view themselves in the last few seconds,
or while the settings screen is open — you are never pulled away from something you are
reading.
The ball still flies on the board whether or not the screen follows it: it shows who is holding the work, which is not the same question as where you are looking.
Leaving a draft for a person to finish
Section titled “Leaving a draft for a person to finish”send_to_tab types and submits. To leave something in the box for a person to add to,
send it as a paste and never send the newline:
shikisha.draft_to_tab("ai", "Read lp.html.
")The text lands in the input box and stays there. The newlines are characters, not
keypresses, so nothing is sent and the person can add their own instructions before
pressing Enter. This program does not count it as a submission either, so on_done
will not fire on that tab.
A draft does not end the chain — it puts a person in it. The ball moves to that tab and waits there, carrying its depth, and the view follows so you arrive where you are needed. Typing there does not break the chain the way typing into any other tab does; you are taking your turn, not taking over. When you send, the count continues from where it was, and the chain limit still applies — a loop with a person in it is still a loop.
It refuses to draft into a shell, and says so in logs/hooks.log. A terminal
program declares whether it understands pasted text; a shell does not, and the same
bytes there would run as a command. Measured: cmd.exe and powershell.exe do not
declare it, Claude Code does. The check reads that declaration rather than guessing
from the command name.
Keep the draft short. A long paste is collapsed to [Pasted text #1 +N lines], and the
person cannot read what they are about to send.
Driving a browser
Section titled “Driving a browser”A browser can join the orchestra. Driven from the window, Windows already carries the engine, so nothing is downloaded and nothing is installed.
With no window – on a server – it uses whatever browser that machine has. Install one
first (apt install chromium) and that is the one it uses. With none, it fetches a
version it names, and only at the moment one is first asked for. Nothing is bundled, so
anybody who never opens a page pays nothing for this.
Declare one alongside the tabs of a desk. A browser you declare becomes a tab,
numbered after the sessions — Ctrl+B and its number switches to it like any other.
{ "name": "LP review", "browsers": [{ "id": "br", "url": "https://example.com/login" }], "tabs": [{ "name": "Claude", "id": "ai", "command": "claude" }]}Where a page is drawn
Section titled “Where a page is drawn”While a device – the window, or a phone – is connected to a server, you choose which machine draws the pages automation opens (Settings, “where pages are drawn”).
- On this machine (the default) – watchable from a phone, still working with nobody connected, and one signed-in session for every device. The screen is relayed as a picture.
- On the connected device – faster and sharper, but it needs that device to be there, and only it can see the page (a page that has to be watched from elsewhere belongs on this machine).
Either way, the page reaches the network from the machine the agents are on. So
browser_open("x", "http://localhost:3000/") is always that machine’s port 3000. Names
are resolved there too, which is how a page reaches a private network only that machine
can see.
A page already open does not move. Changing the setting changes where the next page is drawn.
Hooks on a browser
Section titled “Hooks on a browser”A page has its own vocabulary. Session states — working, done, asking — say nothing about a document, so browsers get their own names.
| File | When |
|---|---|
on_load.lua |
the page finished loading (on every navigation) |
on_press.lua |
the human pressed the banner button |
The banner does not appear on its own. shikisha.browser_ask puts it under the page —
your words on the left, the button on the right — and pressing it calls on_press. The
app draws it, not the page: the page is held back by the banner’s height and can neither
see nor press it, so a press is always a person’s. It stays up across navigations until
shikisha.browser_unask, and a phone looking at the page can press it too.
To let a person choose the page before handing it over, shikisha.browser_nav puts
back / forward / reload / an address box in a row above it. Like the banner, this is
not injected into the page: the page moves down and the app draws in the gap, so it
survives navigation and never covers the site’s own sticky header.
shikisha.browser_nav(page.id) -- all of themshikisha.browser_nav(page.id, { reload = true, url = true }) -- pick some-- back / forward / reload / url / develop (hard reload, picking, DevTools, source, DOM)-- point (phone only: a tap clicks where the finger is, or moves a pointer)shikisha.browser_unnav(page.id) -- take it awayBack and forward grey out when there is nowhere to go. The address box takes an
address or words: an address opens, and anything else is searched for on Google.
point is drawn only for somebody watching from a phone. The same checkboxes live in
the settings screen for a browser tab, so this works with no Lua at all; a call from Lua
wins over the setting.
The banner works the same way: fill in its words and button text under “Banner” in the settings and it is there from the moment the page opens. Then the only file you write is
-- scripts/lp/on_press.luashikisha.draft_to_tab("ai", shikisha.browser_html(page.id))Typing an address still fires on_load, so if you only want the page handed over when
a person says so, leave on_load.lua empty and write on_press.lua. None of this
touches the chain depth — that only counts handoffs to another tab.
What arrives is page, not tab: page.index (the number on screen), page.id (the
name automation points at), page.name (what a person reads), page.url, and
page.complete — false means load never came and this fired at DOM-ready instead.
draft_to_tab and send_to_tab wait until the other side can actually take input, so
handing work to a CLI that has not finished starting does not silently vanish.
Then drive it from automation:
-- Let a person log in. The bar appears across the bottom of the page.local why = shikisha.browser_wait("br", { selector = "#dashboard", -- reaching this ends the wait ask = "Please sign in", -- so does pressing the button timeout_ms = 300000,})shikisha.log("ended by: " .. why) -- selector / button / timeout
shikisha.browser_fill("br", "#title", answer)shikisha.browser_click("br", { xpath = '//button[text()="Save"]' })local html = shikisha.browser_html("br")A selector is "#id" (CSS), { xpath = "..." }, or { ref = N }. XPath earns its
place on forms and admin pages, where “the cell beside the label that reads Name” has
no CSS spelling.
The numbers for { ref = N } come from browser_digest:
local list = shikisha.browser_digest("br")-- [1] textbox "Search" placeholder="Search"-- [2] button "Search"-- [3] link "Help" https://example.com/helpshikisha.browser_fill("br", { ref = 1 }, "haiku")shikisha.browser_click("br", { ref = 2 })The digest distills the page down to only its operable elements. Roles and names
come from the browser’s own accessibility tree (the same computation a screen reader
sees), and JS-clickables with no standard role (a cursor:pointer <div>, say) are
supplemented with a * mark, like div*. It is orders of magnitude shorter than raw
HTML, and removes the need to guess selectors.
Operations on { ref = N } are genuine input (trusted mouse/key events over CDP):
sites that ignore synthetic events cannot tell them from a human’s click or typing.
Multibyte text lands one committed character at a time, no IME involved.
Numbers are bound to the page as it was digested. Navigation or a re-render voids them, and an operation on a stale number stops with a clear “take a new digest” error — it never silently clicks something else.
On top of that, click / fill on { ref = N } return an echo of what was really
operated on as their second value (e.g. visible, link 「Help」; a fill’s echo names
the field by its attributes only, never the value). A mixed-up number denounces
itself in its own answer.
On replay and portability: { ref = N } is an ordinary selector with the same
meaning in every execution mode (automation scripts, the composer’s ▶ Lua run mode,
a page’s 🗣 run). But the numbers refer to “the latest browser_digest listing” —
they are not what you carry around.
That is why execution and recording are independent. Every executed op is
rewritten in a durable form into the replay journal: during a 🗣 run it lands on
that page’s 📼 sheet, and a script of your own drains it with shikisha.take_replay()
into its run’s replay.lua. In it a { ref = N } becomes an anchor derived from the element it actually
touched (a human-made #id, else a unique text/attribute XPath — same hygiene as the
📼 recorder, machine-minted ids refused), and browser_digest never appears. So:
- the currency of execution = refs (maximum capability: shadow DOM reach, genuine input, friendly to small models)
- the currency of portability = replay.lua (plain css / xpath only; paste it into the ▶ run mode, wire it into an automation, or run it on another PC’s SHIKISHA as-is)
A run’s replay.lua is downloaded from the button at the top right of the result view
that opens when the run finishes. An op with no derivable durable anchor is never silently dropped — it stays
as a -- click (…): what was clicked comment.
Looking for an element answers with three states — visible, off_screen,
not_found — because which one it is decides whether to doubt the selector or the
waiting.
Whether a missing element stops the script is chosen per call. The default raises;
{ on_missing = "continue" } returns the state instead. A cookie banner that is
sometimes absent is not a failure, and only the caller knows that.
The button is offered for the whole wait, even when a selector is given. A condition that stops matching after a site redesign should cost a click, not a hang. And since the wait reports which of the three ended it, a selector that has quietly stopped working shows up as every wait ending on the button.
click / fill auto-wait. An action waits until the element appears → is
visible → stops moving (identical rect on consecutive frames) → is enabled
before acting. Retries back off 0/20/100/100/500ms
and cycle scroll alignments to shake off sticky overlays; when navigation destroys
the JS world, an outer retry re-enters the new document. That is why a replay.lua
fired line-after-line with no pauses — acting on the next page right after
browser_go — just works. The wait is capped at 10s per action, and an element that
exists but never settles is acted on anyway (no new failure modes).
Values are never spliced into code. Everything handed to fill goes to the page
as data, so an answer full of quotes and angle brackets arrives intact and stays
inert. There is deliberately no way to hand raw JavaScript to a page.
Only http and https pages open. A single-line <input> cannot hold a newline —
that is HTML, not this program — so multi-line values need a textarea.
5. Safety mechanisms
Section titled “5. Safety mechanisms”Several brakes keep automation from running away.
- Automatic chain limit … the number of consecutive automatic hand-offs between AIs is counted and stops at the limit (10 by default). Typing something yourself resets it to 0
- Manual work wins … nothing is sent automatically for 5 seconds after you touch a tab
- Emergency stop …
Ctrl+B xhalts all automation at once and sends every AI that is mid-turn its own interrupt key (interruptin its profile: Esc for Claude Code, Codex and Gemini, Ctrl+C for Aider).Ctrl+B atoggles automation back on. The status bar carries the same button, in the same corner on every screen - Input lock … put 🔒 on the middle tabs so nobody instructs them by mistake
- Sandbox … automation can neither touch files nor reach the internet by default. Notifications only go to the targets you registered (Slack / Discord / Telegram, this PC, a registered phone)
6. Files and network access (advanced, off by default)
Section titled “6. Files and network access (advanced, off by default)”When you really need it, register a “gateway” inside a desk in config.json and that
desk’s automation can use it. It cannot be edited from the settings screen (the impact is
large, so it is only for people who edit the file directly).
// "desks": [ { "name": "…", here ↓ } ]"capabilities": { "files": { "reports": { "dir": "reports", "read": true, "write": true } }, "http": { "github-issue": { "url": "https://api.github.com/repos/me/proj/issues", "method": "POST", "auth_from_secrets": "github_token" } }}shikisha.write_file("reports", "review.md", tab.output)local prev = shikisha.read_file("reports", "review.md")shikisha.http("github-issue", '{"title":"Findings","body":"..."}')| Command | Description |
|---|---|
shikisha.now([format]) |
The local date and time as text |
shikisha.write_file(gateway, filename, text) |
Write into a registered folder |
shikisha.read_file(gateway, filename) |
Read from a registered folder |
shikisha.http(gateway, body) |
Send to a registered URL (the app adds the credentials) |
Why this is safe: scripts cannot assemble paths or URLs — they can only call registered
names. Auth tokens are invisible to scripts; the app attaches them.
config.json, secrets.json, .env and .lua files can never be read or written, even
when they sit inside an allowed folder.
If you need more freedom, raw paths and raw URLs are available too (empty by default = everything denied):
"capabilities": { "allow_dirs": ["reports"], "allow_hosts": ["api.example.com"]}shikisha.write_path("reports/a.md", "text")shikisha.http_raw("https://api.example.com/hook", '{"x":1}')Hosts are matched exactly and only https is allowed
(tricks like api.example.com.evil.com are rejected).
Every file and network operation is recorded in logs/hooks.log.
Most of it belongs to a desk
Section titled “Most of it belongs to a desk”Gateways, automation permissions, notification destinations and git settings are each desk’s own. There is no app-wide version of any of them: what a desk does not have, it does not have (no gateways, the standard permissions, no destinations, the built-in git settings). Work’s repositories beside your own on one machine never share a chat. The AI providers are the exception: one list for the whole app (Settings › AI agents), which every desk’s tabs and scripts reach by name.
"desks": [ { "name": "work", "id": "work", "capabilities": { "http": { "deploy": { "url": "https://example.com/deploy" } } }, "automation_permissions": { "write_path": { "ai": false } }, "notify": { "work-slack": { "type": "slack", "webhook": "@notify/work/work-slack" }, "This PC": { "type": "windows" } }, "primary_notify": "work-slack", // where notify(text) with no name lands "git": { "protect": ["main", "release/*"] }, "projects": [ { "name": "api", "at": "D:/src/api", "git_account": "work", "bring": [ { "pattern": "node_modules/", "how": "link" }, { "pattern": ".env", "how": "replace", "replace": [ { "find": "^PORT=\\d+$", "with": "PORT=3001", "regex": true } ] }, { "from": "D:/templates/local.json", "to": "config/local.json", "how": "copy" } ] } ] }]A value starting with @ is the name of a secret. Registered from the settings
screen, keys and webhooks are stored encrypted and only their names are written
here. A new desk can start as a copy of the one you are on, keys included.
The git accounts are the app’s, written at the top of the file beside the
providers – "git_accounts": [ { "name": "work", "login": "me-at-work", "owners": ["my-company"] }, { "name": "home", "method": "ssh", "key": "C:/Users/me/.ssh/id_home" } ]
– and every desk’s projects choose among the same list. A token is not written
here either: it is filed under git/<name> from Settings > GitHub / Git.
The displayed token name is label; name is the stable identifier used by projects
and secrets. The UI generates it on creation and keeps it when the label changes.
Existing identifiers and stored tokens remain usable. A project names the one the git column beside its folders signs in with
(git_account), and a git tab names its own. Absent, git on this PC signs in
the way it already does; "@pc:<login>" names one of the GitHub accounts it
holds, for a PC holding two, and "@gh:<host>/<login>" one of the accounts
GitHub CLI (gh) is signed in as. Pull request numbers are read with the same
account. GITHUB_TOKEN in the environment is not read.
bring is what a new worktree of the project gets beyond what git carries (the
project’s page in the settings edits it). A pattern is a line of the project’s
.gitignore and covers everything that line makes git ignore; from/to puts a
file from anywhere at a place inside the worktree. how is copy, replace (copy,
then each find becomes with – a regular expression when regex is set, with ^
and $ at each line), link, or skip. A line with no rule does what the settings
page shows beside it. The project’s setup command runs after all of this.
7. Driving it from outside (the external API)
Section titled “7. Driving it from outside (the external API)”A program outside the app can call the same commands you write in Lua. Same names, same arguments — there is no second vocabulary to learn.
The door is a named pipe, \\.\pipe\shikisha-<pid>. One JSON object per line, one
line of answer back:
→ {"token":"…"} the handshake, once← {"ok":true,"result":"hello"}
→ {"id":"1","method":"send_to_tab","params":["reviewer","status?"]}← {"id":"1","ok":true,"result":null}
→ {"id":"2","method":"list"}← {"id":"2","ok":true,"result":["browser_click","browser_close", … ]}method is a command from section 9 with the shikisha. taken off. params are its
arguments in order. list answers with every command the caller is allowed to run, read off the app’s own
table — so it can never fall behind what the app can actually do.
For loops and branches, hand over a whole chunk in one call:
→ {"id":"3","method":"lua","params":["for i=1,3 do shikisha.send_to_tab(i,'ping') end"]}← {"id":"3","ok":true,"result":[null,null]}The answer to lua is always a pair: the first value is the error, or null when the
chunk ran, followed by whatever it returned.
Who is allowed in
Section titled “Who is allowed in”On the settings screen it is the External control card; changing it there takes effect the moment you save, with no restart. In the file it is one line:
"external_api": { "access": "children" } // the default| Value | Who can call |
|---|---|
children |
Only what the app started — a tab’s CLI, and whatever that starts in turn |
user |
Anything running as you. The token is also written to data\api-token |
off |
Nothing. The pipe is not created at all |
Every tab’s process is launched knowing three things, so an AI sitting in a tab needs no setup at all:
| Variable | Holds |
|---|---|
SHIKISHA_PIPE |
The pipe to connect to |
SHIKISHA_TOKEN |
That tab’s own key, minted for it at launch |
SHIKISHA_TAB |
Which tab it is sitting in |
Because the key is the tab’s own, a call arrives already knowing who is making it — and what that tab sends counts against the same chain limit (section 5) as work handed over on screen. The API is not a way around the brakes. If an AI is what is running in that tab, what it may call is what Settings > Automation permissions (section 9) allows an AI.
What the token protects, and what it does not. The pipe is created with an access list naming your account and nobody else, so another account cannot reach it. Another program running as you can read the environment of your own processes, and an AI in a tab can copy its key into its own log. What this stops is an accident, and another account — not someone who is already you.
The first caller of each session is written to logs/hooks.log, along with any connection
that presented no valid key.
The same commands from a tab: shikisha
Section titled “The same commands from a tab: shikisha”Every tab finds a command called shikisha on its PATH. It is these same commands with
another door: shikisha tab_run shell "make" is shikisha.tab_run("shell", "make"). The first word names the command and the rest are its arguments; an argument
written as JSON ({"want":3}) is passed as that value, and every other one as text. It
talks to the app through the tab’s own key, so what it asks counts against that tab, under
that tab’s permissions – the same table that decides what an AI may call through MCP.
shikisha list shows the commands the tab may call.
The ones an AI in a tab is taught to use:
shikisha ask_tab ID "a line" "what to do" # another AI: it does the work and repliesshikisha tab_run ID "a command" # a terminal: one command, its output backshikisha browser_do ID "what to get done" # a web page: its 🗣 run driven toward the goal, what it found backshikisha tab_list # the tabs of this desk and what each isshikisha tab_conversation ID '{"want":3}' # the last things said in that tab's conversationA terminal or a page is only driven when the person named it with @ in what they last
sent the AI (a terminal can be a server somewhere). Using the wrong command for a tab says
which one to use.
ask_tab, tab_run and browser_do wait for the other tab and print its reply, ending in
one line that says what happened ([shikisha] DONE, STILL WORKING, WAITING, …). From
here they wait a little under two minutes unless given a timeout_ms of their own, because
an AI’s shell gives up on a command at about that point; when the other tab is not done by
then, it says so, and the reply is typed into the asking tab when it comes. What any other
command answers is printed as it is: a text as text, anything else as JSON. shikisha skill prints the skill that explains this to an AI.
This is what @ in the input bar is for: choosing a tab puts <@ID> in what is sent, and
the skill tells the AI to run shikisha ask_tab with that ID. The first @ asks before the
skill is written (Settings > AI agents puts it in or takes it out).
Orchestration: one AI sees a job through with others
Section titled “Orchestration: one AI sees a job through with others”ask_tab is one question and one answer. For a job with several steps and several tabs –
“have <@claude> implement it and <@codex> review it until nothing is left” –
the AI you asked becomes the job’s lead and hands out tasks with the orchestration
commands (the list is in chapter 9). The steps are not a mode of this app; the lead strings
the commands together as your request asks, and the app keeps the record:
shikisha job_open "fix the parser and get it reviewed"shikisha task_add "Implement the fix in src/parser.rs. Done when cargo test passes."shikisha assign t1 claude # the task goes to <@claude>, with how to reportshikisha inbox wait # the lead waits; the report arrives hereshikisha task_add "Review the fix on branch fix-parser." '{"waits_on":["t1"]}'shikisha assign t2 codexshikisha inbox wait '{"dealt":1}' # the first mail is dealt with; wait for the reviewshikisha inbox '{"dealt":2}' # the review's report is dealt with tooshikisha let_go a1 # each worker's tab, once its part is overshikisha let_go a2shikisha job_close "fixed and reviewed"What keeps it going without a person in the loop:
- A report that says how it went. A worker ends with
shikisha report done "<what it did>" "<what it found>" "<what is left>", orfailedin place ofdone, once. A worker that stops without reporting, waits for your approval, hits its usage limit or whose program ends is raised in the lead’s inbox, and you are notified when it waits for you. - Questions go to the lead, not to you.
shikisha ask_leadwaits for the lead’sshikisha answer. - Nothing is lost. The inbox hands the same mail over again until the reader says it has dealt with it, and a tab that is not waiting for its mail is told in one typed line when it is free to read it.
- The next step is always given. Every answer carries
next: what to do next, as the command to run wherever there is one. A refusal says why, and names the command that puts it right when there is one.job_closerefuses while anything is left open and lists what, with the command for each. - Bounded. Work goes only to tabs you named with
@or that the job opened itself (open_ai_tab), the number of assignments per job and how deep jobs may nest are set under Settings > Limits on handing work, and a report counts only from the tab and the run of its program that was given the work.
A decision you should make yourself (merging to main, say) is asked with decision_open ... to person: you are notified, and you answer on the job’s panel. The lead reads the full guide
with shikisha skill orchestration.
Tabs on a server or a MicroVM. An AI there has no shikisha command of its own, so it
cannot report or ask its lead. Put the SHIKISHA bridge on that machine (Settings > Where it runs > the machine >
“Put the SHIKISHA bridge on this machine”): a small program (about 1 MB, in
~/.local/share/shikisha/bridge/) that carries the shikisha command of the AI tabs there to
this app and reads their conversation records there. The box is ticked already when you add a
machine, and a machine added before asks once, the first time one of its tabs is opened. It is put
there only while that box is ticked, runs while this app is using a tab on that machine – and, when you chose to keep that machine’s AIs
running while the app is away, while they run – and is deleted when you untick it.
Until then, a task cannot be assigned to a tab there; ask_tab still works.
The same door, as MCP tools
Section titled “The same door, as MCP tools”An AI client that speaks the Model Context Protocol – Claude Code, and the others – can be given these commands as its own tools:
{ "command": "<path>\\SHIKISHA-TERM.exe", "args": ["--mcp"] }Started by a CLI in a tab, it needs nothing else: the tab’s key is already in its environment, so its calls arrive as that tab, counted against that tab’s chain and its permissions. To point it at another running copy – the one being tested rather than the one you are working in – name that copy instead:
{ "command": "<path>\\SHIKISHA-TERM.exe", "args": ["--mcp", "--pid", "12345", "--token-file", "<its root>\\data\\api-token"] }--pid is that copy’s process id (the door carries it in its name), and --token-file is
where access: "user" leaves its key. --pipe and --token say the same things outright.
Every tool is one command from section 9 with a shikisha_ prefix, so that this app’s
send cannot be mistaken for another server’s, and its arguments go in params in the
order the command takes them:
{ "name": "shikisha_send_to_tab", "arguments": { "params": ["reviewer", "how is it going?"] } }The list of tools is the answer to list – asked of the running app, every time, through
the same door and the same permissions. What a client is offered and what the app will
actually do cannot drift apart, including the part that depends on who is asking. A
command a desk switched off for an AI is not on the list an AI is handed.
A command that refuses comes back as a tool that failed, with the reason in it, rather than as a broken connection: the model reads it and tries something else.
8. Tips for writing it
Section titled “8. Tips for writing it”- Join strings with
..(not+) tab.outputholds only the latest response, never the earlier conversation- Lua patterns are their own thing:
%d(digit),%s(space),.-(shortest match). Write%d, not\d - When you want to do nothing, write
returnand it stops right there - If you get lost, sprinkle
shikisha.log()and readlogs/hooks.log
9. Every command
Section titled “9. Every command”Everything automation can call, in one place. The sections above teach the common ones; this is the complete list.
Whether it may run is decided on each desk’s Automation permissions card
Section titled “Whether it may run is decided on each desk’s Automation permissions card”The same command can be allowed for you and refused for an AI. The settings card lists every command with two boxes: one for a person, one for an AI.
- An AI — a call from an AI tab (a tab whose command is an AI:
claude,codex,gemini,aiderand the like, or a tab talking to a model over an API), and Lua an AI wrote (insiderun_scoped) - A person — everything else: the hooks and scripts you wrote, the run button, and programs you started yourself
An AI you start by hand inside a terminal tab counts as you. Open a
cmdor PowerShell tab, typeclaudein it, and unticking the AI column will not stop that AI — it is calling with the tab’s own key, and the tab is a terminal. To have it counted as an AI, make the tab’s own command the AI.
Nearly everything is ticked in both columns to begin with. These start out closed to an AI, and each one either steps outside this table, destroys something you own, or speaks for you:
| Command | Why it starts closed to an AI |
|---|---|
lua |
Runs code with nothing walled off. Open it and the table means nothing |
read_path / list_path / write_path / http_raw |
Raw paths and raw URLs, past the gateways. Allowed folders and hosts are the escape hatch you opened for your own scripts |
open_tab |
A tab runs whatever its line names, so opening one is starting a program – a shell of its own, outside every other row here |
close_tab / close_pane |
Ends the program in a tab / takes away a place you were looking at |
restart |
Throws away the conversation running in a tab |
sftp_mkdir / sftp_rename / sftp_rm |
Changes files on another machine, where there is no undo and no second copy |
Every git_ and github_ command |
Works as the git account or GitHub account you chose, and what it writes is said in that account’s name |
reply_url |
Hands out a link that can type into the tab, to whoever can reach it |
ai_ask |
Costs money and time, and an AI that can ask an AI can do it in a loop |
A command that is switched off answers with a sentence saying so, and the refusal
is written to logs/hooks.log. Nothing ever fails in silence. shikisha.list()
answers for whoever asked, too: it leaves out what that caller may not call.
The table is each desk’s (in the settings screen it is on the desk’s page). Only the rows you changed are written to the config file; anything left standard is not written at all.
// "desks": [ { "name": "…", here ↓ } ]"automation_permissions": { "lua": { "ai": true }, // open it to an AI as well "send_to_tab": { "ai": false } // close it to an AI}Tabs and turns
Section titled “Tabs and turns”| Command | Description |
|---|---|
shikisha.send_to_tab(tab, "text") |
Give a tab an instruction and run it. Works on this tab too (chain +1) |
shikisha.ask_tab(tab_id, "line", "text", {timeout_ms=…}) |
Ask another tab and wait for its reply. line is one short line for the chat, shown in AIConfer (refused when empty, longer than the settings allow, or more than one line); text is everything the tab is sent. Waits while it is busy, sends, and answers {state, reply, round, max_rounds, same_folder, note} once it has finished (reply from its conversation record). When its turn ends the tab is asked once for a line of its own, and its reply is kept as it was. Through the pipe or MCP only |
shikisha.say("line") |
Say one short line to the other tabs, in AIConfer: something said on its own, not the answer to an ask (that line is asked for when the answer ends). Refused when empty, longer than the settings allow, or more than one line. Called from an AI tab |
shikisha.react(tab_id, "👍") |
Mark the last line a tab said in AIConfer: one of 👍 ❤️ 🎉 👀 ✅ ❓. person marks the person’s last line. The same mark again takes it off. Called from an AI tab |
shikisha.share("commit"/"pr"/"file"/"url", target, "title") |
Show the other tabs a card in AIConfer: a commit in this tab’s folder (HEAD, a hash), a pull request’s address, a file inside this tab’s folder, or a web address. Checked first: what does not exist is refused. title is optional (a commit’s subject, a file’s name by default). Called from an AI tab |
shikisha.tab_run(tab_id, "command", {timeout_ms=…}) |
Run a command in a terminal tab and get its output. Only in a terminal the person named with @ in what they last sent you. Waits while the tab is busy, types the command, waits for it to finish, and returns {state, reply} with the output in reply (PENDING at the timeout, the output then typed into your tab). Through the pipe or MCP only |
shikisha.browser_do(tab_id, "goal", {timeout_ms=…}) |
Have a web page driven toward a goal and get what it found. Only a page the person named with @. Runs the page’s 🗣 words run and returns {state, reply, note}: DONE with what the page shows about the goal, STUCK with why, STOPPED when someone stopped it, PENDING at the timeout. One page at a time. Through the pipe or MCP only |
shikisha.tab_list() |
The tabs of this desk, as {id, name, kind (ai / terminal / page), folder, you, named}. named is a tab the person named in what they last sent the caller. Through the pipe or MCP only |
shikisha.tab_conversation(tab_id, {want=6, before=…}) |
Another tab’s conversation, as the phone’s reader shows it. Read from the CLI’s own record – on this PC, or on the server or MicroVM the tab runs on – so a long answer comes back whole, not cut to the screen. Returns {turns = {{who = "you" / "ai", text}, …}, from, more, source}, oldest first: the last want things said (1 to 40). For older ones pass the from you got as before; more is false once the start is reached. A tab whose CLI keeps no record answers source = "none" – use tab_screen. Through the pipe or MCP only |
shikisha.send(tab, "text") |
Raw keystrokes (newline is \r). For answering a prompt, not for instructing |
shikisha.draft_to_tab(tab, "text") |
Leave the text in the tab’s input box without running it — a person finishes and sends |
shikisha.note(tab, "text") |
Write a line on that tab’s screen. For the person watching only: nothing reaches what runs there, and nobody is asked to answer |
shikisha.words_note(id, "text", bad) |
The same act for a page: a line in the strip under it. A browser tab’s screen is the page itself, so there is nowhere to write one otherwise |
shikisha.state(tab) |
The state right now: WAIT / BUSY / DONE / ASK / EXIT |
shikisha.wait_state(tab, "DONE", ms) |
Wait until it reaches that state; true if it did |
shikisha.tab_output(tab) |
Another tab’s latest reply ("" if there is none yet) |
shikisha.tab_screen(tab) |
What is on that tab’s screen right now. The reply is what a turn produced; this is the glass – for a pager, a menu or any full-screen program it is the only output there is |
shikisha.tab_read(tab, mark) |
That tab’s recorded output from mark onward. Returns the text and the next mark, so a long run is followed in pieces without reading the same piece twice. Starts at 0; a tab that is not being recorded reads as "" and gives the mark back |
shikisha.restart(tab) |
Restart that tab, carrying its conversation over. shikisha.restart(tab, "fresh") starts a new one |
shikisha.open_tab({ command = "codex", name = "Research" }) |
Add a tab, written the way the settings write one: command is what runs in it, and name, id and the rest are the settings’ own keys. It goes into the caller’s folder, or into folder when one is given (a folder the desk already has; host, the machine’s name in the settings, says which one when two machines have a folder at that path). Returns { id = ..., uid = ..., folder = ..., host = ... }: the automation name it went in under, drawn when id is left out (two words, calm-otter), who the tab is (uid: never handed to another tab, whatever either is called later), and where it went. An id another tab on the desk already has is refused. Anything addressed to the new id before the tab is up – send_to_tab, show, close_tab – waits for it |
shikisha.close_tab(tab) |
Close a tab, including on a desk that is not displayed. The external API and MCP return success after the close is carried out; a working tab or one awaiting an answer is left open and returns an error. Lua automation on the displayed desk asks the person, the way the tab’s ✕ does |
An archived folder is refused before a tab is added. Restore it first or choose an active folder; open_tab does not restore folders automatically.
Hand a question to another AI – the commands in the order you would say them:
local t = shikisha.open_tab({ command = "codex", name = "Research" })shikisha.send_to_tab(t.id, "Find out why the build is slow") -- waits for the tab to come upshikisha.wait_state(t.id, "BUSY", 30000)shikisha.wait_state(t.id, "DONE", 600000)local answer = shikisha.tab_output(t.id)shikisha.close_tab(t.id)There is no one command that opens a tab and says something to it, for the same reason
there is none that splits the screen and puts a browser there: every kind of tab is one
command, and every way of using it is some order of these.
The screen
Section titled “The screen”| Command | Description |
|---|---|
shikisha.show(tab) |
Put that tab on screen. 0 is the board. Ignored if the person turned Auto-switch off, just moved the view themselves, or is in the settings |
shikisha.show_panel(name) |
Open the right-hand column on one of its panels and switch to it, the way a button that calls a panel up does: files, git, convo, console, picks, ports. A panel the tab in front does not have joins the column with its own ✕ and stays until that is pressed. Held by the same say as show |
shikisha.open_result(run) |
Open that run’s transcript as a result page and go to it |
shikisha.split_pane("right") |
Divide the pane in focus. "right" beside, "down" below. The new half takes focus |
shikisha.close_pane() |
Close the pane in focus. The tab behind it keeps running |
shikisha.focus_pane("left") |
Move focus to the neighbouring pane ("left" "right" "up" "down") |
shikisha.equalize_panes() |
Put every divider back to even halves |
Put a browser beside the agent — two commands, in the order you would say them:
shikisha.split_pane("right") -- divide, and the new half takes focusshikisha.show("br") -- ...so this puts the browser thereThere is no one command for that on purpose. split_pane and show each do one
thing, and every arrangement anyone wants is some order of the two — a combined
“split and open a browser” would only ever be the first arrangement somebody
thought of.
Waiting and time
Section titled “Waiting and time”| Command | Description |
|---|---|
shikisha.wait(tab, "pattern", ms) |
Wait until the text appears on that tab’s screen; true if it did |
shikisha.sleep(ms) |
Wait (other tabs keep running) |
shikisha.now("%Y-%m-%d") |
The local date/time, formatted. Sorts chronologically by default — good in file names |
shikisha.epoch_ms() |
Milliseconds since the epoch, as a number, for measuring elapsed time |
shikisha.diff(before, after, opts) |
What changed between two texts, written the way git writes a diff. "" when they are the same. opts is { name = "plan.md", context = 3 }: the name goes on the header lines, and the context is how many unchanged lines are kept either side of a change |
shikisha.json_decode(text) |
A JSON text as a Lua value (objects as tables, arrays numbered from 1). nil, why when it is not JSON – the usual way to read an answer an AI was asked to give as JSON |
shikisha.json_encode(value) |
A Lua value as JSON text: a table numbered 1..n becomes an array, any other table an object |
It is handed the two texts and never told where to find them, so the same command serves a reply, a page, a file and a recording:
-- What the AI changed between this answer and the last onelocal was = shikisha.get_var("answer") or ""local d = shikisha.diff(was, now, { name = "answer.md" })if d ~= "" then shikisha.note(tab, d) endshikisha.set_var("answer", now)Remembering, logging, telling someone
Section titled “Remembering, logging, telling someone”| Command | Description |
|---|---|
shikisha.get_var("key") / shikisha.set_var("key", value) |
Remembered variables, shared within the desk |
shikisha.log("text") |
Write a line to logs/hooks.log |
shikisha.notify("text") / shikisha.notify("target", "text") |
Notify Slack / Discord / Telegram, this PC’s own notification area, or a phone that registered itself (only targets you configured). With no target named, it goes to the default one |
shikisha.remote_url() |
The URL a phone can reach this app on, or nil while remote is off. Put it in a notification so “come and help” is one tap away |
shikisha.reply_url(tab) / shikisha.reply_url(tab, "target") |
A link to a page holding that tab’s last answer and a box to reply into it, or nil while remote is off. Each call writes one ticket: it can say something to that one tab and nothing else, it never carries the access token, and it dies at its expiry or when somebody presses disconnect. The second argument names where to report back that the reply landed (default: the primary target). Handing this link out hands out the ability to type into that tab — to anyone who can reach this machine’s private network and read wherever you put it. If that is more than you, they are using your AI account, and most AI subscriptions forbid sharing one; check the terms of your plan |
shikisha.t("key") / shikisha.tf("key", {name="..."}) |
Look up a translated string (tf also substitutes {name}). Used by the built-in orchestrators so they speak the app’s language |
Reporting your own state
Section titled “Reporting your own state”By default these are about the tab that called them, which is why no tab is
usually named. set_status and set_progress take one as a last argument, to
report about another tab. An AI CLI’s own hooks report through here too.
| Command | What it does |
|---|---|
shikisha.set_state("BUSY") |
This tab says what it is doing rather than leaving it to be read off the screen (BUSY / QUESTION / DONE / WAIT). The second argument is the sender’s clock in milliseconds, so reports that overtake each other still apply in the order they were said |
shikisha.set_helper("a1f2", true) |
This tab’s program says a helper it runs on the side began (true) or ended (false) |
shikisha.set_running({"a1f2"}, false) |
This tab’s program says everything it still has running beside its conversation: helpers by id, and whether anything else is still going. Replaces what was said before |
shikisha.set_status("key", "text", tab) |
Say what is being worked on, in its own words (shown under the tab name). Separate keys let several writers speak without overwriting each other; an empty string clears one. Leave tab out and it is this tab |
shikisha.set_progress(0.4, "label", tab) |
How far along it is (0..1), shown beside the state. nil clears it. Leave tab out and it is this tab |
shikisha.set_session("id") |
This tab says which conversation its CLI is running, so a restart can pick it back up |
shikisha.report_prompt("text") |
This tab says what a person just asked its CLI. A folder with Auto on writes its name and summary from these |
Browsing
Section titled “Browsing”A page is addressed by the id you gave it. See “Driving a browser” above.
| Command | Description |
|---|---|
shikisha.browser_open(id, url, profile, private) |
Open a page. profile names its cookie store; private makes a throwaway one |
shikisha.browser_close(id) |
Close it |
shikisha.browser_go(id, "back"/"forward"/"reload"/"to", url) |
Navigate |
shikisha.browser_nav(id, {...}) / shikisha.browser_unnav(id) |
Show / hide back-forward-reload-address above the page |
shikisha.browser_find(id, sel) |
Is it there? "visible" / "hidden" / "missing" |
shikisha.browser_click(id, sel, opts) |
Click it. opts is { on_missing = "continue" } – answer with the state instead of stopping |
shikisha.browser_fill(id, sel, "text", opts) |
Type into it. Does not submit — follow with browser_press. opts is the same as browser_click’s |
shikisha.browser_fill_secret(id, sel, "name") |
Fill in a registered secret. The value never reaches the script (see below) |
shikisha.browser_press(id, "enter") |
Press a key on the page |
shikisha.browser_select(id, sel, "Economy") |
Choose a value in a native dropdown, by the text a person reads. Its own verb because clicking one opens something the page cannot see into |
shikisha.browser_scroll(id, 1, sel) |
Scroll by screenfuls: a number (negative goes back up), "top" or "bottom". Without sel the page moves; with it, that box does. Also answers how far it actually went, so the end of a list is tellable from the middle |
shikisha.browser_settle(id, {ms=1200, expect="quiet"}) |
Wait for the page to stop reacting to the last move – usually a frame or two, ms is only where waiting stops. expect="options" also waits for a suggestion to appear, for the type-then-choose pattern |
shikisha.browser_text(id, sel) |
The visible text |
shikisha.browser_html(id) |
The whole document |
shikisha.browser_digest(id) |
The operable elements, numbered — what to read before deciding a move |
shikisha.browser_elements(id) |
The same reading as a table: one row per element with ref, role, name, value, choices, section, new and can (which of click / fill / select / scroll apply). For building a question out of a page rather than showing it to somebody |
shikisha.browser_fetch(id, url, opts) |
Request from inside the page (keeps its cookies). Returns {status, ok, url, headers, body} |
shikisha.browser_auth(id, "name") |
Answer basic-auth from a registered secret (as above) |
shikisha.browser_state_save(id, "label") |
Save this page’s login — its cookies and its localStorage — under a name. Returns how many cookies were saved. Sign in once, then a later rally can load it |
shikisha.browser_state_load(id, "label") |
Put a saved login back, so the page is signed in without logging in again |
shikisha.browser_snapshot(id, "label") |
Take a picture of the page (PNG) and save it. Returns the file path — a rally can keep a visual record of what it did |
shikisha.browser_ask(id, "text", "label") |
Put a banner with a button under the page. The app draws it; only a person can press it |
shikisha.browser_pressed(id) |
Has it been pressed? |
shikisha.browser_unask(id) |
Take the banner away |
shikisha.browser_wait(id, {ask=..., selector=..., timeout_ms=...}) |
Wait for whichever comes first. Returns "selector" / "button" / "timeout" |
shikisha.browser_devtools(id) |
Open the page’s DevTools as a page of its own, and return its name and whether it was opened just now (open already, it is left as it is). split_pane("right") divides the pane for it, and show(name) puts it there – from the next turn on, since a page just opened is a tab only then. “Open DevTools beside it” on a page’s tab does the same, and a split written down with DevTools in it gets them back, opened afresh, when the app starts again. Closed to an AI by default: everything the page holds, its cookies included, can be read and changed from it |
shikisha.browser_console(id, since) |
What the page said on its console after line since (leave it out for everything kept): a table per line (seq, level – error, warn, info, log or debug –, from – the page’s code, an uncaught error, or the browser about the page –, text, at, ms), and the number of the newest line to pass as since next time. The first call starts listening, and brings what the page has said since it last loaded; what it said on an earlier page is not kept |
shikisha.browser_pick(id, true) |
Arm picking on a page: until it is put away (false, or Esc on the page), a person pressing a part of the page picks that element instead of pressing it. The same switch as the Picked elements panel |
shikisha.browser_picks(id, clear) |
What has been picked on a page, oldest first: a table per element (n, note, tag, role, name, sel, path, source, box, view, url, style, html), and as a second value the same list written out the way the Picked elements panel hands it to an AI. clear = true empties the list as it is read. Values that look like keys, and the secrets the app holds, are already [hidden] in all of it |
Asking a model
Section titled “Asking a model”Two commands, and between them everything a page can be driven with.
| Command | What it does |
|---|---|
shikisha.ai_choose({ state = ..., questions = {...}, model = "conn/name" }) |
Hand over a state and a set of questions, each listing the answers it allows, and get one answer per question back: { choice = "CLICK", confidence = 0.9 }. A type = "score" question answers with score, a type = "noul" one with noul (0 to 1). An answer that was not one of the ones offered is refused rather than acted on |
shikisha.ai_text({ prompt = "...", system = "...", shape = {...}, model = "conn/name" }) |
Ask for words: what to type in a field, what a page amounts to. shape is a JSON Schema, and with it the answer comes back in that shape instead of as a paragraph |
model is connection/model, the same spelling a model tab’s command line
uses. Left out, the settings decide (the browser tab’s Decision model and
Conversation model, and for whatever it leaves unset, the Deciding AI under
AI agents), and where nothing is chosen there, the assistant AI answers.
An AI installed on this PC answers too, on the plan it is signed in with and
with no key: model = "@claude", "@codex" or "@gemini", and with a model
of its own after a slash ("@claude/haiku"). It is asked with no tools, in an
empty folder of its own, and answers the way a conversational model does.
The app keeps running while a model thinks, in a hook or a quick action:
both stop where they asked, the way sleep and ai_ask do, and other tabs
and the screen carry on. So does browser_settle there. Lua an AI writes for
a page runs straight through instead, and waits where it stands.
Two very different services answer ai_choose, and the answer is the same
shape either way. One is built for it: told what the allowed answers are, it
returns one of them with a probability for each, in a fraction of the time a
sentence takes to write. The other is an ordinary conversational model, told
the same thing in words and held to the same shape. That is what lets a page be
driven by whoever has only the ordinary kind – slower, and otherwise the same.
Set which a connection is under its own settings (Answers).
Secrets (passwords and tokens)
Section titled “Secrets (passwords and tokens)”A password or a token is registered under Secrets on the desk’s settings page. A script writes the name it was given and never receives the value.
shikisha.browser_fill_secret("br", "#password", "github")- The name means something inside that desk only. Another desk’s secrets, and the ones the program keeps for itself (an SSH password, say), cannot be reached by naming them
- Registering one asks where the secret may be used. It is filled in on those pages and nowhere else, and stops being filled the moment the page goes somewhere else
- Addresses are written out in full:
https://example.comis that whole site,https://example.com/apionly the pages under/api, andhttps://*.example.comthe site and every subdomain. Anything after?is not looked at - An
http://address reaches a machine that cannot prove who it is, so registering one also asks for “Allow unencrypted connections, at my own risk” - Who may use it is two answers, a person and an AI, and starts as the first alone. Tick the AI only for the secrets a script that an AI’s turn set going should be able to use. Ticking the AI alone is allowed too, and then a script somebody ran by hand cannot reach it
Files on another machine
Section titled “Files on another machine”The files where an SSH tab is connected. Which machine is said by naming the tab that is connected to it, the same way the git commands are told a tab. A path over there is the far end’s; a path here is this machine’s.
Both ends are fenced by what that tab was given. A path on this machine has
to be inside the tab’s working folder, and a path over there inside the folder
the tab was given on that machine, if it was given one. .. does not get out of
either. So a command told one tab cannot reach a file that tab was never handed
– which is the same promise read_file keeps, kept here too.
None of them stop the app while they run. A transfer takes as long as the link takes; the command hands the work over and waits, and every tab on screen carries on. So a loop that sends a folder a file at a time is a loop somebody can watch.
| Command | Description |
|---|---|
shikisha.sftp_ls(tab, "public/") |
A listing: {name, dir, size, modified} each. Folders first, then by name |
shikisha.sftp_ls_here(tab, "dist/") |
The same, on this machine’s side of that tab – so a walk written for one side reads the same written for the other |
shikisha.sftp_stat(tab, "public/index.html") |
One of them, or nil if it is not there |
shikisha.sftp_get(tab, "there", "here", opts) |
Bring a file here. opts is { overwrite = true } (a file that is already here is not replaced otherwise) |
shikisha.sftp_read(tab, "public/index.html") |
The file itself, as a string, without leaving a copy here |
shikisha.sftp_put(tab, "here", "there", opts) |
Send one. opts is { overwrite = true } (a file that is already there is not replaced otherwise) |
shikisha.sftp_mkdir(tab, "public/img") |
Make a folder |
shikisha.sftp_rename(tab, "a.txt", "b.txt") |
Rename or move |
shikisha.sftp_rm(tab, "b.txt") |
Delete. A file, or a folder with nothing in it |
sftp_read is what shikisha.diff is usually handed: read the copy over
there, compare it with the one here, and a script can say what a send would
change before anything is sent.
There is no “send the whole folder”. Write it as sftp_ls_here and
sftp_put in a loop going out, sftp_ls and sftp_get coming back. The
panel’s own folder button is that loop, written as a template rather than built
in – deepest first, biggest first, skip what matches, stop on the first refusal
are all arrangements somebody might want, and a command would be one of them. One command for it could only ever be the first arrangement somebody
thought of – the same reason split_pane and show stayed two.
Deleting, making and renaming are for people by default (automation
permissions). Open the reading ones (sftp_ls / sftp_get / sftp_read) and sftp_put to
an AI first, if any.
The same on a screen. A tab whose command is
sftp://deploy@example.com:22 is the file panel: two lists of files, this
tab’s working folder on the left and that server on the right. It has no way of
moving a file that is not one of the commands above, and it asks the same
permission table – so what you can do by hand and what a script may do cannot
come apart.
The panel is a connection, addressed exactly the way a terminal on another
machine is, so it is a tab the file commands can be told: sftp_put("that name", "dist/a.txt", "public/a.txt") sends to the server the screen is
showing. A terminal tab written to the same address shares the connection with
it; nobody has to say so.
Orchestration: handing work between AI tabs
Section titled “Orchestration: handing work between AI tabs”A job one AI tab (the lead) hands out to others and sees through (see “Orchestration” in
chapter 7). Answered through the pipe or MCP only; each answer carries next, what to do next
(the command, wherever there is one); a refusal says why, and names the command that puts it
right when there is one. Jobs are j1, tasks t1, assignments a1, questions q1, decisions d1.
| Command | Description |
|---|---|
shikisha.job_open("the whole job") |
Start a job led by the calling tab. Refused to a worker already as deep as Settings > Limits on handing work allows |
shikisha.job_status({job="j1"}) |
Where a job stands: its tasks, who is on them, questions and decisions waiting, loose ends, and next |
shikisha.job_close("what was done", {job="j1"}) |
Close a job. Refused while anything is left open (a task not done and not dropped, an assignment at work, a tab nobody let go or kept, an unanswered question, an open decision, unread mail); the refusal lists each with its command |
shikisha.task_add("the task", {waits_on={"t1"}, title=…}) |
Add a task. Open at once, or once every task in waits_on is done. Write it to be read alone: where, what to end up with, what must not change, and how to tell it is done |
shikisha.task_list({open=true, short=true}) |
A job’s tasks. open keeps only those that can be assigned; short cuts each to 120 characters |
shikisha.task_drop("t1", "why it is not needed") |
Take a task out of the job on purpose, so the job can close without it. Not one being worked on (stop it first) or already done. A failed or stopped task is tried again by assigning it again instead |
shikisha.assign("t1", "tab_id") |
Hand a task to an AI tab. Only a tab the person named with @ or that this job opened. Waits while the tab is busy (up to 45 seconds), types the brief in (after one typed line, for a CLI that needs it), and answers once the tab has started on it |
shikisha.report("done" or "failed", "what it did", "what it found", "what is left", {files={…}, report_path=…}) |
A worker reports its assignment, once. Only from the tab, and the run of its program, that was given it |
shikisha.ask_lead("question", {choices={…}, resume="q1"}) |
A worker asks its lead and waits for the answer. If the wait runs out the question stays asked; resume waits for the same one again |
shikisha.answer("q1", "answer") |
The lead answers a question |
shikisha.tell("a1" or "workers" or "workers:idle" or "workers:codex", "text") |
Say something to one worker, all of them, the idle ones or one CLI’s; a worker’s goes to its lead. Read at the next inbox |
shikisha.inbox({wait=true, dealt=12, kinds={…}, peek=true}) |
Read the mail: the lead its job’s, a worker its assignment’s, any other tab its own. Hands over the oldest mail not yet dealt with (up to 25), the same until dealt names that handover. wait waits for some (95 s unless wait_ms); kinds names which kinds wake it |
shikisha.decision_open("t1", "question", {choices={…}, to="person"}) |
Hold a task for a decision. to="person" notifies the person and shows the choices on the job’s panel; the decision arrives in the lead’s mail |
shikisha.decision_make("d1", "choice") |
The lead makes one of its own decisions |
shikisha.let_go("a1") |
Let a worker’s tab go once its assignment is over: closed if this job opened it and nobody has typed into it, kept otherwise (the answer says why) |
shikisha.keep("a1") |
Keep a worker’s tab once its assignment is over |
shikisha.stop("a1" or "all") |
Stop a worker: Esc now, and a tab the job opened is closed if it has not stopped 15 seconds later. The task is held: assigning it again goes on with it, task_drop takes it out |
shikisha.open_ai_tab("claude" or "codex" or "gemini", {folder=…, name=…}) |
Open a new AI tab running that CLI, set up the way every new AI tab is (Yolo only if the person’s setting says so). Open to an AI where open_tab is not: what starts is never a command the caller wrote |
shikisha.worktree_add("branch", {base=…}) |
Make a working folder (git worktree) for a branch, placed by the project’s worktree rules, and put it on the desk. Answers the folder |
Handing a run between participants
Section titled “Handing a run between participants”How the rally works: files in and out, plus a judge. You can build your own the same way.
| Command | Description |
|---|---|
shikisha.contract() |
The promises a tab is asked to keep while it holds a turn: say it in words rather than opening a confirmation prompt, report once, say what you did and what is left, then wait. Send it with the opening instruction, not every turn |
shikisha.exchange_new() |
Make a folder for this run and return its path |
shikisha.exchange_write(path, "text") |
Write a file (overwrites) |
shikisha.exchange_append(path, "text") |
Append to one |
shikisha.exchange_take(path) |
Read it, delete it, return it. nil if absent — this is the hand-over |
shikisha.ai_ask("what you want") |
Ask the assistant AI from Settings > AI agents and get the answer as text; nil and a reason when there is none. The app keeps running while it thinks (the same machinery as sleep: other tabs and the screen carry on). Three minutes by default, {timeout_ms=…} to change it. {light=true} asks for a short answer as cheaply as the AI can give one (its smallest model where it has a choice, no tools, no long instructions); for anything more than a line or two, leave it off. {ai="codex"} asks a different assistant AI, and {ai="model deepseek/deepseek-chat"} one of the registered AI providers |
shikisha.lint(code) |
Compile-check Lua without running it. An error string, or nil if sound |
shikisha.run_scoped(id, code) |
Run AI-written Lua against one page, in a jail: no files, no network, no other tabs. Returns err, out |
shikisha.lua(code) |
Run a whole chunk with everything in reach — loops, branches, several commands at once. Returns err (nil when it ran) followed by whatever the chunk returned. The unwalled twin of run_scoped, so never hand it code you didn’t write |
shikisha.list() |
The commands the caller may run, by name (see automation permissions above). Read off the table itself, so it is never out of date |
shikisha.record(text) / shikisha.record_reset() |
Keep a pasteable record of the run |
shikisha.take_replay() |
Drain the replay journal — the durable spelling of every operation since the last drain |
shikisha.set_result(code, "reason") |
The run’s verdict. Written to data/last-result.json and shown on screen |
shikisha.skip("reason") |
Stop this run here and say so: one line on the tab’s screen and in the log. For when an automation decides there is nothing to do |
Working with git
Section titled “Working with git”Which repository is named by the tab sitting in it. Leave the tab out and it is the tab that called. No path is accepted.
The reading side launches git. The branch shown in the sidebar comes from somewhere else – that path never launches git, which is why it still answers during a rebase.
| Command | What it does |
|---|---|
shikisha.git_status(tab) |
The changed files, one row each: {path, index, work, staged, unstaged, conflict, from}. index and work are git’s own two letters (staged side, working-tree side). staged and unstaged are not opposites — stage one hunk of a file and both are true |
shikisha.git_diff(tab, {path=…, staged=…, encoding=…}) |
The diff, as text. staged=true reads the staged side; path narrows it to one file. Each file’s lines are read in the encoding they are saved in (UTF-8, Shift_JIS, EUC-JP…); encoding says which instead |
shikisha.git_log(tab, count) |
Recent commits: {hash, short, author, date, subject}. 20 by default |
shikisha.git_conflicts(tab) |
Just the paths of the files with a conflict |
shikisha.git_branch(tab) |
The branch: {name, protected, upstream, ahead, behind, base, base_behind, catch_up, catching_up}. protected marks one this folder guards, so committing straight onto it is worth asking about. upstream is the branch on the server this one is counted against (origin/main), ahead the commits here not there yet and behind the other way round, as of the last fetch – all three absent when there is no such branch at all. It is the branch this one follows, else the branch of its own name on the server it would push to, and by_name is true for that second case: the work is there, the branch is simply not set to follow it (a push from a terminal with no --set-upstream). base is what the branch was cut from when that is written down, base_behind how many commits it is behind that base as of the last fetch, catch_up the commands bringing its latest in would run, and catching_up that base’s name while a merge of it has stopped half done. nil when the head is detached |
shikisha.git_graph(tab, {all=…, remotes=…, count=…}) |
The history: {graph, hash, short, author, date, subject}. graph is git’s own drawing, and the rows with no commit on them (a merge closing) are kept |
shikisha.git_detail(tab, hash) |
One commit in full: {hash, parents, author, author_date, committer, commit_date, subject, body, files} |
shikisha.git_branches(tab) |
Every branch: {name, current, protected} |
shikisha.git_checkout(tab, "name") |
Move onto that branch |
shikisha.git_merge(tab, "name") |
Bring that branch in. A conflict stops it, and shows up in git_conflicts |
shikisha.git_catch_up(tab, "origin/main") |
Bring the latest of a base in: fetch that one branch from its server, then merge what was fetched (never the local copy). Refused while anything tracked is uncommitted; a conflict stops it where the merge stopped, with the files in git_conflicts. Answers {taken}, the number of commits that came in (0 when there was nothing new). A third argument names the branch as it was pushed (git_catch_up(tab, "origin/main", "feature")): it is fetched first, and a folder behind it is refused before anything is merged – the way a pull request’s conflict is settled |
shikisha.git_set_base(tab, "origin/develop") |
Write down what the branch in front was cut from, so bringing its latest in knows where from. A worktree made in the app has it written already |
shikisha.git_remote_branches(tab) |
The branches the servers have, as last fetched: {name, catch_up}, where catch_up is the commands git_catch_up would run for that base |
shikisha.git_fetch(tab) / shikisha.git_pull(tab) / shikisha.git_push(tab) |
Talk to the server. Everything else waits until it answers (up to three minutes). git_push sets the upstream and retries when the branch has never been sent, and says so in its answer. They sign in as the git account chosen for the tab (see below), and refuse to run where none is chosen |
shikisha.git_hunks(tab, {path=…, staged=…, commit=…, encoding=…}) |
The diff cut into hunks: {file, header, start, end, patch, encoding, exact}. Each patch is a whole patch on its own. encoding is what the file’s lines were read as; exact is false when they could not be read in it without loss, and such a patch is refused by git_apply |
shikisha.git_apply(tab, patch, {cached=…, reverse=…, encoding=…}) |
Apply a patch. cached puts it in the next commit, reverse takes it back out. Pass the hunk’s encoding so its lines go back as the file’s own bytes (UTF-8 when left out). Staging one hunk is these two together |
shikisha.git_stage(tab, paths) |
Add to the next commit. One path as a string, or several in a table |
shikisha.git_unstage(tab, paths) |
Take back out of the next commit |
shikisha.git_discard(tab, paths, {staged=…, plan=…}) |
Throw the change to those files away. Without staged, what goes is the half that is not in the next commit, and a file git has never seen is taken off the disk; with it, the files go back to the way the last commit has them, and one the last commit does not have comes out of the next commit and off the disk. Files in conflict are left alone. Nothing here can be undone – what it throws away was never committed. plan=true answers without running anything, so a screen can show what would run before it asks. Either way the answer is {plan, said}, where said is those commands as a person would type them |
shikisha.git_branch_create(tab, "name") |
Make a branch and move onto it. Staged work moves with you, which is what makes this the way out of a refusal on a shared branch |
shikisha.git_commit(tab, "message", opts) |
Commit what was added and answer with the short hash. It stops on a protected branch – make a branch, or pass {allow_protected=true} to say you meant it. Which branches those are comes from Settings > Protected branches (main and master until somebody says otherwise), and each working folder may name its own |
shikisha.git_run(tab, "args…") |
Run any git and answer with its output. No shell is involved: ; and && arrive as arguments and git refuses them. It signs in as the chosen git account, and where none is chosen, the way git on this PC already does |
Which account signs in. Nothing chosen is git on this PC as it already is – its credential helper and keys – which is what most people have and never think about. A git tab uses the account chosen on its own page; any other tab uses the one its folder’s project chose, on the project’s page. The accounts themselves are the app’s (Settings > GitHub / Git): a token over HTTPS or an SSH key file, and the name and email its commits carry, which git_commit and git_merge use too. Beside them the same page lists the GitHub sign-ins git on this PC and GitHub CLI (gh) hold, each of which a project can choose by name (@pc:<login>) – what a PC holding two GitHub accounts needs, since git cannot tell which to use. When a fetch, pull or push is refused as the account, the git column says so with a button to that page. A git typed in a terminal tab signs in as the same account: the tab is started with the settings for it (GIT_CONFIG_*, and the credential helper under that account’s own server), so anything running in that tab, an AI included, signs in as it too. Nothing is taken away – a repository on another server goes on signing in the way this machine already does – and a tab picks a change up the next time it opens.
All of these are open to a person only, to begin with (automation permissions). If an AI
is to be let in, git_status / git_diff / git_log are the place to start. Opening
git_run is the same as handing it all of git.
GitHub
Section titled “GitHub”The issues and pull requests of the repository a tab works in, signed in as the git account that tab’s project chose (a git tab’s own). Each waits for GitHub, like the git commands that talk to a server. A table comes back; the Issues tab in the list shows the same answers.
| Command | What it does |
|---|---|
shikisha.github_issues(tab, {state=…, mine=…, text=…, page=…}) |
One page of issues: {repo, total, page, per_page, items}, each item {kind, number, title, state, reason, author, labels, assignees, comments, updated, url, workspace}. state is open (default), closed or all; mine is issues assigned to the account; text is GitHub’s own search words (label:bug) |
shikisha.github_prs(tab, {state=…, mine=…, review=…, text=…, page=…}) |
The same for pull requests. state can also be merged; mine is ones the account opened; review is ones waiting for its review. An item carries draft too |
shikisha.github_issue(tab, number) |
One issue in full: the row above plus body, created, comments ({author, bot, body, created, url}) and events ({kind, actor, subject, reason, created}) |
shikisha.github_pr(tab, number) |
One pull request in full, as above plus head, base, fork, merged, mergeable, merge_state, additions, deletions, changed_files, reviewers, review (approved / changes_requested / empty) and checks ({failed, pending, passed, total, items}) |
shikisha.github_labels(tab) / shikisha.github_assignees(tab) |
The labels an issue can have, and the logins it can be assigned to |
shikisha.github_issue_create(tab, {title=…, body=…, labels=…, assignees=…}) |
Open an issue. Answers {number, url} |
shikisha.github_pr_create(tab, {title=…, body=…, head=…, base=…, draft=…}) |
Open a pull request from the branch head into base, as a draft when draft = true. Answers {number, url} |
shikisha.github_comment(tab, number, "text") |
Comment on an issue or a pull request. Answers {id, url} |
shikisha.github_issue_state(tab, number, state, {duplicate_of=…}) |
open, completed, not_planned, or duplicate (with duplicate_of, which also posts “Duplicate of #n”) |
shikisha.github_pr_state(tab, number, "open" or "closed") |
Close a pull request without merging it, or open it again |
shikisha.github_pr_merge(tab, number, method) |
Merge: squash (default), merge or rebase. The branch is left where it is |
Files and the network
Section titled “Files and the network”Off unless you register a gateway — see section 6.
| Command | Description |
|---|---|
shikisha.read_file(name, rel) / shikisha.write_file(name, rel, data) |
Through a registered file gateway |
shikisha.list_files(name, rel) |
What is in that folder, one level: {name, dir, size, modified} each, folders first and then by name – the shape sftp_ls answers in, so a walk written for one side reads the same on the other |
shikisha.http(name, body) |
Through a registered HTTP gateway |
shikisha.read_path(p) / shikisha.write_path(p, data) / shikisha.list_path(p) / shikisha.http_raw(url, body) |
Raw path / raw URL. Always fails unless allow_dirs / allow_hosts says otherwise |