minicode

Internal & Arsitektur (untuk kontributor)

Terminal Contract (FROZEN) — 35 invariant, v7.4

Perubahan v7.4 (P12 S2.2 WorkMode): invarian baru I35 — WorkMode (lapis intent DISCUSS/PLAN/EXECUTE) sebagai dimensi TERPISAH dari permission

mode. Command /work-mode <DISCUSS|PLAN|EXECUTE> (aksi operator eksplisit, transisi berlaku pada sesi yang sedang berjalan); footer menampilkan <WorkMode>·<permission mode> saat WorkMode ≠ EXECUTE (PLAN·auto, DISCUSS·auto, PLAN·readonly); EXECUTE (default) mempertahankan label permission mode seperti sebelumnya. Tab/Shift+Tab dan /mode TETAP khusus permission mode. Rincian di I35.

Perubahan v7.3 (P11 Presentation & UX): invarian baru I34 — subcommand exec dan daemon menerima argv SCOPE dari router (flag global dikenal yang mendahului token subcommand tetap berlaku; token subcommand tidak ikut ke handler). Kalimat lama I28 "Flag exec diparse dari argv SESUDAH token subcommand" sudah tidak lengkap sejak P11.2 menerima bentuk flag-mendahului, diganti rujukan ke I34. Registrasi peta proteksi: test/cli-subcommands.test.ts sudah ada SEBELUM P11.3 — yang ditambahkan hanya describe cli: daemon scoped argv (P11.3) di dalamnya, BUKAN file baru; test/exec-json-envelope.test.ts + test/cli-session.test.ts melindungi kontrak exec scoping (P11.2) dan verification summary (P11.1). Nol perubahan perilaku runtime — dokumen mengejar kontrak yang sudah diterima + diuji.

Perubahan v7.2 (2026-09-25): mouse tracking ?1002h + ?1006h, drag selection app-level, Ctrl+C copy selection, wheel tetap scroll, popup turun ke ?1000h, dan MINICODE_MOUSE_SELECTION=0 sebagai kill switch.

Perubahan v7.1 (2026-09-25): mouse wheel X10/SGR untuk scroll transkrip; klik mouse tetap diabaikan; mouse tracking dibatasi pada lifecycle TUI; /copy [n] menyalin 1–10 turn terakhir; abort turn TUI memakai Esc.

Perubahan v7 (temuan audit audit-tui): penanganan sinyal fatal SIGTERM/SIGHUP (I31), navigasi transkrip Home/End + Shift+PgUp/PgDn (I14), hint abort sekali saat busy (I14), idle guard form dua-tahap (I21), spark statis via MINICODE_MOTION=0 (I13), parkir kursor di dalam blok sync-update (I25).

Satu-satunya tampilan interaktif adalah TUI fullscreen: transkrip ala shell + status bar + popup komposit. REPL linier, footer lengket (chrome), overlay inline, dan jendela info DIHAPUS — tak ada mode, tak ada flag (--tui usang = no-op). Popup = satu kotak: daftar DAN input teks/pilihan terjadi di dalamnya (form); tak ada ketikan di luar kotak. Bahasa UI dwibahasa (en/id) via i18n nol-dependensi. Perubahan perilaku apa pun di bawah wajib: update dokumen ini + test yang menjaganya (lihat "Peta proteksi").

#Sifat produk

  • Interaktif (TTY mampu): aplikasi fullscreen alternate-screen dari start sampai quit. Area transkrip bergaya shell (prompt minicode ›, jawaban model, ledger ›), status bar bawah, popup kecil di atas transkrip yang tetap terlihat (redup) di belakang. Keluar = buffer utama kembali persis (transkrip sesi tidak di-dump ke scrollback).
  • Non-interaktif (one-shot prompt, exec, pipe/redirect/CI): cetak polos, tak pernah membuka TUI.
  • Tanpa panel/sidebar; mouse app-level untuk wheel scroll dan drag-selection transkrip (bukan native terminal selection); tanpa animasi dekoratif (pulse spark status bar dapat dimatikan MINICODE_MOTION=0 — I13).

#Contract stdout/stderr

StreamIsiCatatan
stdoutOutput PROGRAM yang bermakna: teks model, receipt perubahan (› write_file …), daftar/artefak perintahHarus bersih dari cursor-control pada non-TTY
stderrHuman-facing progress/diagnostic: ledger tool (› … hijau/merah), reasoning (verbose), warning, errorBoleh transient rendering bila stderr TTY
keduanyaWarna hanya bila stream TTY (stdout.isTTY); NO_COLOR menang; TERM/COLORTERM env TIDAK menyalakan warna pada pipe/redirectLihat src/ui/render/theme.ts colorLevel()

Non-TTY (pipe/redirect/CI/file): 0 cursor control, 0 alternate screen, 0 animasi spinner, 0 ANSI yang tidak diperlukan, output deterministic.

#Klasifikasi writer (wajib dipatuhi)

  1. permanent user-visible output — teks model, receipt, hasil perintah → stdout (idle non-interaktif).
  2. frame TUI — SATU-SATUNYA penulis layar saat interaktif adalah App (src/ui/tui/app.ts): transkrip viewport + prompt + status bar + komposit popup, satu repaint sinkron per event. Modul lain DILARANG menulis process.stdout langsung saat App aktif (popup melukis regionnya via paintRegion, bukan frame penuh).
  3. diagnostic/logging — [warn], [router], [mcp], [vector], dst. dari layer non-UI → boleh process.stderr.write mentah.
  4. machine-readable output — mis. --json, protokol MCP server → stdout saja, tanpa apa pun yang lain.
  5. debug-only output — guard verbose/env; tidak pernah default.

#Ownership

  • App pemilik tunggal layar (alt-screen enter saat start, exit saat quit — selalu berpasangan, termasuk exception/SIGINT via handler process.on("exit") sinkron best-effort). Turn berjalan = indikator spark pada status bar.
  • Sinyal fatal (I31): saat sesi TUI hidup, App.run() memasang process.on("SIGTERM")/process.on("SIGHUP") (handler terpisah karena Bun tak menyampaikan nama sinyal — satu handler bersama tak bisa membedakan) → restore terminal (alt-screen exit + raw mode pulih) lalu process.exit(128+n). Pasangan ketat: keduanya dilepas di cleanup run() — tanpa sesi TUI, sinyal TIDAK di-intercept (perilaku default proses tetap). Tanpa ini SIGTERM meninggalkan terminal di alt-screen + raw mode (user harus reset).
  • App me-repaint live mengikuti event bus (stream teks, ledger, thinking; coalesce 30ms) — TANPA ini layar buta selama turn. ?1002h + ?1006h mengaktifkan tracking drag; ?1000h + ?1006h saat popup; ?1000l/?1002l/?1006l pada quit/child/sinyal. Suspend menahan repaint (popup melukis sendiri); quit menahan semua.
  • Popup komposit (/model, /provider, /sessions, form, approval): controller/view memanggil app.suspend() (lepas listener stdin + redupkan layar), view melukis HANYA region kotak via paintRegion (tanpa clear — transkrip di belakang tetap tampil), app.resume() repaint penuh saat tutup. Tanpa suspend: byte masuk ke dua tempat (App + popup) dan frame App menimpa popup — dilarang.
  • Saat turn berjalan SEMUA input dibekukan kecuali abort (Esc) dan scroll (mouse wheel, PgUp/PgDn). Enter saat busy = sunyi (bukan antre, bukan petunjuk). Abort pertama menampilkan hint "tekan Esc lagi untuk keluar" di slot indikator SEKALI (anti keluar sesi tak sengaja saat double-tap "memastikan" — I14); abort kedua dalam 1,5 dtk = quit.
  • Checkpoint = milik lifecycle sesi. Bus kernel memanggil handler turn:completed secara sinkron dan tak menunggu promise-nya, jadi operasi tulis checkpoint (async) harus DIMILIKI: operasi yang dimulai dicatat, kegagalannya ditangani saat lahir (bukan unhandled rejection, bukan sukses palsu) dan dilaporkan lewat diagnostic [warn] checkpoint failed: … ke stderr. close() = BARRIER: setelah ia kembali, tak ada pekerjaan checkpoint milik sesi itu yang masih berjalan (manifest sudah tertulis atau kegagalannya sudah tercatat).
  • screen.ts = pemilik ?1049h/?1049l + paintRegion (cursor-addressed, tanpa clear) + clearRegion. Refcount nested, idempoten, null-screen fail-closed di non-TTY/TERM=dumb. Siklus hidup region: render yang menyusut/bergeser membersihkan union lama+baru (tanpa baris hantu); tutup menghapus region sendiri sebelum pemilik repaint.
  • Isi popup: kotak terpusat (dialog.ts: border, judul opsional dalam garis atas, TANPA bayangan) di atas konten redup; dialogBox untuk komposit, dialogFrame (backdrop ░ faint) hanya untuk layar yang memiliki buffer sendiri.
  • Lebar kotak popup TETAP per permukaan (model 64, picker 64, provider 76, form 64 — min=max): geometri stabil, tak bernapas saat filter/error. Nama/ID di DEPAN label (truncasi memakan ekor — tanggal/konteks boleh hilang, id selamat).
  • Form dalam popup (form.ts): field text/secret(mask •)/select/confirm, Tab/↑↓ pindah, ←/→ pilih atau geser kursor, Enter validasi+submit, Esc dua-tahap (tekan-1 bersihkan field aktif, tekan-2 batal — isi tidak pernah hilang sekali tekan); error validasi merah dalam box; kursor terminal diparkir di field aktif. Add/edit provider, delete confirm, dan wizard input WAJIB lewat ini (bukan askLine liar di luar kotak).
  • Dropdown / tetap inline menempel prompt (harus mengikuti kursor ketik): berbingkai, terhapus di SEMUA jalur keluar.
  • Kursor diparkir di SEMUA permukaan (picker/manager/form/App): baris item/field aktif — bukan ditinggal di posisi paint terakhir.
  • i18n dwibahasa (src/ui/i18n/): kamus statis en.ts/id.ts (id WAJIB kunci = en — tsc mengawal), t(key, params) interpolasi {nama}, fallback kunci hilang → en. Prioritas: /lang (sesi) > MINICODE_LANG > state.json lang > locale OS (LANG/LC_ALL awalan id) > en. Semua string user-visible TUI lewat t() — literal Indonesia hardcode di luar dict dilarang (test anti-regresi). Getter runtime, bukan const beku.
  • Keybinding konsisten (kontrak I25): Esc dua-tahap di semua input teks; backspace/delete selalu edit teks (aksi hapus-data via d/ Del + confirm dalam popup, tercatat); navigasi daftar = ↑↓ + `pgup/ pgdn + home/end` di semua popup ber-daftar; hint footer tiap permukaan hanya tombol yang benar-benar berlaku di sana.
  • Approval tool & ask_user: blok pertanyaan + keputusan DICATAT di transkrip (jejak audit) dan layar di-repaint sebelum menjawab; input via askLine di baris kursor (aman: input App beku saat busy; Esc/Ctrl+C = abort + deny).
  • Layar tak mampu (non-TTY, rows < 10, TERM=dumb): view menolak BERSUARA + batal (fail-closed). TAK BOLEH diam.
  • /status & /history = readout ke transkrip (bukan popup); /help ringkas (≤6 baris, penuh via /help tombol); /expand membuka buffer isi tool; pilihan /model persist antar sesi + struk model: x di transkrip.

#Invariant (nomor dipakai di peta proteksi)

  1. Satu tampilan interaktif: TUI fullscreen; frame penuh tiap repaint (tepat rows baris); scrollback utama tak tersentuh selama sesi.
  2. stdout/stderr contract seperti tabel di atas (TTY & non-TTY deterministic).
  3. Satu kepemilikan layar (App); popup satu lifecycle (suspend/resume + paintRegion).
  4. Permanent output tidak bergantung pada painter TUI.
  5. Tidak ada popup yatim / input bocor setelah success / failure / cancellation / SIGINT / session end. Tidak ada modal yatim.
  6. Non-TTY: 0 cursor control / alternate screen / spinner animation / ANSI tak diperlukan.
  7. Tool activity default compact; ledger › nama target satu baris.
  8. Streaming tanpa duplicate output / overlap / stale status.
  9. Foreign stderr writers boleh mentah di layer non-UI.
  10. (Dilebur ke I3 — nomor dipertahankan agar referensi lama tak patah.)
  11. Resize memakai lebar/tinggi SAAT PAINT (bukan saat event) — termasuk geometri popup, dialog, dan viewport TUI.
  12. Long session tetap readable (cap 5000 baris, tertua dibuang). Proyeksi presentasi mempertahankan cap yang sama dan menambahkan satu marker … N baris awal di luar viewport — riwayat penuh di model yang melacak jumlah baris yang di-evict; total() tetap monotonik. Buffer jawaban live dibatasi 1.000.000 karakter dan menampilkan marker truncasi; satu layout paint tidak boleh memproyeksikan transcript dua kali.
  1. Status bar: 1 baris dasar (✦ 00.00.00 mode model cwd) dari sumber yang sama dengan angka sesi. ✦ tetap menjadi indikator proses hidup; timer HH.MM.SS berada tepat di sebelahnya. Idle=redup; saat aktif hanya detik putih, menit ikut terang setelah 60 dtk, dan jam setelah 1 jam. Reasoning memakai dots ... → .. → . → .. → ..., sedangkan teks Working/Thinking/Running tidak tampil. Bullet separator dihapus; field dipisahkan oleh spasi dan warna. Prompt minicode › tampil saat boot/idle, hilang saat busy, dan kembali otomatis setelah turn selesai; aksi edit/histori tetap dapat membuka composer. MINICODE_MOTION=0 membuat dots dan sparkle statis tetapi tidak menghilangkan feedback; satu baris kosong memisahkan composer dari footer.
  2. Binding TUI: mouse wheel (X10/SGR) scroll transkrip; drag kiri app-level membuat selection transkrip dan Ctrl+C menyalinnya; PgUp/PgDn scroll; **Shift+PgUp/PgDn (ESC[5;2~/6;2, modifier 2) = setengah halaman; Home/End saat prompt KOSONG = lompat baris-teratas/ekor transkrip (baris berisi = editing awal/akhir baris; dropdown terbuka = jalur engine)**; Up/Down histori; Tab/Shift+Tab mode; Ctrl+O/T compact/thinking; Ctrl+D baris-kosong keluar; Esc membersihkan selection lebih dulu, lalu batal input / abort turn (busy: SEMUA input dibekukan kecuali abort, scroll, dan selection — Enter pun sunyi; berlaku juga saat layar MENCIUT: abort+scroll selalu lolos agar turn bisa dibatalkan tanpa kill -9; abort pertama menampilkan status Stopping + hint keluar sekali, abort kedua dalam 1,5 detik menutup sesi). Ini adalah selection buffer TUI, bukan native terminal selection. Idle menampilkan timer redup di footer dan prompt minicode ›; prompt hilang saat busy, lalu otomatis muncul kembali kosong setelah turn selesai. Scroll ke atas + stream masuk = indikator ↓ N baris baru.
  3. (Dihapus bersama jendela info — nomor dipertahankan.)
  4. Popup komposit: region tanpa clear + union-clear anti-hantu + clearRegion saat tutup + suspend/resume berpasangan; layar tak mampu = tolak bersuara + batal.
  5. Transkrip append-only di memori (cap 5000), viewport ikut ekor otomatis; ketikan baru kembali ke ekor; stream turn TIDAK merampas posisi baca; repaint live coalesce 30ms dan clock activity 200ms (layar tak buta saat turn). Composer memakai tinggi tetap: prompt saat editing, dots saat reasoning, atau tool target; satu baris kosong selalu memisahkannya dari footer. Proyeksi presentasi: baris running di-pin dari snapshot, elapsed tetap pada ledger tool, status tool final memakai glyph + kata (completed/failed/denied/cancelled/interrupted), retry dan grup anak mengikuti model, serta ringkasan turn masuk sebagai system entry.
  1. Prompt multiline (Ctrl+J newline), histori memori-sesi (tak persist ke berkas histori lama). /copy [n] menyalin 1–10 turn terakhir (default 1, oldest-to-newest) melalui OSC 52.
  2. Dropdown / inline berbingkai; Enter pada menu melengkapi + kirim.
  3. Lebar kotak popup TETAP per permukaan (model 64, picker 64, provider 76, form 64 — min=max); nama/ID di depan label; seleksi murni warna; search selalu tampil.
  4. Form dalam popup: semua input teks/pilihan di dalam kotak (text/secret/ select/confirm + validasi inline + kursor diparkir); tak ada ketikan di luar kotak. Esc dua-tahap: tekan-1 KEMBALIKAN nilai bawaan field (prefill/edit tak hilang sekali tekan), tekan-2 batal. Idle guard dua-tahap (temuan audit TUI-004): 80 dtk tanpa input = peringatan statis di footer form ("batal dalam 10 dtk" — bukan countdown hidup), 10 dtk berikutnya = auto-batal (cancel), total semantik 90 dtk lama; SATU ketikan apa pun me-reset kedua timer.
  5. Thinking terlihat: composer memakai dots clock; transkrip minimized = bersih tanpa marker (… thinking), expanded = alir redup; isi tetap tersedia di /expand; fase berakhir = commit (tak ada thinking yatim).
  6. Approval & ask_user tercatat di transkrip (pertanyaan + keputusan) dan terlihat sebelum menjawab; non-TTY = deny/null fail-closed; stdout di-pipe tanpa sink TUI = deny/null (prompt tak terlihat + mencemari output program bila dipaksa).
  7. /expand query Content Store (buka-ulang identik, tidak sekali-habis); dengan id membaca satu tool, tanpa id membaca seluruh entry store yang masih hidup; miss → durable sqlite → penanda retensi. Pilihan /model persist antar sesi + struk model: x; /help ringkas (penuh via /help tombol).
  8. Keybinding konsisten: Esc dua-tahap (isi tak hilang sekali tekan); backspace/delete edit-teks (hapus-data = confirm); navigasi daftar (↑↓, pgup/pgdn, home/end) di semua popup; hint footer akurat per permukaan; kursor diparkir di semua permukaan.
  9. i18n dwibahasa: semua string user-visible lewat t(); id kunci = en (tsc mengawal); fallback kunci hilang → en; prioritas sesi > env > state.json > locale OS > en (di OS: LC_ALL > LANG, string kosong diabaikan); literal Indonesia hardcode dilarang.
  10. Sinyal fatal saat TUI hidup (SIGTERM/SIGHUP): restore terminal dulu (alt-screen exit + raw mode pulih + handler exit sinkron) lalu exit 128+n (143/129). Pasangan ketat per-run — handler dipasang di run() dan dilepas di cleanup; tanpa sesi TUI, sinyal tak disentuh. (Temuan audit TUI-001: handler exit Node tak jalan pada SIGTERM default — terminal tertinggal alt-screen + raw.)
  11. Proyeksi Presentasi V2.1 TUI memakai snapshot struktural dari composition root, bukan parser string. view, cap, scroll, approval, transient, resize, cursor, dan PTY path tetap memakai kontrak lama.
  12. Proyeksi linear dan ACP memakai snapshot/event presentasi yang sama: linear menambahkan status/duration/receipt/retry tanpa mengubah stdout/stderr split, sedangkan ACP mengirim lifecycle turn.*, tool.*, dan approval.* terstruktur plus teks delta. Exit code, pipe safety, dan determinisme non-TTY tetap sama.
  1. Painter transient stderr (spinner setup/cek-update, garis status turn) DITAHAN selama layar interaktif memegang terminal: beginInteractiveScreen (src/ui/runtime/statusline.ts) membisukan paintWrite (nol byte + baris painter dibersihkan sekali saat layar mengambil alih), nested dihitung, release idempoten. Pemegang: askLine/askSecret/runPicker (raw-mode) dan openAltScreen (alt-screen, release di close()). Tanpa ini tick \r\x1b[2K spinner menghapus baris prompt wizard (laporan "macet di Menyiapkan sesi…").
  2. Kontrak exit code: 0 sukses/help, 1 gagal runtime/lookup, 2 salah pakai (argumen hilang/malformed, subcommand asing). Flag sebagai positional id = salah pakai (exit 2). exec --json yang gagal di fase setup (tanpa provider) TETAP memancarkan satu baris `{"type":"summary", "ok":false,…}` (di-scrub) di stdout — pipeline CI tak pernah menerima stream kosong; jalur manusia (stderr + exit 1) tidak berubah. Flag handler exec/daemon dibaca dari argv scope — lihat I34.
  3. minicode acp = server JSON-RPC stdio satu-JSON-per-baris untuk IDE (subset: initialize/run/cancel/shutdown; BUKAN klaim ACP penuh): stdout murni mesin (semua di-scrub), diagnostik manusia ke stderr, approval deny-headless, satu run dalam satu waktu, shutdown/EOF = exit 0.
  4. askLine (input inline: dropdown /, prompt wizard/ask_user) menginvalidasi jangkar relatifnya saat geometri terminal berubah: render pertama sesudah columns/rows berbeda dari geometri render TERAKHIR me-reset prevRows/prevInputRows/prevCursorRow lalu menggambar dari kursor saat ini (lastGeoCols/Rows di src/ui/input/input.ts). Jangkar basi pasca-reflow mendarat di baris salah — menimpa area output dan menghapus baris yang salah (temuan snap kiri/kanan). SENGAJA tanpa listener resize baru (frame basi cukup ditimpa render berikutnya), dan printedW tidak ikut di-reset — nilainya konservatif; dikosongkan justru membiarkan ekor lama.
  5. Routing argv-scope (P11.2/P11.3): handler exec dan daemon menerima argv SCOPE dari router — flag global dikenal yang mendahului token subcommand tetap berlaku, token subcommand dibuang, urutan token sisanya utuh. Ditunjukkan tes: --json exec "t" identik dengan exec --json "t" (prompt model = teks user persis, tanpa token routing maupun nilai flag); --json daemon status mencapai aksi status alih-alih error subcommand asing; aksi/opsi yang tidak dikenal TETAP salah pakai sesuai I28; posisi argumen pertama (prompt/aksi) tidak bergeser. Hanya kedua handler itu menerima scope — subcommand lain menerima args mentah. Invarian ini tidak mengubah semantik, aksi, atau exit code masing-masing aksi; yang berubah hanya invokasi flag-mendahului yang dulu salah rute menjadi rute benar.
  6. WorkMode = lapis intent di ATAS permission mode (P12 S2.2) — dua dimensi yang BERJALAN BERSAMA, bukan pengganti: Nilai: DISCUSS (read-only penuh) · PLAN (ceiling rencana) · EXECUTE (tanpa ceiling tambahan). Default EXECUTE = perilaku lama (kompatibel: sesi tanpa WorkMode yang eksplisit tetap EXECUTE). Gerbang penolakan: permission handler tetap satu-satunya yang menentukan keamanan eksekusi. WorkMode hanya MEMASANG CEILING di atasnya; ia tidak pernah memperluas kewenangan permission mode dasar (INV-WM1). Command /work-mode <DISCUSS|PLAN|EXECUTE>: aksi operator EKSPLISIT. Tanpa argumen → tampilkan mode aktif + usage; nilai invalid → pesan kesalahan dan mode AKTIF TIDAK berubah; sesi tanpa kontrol WorkMode (handler izin terinjeksi = policy, bukan mode) → ditolak, bukan diam-diam tampak berubah. Transisi berlaku pada SESI YANG SEDANG BERJALAN: ceiling permission dievaluasi per pemanggilan tool, daftar tool yang dilihat model ikut turun/naik, hooks dievaluasi terhadap WorkMode aktif, dan delegasi berikutnya mewarisi WorkMode parent yang aktif. WorkMode TIDAK bisa dinaikkan otomatis oleh model, anak, atau argumen delegasi. Pemisahan dari permission mode: Tab/Shift+Tab dan /mode TETAP khusus permission mode — WorkMode tak masuk siklus Tab. Permission mode yang memutuskan allow/deny per tool; WorkMode memilih postur (diskusi/rencana/bebas) dan hanya mempersempit. Hooks: gate shouldRunHooks(permissionMode, workMode) dievaluasi per pemanggilan dengan WorkMode AKTIF (transisi /work-mode langsung berlaku); sisi permission mode memakai nilai startup sesi (semantik lama dipertahankan — Tab/Shift+Tab menggeser mode izin di dalam handler, bukan nilai gate hooks). WorkMode DISCUSS tetap menahan hook walau permission mode auto. Footer: mode WorkMode aktif tampil sebagai <WorkMode>·<permission mode> hanya saat WorkMode ≠ EXECUTE (mis. PLAN·auto, DISCUSS·auto, PLAN·readonly); saat EXECUTE label permission mode apa adanya (mis. auto) — perilaku lama dipertahankan.

#Grammar (ringkas)

prompt minicode › tampil saat boot/idle dan hilang saat busy (dropdown / berbingkai di atasnya) · status bar ✦ 00.00.00 mode model cwd (timer redup idle; detik putih aktif, menit mulai terang di 60 dtk, jam di 1 jam; tanpa bullet separator; mode = permission mode, atau <WorkMode>·<permission mode> bila WorkMode ≠ EXECUTE) + satu spacer sebelum footer · ledger tool › name target (hijau) / merah saat error · error ✗ pesan sekali per kegagalan · pesan error TUI = formatter kanonik (formatError, src/ui/render/errors.ts): credential di-redaksi (bearer/key/query), escape ANSI dibuang, body provider diringkas + dipotong; e.message mentah tak pernah tampil · thinking: dots clock / alir redup · popup: konten redup di belakang + kotak terpusat (form di dalam) + Esc tutup · approval tercatat · /status, /history, /help ringkas & /expand mengalir ke transkrip · /mode = permission mode · /work-mode = WorkMode (intent layer) · Projection V2: › name target … running, ledger status ber-glyph/kata, retry/grup anak, ringkasan turn, dan marker evict sesuai flag ·

Esc saat turn = abort (+ hint sekali); busy = input beku total · Mouse wheel = scroll transkrip, PgUp/PgDn = scroll, Shift+PgUp/PgDn = setengah halaman, Home/End (prompt kosong) = lompat top/ekor · indikator ↓ N baris baru · tab dihitung 8 kolom (batas atas stop terminal — tak pernah undercount) · C1/bidi/tag dibuang dari teks tak-terpercaya (sanitasi) · truncate/chunk hanya menyalin SGR (non-SGR dibuang) · penanda (aktif) di luar budget truncasi (tak termakan URL panjang).

#Residual risk (DISENGAJA — jangan "perbaiki" tanpa keputusan)

  1. Foreign partial stderr write tanpa newline (non-interaktif).
  2. kill -9 di tengah sesi: ketik reset (pola standar). SIGTERM/SIGHUP SUDAH di-handle (I31 — restore + exit 128+n); hanya kill -9/terminal close yang masih menyisakan alt-screen.
  3. Transkrip TUI hilang saat quit (disengaja — TUI fullscreen).
  4. Terminal purba tanpa alternate screen: pesan satu baris + keluar (bukan error).
  5. Approval saat user scroll ke atas: blok tercatat di ekor (tak terlihat sampai PgDn), tapi prompt jawaban + bell selalu terlihat di bawah.
  6. Area yang dibuka kembali saat kotak menyusut = kosong (bukan transkrip di belakangnya) sampai popup tutup + repaint penuh.
  7. Jalur end-to-end mouse SGR bergantung pada TRANSPORT PTY: kalau byte SGR tak sampai ke stdin anak, selection → OSC52 tak bisa diverifikasi lewat byte stream nyata. Pada lingkungan diagnostik Windows (ConPTY + node-pty 1.2.0-beta.15) byte SGR terbukti sampai ke anak yang membaca dalam raw mode — kegagalan lama yang disangka "ConPTY membuang SGR" adalah artefak line buffering probe, bukan keterbatasan transport. Perilaku platform lain belum diuji; capability gate mengukur kemampuannya secara langsung (bukan asumsi platform): di luar CI Linux, SGR yang memang tak diantarkan → SKIP beralasan; di CI Linux jalur SGR end-to-end WAJIB terverifikasi, jadi probe yang verdict-nya bukan "supported" → GAGAL dengan diagnosa (bukan skip senyap yang menghapus coverage).

#Peta proteksi (test → invariant)

Cara membaca peta ini. Setiap baris hanya boleh menyatakan perilaku yang

benar-benar di-assert oleh test yang disebut. Kehadiran nama file test

bukan bukti klaim. Baris yang hanya "nominal" (test ada, tapi assertion-nya

tak menyentuh perilaku yang diklaim) dipindah ke bagian

Gap proteksi di bawah — jangan

dipoles dengan mengganti kata-kata.

  • test/tui-app.test.ts — I1/I13/I14/I17-I20 (boot/exit pairing, transcript viewport, mouse-wheel/drag-selection/PgUp/PgDn scroll + indikator, prompt editing, dots/timer, status bar, Esc-only abort, quit, suspend/resume, busy-freeze, live repaint; I14: Home/End jump + Shift+PgUp/PgDn + hint abort pertama ada di test/tui-audit-fixes.test.ts, bukan di file ini; I17: kunci posisi baca saat stream masuk; basis monotonik ada di test/tui-transcript.test.ts + test/presentation-projections.test.ts; P0: jalur error TUI meredaksi credential — sentinel bearer tak pernah tampil di layar).
  • test/prompt-engine.test.ts — decoder streaming CSI/SGR/X10: wheel menjadi key scroll, klik mouse tidak masuk teks.
  • test/errors-usage.test.ts — format error UI: formatError (kanonik untuk TUI/wizard/one-shot) meredaksi credential (bearer, kunci provider, query, token di batas potong 150 kolom), membuang escape ANSI, memetakan kategori/kind provider, dan menjaga pesan biasa tetap informatif.
  • test/checkpoint-lifecycle.test.ts — checkpoint = milik lifecycle sesi: manifest tersedia saat close() kembali (barrier), tak ada operasi yang tertinggal, kegagalan checkpoint tercatat + dilaporkan tanpa unhandled rejection, dan semua operasi dari beberapa turn selesai saat drain.
  • test/tui-transcript.test.ts — projection source-mapped: wrap, CJK/ANSI, user prefix, tabel TSV, selection text, cap buffer live, dan render markdown inline jawaban model (heading/bold/code; delimiter fence disembunyikan, isi fence dan gema user tetap literal).
  • test/screen-buffer.test.ts — parser frame harness (unit).
  • test/tui-transcript.test.ts — I7/I12/I17/I22 (ledger satu baris per tool, cap 5000 + marker evict, total() monotonik kebal evict, viewport, thinking minimized/expanded).
  • test/presentation-projections.test.ts — I12/I17/I32 (projection snapshot, running/elapsed, status terminal, retry/child grouping, summary, marker evict + total() monotonik, dan parity flag OFF).
  • test/presentation-linear-acp.test.ts — I33 (linear projection status/duration/receipt dan lifecycle ACP terstruktur; parity policy-vs-legacy).
  • test/presentation-policy.test.ts — I32/I33 (seleksi node per mode, envelope machine, digest divergensi; purity tanpa clock/IO).
  • test/writer-inventory.test.ts — OAP-008 (setiap writer langsung terdaftar dengan owner; jumlah tak boleh naik diam-diam).
  • test/exec-json-envelope.test.ts + test/cli-session.test.ts (envelope machine) — I28/I33/I34 (summary berversi, tanpa ANSI/secret, tanpa raw kernel; P11.2 scoping prompt: flag-mendahului vs exec-dahulu identik, kata exec milik user tetap utuh).
  • test/tui-popup.test.ts — I16 (komposit di atas transkrip, anti-bocor, anti-hantu).
  • test/form.test.ts — I21 (field, validasi, secret, select, confirm, batal; Esc-1 kembalikan prefill, Esc-2 batal).
  • test/approval-tui.test.ts — I23 (blok + keputusan tercatat, deny; stdout-pipe tanpa sink = deny).
  • test/footer-render.test.ts — I13 (footer minimal tanpa label status).
  • test/work-mode-runtime.test.ts — I35 (WorkMode runtime: ceiling permission dievaluasi per pemanggilan tool; transisi EXECUTE↔DISCUSS↔PLAN pada sesi yang sama; daftar tool yang dilihat kernel ikut berubah; hooks mengikuti WorkMode aktif; delegasi mewarisi WorkMode parent terbaru; argumen delegasi tak bisa menaikkan WorkMode; /work-mode end-to-end lewat TUI termasuk nilai invalid; /mode + Shift+Tab tak menyentuh WorkMode; label footer PLAN·auto/DISCUSS·auto).
  • test/work-mode-contract.test.ts — I35 (ceiling handler WorkMode di atas handler PM: 18 sel matriks, gerbang approval PLAN × auto/ask, describeDenial per pemilik keputusan, always/K-1 tetap utuh, __setMode tak mencabut ceiling).
  • test/work-mode-tool-visibility.test.ts — I35 (visibilitas tool = irisan permission mode ∧ WorkMode; PLAN × readonly tak memperluas readonly; tool MCP ikut dibatasi).
  • test/work-mode-hooks.test.ts — I35 (hooks: DISCUSS menahan, PLAN/ EXECUTE mengikuti permission mode; pemanggil lama tanpa WorkMode kompatibel).
  • test/work-mode-subagent.test.ts — I35 (child mewarisi WorkMode parent; tak ada eskalasi lewat mode child; default tanpa WorkMode = perilaku lama).
  • test/work-mode-cli.test.ts — I35 (--work-mode/env: flag > env > default EXECUTE; nilai invalid → EXECUTE + peringatan; ACP workMode terpisah dari mode).
  • test/tui-app.test.ts, test/tui-popup.test.ts — I13/I17 (composer editing/busy, dots clock, timer, cursor, resize, popup). *Klausul "satu baris kosong memisahkan composer dari footer" TIDAK punya assertion — lihat Gap proteksi.*
  • test/markdown-table.test.ts — I8/I11 (pipe-table parser, streaming parity, ANSI/CJK width, TTY/non-TTY, TUI resize, machine-output policy).
  • test/terminal-contract.test.ts — I2/I3/I7/I8/I9 (split stdout/stderr, kepemilikan layar tunggal, ledger satu baris per tool, streaming tanpa duplikasi, foreign writer boleh mentah).
  • test/transient-arbitration.test.ts — I3/I9/I27 (hold layar interaktif: picker/askLine, nesting, delayMs).
  • test/statusline-bun-guard.test.ts — I4 (painter Bun/Windows: painter dinonaktifkan sendiri, write asli dipulihkan).
  • test/turn-status.test.ts — garis status turn lifecycle deterministik (grace 250 ms, latch anti-strobe, pembersihan saat endTurn).
  • test/tui-format.test.ts — I2/I7/I8 (preview arg sanitasi satu-baris; stdout = output program / stderr = ledger; 20 tool berurutan = 1 baris masing-masing tanpa overlap) + I18 (envelope OSC52 /copy benar saat TTY, ditolak tanpa TTY, dan 1–10 turn terakhir oldest-to-newest).
  • test/theme.test.ts — I6 (strip DCS/APC/PM/SOS + final CSI non-huruf).
  • test/ansi-fragmentation.test.ts — I6/I8 (setiap titik split ≡ string utuh).
  • test/non-tty-output.test.ts — I6.
  • test/thinking-truncation.test.ts — I12 (buffer thinking >200 KB → /expand membawa marker truncasi).
  • test/collapse.test.ts, test/presentation-expand.test.ts, test/presentation-store.test.ts — I24 (semantik /expand pada Content Store: buka-ulang identik, dengan id vs tanpa id, miss → durable sqlite + penanda retensi).
  • test/ui-combined.test.ts — DIHAPUS bersama chrome.
  • test/repl-linear.test.ts, test/footer-chrome.test.ts, test/info-window.test.ts — DIHAPUS bersama REPL/chrome/jendela.
  • test/cli-commands.test.ts — /status cetak, /sessions popup.
  • test/screen.test.ts — I16 (pairing + paintRegion).
  • test/dialog.test.ts — I11/I16/I20 (geometri, backdrop, box).
  • test/tui-overlay.test.ts — I11/I16 (popup region + gate layar).
  • test/ui-boundary.test.ts — I1/I3 (src/ui tak impor keluar).
  • test/i18n.test.ts — I26 (resolusi, fallback, interpolasi, kelengkapan; LC_ALL > LANG, string kosong diabaikan).
  • test/i18n-hardcode.test.ts — I26 (lexer sadar-state: tanpa literal Indonesia di luar kamus; kontrol positif id.ts kena, negatif en.ts bersih).
  • test/tui-lang.test.ts — I26 (locale sesi /lang end-to-end + persisten; nilai tak dikenal ditolak) + I24 (/help satu sumber; /copy 2 menyalin dua turn terakhir).
  • test/cli-help-language.test.ts — I26/I25 (label en, kelengkapan /help).
  • test/tui-geometry.test.ts — I20/I25 (lebar tetap per permukaan, step wizard, form error).
  • test/sanitize.test.ts — I2 (C1/bidi/tag dibuang, ZWJ dipertahankan; NO_COLOR menang di cleanUntrusted).
  • test/width.test.ts — I2/I7 (tab 8 kolom; truncate/chunk hanya-SGR + auto-reset; chunk(0) kosong).
  • test/exit-codes.test.ts — I28 (0/1/2 di semua handler; lookup tetap 1; hermetic subprocess).
  • test/exec-json-envelope.test.ts — I28 (summary ok:false di stdout saat setup gagal; jalur manusia tak berubah).
  • test/cli-subcommands.test.ts — I34 (daemon scoped argv: `--json daemon status mencapai aksi status, bukan exit 2 palsu; bentuk daemon`-dahulu tidak berubah; aksi asing tetap exit 2; HOME hermetik, exit 3 not-alive mengikuti kontrak daemon yang sudah ada — bukan klaim baru I34).
  • test/acp.test.ts — I29 (parser/framing/params + smoke spawn stdio: initialize→shutdown exit 0, tanpa provider).
  • test/input-resize.test.ts — I30 (gagal di kode lama: CUP \x1b[1A ke jangkar basi sesudah resize; multiline, dropdown terbuka, resize ganda, resize cepat ×10 → nilai submit utuh).
  • test/tui-diff.test.ts — I2/I7 (path & isi disanitasi).
  • test/highlight.test.ts — I2/I8 (CR dibuang; escape dibuang di hulu).
  • test/tui-theme-highlight.test.ts — I2 (section satu-baris; markdown buang escape di hulu).
  • test/provider-manager-flows.test.ts — I21 (Ctrl+C dua-tahap revert prefill; penanda aktif anti-truncasi).
  • test/tui-signal.test.ts — I31 (SIGTERM→143, SIGHUP→129: alt-screen exit + raw mode pulih; handler dilepas saat quit — pasangan ketat; tanpa run() sinyal tak di-intercept).
  • test/tui-audit-fixes.test.ts — I14 (Home/End jump top/tail saat prompt kosong + baris berisi tetap editing; Shift+PgUp/PgDn half-page + dekode ESC[5;2~; hint abort pertama), I13 (MINICODE_MOTION=0: spark statis, env dibaca saat dipakai), I25 (kursor CUP parkir di dalam blok ?2026 yang sama), I21 (konstanta idle form 80+10 dtk + teks peringatan).
  • test/pty.test.ts — I13/I17/I31/I28 via BYTE STREAM NYATA (harness PTY test/helpers/pty-harness.ts, CLI di pseudo-terminal sesungguhan + provider fake hermetic): boot → ?1049h + timer idle, turn penuh end-to-end, drag-selection mouse SGR → Ctrl+C → OSC52 (koordinat mouse dihitung dari LAYAR NYATA, bukan hardcoded), resize → repaint, SIGTERM → restore + exit 143 (POSIX). *Klausa "exec --json bersih tanpa ANSI" di test ini berjalan sebagai proses biasa tanpa PTY (lihat komentar di berkasnya) — jadi ia bukan bukti byte stream; bukti envelope ada di test/exec-json-envelope.test.ts.* Platform tak mampu (ConPTY rusak) = SKIP beralasan, bukan hijau palsu. Jalur mouse SGR punya capability gate (ptySgrMouseDelivered()): transport yang TAK mengantarkan byte SGR → SKIP beralasan; probe tak konklusif → SKIP dengan alasan "tak konklusif" (BUKAN bukti SGR tak didukung); di CI Linux verdict WAJIB "supported" — selain itu test SGR GAGAL dengan diagnosa, bukan skip senyap. Parser SGR dan selection SGR tetap terverifikasi terpisah di test/prompt-engine.test.ts dan test/tui-app.test.ts (fake TTY).

#Gap proteksi (terbukti, belum ditutup)

Bagian ini menahan klaim yang benar secara implementasi tetapi belum punya assertion yang menjaganya. Menutup gap = menambah test; mengubah kata-kata di sini TIDAK menutup gap.

KlaimStatus implementasiYang hilang
I14 — "berlaku juga saat layar MENCIUT: abort+scroll selalu lolos"Terimplementasi (src/ui/tui/app.ts: scroll/abort tetap lolos saat tooSmall())Tidak ada assertion. test/tui-app.test.ts ("terminal menciut tengah sesi") hanya asserting bahwa ketikan diabaikan + Ctrl+D keluar.
I13/I17 — "satu baris kosong memisahkan composer dari footer"Terimplementasi (src/ui/tui/app.ts merender satu spacer per frame)Kata "spacer" tidak muncul di berkas test mana pun — tak ada assertion tinggi-geometry maupun rendah.
I20 — lebar kotak popup tetap per permukaanTerimplementasitest/tui-app.test.ts menandai salah satu test-nya sebagai (I20), padahal test itu milik I13 (status bar). Lebar popup diverifikasi di test/tui-geometry.test.ts + test/dialog.test.ts; label di tui-app menyesatkan.
I5 — tak ada popup yatim / input bocor setelah success/failure/cancel/SIGINT/session endTak terverifikasitest/turn-status.test.ts dan test/statusline-bun-guard.test.ts tidak meng-assert apa pun soal popup yatim atau kebocoran input.
I26 pada jalur presentasii18n ada dan dijaga terpisahtest/presentation-projections.test.ts mengunci locale en di setiap test dan tak pernah menguji locale id; klaim i18n dijaga test/i18n.test.ts + test/i18n-hardcode.test.ts.
I2/I6/I28 pada proyeksi linear/ACPAlur linear/ACP adatest/presentation-linear-acp.test.ts tak meng-assert split stdout/stderr, absence cursor-control, maupun exit code secara langsung.