コンテンツにスキップ

自動化リファレンス

タブの状態が変わったときに、自動でなにかを実行する仕組みです。

書き方はLuaという小さなスクリプト言語ですが、必要なのは数行だけです。

このドキュメントは人間向けの説明であると同時に、 設定画面の「AIに書いてもらう」機能がAIへ渡す仕様書でもあります。

翻訳は docs/AUTOMATION.<コード>.md として同じ場所に置きます。言語設定に応じて自動で選ばれます。


1. いつ実行されるか(イベント)

Section titled “1. いつ実行されるか(イベント)”

自動化フォルダの中に、イベント名のファイルを置くと、その瞬間に実行されます。 必要なものだけ置けば構いません。

ファイル名 いつ動くか
on_start.lua タブが起動して落ち着いたとき (下記)
on_done.lua 聞いたことへのAIの応答が完了したとき
on_question.lua AIが確認・選択肢を出してきたとき
on_notify.lua プログラムが端末に通知を出したとき(ベル・OSC通知、ssh越しでも)。2つめの変数に本文が入る。shikisha.notify(...) で転送・振り分け・記録できる(画面のトーストはそのまま出る)
on_exit.lua セッションが終了したとき(切断・クラッシュを含む)
on_busy.lua 応答が始まったとき(上級者向け)。設定の**「作業が長引くタブの様子見」**に秒数を入れると、そのタブが作業を続けている間、その間隔でもう一度呼ばれます
_shared.lua 上記より先に読まれる。共通の下請け関数を置く場所

on_done.luaon_busy.lua は、そのタブに何か送ったあとだけ動きます。 どんなプログラムも起動時に何か出力するので画面は「動いて→止まる」となり、 これは応答と同じ形をしています。この条件が無いと、起動時のバナーが応答として 他のタブへ転送されてしまいます

on_start.lua はタブが出た瞬間には動きません。AI CLIは自分の入力欄を描き終わるまで 入力を受け取らないため、出力が出て画面が落ち着いてから実行されます (たいてい1〜2秒)。 自分で待つ必要はありません。

ファイルの中身は処理の本体だけを書きます。function ... end は不要です。

-- on_done.lua の例
shikisha.send_to_tab(2, "このコードをレビューして:\n" .. tab.output)

どのイベントでも tab が使えます。

変数 内容
tab.index タブ番号(1から)
tab.name タブ名
tab.id 設定で付けた「自動化での呼び名」。付いていなければ nil。名前を変えても壊れない唯一の手がかりなので、条件分岐はこれで書きます
tab.output 直前の応答テキスト(過去の履歴は含まれない)
tab.state "BUSY" / "DONE" / "QUESTION" / "WAIT" / "EXIT"
tab.profile 適用中のプロファイル名
tab.chain_depth 自動転送が何回連鎖したか。0なら人間が始めた会話
tab.locked 入力ロック中かどうか
tab.is_model CLIではなく、APIでモデルと話すタブかどうか
tab.reply モデルタブの返答そのもの(そのタブでのみ)。tab.output は同じ内容が画面に描かれたもの(折り返し済み)

on_question.lua は2つめの変数 screen に画面テキスト全体、on_notify.lua は2つめの変数に通知の本文が入ります。


タブは**「自動化での呼び名」**で指します。設定のタブ画面にある項目で、config.json では id です。

{ "name": "検査", "id": "reviewer", "command": "codex" }
shikisha.send_to_tab("reviewer", "レビューして") -- 推奨
shikisha.send_to_tab(2, "レビューして") -- 番号でも可(並べ替えで変わる)

画面に出ているタブ名では届きません。 タブ名は自由に付けられて、同じ名前を2つ以上のタブに 付けることもできる見出しです。同じ名前のタブが2つあると、どちらに届いたのか誰にも分からなく なるため、指す先は呼び名だけにしています。呼び名はデスクの中で重ならず、タブ名を 変えても変わりません。

呼び名を自分で決めていないタブにも必ず付いています。画面から追加したタブには短い英単語 (pandafinch など)が付き、config.jsonid を書かずに置いたタブにはタブ名から (タブ名が無ければコマンドから)作られます。設定のタブ画面で確認や変更ができます。

命令 説明
shikisha.send_to_tab(タブ, "文字列") タブに指示を渡して実行させる。 自分自身にも使えます(自動チェーン+1)
shikisha.send(tab, "文字列") 生のキー入力を送る(改行は \r)。指示ではなく、確認への返答用
shikisha.note(タブ, "文字列") そのタブの画面一行書く。見ている人へのお知らせで、中で動いているものには何も届かず、返事も求めません
shikisha.wait(tab, "正規表現", ミリ秒) 画面にその文字が出るまで待つ。出たら true
shikisha.sleep(ミリ秒) 待つ(待っている間も他のタブは動きます)
shikisha.state(tab) 今の状態を読む(ループの終了条件に使う)
shikisha.wait_state(tab, "DONE", ミリ秒) その状態になるまで待つ
shikisha.notify("宛先", "文字列") Slack / Discord / Telegram、このPCのWindows通知、登録済みのスマホへ(設定済みの宛先のみ)
shikisha.restart(tab) そのタブを会話ごと再起動する。shikisha.restart(tab, "fresh") なら新しい会話で
shikisha.log("文字列") logs/hooks.log に記録
shikisha.set_session("id") このタブのCLIが動かしている会話のIDを伝える(再起動で引き継ぐため)。タブを指定しないのは、呼んだ側がそのタブだから
shikisha.report_prompt("文字列") このタブのCLIが今依頼された内容を伝える。「自動」がオンの作業フォルダは、これをもとに名前と概要を書く。Claude Code と Codex のフックはここを通るので、端末に直接打った依頼も届く
shikisha.set_state("BUSY") このタブが今どの状態かを画面の推測ではなく自分で伝える(BUSY / QUESTION / DONE / WAIT)。AI CLI 自身のフックはここを通って状態●を動かす。第2引数は送信時刻(ミリ秒)で、追い越して届いても言われた順に適用される
shikisha.set_status("key", "文字列", タブ) 今なにをしているかを自分の言葉で伝える(タブ名の下に出ます)。key を分ければ複数の書き手が上書きし合わない。空文字でその1件を消す。タブ を省くとこのタブについて
shikisha.set_progress(0.4, "ラベル", タブ) どこまで進んだか(0〜1)。状態の隣に出ます。nil で消す。タブ を省くとこのタブについて

このアプリを知らないCLIでも同じことができます。 どの端末でも通じる通知のエスケープを そのまま受け取ります。設定は要りません(ssh越しやコンテナの中など、こちらのものが何も 入っていない場所で効きます):

ターミナルウィンドウ
printf '\e]777;notify;ビルド;テスト3件失敗\a' # 見出しと本文
printf '\e]9;ビルド完了\a' # 本文だけ

そのタブの名前の下に出ます。別のタブを見ているときは1行のトーストも出ます (そのタブを見ているなら、もう知っていることなので出しません)。 | shikisha.get_var("キー") / shikisha.set_var("キー", 値) | 記憶しておける変数。デスク内で共有 |

on_question.lua文字列を返すと、それが自動的に送信されます。 nil を返す(または何も返さない)と、人間の判断待ちになります。

AI CLIは「貼り付けられた指示」と「それを実行する改行」を別の出来事として扱い、 貼り付けを取り込む前に届いた改行は捨てますsend_to_tab はそこを引き受けます。

-- 正しい。1回の呼び出しで、入力されて実行される
shikisha.send_to_tab(tab, "あなたはビアンカ派閥として相手を論破してください")
-- 誤り。入力欄に文章が入ったまま止まる
shikisha.send(tab, "あなたはビアンカ派閥として相手を論破してください")
shikisha.send(tab, "\r")

sleep で誤魔化さないでください。 固定の待ち時間は「相手が何秒で準備できるか」の 推測でしかなく、機種・モデル・プロンプトの長さで変わります。いつか必ず破綻します。 send_to_tab は時計ではなく実際の出来事を待ちます。

send は、AIが既に待っているキー入力を送るときに使ってください (確認に "1\r" で答える、シェルを操作する、など)。


起動時に最初の指示を渡す(on_start.lua)

Section titled “起動時に最初の指示を渡す(on_start.lua)”
shikisha.send_to_tab(tab, "このプロジェクトの昨日の変更点をまとめて")

これだけです。フックはプログラムが入力を受け取れる状態になるまで待ってから動きます。

起動しただけで前日の作業を再開する(on_start.lua)

Section titled “起動しただけで前日の作業を再開する(on_start.lua)”
if not shikisha.wait(tab, "%$ $", 15000) then return end
shikisha.send(tab, "cd /srv/myproj\r")
shikisha.wait(tab, "%$ $", 5000)
shikisha.send(tab, "claude --continue\r") -- 直前の会話をそのまま再開する

過去の会話を選んで再開したい場合は claude --resume を使います。 一覧が出るので、そこから選ぶ操作も自動化できます:

shikisha.send(tab, "claude --resume\r")
if shikisha.wait(tab, "[Ss]elect", 8000) then
shikisha.send(tab, "\r") -- 一番上のセッションを選ぶ
end

危険な確認だけ人間に回して、あとは自動で承認(on_question.lua)

Section titled “危険な確認だけ人間に回して、あとは自動で承認(on_question.lua)”
if screen:match("削除") or screen:match("rm %-rf") then
return nil -- 人間に任せる
end
return "1\r" -- 選択肢1を選ぶ

AとBでレビューを往復させ、5回で打ち切る(on_done.lua)

Section titled “AとBでレビューを往復させ、5回で打ち切る(on_done.lua)”
-- 人間が直接指示したときは反応しない
if tab.chain_depth == 0 then return end
local rounds = shikisha.get_var("rounds") or 0
if tab.output:match("LGTM") or rounds >= 5 then
shikisha.notify("slack", "レビュー完了(" .. rounds .. "往復)")
return -- 何もしない = ループ終了
end
shikisha.set_var("rounds", rounds + 1)
shikisha.send_to_tab(1, "指摘を修正して:\n" .. tab.output)

切断されたら自動で再接続する(on_exit.lua)

Section titled “切断されたら自動で再接続する(on_exit.lua)”
local n = (shikisha.get_var("retry") or 0) + 1
if n > 5 then
shikisha.notify("slack", tab.name .. " が繰り返し落ちています")
return
end
shikisha.set_var("retry", n)
shikisha.sleep(2000)
shikisha.restart(tab) -- 再起動後は on_start がもう一度動く

定期的に様子を見る(on_busy.lua)

Section titled “定期的に様子を見る(on_busy.lua)”

sleep で待っている間も画面や他のタブは止まりません。間隔は自分で決められます。

-- 処理中の間だけ、30秒おきに記録する
while shikisha.state(tab) == "BUSY" do
shikisha.sleep(30000)
shikisha.log(tab.name .. " はまだ処理中")
end

tab.state呼ばれた瞬間の状態なので、ループの条件には shikisha.state(tab)(今の状態)を使ってください。 タブが終了・再起動すると、待機中のループは自動で破棄されます。

ループを書かない方法もあります。設定の**「作業が長引くタブの様子見」**に秒数を入れると、 そのタブが作業を続けている間、on_busy.lua がその間隔でもう一度呼ばれます。 呼ばれるたびに今の状態と画面が渡されるので、見張りをループではなく if 1つで書けます:

-- ここまで続く手番は、考えているのではなく返事が止まっている
local 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 .. " が15分、何も言わずに作業しています")
shikisha.set_var("since_" .. tab.index, shikisha.epoch_ms())
end

呼ばれ直すのは、もともと知らされたタブについてだけです。人を待っているタブについては呼ばれません。

完了したらSlackに通知するだけ(on_done.lua)

Section titled “完了したらSlackに通知するだけ(on_done.lua)”
shikisha.notify("slack", tab.name .. " が完了しました:\n" .. tab.output)

動かす前にやめて、そう書き残す(on_done.lua)

Section titled “動かす前にやめて、そう書き残す(on_done.lua)”
if shikisha.state(2) ~= "WAIT" then
shikisha.skip("タブ2がまだ動いている") -- この行より下は実行されません
end
shikisha.send_to_tab(2, tab.output)

skip は呼ばれたところで実行を終え、そのタブの画面と logs/hooks.log に1行残します。 「今回は動かさない」と決めたときは必ずこれを使ってください。黙って何もしなかった受け渡しは、 壊れて動かなかった受け渡しと見分けがつきません。


渡るのは応答だけで、まわりの飾りは落とします。起動バナー、入力欄の枠、 CLI が下端に出し続けるヒント行やステータス行 (? for shortcuts、 モデル名と作業フォルダの表示) は含まれません。

見分けるのは位置と変化で、文字列の一致では判定しません。カーソルより下は 何が書かれていようと入力欄です。それ以外は、実行した瞬間に撮った画面と見比べ、 応答が存在する前から画面にあったものを落とします。CLI が文言を変えても翻訳しても 効きますし、応答にどんな文章が入っていても消される心配がありません。

指示そのものも返しません。応答が始まるのは実行した行の次の行からです。 指示が長くて折り返していると、以前はその後半だけが答えの先頭に付いてきて、 相手が言ったことのように見えていました。

ひとつだけ手が届かないことがあります。応答の最中に画面の幅を狭めると、応答が欠けます。 端末が保存している各行を新しい幅で切り捨てるためで、切られた文字は戻せません (広げる・高さを変えるのは無害です)。狭めたことは logs/hooks.log に記録されるので、 応答が短いときの原因は追えます。

**タブに仕事を渡しても画面は動きません。**見せたいときは、そう書きます。

shikisha.show("reviewer") -- このタブを画面に出す
shikisha.send_to_tab("reviewer", msg) -- そのうえで仕事を渡す

この2行の順で書けば、勝手に切り替わることはありません。shikisha.show(0) で盤面へ戻ります。

**人間のほうが常に優先されます。**基本設定の「自動切り替え」を切っているとき、 直前に自分で画面を動かしたとき、設定画面を開いているときは show は何もしません。 読んでいる途中で引き剥がされることはありません。

なお、画面が追わなくてもボール自体は盤面を飛びます。ボールは「今どのタブが仕事を持っているか」 を示すもので、「自分がどこを見ているか」とは別の話だからです。


send_to_tab は打ち込んで送信します。送らずに入力欄へ置いておきたいときは、 ペーストとして送り、改行を送らないようにします。

shikisha.draft_to_tab("ai", "lp.html を読んでください。
")

入力欄に入ったまま止まります。改行はキーではなく文字として扱われるので送信されず、 人が続きを書いてから自分で Enter を押せます。このソフト自身も送信と見なさないため、 そのタブで on_done が撃たれることもありません。

下書きは連鎖の終わりではなく、輪の中に人を入れることです。 ボールは深さを保ったまま そのタブへ移って待ち、画面も追いかけるので、呼ばれた場所に自分が着きます。 そのタブで打鍵しても、他のタブと違って連鎖は切れません。乗っ取りではなく、自分の番 だからです。送れば数え上げはそのまま続き、連鎖の上限も効きます。 人が入っていても、回り続ける輪は輪なので。

シェルへは置きません。断って logs/hooks.log に理由を残します。 端末のプログラムは「貼り付けを理解するか」を自分で申告します。シェルは申告しないので、 同じものを送ると中身がコマンドとして実行されてしまいます。実測では cmd.exepowershell.exe も申告せず、Claude Code は申告しました。 コマンド名から推測するのではなく、その申告を読んでいます。

下書きは短くしてください。長いと [Pasted text #1 +N lines] に畳まれ、 これから送る中身が人に読めなくなります。


ブラウザも楽団に加われます。窓で動かすぶんには、エンジンは Windows に最初から 入っているので、何もダウンロードせず、何もインストールしません。

窓の無い版(サーバーに置いたとき)は、その機械に入っているブラウザを探して使います。 apt install chromium などで先に入れておけば、そちらを使います。無ければ、版を決めた ものをそのとき初めて取りに行きます。配布物には入っていないので、ブラウザを使わない 人は1バイトも払いません。

デスクのタブと並べて宣言します。宣言したブラウザは、セッションの続きの 番号でタブになります。 Ctrl+B に続けてその番号を押せば、他のタブと同じように 切り替わります。

{
"name": "LP検討",
"browsers": [{ "id": "br", "url": "https://example.com/login" }],
"tabs": [{ "name": "Claude", "id": "ai", "command": "claude" }]
}

端末(窓やスマホ)がサーバーにつながっているとき、AI が開いたページをどちらで描くかを 選べます(設定 → ページを描く場所)。

  • この機械で描く(既定) — スマホからも見られ、誰もつないでいなくても動き続け、 ログインは全端末で1つ。画面は絵として中継されます
  • つないだ端末で描く — 速くて文字もきれい。ただしその端末がつながっている必要があり、 その画面はその端末からしか見えません(他所から見たいページは、この機械側に置いてください)

どちらを選んでも、ページの通信は AI が走っている機械から出ます。 だから browser_open("x", "http://localhost:3000/") は、いつでもその機械の 3000 番です。 名前の解決もその機械で行われるので、その機械からしか見えない私設ネットワークにも届きます。

開いているページは移りません。設定を変えると、次に開くページから変わります。

ブラウザにはブラウザの言葉があります。セッションの状態(実行中・完了・質問)は ページには当てはまらないので、別の名前を使います。

ファイル いつ呼ばれるか
on_load.lua ページの読み込みが終わった(移動のたび
on_press.lua 人が帯のボタンを押した

帯は放っておいても出ません。 shikisha.browser_ask を呼んだときだけ、 ページの下に出ます(左に文言、右にボタン)。押されると on_press が呼ばれます。 出しっぱなしにしておけば、人がいつ押しても受け取れます。

-- scripts/lp/on_load.lua
shikisha.browser_ask(page.id, "ログインが終わったら押してください", "できました")
-- scripts/lp/on_press.lua — 押されたら続きをやる
shikisha.browser_unask(page.id)
shikisha.draft_to_tab("ai", shikisha.browser_html(page.id))

帯を描くのはアプリで、ページではありません。ページは帯の高さだけ縮み、帯を見ることも 押すこともできないので、押されたなら必ず人です。ページが移動しても消えず、 shikisha.browser_unask まで残ります。スマホからページを見ている人も押せます。

ページの上に、戻る・進む・更新・URL欄を出す

Section titled “ページの上に、戻る・進む・更新・URL欄を出す”

人に自分でページを選んでもらってから解析させたいときは、shikisha.browser_nav でページの上に操作を出せます。帯と同じく、ページの中には描きません。 ページを一段下げて、空いた場所にアプリが描くので、遷移しても消えず、 サイト自身の固定ヘッダーを覆うこともありません。

shikisha.browser_nav(page.id) -- 全部出す
shikisha.browser_nav(page.id, { reload = true, url = true }) -- 選んで出す
-- 指定できるのは back / forward / reload / reload_hard(スーパーリロード)/ url
shikisha.browser_unnav(page.id) -- 引っ込める
名前 出るもの
back ← 戻る(戻れないときは押せません)
forward → 進む
reload ⟳ 更新
url URL欄。人が打った先へ移ります(http/https のみ)

設定画面のブラウザタブでも同じことを選べます。 そちらで選んでおけば、 on_load に何も書かなくても最初から出ます。Luaから呼べば設定より優先されます。

帯も同じです。 設定画面の「帯(ボタン)」に文言とボタンの字を書けば、 開いた時点から出ます。そうすると書くのは on_press.lua の1枚だけになります。

-- scripts/lp/on_press.lua — これだけで「人が選んで、押したら渡る」
shikisha.draft_to_tab("ai", shikisha.browser_html(page.id))

URLを打っても on_load は呼ばれます。 移動のたびに解析させたくなければ、 on_load.lua は空のままにして on_press.lua にだけ書いてください。 それで「人が選んで、押したときだけ渡る」になります。

なお連鎖の深さは動きません。 深さが増えるのは他のタブへ渡ったときだけです。

受け取るのは tab ではなく page です。

中身
page.index 画面の番号(人が押す番号と同じ)
page.id 自動化から指す呼び名
page.name 人が読む名前
page.url いま開いているURL
page.complete 参照しているものまで読み終わったか
-- scripts/lp/on_load.lua
if shikisha.get_var("saved") == page.url then return end -- 移動のたびに来る
shikisha.set_var("saved", page.url)
local name = shikisha.now() .. ".html"
shikisha.write_file("tmp", name, shikisha.browser_html(page.id))
shikisha.draft_to_tab("ai", "tmp/" .. name .. " を読んでください。\n\n")

page.complete が false のときは、load が来ないので読み込み途中で呼ばれています。 広告のページでは外部の計測タグが終わらないことがあり、そのまま待つと永久に来ません。 中身が要るなら browser_wait でセレクタを待ってください。

渡す相手が起動しきっていなくても構いません。 draft_to_tabsend_to_tab は、 相手が入力を受け取れるようになるまで待ってから渡します。捨てられて消える、 ということは起きません(30秒待って駄目なら、その旨を知らせます)。

あとは自動化から動かします。

-- ログインは人にやってもらう。ページ下に帯が出る
local why = shikisha.browser_wait("br", {
selector = "#dashboard", -- ここへ着いたら抜ける
ask = "ログインしてください", -- ボタンでも抜ける
timeout_ms = 300000,
})
shikisha.log("抜けた理由: " .. why) -- selector / button / timeout
shikisha.browser_fill("br", "#title", answer)
shikisha.browser_click("br", { xpath = '//button[text()="保存"]' })
local html = shikisha.browser_html("br")

セレクタは "#id"(CSS)か { xpath = "..." }{ ref = N } です。XPath は入力フォームや 管理画面で効きます。「『名前』というラベルの隣のセル」は CSS では書けません。

{ ref = N } の番号は browser_digest が発行します:

local list = shikisha.browser_digest("br")
-- [1] textbox "検索" placeholder="検索"
-- [2] button "検索"
-- [3] link "ヘルプ" https://example.com/help
shikisha.browser_fill("br", { ref = 1 }, "俳句")
shikisha.browser_click("br", { ref = 2 })

digest はページを操作できる要素だけに蒸留した一覧です。role と名前はブラウザ自身の アクセシビリティツリー(スクリーンリーダーが見るものと同じ計算結果)から取り、標準の role を持たない JS クリッカブル(cursor:pointer<div> など)は div* のように * 印で補完します。生 HTML を読むより桁違いに短く、セレクタを推測で書く必要がなくなります。

そして { ref = N } への操作は 本物の入力(CDP 経由の信頼済みマウス/キーイベント)に なります。合成イベントを無視するサイトでも、人間のクリック・タイプと区別が付きません。 日本語などのマルチバイト文字も IME を経ずに1文字ずつ確定入力されます。

番号はその時点のページに紐づきます。ページが変わる(遷移・再描画)と失効し、古い番号への 操作は「digest を取り直して」という明確なエラーで止まります — 別の要素を黙って クリックすることはありません。

さらに { ref = N } への click / fill は 2値目に「実際に操作した要素」のエコーを 返します(例: visible, link 「ヘルプ」。fill のエコーは欄の属性だけで、値は含みません)。 番号を取り違えても、返ってきたエコーがその場で告発します。

再現・持ち運びについて: { ref = N } はどの実行モード(automation スクリプト、 composer の ▶ Lua 実行、operate のラリー)でも同じ意味を持つ通常のセレクタです。ただし 番号は「直前の browser_digest の一覧」への参照なので、ref のまま持ち運ぶものではありません。

そこで 実行と記録は独立しています。operate のラリーでは、実行された各操作が 耐久形に書き直されて run フォルダの replay.lua に積まれます — { ref = N } は 「実際に触った要素」から導出したアンカー(人が付けた #id、無ければ一意なテキスト/属性の XPath。📼 レコーダと同じ流儀で、機械生成の id は拒否)に置き換わり、browser_digest は 一行も現れません。つまり:

  • 実行の通貨 = ref(能力最大: shadow DOM も届く・本物入力・計量モデルに優しい)
  • 持ち運びの通貨 = replay.lua(素の css / xpath だけ。▶ 実行モードに貼っても、 automation に組み込んでも、別 PC の SHIKISHA でも、そのまま動く)

replay.lua は、🎯 ターゲットパネルのプルダウン横の「⬇ 再現Lua」ボタン、または操作終了時に 開く結果ビュー右上の同名ボタンからダウンロードできます。耐久アンカーを導出できなかった操作は 黙って欠落させず、-- click (…): 何を押したか というコメントで残ります。

要素を探すと3つの状態が返ります(visible / off_screen / not_found)。 セレクタを疑うのか、待ちを疑うのかが、これで決まるからです。

見つからないときに止めるかどうかは、呼び出しごとに選べます。 既定では止まり、 { on_missing = "continue" } なら状態を返して進みます。出たり出なかったりする Cookieバナーは失敗ではありませんが、それを知っているのは呼んだ側だけです。

セレクタを指定していても、ボタンは待っている間ずっと出します。 サイトの改修で 条件が合わなくなったとき、止まるのではなくクリック1回で済むようにするためです。 そして抜けた理由が返るので、毎回 button で抜けているなら、そのセレクタは 一度も効いていないと分かります。

click / fill は自動で待ちます(auto-wait)。 要素が 現れる → 見える → 動きが 止まる(連続フレームで矩形が同一) → 無効でない まで待ってから操作します。リトライは 0/20/100/100/500ms のバックオフ、 リトライ毎にスクロール位置を変えて sticky なオーバーレイを外し、ページ遷移で JS 世界が 消えても外側のリトライが新しいドキュメントに入り直します。だから browser_go の直後に 次ページの要素を操作する連打スクリプト(replay.lua)がそのまま通ります。待ち時間の 上限は操作あたり 10 秒で、要素が存在するのに最後まで安定しなかった場合は従来どおり その場で操作します(新しい失敗モードは増やしません)。

値がコードになることはありません。 fill に渡したものは全てデータとしてページへ 届くので、引用符や山括弧だらけの回答でも、そのまま入って何も起こしません。 生のJavaScriptをページへ渡す口は、意図的に用意していません。

開けるのは httphttps だけです。1行の <input> は改行を保持できません (このソフトではなく HTML の仕様です)ので、複数行を入れるなら textarea が要ります。


自動化が暴走しないよう、いくつもの歯止めがあります。

  • 自動チェーン上限 … AI同士の自動転送が続いた回数を数え、上限(既定10回)で止まります。 人間が手で入力すると0に戻ります
  • 手動操作の優先 … 人間が触った直後5秒は自動送信されません
  • 緊急停止Ctrl+B x で全自動化を即停止し、作業中の AI にはそれぞれの中断キーを 送ります(プロファイルの interrupt。Claude Code・Codex・Gemini は Esc、Aider は Ctrl+C)。 Ctrl+B a で自動化をON/OFF。ステータス行にも同じボタンがあり、どの画面でも同じ位置に出ます
  • 入力ロック … 中間タブを🔒にしておけば、人間が誤って指示を出せません
  • サンドボックス … 自動化からはファイル操作もインターネット接続も既定ではできません。 通知先も、設定に登録済みのもの(Slack / Discord / Telegram、このPC、登録済みのスマホ)にしか送れません

6. ファイル・通信を使う(上級者向け・既定は無効)

Section titled “6. ファイル・通信を使う(上級者向け・既定は無効)”

必要な場合だけ、config.jsonデスクの中に「窓口」を登録すると、そのデスクの自動化から使えるようになります。 設定画面からは編集できません(影響が大きいため、ファイルを直接編集する人だけの機能です)。

// "desks": [ { "name": "…", ここに ↓ } ]
"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":"指摘","body":"..."}')
命令 説明
shikisha.now([書き方]) いまの日時を文字列で返す
shikisha.write_file(窓口, ファイル名, 文字列) 登録済みフォルダへ書き込む
shikisha.read_file(窓口, ファイル名) 登録済みフォルダから読む
shikisha.http(窓口, 本文) 登録済みURLへ送信(認証はアプリが付与)

保存するものに時刻の名前を付けたいことは、よくあります。

shikisha.now() -- 20260807012604 (既定)
shikisha.now("%Y-%m-%d") -- 2026-08-07
shikisha.now("%Y%m%d") .. ".html"

書き方は os.date と同じです。使えるのは %Y %y %m %d %H %M %S と、 % そのものを出す %% です。知らない書き方はそのまま残ります。

os は渡していません。 日時のほかにプロセスを起こす道具もファイルを消す道具も 入っているので、日時のために丸ごと渡すわけにはいきません。

この方式の安全性: スクリプトはパスもURLも組み立てられず、登録済みの名前しか 呼べません。認証トークンはスクリプトから見えず、アプリが付与します。 config.json / secrets.json / .env / .lua ファイルは、許可フォルダ内にあっても 常に読み書きできません。

さらに自由度が必要なら、生パス・生URLも使えます(既定は空=全拒否):

"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}')

接続先はホスト名の完全一致で照合し、https のみ許可されます (api.example.com.evil.com のようなすり抜けは弾かれます)。 ファイル・通信は必ず logs/hooks.log に記録されます。

窓口・自動化の権限・通知先・モデル接続先・git の設定は、デスクごとに持ちます。 アプリ全体の設定というものはなく、デスクに書いていないものは「無い」という意味です (窓口なし、標準の権限、通知先なし、接続先なし、組み込みの git 設定)。 会社のリポジトリと自分のものを1台で扱っても、片方の API キーや通知先が もう片方から使われることはありません。

"desks": [
{
"name": "会社",
"id": "kaisha",
"capabilities": { "http": { "deploy": { "url": "https://example.com/deploy" } } },
"automation_permissions": { "write_path": { "ai": false } },
"notify": {
"work-slack": { "type": "slack", "webhook": "@notify/kaisha/work-slack" },
"このPC": { "type": "windows" }
},
"primary_notify": "work-slack", // 宛先を書かない notify(text) の届き先
"providers": {
"work-azure": { "base_url": "https://….openai.azure.com/…", "api_key": "@provider/kaisha/work-azure" }
},
"git": { "protect": ["main", "release/*"] },
"git_accounts": [
{ "name": "work", "login": "me-at-work", "user_name": "山田 太郎", "user_email": "[email protected]", "owners": ["my-company"] },
{ "name": "home", "method": "ssh", "key": "C:/Users/me/.ssh/id_home" }
],
"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" }
]
} ]
}
]

@ で始まる値は秘密情報の名前です。設定画面から登録すると、キーやWebhookは 暗号化して保存され、ファイルには名前だけが書かれます。新しいデスクを作るときは、 今のデスクの設定をコピーして始めることもできます(キーも新しいデスク用にコピーされます)。

git アカウントのトークンもここには書きません。デスクの設定の「git アカウント」で 入れると、git/<デスクのid>/<アカウント名> という名前で保存されます。 アカウントが自動で選ばれることはありません。 git 欄でサインインするアカウントは、 プロジェクトが git_account で選び、git タブは自分で選びます。"@pc" は、この PC の git に設定済みのサインインを使うという選択です。プルリクエスト番号も同じアカウントで 読みます。環境変数 GITHUB_TOKEN は読みません。

bring は、プロジェクトの新しいワークツリーに、git が運ばないものをどう持っていくかです (設定のプロジェクトのページで編集します)。pattern はプロジェクトの .gitignore の1行で、 その行が無視させているものすべてに効きます。fromto は、どこかにあるファイルを ワークツリー内の決まった場所に置きます。howcopy(コピー)、replace(コピーして、 各 findwith に置換。regex を付けると正規表現で、^$ は各行の先頭と末尾)、 link(リンク)、skip(持っていかない)です。規則のない行は、設定ページの横に表示された 動きになります。プロジェクトのセットアップのコマンドは、これらのあとに実行されます。


アプリの外にいるプログラムから、Luaで書くのと同じ命令を呼べます。名前も引数も同じで、 覚え直す語彙はありません。

入口は 名前付きパイプ \\.\pipe\shikisha-<pid> です。1行1JSON、返事も1行:

→ {"token":"…"} 最初の1回だけ、合言葉
← {"ok":true,"result":"hello"}
→ {"id":"1","method":"send_to_tab","params":["reviewer","状況は?"]}
← {"id":"1","ok":true,"result":null}
→ {"id":"2","method":"list"}
← {"id":"2","ok":true,"result":["browser_click","browser_close", … ]}

method は9章の命令から shikisha. を取った名前、params はその引数を順に並べたものです。 listその呼び出し元が実行してよい命令を返します。アプリ自身の表と権限を読んで答えるので、 実際にできることと食い違うことがありません

ループや分岐は、まとまったコードを1回で渡せます:

→ {"id":"3","method":"lua","params":["for i=1,3 do shikisha.send_to_tab(i,'ping') end"]}
← {"id":"3","ok":true,"result":[null,null]}

lua の答えは必ず2つ組で、1つめがエラー(実行できたときは null、続けてコードが 返した値が並びます。

設定画面では 「外部制御」 のカードです。保存した瞬間に切り替わります(再起動は不要)。 設定ファイルなら1行です。

"external_api": { "access": "children" } // 既定
呼べる相手
children アプリが起動したものだけ(タブのCLIと、それがさらに起動したもの)
user あなたの権限で動くものすべて。合言葉は data\api-token にも書かれます
off 誰も。パイプ自体を作りません

タブのプロセスは、起動の時点で次の3つを知らされています。タブの中のAIは、何の準備もなしに そのまま呼べます。

変数 中身
SHIKISHA_PIPE つなぐ先のパイプ
SHIKISHA_TOKEN そのタブ専用の合言葉(起動時に発行)
SHIKISHA_TAB 自分がどのタブにいるか

合言葉がタブごとなので、呼び出しは誰からのものか分かった状態で届きます。そのタブでAIが 動いていれば、呼べる命令は設定の**「自動化の権限」(9章)で決めた範囲になります。そしてそのタブが 送ったものは、画面越しに渡したときと同じ連鎖の上限**(5章)に数えられます。 外部APIはブレーキの抜け道ではありません。

合言葉が守るもの・守らないもの。 パイプはあなたのアカウントだけを許可するアクセス制御を 付けて作られるので、別アカウントからは届きません。ただしあなたの権限で動く別のプログラムは あなたのプロセスの環境変数を読めますし、タブの中のAIが自分の合言葉をログに書くこともあります。 これで防げるのは「事故」と「別アカウント」までで、すでにあなたである相手は防げません。

そのセッションで最初につないできた相手は logs/hooks.log に記録されます。合言葉が違う接続も 同じく残ります。

Model Context Protocol を話すAIクライアント(Claude Code など)には、ここの命令を そのまま道具として渡せます。

{ "command": "<パス>\\SHIKISHA-TERM.exe", "args": ["--mcp"] }

タブの中のCLIが起動したときは、これだけで繋がります。そのタブの合言葉が既に環境変数に 入っているので、呼び出しはそのタブからのものとして届き、そのタブの連鎖の上限と権限に 数えられます。別の起動中のコピー(あなたが作業している方ではなく、試している方)へ 向けるときは、そのコピーを名指しします。

{ "command": "<パス>\\SHIKISHA-TERM.exe",
"args": ["--mcp", "--pid", "12345", "--token-file", "<そのコピーの置き場>\\data\\api-token"] }

--pid はそのコピーのプロセスID(パイプの名前がそれを持っています)、--token-fileaccess: "user" が合言葉を置く場所です。--pipe--token なら直接書けます。

道具の名前は9章の命令に shikisha_ を付けたものです(このアプリの send が、別のサーバーの send と混ざらないため)。引数は命令が取る順に params へ並べます。

{ "name": "shikisha_send_to_tab", "arguments": { "params": ["reviewer", "状況は?"] } }

道具の一覧は list の答えそのもので、毎回、同じ入口と同じ権限で動いているアプリに尋ねます。 クライアントに見えるものと、アプリが実際にできることは食い違いません。 誰が尋ねたかで 変わる部分も同じです。デスクの設定でAIに対して切ってある命令は、AIに渡される一覧には出ません。

断られた命令は、接続が壊れたのではなく「失敗した道具」として理由つきで返ります。 モデルはそれを読んで別の手を試せます。


  • 文字の連結は .. です(+ ではありません)
  • tab.output は直前の応答だけが入ります。過去の会話は含まれません
  • 正規表現はLua独自です。%d(数字)、%s(空白)、.-(最短一致)など。 \d ではなく %d と書きます
  • 何もしたくないときは return と書けば、その場で終わります
  • 迷ったら shikisha.log() を仕込んで logs/hooks.log を見てください

自動化から呼べるものを、すべてここに置きます。上の章はよく使うものの説明で、 こちらが全部です。

実行してよいかは、デスクの設定の「自動化の権限」が決めます

Section titled “実行してよいかは、デスクの設定の「自動化の権限」が決めます”

同じ命令でも、あなたが実行するときAIが実行するときで、許すかどうかを別々に 決められます。設定の 「自動化の権限」 に命令が全部並んでいて、それぞれに 「人間が実行」「AIが実行」の2つのチェックがあります。

  • AIAI用のタブ(そのタブのコマンドが claude / codex / gemini / aider などの AIのもの、またはモデルAPIのタブ)からの呼び出しと、AIが書いたLua(run_scoped の中)
  • あなた — それ以外すべて。自分で書いたフック・スクリプト、▶ の実行、 自分で起動した外部プログラム

端末のタブで自分で起動したAIは「人間」に数えます。 cmd や PowerShell のタブを開いて、 その中で claude と打って動かした場合です。「AIが実行」のチェックを外しても、そのAIは 止まりません。 そのタブの資格で呼んでいて、そのタブは端末だからです。 AIとして数えるには、タブのコマンド自体をAIにしてください

どちらの列も、既定ではほとんど入っています。AIに対して既定で閉じているのは次の6つだけで、 どれも「この表そのものを回り込める」か「あなたの持ち物を壊せる」かのどちらかです。

命令 既定でAIに閉じている理由
lua 囲いなしでコードを実行できる。開ければこの表が意味を持たなくなる
read_path / write_path / http_raw 窓口を通さない生のパス・URL。許可フォルダと許可ホストは、あなたが自分のスクリプトのために開けたもの
close_pane 人が見ていた場所を消す
restart 会話ごとタブを作り直す

許可されていない命令を呼ぶと、なぜ止まったのかが書かれたエラーが返り、 logs/hooks.log にも1行残ります。黙って何も起きないことはありません。 shikisha.list() も、呼んだ相手に許されているものだけを返します。

この表はデスクごとです(設定画面ではデスクのページにあります)。 設定ファイルに書かれるのはあなたが変えた行だけで、標準のままの命令は何も書かれません。

// "desks": [ { "name": "…", ここに ↓ } ]
"automation_permissions": {
"lua": { "ai": true }, // AIにも開ける
"send_to_tab": { "ai": false } // AIには閉じる
}
命令 説明
shikisha.send_to_tab(タブ, "文字列") タブに指示を渡して実行させる。 自分自身にも使えます(チェーン+1)
shikisha.send(タブ, "文字列") 生のキー入力(改行は \r)。指示ではなく、確認への返答用
shikisha.draft_to_tab(タブ, "文字列") 入力欄に置くだけで実行しない。人が書き足して送ります
shikisha.note(タブ, "文字列") そのタブの画面一行書く。見ている人へのお知らせで、中で動いているものには何も届かず、返事も求めません
shikisha.state(タブ) 今の状態: WAIT / BUSY / DONE / ASK / EXIT
shikisha.wait_state(タブ, "DONE", ミリ秒) その状態になるまで待つ。なれば true
shikisha.tab_output(タブ) 他のタブの最新の返答(まだ無ければ ""
shikisha.tab_screen(タブ) そのタブの画面に今出ているもの。応答は「手番が生んだもの」、こちらは「画面そのもの」で、ページャやメニューなど全画面のプログラムでは画面が唯一の出力です
shikisha.tab_read(タブ, 印) そのタブの記録を「印」から読む。本文と次の印を返すので、長い実行を少しずつ追えます(同じところを二度読みません)。最初は 0。記録していないタブは "" と、渡した印をそのまま返します
shikisha.restart(タブ) そのタブを会話ごと再起動する。shikisha.restart(タブ, "fresh") なら新しい会話で
命令 説明
shikisha.show(タブ) そのタブを画面に出す。0 は盤面。「自動切り替え」を切っているとき、直前に人が画面を動かしたとき、設定画面を開いているときは何もしません
shikisha.open_result(run) その実行の記録を結果ページとして開き、そこへ移動する
shikisha.split_pane("right") フォーカス中のペインを割る。"right" は横、"down" は下。新しい方にフォーカスが移る
shikisha.close_pane() フォーカス中のペインを閉じる。中のタブは動いたまま
shikisha.focus_pane("left") 隣のペインへフォーカスを移す("left" "right" "up" "down"
shikisha.equalize_panes() 仕切りを全部半々に戻す

AIの隣にブラウザを置く — 言う通りの順に2行書くだけです。

shikisha.split_pane("right") -- 割ると、新しい方にフォーカスが移るので
shikisha.show("br") -- そこにブラウザが入る

これを1つの命令にしていないのは意図的です。split_paneshow はそれぞれ1つのことしか しません。欲しい配置はこの2つの順番の組み合わせで全部書けるので、「割ってブラウザを開く」 という合体命令を作っても、最初に思いついた1通りの配置にしかならないからです。

命令 説明
shikisha.wait(タブ, "正規表現", ミリ秒) そのタブの画面にその文字が出るまで待つ。出れば true
shikisha.sleep(ミリ秒) 待つ(待っている間も他のタブは動きます)
shikisha.now("%Y-%m-%d") 現地の日時を整形して返す。既定は時系列に並ぶ形なので、ファイル名向き
shikisha.epoch_ms() エポックからのミリ秒(数値)。経過時間の計測用
shikisha.diff(前, 後, 設定) 2つの文章の差分を、git と同じ書き方で返す。同じなら ""設定{ name = "plan.md", context = 3 } で、name は見出し行に出る名前、context は変更の前後に残す行数
shikisha.json_decode(文字列) JSON の文字列を Lua の値にする(オブジェクトはテーブル、配列は1から始まる番号付き)。JSON でなければ nil, 理由。AI に JSON で答えさせたときの読み取りに使う
shikisha.json_encode(値) Lua の値を JSON の文字列にする。1..n の番号だけのテーブルは配列、それ以外はオブジェクトになる

渡すのは文章そのもので、どこから持ってきたかは問いません。返事でも、ページでも、 ファイルでも、記録でも、同じ命令で比べられます。

-- 前の返事と今の返事で、どこが変わったか
local was = shikisha.get_var("answer") or ""
local d = shikisha.diff(was, now, { name = "answer.md" })
if d ~= "" then shikisha.note(tab, d) end
shikisha.set_var("answer", now)
命令 説明
shikisha.get_var("キー") / shikisha.set_var("キー", 値) 記憶しておける変数。デスク内で共有
shikisha.log("文字列") logs/hooks.log に1行書く
shikisha.notify("文字列") / shikisha.notify("宛先", "文字列") Slack / Discord / Telegram、このPCのWindows通知、登録済みのスマホへ(設定済みの宛先のみ)。宛先を書かなければ既定の宛先へ
shikisha.remote_url() スマホからこのアプリにつながるURL。リモートが切れているときは nil。通知に入れておくと「手伝いに来て」がワンタップになります
shikisha.reply_url(tab) / shikisha.reply_url(tab, "宛先") そのタブの最後の回答と入力欄だけを置いたページへのリンク。リモートが切れているときは nil。呼ぶたびに切符を1枚書きます。できるのはそのタブに1件送ることだけで、盤面のトークンは載らず、期限切れか「切断」で無効になります。第2引数は送信できたことを報告し返す宛先(省略時はプライマリ)。このリンクを渡すことは、そのタブに書き込む権限を渡すことです —— このPCのプライベートネットワーク(Tailscale か同じLAN)に届き、かつリンクを読める人が対象です。それがあなた以外にもいる場合、その人はあなたのAIアカウントを使うことになり、多くのAIサブスクリプションは複数人での利用を禁止しています。契約中の規約を確認してください
shikisha.t("キー") / shikisha.tf("キー", {name="…"}) 訳語を引く(tf{name} も差し込む)。組み込みの進行役がアプリの言語で話すために使っています

既定では呼んだ側のタブについての報告です。だから普通はタブを書きません。 set_statusset_progress だけは、最後の引数で別のタブについて報告できます。 AI CLI 自身のフックもここを通ります。

命令 説明
shikisha.set_state("BUSY") このタブが今どの状態かを、画面の推測ではなく自分で伝える(BUSY / QUESTION / DONE / WAIT)。第2引数は送信時刻(ミリ秒)で、追い越して届いても言われた順に適用される
shikisha.set_status("キー", "文字列", タブ) 今なにをしているかを自分の言葉で伝える(タブ名の下に出ます)。キー を分ければ複数の書き手が上書きし合わない。空文字でその1件を消す。タブ を省くとこのタブについて
shikisha.set_progress(0.4, "ラベル", タブ) どこまで進んだか(0〜1)。状態の隣に出ます。nil で消す。タブ を省くとこのタブについて
shikisha.set_session("id") このタブのCLIが動かしている会話のIDを伝える(再起動で引き継ぐため)
shikisha.report_prompt("文字列") このタブのCLIが今依頼された内容を伝える。「自動」がオンの作業フォルダは、これをもとに名前と概要を書く

ページは付けた id で指します。上の「ブラウザを動かす」も参照してください。

命令 説明
shikisha.browser_open(id, url, profile, private) ページを開く。profile はcookieの入れ物の名前、private は使い捨て
shikisha.browser_close(id) 閉じる
shikisha.browser_go(id, "back"/"forward"/"reload"/"to", url) 移動する
shikisha.browser_nav(id, {…}) / shikisha.browser_unnav(id) ページの上に戻る・進む・更新・URL欄を出す/消す
shikisha.browser_find(id, セレクタ) あるか: "visible" / "hidden" / "missing"
shikisha.browser_click(id, セレクタ, opts) 押す。opts{ on_missing = "continue" }(無ければ止めずに状態を返す)
shikisha.browser_fill(id, セレクタ, "文字列", opts) 入力する。送信はしません — 続けて browser_pressoptsbrowser_click と同じ
shikisha.browser_fill_secret(id, セレクタ, "名前") 登録済みの秘密情報を入力する。値はスクリプトに渡りません(下の「秘密情報」)
shikisha.browser_press(id, "enter") ページ上でキーを押す
shikisha.browser_text(id, セレクタ) 見えている文字
shikisha.browser_html(id) 文書全体
shikisha.browser_digest(id) 操作できる要素の一覧(番号付き)。次の手を決める前に読むもの
shikisha.browser_fetch(id, url, opts) ページの中から通信する(cookieを引き継ぐ)。{status, ok, url, headers, body} を返す
shikisha.browser_auth(id, "名前") 登録済みの秘密情報でBasic認証に答える(同上)
shikisha.browser_state_save(id, "名前") このページのログイン(cookie と localStorage)を名前を付けて保存。保存した cookie 数を返す。一度ログインすれば後のラリーで読み込める
shikisha.browser_state_load(id, "名前") 保存済みのログインを入れ直す。ログインし直さずにサインイン状態にする
shikisha.browser_snapshot(id, "名前") ページの画像(PNG)を撮って保存。ファイルパスを返す。ラリーが「何をしたか」の視覚記録を残せる
shikisha.browser_ask(id, "文字列", "ラベル") ページの下にボタン付きの帯を出す。描くのはアプリで、押せるのは人だけ
shikisha.browser_pressed(id) 押されたか
shikisha.browser_unask(id) 帯を消す
shikisha.browser_wait(id, {ask=…, selector=…, timeout_ms=…}) 早い者勝ちで待つ。"selector" / "button" / "timeout" を返す

秘密情報(パスワードやトークン)

Section titled “秘密情報(パスワードやトークン)”

パスワードやトークンは、**そのデスクの設定の「秘密情報」**に登録します。スクリプトは 登録した名前を書くだけで、値そのものは受け取りません。

shikisha.browser_fill_secret("br", "#password", "github")
  • 名前はそのデスクの中でだけ通じます。別のデスクの秘密情報や、このソフトが 自分で使っている分(SSHのパスワードなど)は、名前を書いても届きません
  • 登録するときに秘密の利用を許可するURLを書きます。そこに書いたURLのページでだけ入力されます。 ページが別のサイトへ移ったら、その時点で入力されなくなります
  • URLは省略せずに書きます。https://example.com はそのサイトの全ページ、 https://example.com/api/api で始まるページだけ、https://*.example.com は example.com とサブドメイン全部。? 以降は見ません
  • http:// で書けば暗号化されない相手にも入れられますが、登録のときに 「自己責任で非暗号化通信を許可する」を入れる必要があります
  • 利用権限人間AIの2つで、はじめは人間だけです。AIタブがきっかけで動いた スクリプトから使わせたいものにだけ AI を入れてください。AI だけを入れることもでき、 そのときは人が手で動かしたスクリプトからは届きません

別のマシンのファイルを読み書きする

Section titled “別のマシンのファイルを読み書きする”

SSHのタブがつながっている先のファイルです。どのマシンかは、そこにつながっているタブで指します (git の命令と同じ指し方)。パスは向こう側のもの、こちら側のパスはこのPCのものです。

両側とも、そのタブに与えられた範囲の中だけです。 こちら側のパスはそのタブの作業フォルダの中、 向こう側のパスはそのタブに与えられたフォルダの中(与えられていれば)。.. で外へは出られません。 あるタブに渡していないファイルには、そのタブを指した命令では届かない —— read_file と同じ約束を、 ここでも守ります。

どれもアプリを止めません。 転送は回線のぶんだけ時間がかかりますが、命令は仕事を渡して待つだけで、 画面のタブは動き続けます。フォルダを1ファイルずつ送るループも、見ていられます。

命令 説明
shikisha.sftp_ls(タブ, "public/") 一覧。1件ずつ {name, dir, size, modified}。フォルダが先、次に名前順
shikisha.sftp_ls_here(タブ, "dist/") 同じものを、そのタブのこちら側で。片側で書いた歩き方がもう片側でもそのまま読めます
shikisha.sftp_stat(タブ, "public/index.html") 1件ぶん。無ければ nil
shikisha.sftp_get(タブ, "向こうのパス", "こちらのパス", opts) 持ってくる。opts{ overwrite = true }(既定では、こちらにもうあるファイルは上書きしません)
shikisha.sftp_read(タブ, "public/index.html") 中身を、こちらに残さず文字列で返す
shikisha.sftp_put(タブ, "こちらのパス", "向こうのパス", opts) 送る。opts{ overwrite = true }(既定では、もうあるファイルは上書きしません)
shikisha.sftp_mkdir(タブ, "public/img") フォルダを作る
shikisha.sftp_rename(タブ, "a.txt", "b.txt") 名前を変える・移す
shikisha.sftp_rm(タブ, "b.txt") 消す。ファイルと、空のフォルダだけです

sftp_readshikisha.diff に渡すためにあります。向こうの1枚を読んでこちらの1枚と比べれば、 送る前に何が変わるかを出せます。

フォルダごと送る命令はありません。 送るときは sftp_ls_heresftp_put、取り寄せるときは sftp_lssftp_get を繰り返して書きます。 パネルのフォルダ送信ボタンも、そのループをテンプレートとして書いたものです。深い順・大きい順・同じものは飛ばす・ 最初の失敗で止める——どれも誰かが欲しがる並べ方で、命令にするとそのうち1つだけしか選べなくなります。1つの命令にすると、 最初に思いついた1通りのやり方しか選べなくなるからです(split_paneshow と同じ理由)。

既定では、消す・作る・名前を変えるは人間だけです(自動化の権限)。AIに開くなら、まず読む (sftp_ls / sftp_get / sftp_read)と送る(sftp_put)からどうぞ。

同じことを、画面から。 コマンドが sftp://[email protected]:22 のタブは、ファイルのパネルです。 左にこのタブの作業フォルダ、右にそのサーバーの、2枚の一覧が出ます。画面が持っている手立ては上の命令だけで、 権限も同じ表を見ます。手でできることと台本にできることが食い違わないためです。

パネルは接続そのもので、別のマシンの端末とまったく同じ書き方で指します。ですからファイルの命令に そのまま渡せます(shikisha.sftp_put("その呼び名", "dist/a.txt", "public/a.txt") で、画面に出ている サーバーへ送られます)。同じアドレスの端末タブが開いていれば、接続の1本は自動的に共有されます。

参加者のあいだで実行を受け渡す

Section titled “参加者のあいだで実行を受け渡す”

ラリーの仕組みそのものです。ファイルの受け渡しと審判。同じ道具で自作もできます。

命令 説明
shikisha.contract() 手番を持っている間、そのタブに守ってほしいこと: 確認のプロンプトを出さずに言葉で書く・報告は一度だけ・やったことと残りを書く・報告したら待つ。最初の指示と一緒に渡す(毎手番ではなく)
shikisha.exchange_new() この実行用のフォルダを作り、その場所を返す
shikisha.exchange_write(パス, "文字列") ファイルに書く(上書き)
shikisha.exchange_append(パス, "文字列") 追記する
shikisha.exchange_take(パス) 読んで、消して、返す。無ければ nil — これが受け渡しの本体
shikisha.ai_ask("聞きたいこと") 基本設定のアシスタントAIに聞いて、答えの文字列を返す。答えが得られなければ nil と理由。待っている間もアプリは止まりませんsleep と同じ仕組みで、他のタブも画面も動き続けます)。既定の待ち時間は3分、{timeout_ms=…} で変えられる。{light=true} にすると、そのAIでいちばん安い聞き方で短い答えをもらう(選べるなら最小のモデル、道具なし、長い指示なし)。1〜2行より長い答えがほしいときは付けない。{ai="codex"} で別のアシスタントAIに、{ai="model deepseek/deepseek-chat"} でこのデスクのモデル接続先に聞く
shikisha.lint(コード) Luaを実行せずに構文検査する。壊れていればエラー文字列、健全なら nil
shikisha.run_scoped(id, コード) AIが書いたLuaを1つのページに対してだけ実行する囲い。ファイルも通信も他のタブも触れません。err, out を返す
shikisha.lua(コード) まとまったコードを、何にでも手が届く場所で実行する(ループも分岐も、複数の命令も一度に)。err(実行できたら nil)に続けて、コードが返した値をそのまま返す。run_scoped の囲いなし版なので、自分で書いていないコードを渡さないこと
shikisha.list() 呼んだ相手が実行してよい命令の名前を返す(9章の「自動化の権限」)。表そのものを読むので、古くなりようがない
shikisha.record(文字列) / shikisha.record_reset() 貼り直せる形で実行の記録を残す
shikisha.take_replay() 再生用の記録を取り出す(前回取り出して以降の全操作を、壊れにくい書き方で)
shikisha.set_result(コード, "理由") この実行の判定。data/last-result.json に書かれ、画面にも出ます
shikisha.skip("理由") ここで実行を終え、そう書き残す(そのタブの画面とログに1行)。「今回は動かさない」と決めたときに使います

どのリポジトリかは、そこに居るタブで指します。タブ を省くと呼んだ側のタブです。 パスは受け取りません。

読む側(shikisha.git_status など)は git を起動します。サイドバーのブランチ表示とは別の 経路で、あちらは git を起動しないので rebase 中でも答えます。

命令 説明
shikisha.git_status(タブ) 変わったファイルの一覧。1件ずつ {path, index, work, staged, unstaged, conflict, from}indexwork は git が出す2文字(ステージ側と作業ツリー側)そのまま。stagedunstaged は反対語ではない(hunk を1つだけステージすると両方 true)
shikisha.git_diff(タブ, {path=…, staged=…, encoding=…}) 差分をそのまま文字列で。staged=true でステージ済みの側、path で1ファイルに絞る。ファイルごとに保存されている文字コード(UTF-8・Shift_JIS・EUC-JP など)で読む。encoding で指定もできる
shikisha.git_log(タブ, 件数) 最近のコミット。{hash, short, author, date, subject}。既定20件
shikisha.git_conflicts(タブ) 衝突しているファイルのパスだけ
shikisha.git_branch(タブ) 今のブランチ {name, protected, upstream, ahead, behind, base, base_behind, catch_up, catching_up}protected は「このフォルダが守っているので直接コミットしない方がよい」の印。upstream は追いかけているブランチ(origin/main)、ahead はここにあって向こうにまだ無いコミットの数、behind はその逆で、どちらも最後にフェッチした時点の数。何も追いかけていなければ3つとも無い。base は書き留めてある起点、base_behind は最後にフェッチした時点で起点より遅れているコミット数、catch_up はその最新を取り込むときに実行するコマンド、catching_up はその起点のマージが途中で止まっているときの起点名。detached なら nil
shikisha.git_graph(タブ, {all=…, remotes=…, count=…}) 履歴。{graph, hash, short, author, date, subject}graph は git が描いた枝の絵で、コミットの無い行(マージの合流)もそのまま入る
shikisha.git_detail(タブ, ハッシュ) そのコミットの全部。{hash, parents, author, author_date, committer, commit_date, subject, body, files}
shikisha.git_branches(タブ) ブランチの一覧。{name, current, protected}
shikisha.git_checkout(タブ, "名前") そのブランチへ移る
shikisha.git_merge(タブ, "名前") そのブランチを取り込む。衝突したら止まり、git_conflicts に出る
shikisha.git_catch_up(タブ, "origin/main") 起点の最新を取り込む。その枝だけをサーバからフェッチし、フェッチしたものをマージする(手元の起点は使わない)。追跡中のファイルに未コミットの変更があれば断る。衝突したらマージの途中で止まり、ファイルは git_conflicts に出る。{taken}(取り込んだコミット数。新しいものが無ければ 0)を返す。3つ目の引数にプッシュ済みの枝の名前を渡すと(git_catch_up(タブ, "origin/main", "feature"))、それを先にフェッチし、フォルダがそれより遅れていればマージする前に断る。プルリクエストの衝突を解くときの手順
shikisha.git_set_base(タブ, "origin/develop") 手前の枝の起点を書き留める。最新を取り込むときの取り込み元になる。アプリで作ったワークツリーには最初から書かれている
shikisha.git_remote_branches(タブ) サーバにある枝(最後にフェッチした時点)。{name, catch_up} で、catch_up はその枝を起点にしたとき git_catch_up が実行するコマンド
shikisha.git_fetch(タブ) / shikisha.git_pull(タブ) / shikisha.git_push(タブ) サーバと話す。返るまで他のことは止まります(最大3分)。押しっぱなしにできる画面が要るなら、待ちは呼ぶ側で組むこと。git_push は一度も送っていないブランチなら upstream を付けて送り直し、その旨を返す。そのタブに選ばれた git アカウント(下記)でサインインし、選ばれていなければ動かない
shikisha.git_hunks(タブ, {path=…, staged=…, commit=…, encoding=…}) 差分をまとまり(hunk)に切って返す。{file, header, start, end, patch, encoding, exact}patch はそれ自体が完結したパッチ。encoding はファイルの行を読んだ文字コード。その文字コードで欠けずに読めなかったときは exact が false になり、そのパッチは git_apply が断る
shikisha.git_apply(タブ, パッチ, {cached=…, reverse=…, encoding=…}) パッチを当てる。cached で次のコミット側へ、reverse で逆向き(取り消し)。hunk の encoding を渡すと、ファイル元のバイトのまま戻る(省略時は UTF-8)。hunk 単位のステージはこの2つの組み合わせ
shikisha.git_stage(タブ, パス) 次のコミットに入れる。パスは文字列1つでも、テーブルで複数でも
shikisha.git_unstage(タブ, パス) 次のコミットから外す
shikisha.git_branch_create(タブ, "名前") ブランチを作って、そこへ移る。ステージしたものは持ったまま移るので、共有ブランチで断られたときの行き先になる
shikisha.git_commit(タブ, "メッセージ", opts) 入れたものをコミットし、短いハッシュを返す。保護ブランチでは止まる(新しいブランチを作るか、承知のうえなら {allow_protected=true})。どのブランチを守るかは 設定 → 保護ブランチ(既定は main / master)で決まり、作業フォルダごとに変えられる
shikisha.git_run(タブ, "引数…") 任意の git を実行して、その出力を返す。シェルは通りません;&& は git の引数になって断られる)。選ばれた git アカウントでサインインし、選ばれていなければ資格情報なしで動く

サインインするアカウント。 git タブは、そのタブのページで選んだアカウントを使います。ほかのタブは、作業フォルダのプロジェクトが選んだアカウント(プロジェクトのページ、または git 欄の上のメニュー)を使います。アカウント自体はデスクごとに登録します(デスクの設定 → git アカウント)。HTTPS のトークンか SSH の鍵ファイルと、コミットに付く名前とメールを持ち、名前とメールは git_commitgit_merge でも使います。「この PC の git の設定を使う」もほかと同じ選択肢の一つで、git 自身の資格情報ヘルパーと鍵を使います。ターミナルのタブで打った git も、同じアカウントでサインインします。タブを起動するときにその設定(GIT_CONFIG_* と、そのアカウントのサーバーに対する資格情報ヘルパー)を渡すので、そのタブで動くプログラム(AI を含む)も同じアカウントとして git を実行します。取り上げるものはなく、ほかのサーバーのリポジトリは、この PC がこれまでどおりの方法でサインインします。設定の変更は、そのタブを次に開いたときから効きます。

既定では、どれも人間だけが実行できます(自動化の権限)。AI に開けるなら git_status / git_diff / git_log から。git_run を開けることは「git の全権を渡す」と 同じ意味です。

タブが作業しているリポジトリの Issue とプルリクエストを扱います。サインインには、そのタブのプロジェクトが選んだ git アカウント(git タブならタブ自身の選択)を使います。どれも、サーバと話す git の命令と同じく GitHub の返事を待ちます。返るのはテーブルで、一覧の「Issue」タブが表示しているのと同じ答えです。

命令 すること
shikisha.github_issues(タブ, {state=…, mine=…, text=…, page=…}) Issue の一覧を1ページ分: {repo, total, page, per_page, items}。各項目は {kind, number, title, state, reason, author, labels, assignees, comments, updated, url, workspace}stateopen(既定)・closedallmine はそのアカウントが担当の Issue、text は GitHub の検索語(label:bug など)
shikisha.github_prs(タブ, {state=…, mine=…, review=…, text=…, page=…}) プルリクエストの一覧。statemerged も使え、mine はそのアカウントが作ったもの、review はそのアカウントのレビュー待ち。項目に draft も付く
shikisha.github_issue(タブ, 番号) Issue を1件、全部: 上の項目に加えて bodycreatedcomments{author, bot, body, created, url})・events{kind, actor, subject, reason, created}
shikisha.github_pr(タブ, 番号) プルリクエストを1件、全部: 上に加えて headbaseforkmergedmergeablemerge_stateadditionsdeletionschanged_filesreviewersreviewapproved / changes_requested / 空)・checks{failed, pending, passed, total, items}
shikisha.github_labels(タブ) / shikisha.github_assignees(タブ) Issue に付けられるラベルと、担当にできる人のログイン名
shikisha.github_issue_create(タブ, {title=…, body=…, labels=…, assignees=…}) Issue を作る。{number, url} を返す
shikisha.github_pr_create(タブ, {title=…, body=…, head=…, base=…, draft=…}) ブランチ head から base へのプルリクエストを作る。draft = true なら下書き。{number, url} を返す
shikisha.github_comment(タブ, 番号, "本文") Issue やプルリクエストにコメントする。{id, url} を返す
shikisha.github_issue_state(タブ, 番号, 状態, {duplicate_of=…}) opencompletednot_plannedduplicateduplicate_of と一緒に。「Duplicate of #番号」のコメントも付く)
shikisha.github_pr_state(タブ, 番号, "open" か "closed") プルリクエストをマージせずに閉じる、または開き直す
shikisha.github_pr_merge(タブ, 番号, 方法) マージする: squash(既定)・mergerebase。ブランチはそのまま残す

「窓口」を登録しない限り使えません。6章を参照してください。

命令 説明
shikisha.read_file(名前, 相対パス) / shikisha.write_file(名前, 相対パス, データ) 登録済みのファイル窓口を通して
shikisha.list_files(名前, 相対パス) そのフォルダの中身を1段ぶん。1件ずつ {name, dir, size, modified}、フォルダが先で次に名前順。sftp_ls と同じ形なので、片側で書いた歩き方がもう片側でもそのまま読めます
shikisha.http(名前, 本文) 登録済みのHTTP窓口を通して
shikisha.read_path(パス) / shikisha.write_path(パス, データ) / shikisha.list_path(パス) / shikisha.http_raw(url, 本文) 生のパス・生のURL。allow_dirs / allow_hosts が空のあいだは必ず失敗します