---
name: hanoman
description: >-
  Pakai saat mengerjakan project hanoman: orchestrator + dashboard workflow
  docs-driven untuk nafanesia.id — perencanaan produk, arsitektur (Fastify +
  SQLite/Prisma + node-pty/tmux + git worktree), distribusi paket npm global
  (`hanoman start|doctor|update|migrate-from-postgres`), sesi Claude Code
  interaktif, fase spec/plan/execute, backlog & PRD, terminal realtime, modul
  VPS/sync, auth, keamanan, design system, docs Source of Truth, atau operasi
  agent di dalam repo hanoman.
---

# hanoman

## Ikhtisar

hanoman adalah **orchestrator workflow docs-driven** untuk nafanesia.id: ia menyuruh **Claude Code** membangun project terhadap dokumentasi sebagai kebenaran, lalu memantau semua sesi dalam satu dashboard yang tenang. Manusia menuang ide / menulis brief / memfilekan QA finding → hanoman brainstorm sampai **MVP objective** terkunci → **scaffold** doc index (from-scratch) atau **reverse-engineer** docs dari codebase (existing). Brief & finding menjadi **spec** di backlog; spec di-**plan** lalu di-**execute** oleh Claude Code sebagai **sesi interaktif** di **git worktree terisolasi** per backlog. Pakai skill ini untuk menjaga keputusan produk, arsitektur, sesi, keamanan, dan docs tetap selaras dengan `internal/docs/**`.

## Bacaan Awal

Saat memulai kerja hanoman, baca hanya doc yang dibutuhkan task:

- Index Source of Truth: `internal/docs/README.md`
- Blueprint satu halaman: `internal/docs/entrypoints/blueprint.md`
- Entrypoints: `internal/docs/entrypoints/{brd,prd,frd,rd}.md`
- Product: `internal/docs/product/blueprint.md` · `scope-principles.md` · `onboarding.md`
- Requirements detail: `internal/docs/requirements/{prd,frd,rd}.md`
- Standar acceptance (EARS): `internal/docs/requirements/acceptance-criteria-ears-standard.md`
- Tech stack: `internal/docs/architecture/stack.md`
- Data model (tujuh model): `internal/docs/architecture/data-model.md`
- Kontrak API: `internal/docs/architecture/api-contract.md`
- NFR: `internal/docs/architecture/nfr.md`
- Kontrak agent: `internal/docs/operations/agent-documentation-workflow.md`
- Standar keamanan: `internal/docs/security/security-standard.md`
- Design system (editorial, bone paper, brass accent): `internal/docs/design-system/design-system.md`
- Implementasi frontend: `internal/docs/frontend/frontend-implementation.md`
- Roadmap & GTM: `internal/docs/operations/{roadmap,gtm}.md`
- Deploy: `internal/docs/operations/deploy-vps.md` (single-host VPS, npm + systemd) · `production.md` (prod di samping dev) · `npm-readme.md` (README yang terbit bersama paket npm)
- ADR (nomor unik & imutable): daftar lengkap di `internal/docs/README.md`, narasinya di `internal/docs/adr/README.md`; yang paling sering diacu — 0086 (SQLite satu-satunya provider) & 0087 (distribusi npm global), 0024 (sesi interaktif menggantikan run), 0023 (guardrail SoT dicabut), 0037 (guardrail safety dicabut), 0002 (isolasi worktree), 0015 (satu backlog satu sesi), 0016 (sesi tmux), 0028 (auth sesi opaque), 0011/0018 (docs & coverage live/derived), 0035 (sesi tembus batas fase), 0041 (PRD sebagai dokumen), 0043–0048 (sync/device-token/auto-update).
- Kontrak agent repo: `AGENTS.md` · `CLAUDE.md` (root repo).

## Sub-Skill

Pakai skill lebih sempit saat task cocok:

- `hanoman-devops` (`internal/skills/hanoman-devops/SKILL.md`) — deploy & operasikan aplikasi hanoman di server: instalasi paket npm global + systemd, VPS single-host di belakang reverse proxy TLS, prod di samping dev lewat `HANOMAN_HOME`, migrasi sekali-jalan dari Postgres, `hanoman update` (SPEC-398), rollout sync hub/client (SPEC-213), dan verifikasi/troubleshoot.

## Aturan Produk

- Bentuk produk: **instrument panel yang tenang**. Overview sebagai beranda; tiap area (Projects/PRD/Backlog/Terminal/Docs/VPS/Settings) satu klik dari sidebar; Terminal adalah pusat gravitasi saat sesuatu berjalan.
- **Manusia terakhir yang memutuskan.** Otomasi penuh boleh, tapi selalu bisa diinterupsi/di-steer.
- **Satu workspace dulu** (nafanesia.id). Multi-tenant adalah pasca-MVP.
- Objektif MVP: satu operator menjalankan & memantau Claude Code di banyak project sekaligus, dengan docs sebagai Source of Truth, tanpa kehilangan kendali atas sesi berjalan.
- Empat lakon (temperamen produk): **Anoman Duta** (kepercayaan dibuktikan spec & docs), **Anoman Obong** (sesi menyelesaikan tugas & lapor balik), **Gunung Dronagiri** (ragu → dokumentasikan semuanya), **Chiranjivi** (docs abadi melampaui commit).
- PRD (SPEC-210) duduk di hulu Backlog: brief + brainstorm → dokumen PRD sebelum fitur dipecah ke spec + plan.

## Aturan Arsitektur

- Dashboard: **React + TypeScript + Vite**. Server: **Node.js + TypeScript (Fastify)**. DB: **SQLite via Prisma 6** — satu berkas di `$HANOMAN_HOME` (default `~/.hanoman/hanoman.db`), **tanpa Docker/Postgres/Redis** (SPEC-398/ADR-0086). Lokasi data ditentukan tiga fungsi murni di `runner/src/paths.ts` (`resolveHome`/`resolveDbUrl`/`dbFilePath`), dipakai server **dan** CLI; `DATABASE_URL` non-`file:` **melempar** dan menunjuk `hanoman migrate-from-postgres`.
- **Distribusi = paket npm global** (SPEC-398/ADR-0087): `npm i -g hanoman` → `hanoman`. `hanoman` telanjang = `start` (migrate deploy → **spawn** `node dist/server.js` sebagai proses anak dengan `NODE_ENV=production`); `doctor` melaporkan prasyarat non-npm (node ≥ 20 · git · tmux · `claude`/`codex` · izin tulis home · aset web) dengan exit code; `update [--check]` membandingkan semver vs registry npm lalu menjalankan `npm i -g hanoman@latest`; `migrate-from-postgres` memindahkan instance lama. `resolveLayout()` mengenali **dua** layout (paket npm vs checkout repo), aset dashboard dipilih `pickWebDir()`. Deteksi update tetap **read-only** di server (ADR-0048 utuh) — yang memasang adalah CLI, karena server yang me-`npm i` dirinya sendiri lalu keluar akan memutus sesi tmux. Staging rilis `dist-npm/` dirakit `hanoman __pack` (`pnpm release`); **`npm publish` tindakan manusia**.
- **Update sekali klik, tapi server tetap tak memasang apa pun** (SPEC-405/ADR-0088, mengamandemen
  ADR-0048 & membalik satu alternatif yang ditolak ADR-0087): `POST /api/update/apply` hanya membuat
  proses server **keluar dengan `UPDATE_RESTART_EXIT = 75`**; yang menjalankan `npm i -g hanoman@latest`
  → `prisma generate` → `migrate deploy` → spawn lagi adalah **CLI parent `hanoman start`**, yang sejak
  ADR-0087 memang sudah men-spawn server sebagai proses ANAK. **Supervised-only**: digerbangi
  `process.env.HANOMAN_SUPERVISOR === "1"` yang HANYA disuntik `serverEnv()` di
  `cli/src/commands/start.ts` dan diekspor sebagai `UpdateStatus.canApply` — **dibaca dari
  `process.env` langsung, bukan `effectiveBool()`**, karena helper itu membaca cache config DB lebih
  dulu sehingga siapa pun yang bisa menulis config bisa mengaku disupervisi. Endpoint punya **dua
  langkah**: tanpa `confirm` ia dry-run `409 confirm-required` + jumlah sesi hidup yang dihitung saat
  itu juga (jumlah itu sengaja **tidak** masuk `UpdateStatus` — grup siar `update` di-recompute tiap
  300 tick); sesi hidup **tak memblokir** apa pun di server. Premis "restart memutus sesi tmux"
  **tidak akurat**: `pty.ts` memakai `tmux new-session -d`, tmux daemon terpisah — yang putus hanya
  jembatan `tmux attach` + WebSocket, dan klien sudah reconnect ber-backoff (ADR-0016). **Install
  gagal tak fatal** (respawn versi lama + cetak alasan), **migrasi gagal fatal**, jatah
  `MAX_UPDATE_RESTARTS = 5` dengan alasan dicetak saat habis. **Dua gotcha wajib:** `prisma generate`
  dijalankan **tanpa cek dulu** karena `@prisma/client` sudah ter-cache di proses supervisor sejak
  boot (`ensurePrismaClient` akan menjawab "siap" memakai modul LAMA — kelas jebakan `existsSync` di
  ADR-0087); dan `capabilityForRoute` dulu memetakan prefix status (`update`/`limits`/`events`/`fs`/
  `health`) ke `GLOBAL_READ` **tanpa melihat method**, jadi menambah endpoint tulis di bawahnya
  berarti setiap agent token bisa me-restart instance — kini `GLOBAL_READ` hanya untuk method baca.
- Realtime: **WebSocket hanya untuk terminal PTY**; sisanya **HTTP polling** (projects, backlog, notifications, limits, vps). Jaga UI responsif — log sesi streaming, jangan blok main thread.
- Terminal server: **node-pty + tmux** (socket `-L hanoman`, `remain-on-exit on`); terminal web: **xterm.js** merender TUI Claude Code apa adanya. tmux menahan sesi hidup lintas restart API (ADR-0016).
- **Tidak ada** message queue, Redis, worker terpisah, scheduler cron, maupun webhook GitHub — semua dicabut saat pindah ke sesi interaktif (ADR-0024). Satu-satunya kerja latar = dua `setInterval` di `server.ts` untuk monitor VPS (health 5 mnt, audit 24 jam).
- Server **bind `127.0.0.1:8787`** di belakang reverse proxy TLS; `HOST=0.0.0.0` hanya bila ada TLS di depan.
- `runner/src/*` adalah **library**, bukan proses: `git.ts` (worktree), `prompt.ts` (prompt + `PIPELINES` fase), `reverse-standard.ts`, `settings.ts`. Tak ada lagi invokasi `claude` headless; flow CLI lama (execute/spec/plan/qa) sudah dicabut (ADR-0024).
- **Bersihkan branch tak terpakai** (SPEC-360/ADR-0077): daftar branch ter-merge = **nilai turunan git**
  (`GET /projects/:id/branches/unused`, `git branch --merged`, base `?base=→main→master→branch aktif`,
  ref origin dibanding `origin/<base>` — jangan hardcode `"main"`). Lima kunci proteksi per-branch
  (`current`/`base`/`worktree`/`spec-open`/`session`) **ditegakkan ulang** di `POST …/branches/delete`
  (yang menurunkan ulang daftarnya sendiri), jadi klien tak bisa menyelundupkan branch lewat body;
  scope (`local`/`remote`/`both`) menyempit per branch. Eksekusi tetap lewat `runGitOp` `delete-branch`
  (SPEC-206) — satu jalur, **tanpa `-D`/force**. Kunci `session` terpisah dari `worktree` karena sesi
  lahir `--detach` (ADR-0002) sehingga tak muncul di `git worktree list`. **Tiga gotcha git terukur:**
  `git branch --merged --format` memancarkan baris `(no branch)` di worktree detached; `origin/HEAD`
  dipendekkan git jadi bare `origin` (cermin `services/branches.ts`); dan `--end-of-options` **tak
  berlaku** untuk argumen `--merged` → base wajib di-resolve ke SHA lebih dulu. Ini pagar keselamatan
  data untuk satu endpoint bulk, **bukan** guardrail eksekusi — ADR-0037 tetap utuh.
- Docs SoT & coverage dipindai **live dari path efektif** tiap request (ADR-0011/0018), bukan tabel DB.
- Verifikasi doc terkini via Context7 sebelum mengubah keputusan platform/framework.

## Aturan Sesi & Eksekusi

- Mesin eksekusi nyata = **`server/src/services/pty.ts`**: `createSession()` men-spawn agen (`<prompt>` + flag agen) di window tmux; node-pty `tmux attach` menjembatani ke WebSocket, poll 500 ms mengawasi exit + perubahan phase-file lalu broadcast frame. **tmux adalah satu-satunya sumber kebenaran pekerjaan berjalan — tidak ada baris `Run` di DB.**
- **Dua agen** (SPEC-338/ADR-0074): `Agent = "claude" | "codex"`. `Setting.agent` = default global untuk SEMUA sesi yang men-spawn agen (backlog, reverse, prd, scaffold, breakdown, terminal-agen, konflik-integrasi); sesi backlog bisa override lewat `agent` di `POST /terminal/sessions`. Argv dirakit `runner/src/agent-cli.ts` (`agentFlags()`, murni & bertest), agen sesi disimpan di tmux `@hanoman_agent`. Padanan flag: `--model`→`-m`, `--effort`→`-c model_reasoning_effort`, `--dangerously-skip-permissions`→`--dangerously-bypass-approvals-and-sandbox`, `--settings`→`-c hooks.<Event>=<toml>` (+`--dangerously-bypass-hook-trust`, wajib — tanpa itu TUI mentok di "Hooks need review"). Model codex di `Setting.codex`; `HANOMAN_CODEX_BIN` cermin `HANOMAN_CLAUDE_BIN`. **Tanpa migration** (`Setting` kolom `Json`). Tiga perbedaan sadar: codex **tak punya event `Notification`** (marker keputusan pakai `Stop`+`UserPromptSubmit` → marker juga menyala saat sesi selesai wajar); codex **mendiamkan hook `type:"prompt"`** (mode goal jadi gate sh deterministik: phase file lengkap + plan tanpa `- [ ]`, exit 2 = continuation prompt), berpagar `GOAL_MAX_BLOCKS=25`. **`armGoalInTui` tak lagi khusus claude** sejak SPEC-397/ADR-0085 — lihat butir mode goal di bawah. **Gotcha wajib:** codex menolak jalan di direktori belum-dipercaya dan `-c projects."…".trust_level` TAK membukanya — `services/codex-trust.ts` menulis satu entri `[projects."<repoDir>"]` per project (worktree mewarisi trust root). Limit langganan punya DUA sumber terpisah: `services/limits.ts` (claude, panggilan API live 30 dtk) dan `services/codex-limits.ts` (codex, SNAPSHOT `rate_limits` dari rollout `$CODEX_HOME/sessions/**` — nol jaringan, nol sentuhan token; >12 jam → `stale`). Dua badge & dua grup siar (`limits` + `codexLimits`), sengaja tak digabung karena kesegarannya beda. Gotcha: label window WAJIB dari `window_minutes` (`primary` bisa 5-jam ATAU mingguan), `resets_at` codex = epoch DETIK.
- **Sesi penyelesai konflik ikut `Setting.agent`** (SPEC-377, tanpa ADR — memulihkan ADR-0074 di dua call
  site yang terlewat): rebase/merge jalan deterministik di worktree isolasi; yang **konflik** menyerahkan
  worktree itu ke sesi agen, dan sesi itu lahir dari **`sessionAgentDefaults()`**, bukan `sessionModel()`.
  `sessionModel()` **sengaja khusus claude** — ia tak pernah melihat `Setting.agent`/`Setting.codex` — jadi
  memakainya di titik kelahiran sesi berarti `createSession` jatuh ke `opts.agent ?? "claude"` dan sesi
  lahir claude ber-model default apa pun isi Settings. Terukur: `{agent:"codex", codex:{model:"gpt-5.6-terra"}}`
  tetap melahirkan `--model claude-opus-5 --effort xhigh --dangerously-skip-permissions`. Berlaku untuk
  **ketiga** pintu konflik — `POST /specs/:id/integrate` (backlog), `finishGraphOp` di `routes/ide.ts`
  (git graph merge·rebase·pull·drop, satu titik menutup keempatnya), dan `POST /terminal/sessions/:id/integrate`
  (PRD, sudah benar sejak SPEC-338). Wajib disertai **`ensureCodexTrust(repoDir)`** saat agennya codex:
  tanpa itu sesi mentok di layar trust tanpa manusia di pane. Tak ada override per-request — pilihan agen
  hidup di Settings (kartu "Agen sesi" memang sudah menjanjikan "worktree, fase, stage, review, **integrate**").
  Aturan umumnya: **setiap titik kelahiran sesi baru wajib lewat `sessionAgentDefaults()`** — kecuali tiga
  pintu konflik yang kini lewat `conflictSessionDefaults()` (di bawah); `sessionModel()` tersisa hanya untuk
  `POST /vps/:id/session` dan menunggu dipensiunkan.
- **Sesi konflik boleh punya default sendiri** (SPEC-383/ADR-0081): blok `Setting.conflict`
  `{enabled,agent,model,effort}` (kolom `Json` → **tanpa migration**, tanpa endpoint baru) dibaca
  `conflictSessionDefaults()` dan dipakai **ketiga** pintu konflik (backlog `POST /specs/:id/integrate`,
  `finishGraphOp` di `routes/ide.ts`, PRD `POST /terminal/sessions/:id/integrate`). **OPT-IN**: selama
  `enabled` mati helper itu **mendelegasikan penuh** ke `sessionAgentDefaults()` — perilaku SPEC-377 tanpa
  selisih satu argv pun. Alasan pemisahannya: menyelesaikan konflik itu sempit, tak berfase, tak berplan,
  dan sering beruntun — tak perlu effort sesi Execute. **Satu triple, bukan blok per-agen** seperti `Setting`
  akar: menukar `agent` menukar model/effort sekalian (cermin `pickAgent` di `StartSessionModal`), effort
  codex dikoersi `coerceCodexEffort` di helper. **Gotcha wajib:** `ensureCodexTrust` HARUS diturunkan dari
  agen **hasil helper**, bukan `Setting.agent` — dengan blok ini keduanya bisa berbeda, dan membaca yang
  salah mengulang bug SPEC-377 (sesi codex mentok di layar trust) dalam bentuk baru. Tetap **tak ada**
  override per-request; pilihan hidup di Settings. UI: kartu "Konflik rebase & merge" di tab Model sesi;
  saat mati kartunya **menampilkan nilai warisan** supaya tak ada pertanyaan "lalu konflik pakai apa".
  Tab itu sekalian ditata ulang **bersumbu agen** (dua blok berjudul "Claude Code"/"Codex CLI" + badge
  `dipakai sesi baru`): sebelumnya blok claude cuma berbunyi "Model"/"Effort" — nama agennya hanya di
  `aria-label` — sementara judul "default global" tetap terpampang meski agen aktifnya codex. Katalog claude
  di Settings kini dibaca dari `MODELS`/`EFFORTS` (`@hanoman/shared`), sumber yang sama dengan picker Start.
- **Katalog codex per model** (SPEC-339): effort adalah properti MODEL, bukan properti CLI. `CODEX_MODELS` (shared) membawa `efforts`/`fallback`/`minClient` per entri; `CODEX_EFFORTS` tinggal gabungan, **bukan** sumber pilihan UI — picker WAJIB `codexEfforts(model)`. Isi katalog: `gpt-5.6-sol` (default global) & `gpt-5.6-terra` = ultra/max/xhigh/high/medium/low, `gpt-5.6-luna` = **tanpa ultra**, `gpt-5.5` = tanpa max & ultra. Koersi effort dilakukan di **`createSession`** (titik cekik tunggal — jalur ber-`AgentToken` pun lewat sana), dan model pensiun (`gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex-spark`) diremap ke `gpt-5.5` saat `getSetting()` membaca; sengaja bukan ke 5.6 agar setelan lama tak pindah ke model yang CLI-nya belum sanggup. **Gotcha wajib:** trio 5.6 butuh codex CLI **≥ 0.144.0** dan manifest model disaring server **berdasarkan versi klien** (cache `~/.codex/models_cache.json`) — CLI lama tak akan pernah melihat model itu, dan `max` bahkan belum ada di enum effort 0.142.5. `GET /api/codex/version` memberi catatan lunak di Settings & picker Start, **tanpa** memblokir Start. Rujukan otoritatif katalog = `codex debug models`, bukan ingatan.
- **Riwayat sesi** (SPEC-362/ADR-0079): tmux tetap sumber kebenaran sesi **hidup**, tapi setiap sesi kini
  meninggalkan baris `SessionHistory` (LOCAL-only, tak disync) yang **lahir bersama sesinya** (sesi berjalan
  pun tercatat, `endedAt: null`) dan ditutup saat `killSession`. `pty.ts` tetap **nol dependensi DB** — ia
  hanya menembakkan `registerSessionHooks({onBirth,onDeath})` dari **dua titik cekik**
  `createSession`/`killSession`; jangan menambahkan pencatatan di call site (ada 12, dan flow baru akan
  menambah lagi). `onBirth` **tak** menembak saat re-attach. Transkrip di-`capture-pane` **tanpa `-e`**
  SEBELUM pane dibunuh (sesudah itu scrollback lenyap), disimpan sebagai berkas di `HANOMAN_TRANSCRIPT_DIR`
  (`services/transcript-store.ts`) dengan cap 1 MiB **menyimpan ekor**; DB hanya pointer. **PK baris = uuid,
  BUKAN `sessionId`**: id sesi spec deterministik dan berulang tiap reopen — PK `sessionId` akan menimpa
  riwayat lama. `GET/DELETE /api/terminal/history*` sengaja di bawah prefix `/terminal` agar mewarisi
  capability `sessions`; `skip`/`take` DB sah di sana (tak ada overlay live seperti `GET /specs`, ADR-0038).
  UI = modal di Terminal (grid tak berubah ukuran) + "Mulai lagi" ber-`restartableKind`.
- **Penghapusan worktree saat sesi ditutup digerbangi `ownsWorktree`** (`services/session-worktree.ts`,
  SPEC-362): `DELETE /terminal/sessions/:id` hanya memanggil `realGit.removeWorktree` bila cwd sesi
  benar-benar berada DI DALAM `<repoDir>/.worktrees/`. Jangan pernah memutuskannya dari substring
  `"/.worktrees/"` pada cwd — itu menguji **bentuk path**, bukan **hubungan cwd↔repoDir**, dan begitu
  sebuah project di-bind ke checkout di bawah `.worktrees/` (persis saat hanoman didogfood di
  worktree-nya sendiri) terminal biasa ber-`cwd === repoDir` ikut lolos dan checkout project itu
  sendiri terhapus. `realGit.removeWorktree` juga **melempar** bila diminta menghapus repo itu sendiri:
  `git worktree remove` di dalamnya gagal-diam (`tryGit`), jadi `rmSync` terakhir tetap jalan meski
  git menolak.
- **`pkill -f` satu sesi membunuh agen sesi LAIN** (SPEC-402, tanpa ADR — QA): prompt sesi
  diserahkan sebagai **argumen positional** agen (`claude "$(cat <promptfile>)"`, SPEC-223 — dan itu
  tak bisa dihindari: `claude`/`codex` tak punya opsi prompt-dari-berkas, stdin dipakai TUI), jadi
  **seluruh prompt hidup di ARGV** proses agen. Karena klausa scope verifikasi (ADR-0080) memuat
  `vitest` (5×), `tsc`, `node server/dist/server.js`, setiap sesi ber-`verifyScope=changed` **cocok**
  dengan `pkill -f vitest` / `pkill -f tsc`. BSD `pkill` **mengecualikan leluhurnya sendiri**
  (`man pkill`: "-a … by default the current pgrep or pkill process and all of its ancestors are
  excluded"), jadi pola itu berperilaku sebagai "bunuh semua sesi lain, sisakan sesi saya" — dan
  itulah "intermitten"-nya: yang mati selalu sesi tetangga. Terukur 29 Jul: `pkill -f "tsc" ;
  pkill -f "vitest"` di sesi `spec-389` pukul 15:36:04.4Z → pane `spec-319` & `spec-390` mati
  **status 143** pukul 15:36:08 & 15:36:10, sementara `spec-389` sendiri selamat. Mitigasinya
  **klausa kontrak** di `runner/src/verify-scope.ts` (bunuh per-PID/port, atau sempitkan pola ke path
  worktree sendiri), bukan hook deny — ADR-0037 tetap utuh.
- **Pane mati ≠ pekerjaan selesai** (SPEC-402): `SessionInfo.exitCode` (`#{pane_dead_status}`, hanya
  saat `exited`) mengalir ke `SessionDTO`/klien, dan pane berkode ≠ 0 diberi pil **"Gagal · exit
  <n>"** (`--status-err-tint`), bukan pil hijau "Selesai". `markExited` **menyimpan** kode dari frame
  `exit` (dulu dibuang), jadi sesi yang mati di depan mata operator langsung terbaca gagal; nilai
  yang sama datang lagi dari daftar sesi sehingga labelnya selamat dari refresh. Sesudah itu tombol
  "Lanjutkan" (ADR-0084) baru punya makna — sebelumnya sesi terputus tampak tuntas.
- **Kegagalan `tmux` BUKAN "tak ada sesi"** (SPEC-402): `listPanes()` mengembalikan `[]` hanya untuk
  `no server running`/`error connecting to` (`TmuxError.noServer`); kegagalan lain **dilempar**.
  Dulu `catch { return []; }` menelan semuanya, dan loop poll 500 ms membacanya sebagai "semua sesi
  dibunuh dari luar" → `end(id, 0)` = **"— sesi berakhir (exit 0) —"** untuk SETIAP terminal yang
  terbuka, pada agen yang masih bekerja. Dua pemberat: dedup siaran `services/events.ts` tak pernah
  mengirim ulang kebenaran (`exited:false`) sehingga pil palsu itu **lengket**, dan `getSession()`
  yang bersumber sama menggerbangi kelahiran sesi — `undefined` palsu membuat `startSpecSession`
  memanggil `realGit.addWorktree` yang merebut path dengan `remove --force` + `rmSync` **atas
  worktree sesi yang sedang berjalan**. Loop poll melewatkan tick yang gagal, `events.ts` melewatkan
  siaran grup (mekanisme lama), `server.ts` melewatkan `reconcileHistory` (daftar kosong palsu akan
  menutup baris riwayat sesi yang masih hidup). Satu pengecualian sadar: `sessionPhasesBySpec()`
  tetap **lunak** (peta kosong) karena overlay stage forward-only.
- **Satu backlog = satu sesi** (ADR-0015): id sesi diturunkan deterministik dari id spec — menekan Start dua kali = **re-attach**, bukan spawn kedua.
- **Sesi backlog DILANJUTKAN, bukan diulang** (SPEC-394/ADR-0084, memulihkan substansi ADR-0017 yang
  ikut tercabut bersama ADR-0024 atas premis "sesi tmux tak pernah terputus" — benar untuk restart
  API, **salah** untuk mesin restart / agen keluar / operator menutup sesi): `startSpecSession` punya
  **tiga** keadaan, bukan dua. **live** = pane tmux hidup → re-attach. **resume** = `stage ≠ done` +
  `baseSha` ada + artefak masih ada → lanjutkan (`201 { id, resumed: true }`). **fresh** = selain itu.
  **Pane MATI bukan sesi** — `remain-on-exit on` menahannya hanya agar layar terakhirnya terbaca, dan
  mengembalikannya sebagai sesi membuat tombol "Lanjutkan" **diam** (UI sudah menghitung `!exited`,
  jadi tombol itu muncul persis saat pane mati); ia dibunuh dulu (menutup `SessionHistory` + simpan
  transkrip, ADR-0079) lalu sesi dilahirkan ulang. Dua bentuk resume: worktree `.worktrees/<id>` yang
  masih sah dipakai **apa adanya** — satu-satunya jalur yang TIDAK memanggil `addWorktree`, karena
  helper itu selalu merebut path dengan `remove --force` + `rmSync` — atau, bila worktree hilang,
  dibangun ulang `--detach` di tip **`origin/hanoman/<id>` → `hanoman/<id>` → `Spec.headSha`**.
  Urutan itu mengikat: `origin/…` adalah ref yang `git push` di akhir sesi harus fast-forward, dan
  worktree yang lahir dari `branchFrom` membuat push itu **ditolak non-fast-forward** (terukur —
  sesi ulangan bahkan tak bisa menyimpan hasil ulangannya). `baseSha` & `headSha` **tak pernah
  ditulis ulang saat resume** (rentang review ADR-0030 tetap dari basis asli); `baseSha` null =
  belum pernah punya worktree = bukan resume. Prompt-nya `resumePrompt` yang menyebut baris fase
  yang sudah tercatat + fase berikutnya + bentuk worktree-nya, dan **tak mengulang** klausa keputusan
  pasca-Audit (ADR-0040) begitu `Audit` tercatat — keputusannya sudah mewujud sebagai baris fase.
  Server **tak pernah menulis** ke `$HANOMAN_PHASE_FILE` (tetap milik agen, append-only). Ia hidup
  di luar worktree, jadi ia **selamat** dari penghapusan worktree — itulah kenapa server bisa
  menyebutkannya dan agen tidak bisa menurunkannya sendiri. `stage = done` tetap jalur SPEC-172
  (`continuePrompt`, worktree dari `branchFrom`) — kerjanya umumnya sudah ter-merge. `worktreeAlive`
  bertanya ke **git** (`rev-parse --is-inside-work-tree` + toplevel = path itu sendiri), bukan
  `existsSync`: direktori telanjang di dalam repo pun "ada". Berlaku juga untuk governor scheduler
  (jalur peluncuran sama).
- **Gerbang pane-mati hidup di titik cekik `createSession`** (SPEC-394/ADR-0084), bukan hanya di
  `startSpecSession` — jadi ia menutup sekaligus jalur yang **tak punya gerbang sendiri**: sesi
  konflik `merge-<spec>` (`routes/specs.ts`) & `finishGraphOp` (`routes/ide.ts`), dan konsol VPS
  `vpsc-<id>` (`routes/vps.ts`). Kelima route **project-level** (reverse · scaffold · prd ·
  breakdown · cross-audit) punya gerbang `getSession` sendiri di depan `createSession`, jadi
  masing-masing ikut disempitkan ke `!exited`. **`attach()` pada pane mati TETAP sah** — itu justru
  cara membaca layar terakhir sesi yang sudah selesai; jangan ikut dipagari. **Pasangan wajib untuk
  flow project-level:** kelimanya memanggil `realGit.addWorktree` **setelah** gerbangnya, jadi
  memperbaiki gerbangnya SENDIRIAN menukar gejala "tombol diam" (tak merusak apa pun) dengan
  **kehilangan dokumen yang belum di-commit** — regresi yang lebih buruk daripada bugnya. Helper
  `ensureWorktree()` di `routes/terminal.ts` melewati `addWorktree` bila `worktreeAlive(wt)`, dan
  prompt-nya diberi satu kalimat `RESUMED_WORKTREE_NOTE`. Flow dokumen sengaja **tidak** memakai
  `resumePrompt`: deliverable-nya dokumen, dan fasenya tak punya artefak berkotak seperti `- [ ]`
  di plan. Konsekuensi yang diterima sadar: "mulai benar-benar dari nol" untuk flow dokumen kini
  menuntut operator menutup sesinya dulu (Tutup memang menghapus worktree, SPEC-362).
- Sesi berjalan di worktree sendiri di `<repoDir>/.worktrees/<id>`, dibuat `--detach` dari `branchFrom` (default `main`); `baseSha` dicatat untuk rentang review (ADR-0030). Jenis sesi: **spec-flow** (feature/qa/audit), **reverse** (project-level), **prd**, **plain terminal** (claude di repoDir; atau shell mentah non-claude via `{shell:true}`, SPEC-236/ADR-0056), **integrate-conflict** (merge-<id>), **vps**. Flow **audit** (SPEC-237/ADR-0057) = audit-only: pipeline `Audit → Laporan`, hanya dokumen SoT (`research/audit-<id>-<slug>.md`), tanpa Execute; bisa dinaikkan jadi Finding QA.
- **Fase bukan proses melainkan giliran** dalam satu sesi: `runner/src/prompt.ts` `PIPELINES` mendefinisikan nama fase per flow; prompt menyuruh agen `echo "<Fase> done" >> $HANOMAN_PHASE_FILE`. Server membaca file append-only itu (`services/session-phases.ts`) untuk menurunkan fase aktif → `Stage`. Konteks terbawa antar fase karena semuanya satu sesi.
- **Kontrak otonomi** (ADR-0035): agen menembus batas antar-fase tanpa berhenti — checkpoint "review" milik skill superpowers **bukan** titik berhenti — dan hanya berhenti untuk bertanya di terminal saat butuh keputusan manusia sejati. Waspada: subagent async bisa bikin agen `end_turn` dan runner mengira fase selesai (fase jadi dangkal).
- **Audit lintas project** (SPEC-337/ADR-0075): flow `cross-audit` — satu sesi mengaudit project utama
  **+ tetangga `ProjectLink`-nya** (relasi berarah `from → to`, satu hop, kedua arah). Pipeline & deliverable
  sama dengan audit-only (`Audit → Laporan`, dokumen SoT, tanpa perbaikan kode); bedanya prompt memuat path
  checkout tetangga (**read-only** — hanya worktree sendiri yang boleh ditulis) dan sesi memegang **kunci
  audit** untuk menarik timeline error gabungan lewat `GET /api/audit/logs`. Kunci hidup di tmux option
  (`@hanoman_audit_key`/`@hanoman_audit_projects`), mati bersama pane, tak pernah keluar lewat API. Dua pintu:
  backlog `source: "cross-audit"` (berdokumen) dan sesi lepas `{project, flow:"cross-audit"}` (tanya-jawab,
  tanpa Spec/fase). Agennya **hanoman sendiri** — bukan agent token eksternal (ADR-0065). **Jalan di
  claude maupun codex** (ADR-0074): kunci audit dikirim lewat env, jadi tak ada percabangan per agen.
- **Eskalasi audit dinamis** (SPEC-340/ADR-0076, memperluas ADR-0057): audit **dan** cross-audit punya
  **tiga** pintu tindak lanjut — Finding QA · Feature brief · PRD — bukan lagi hanya QA. Rekomendasi
  hanoman **terbaca mesin**: fase Laporan menulis satu blok ```json kanonik
  `{escalation:{target:"none|qa|brief|prd",reason,alternatives,prefill}}` di dokumen audit (pola
  manifest breakdown ADR-0069), di-parse `services/audit-escalation.ts` (defensif — rusak/absen →
  `null`) dan disajikan `GET /api/specs/:id/escalation` sebagai **nilai turunan** freshest-wins
  (ADR-0018) — **tak ada kolom DB, tanpa migration**. UI menyorot target rekomendasi (primary +
  badge) tapi ketiga tombol selalu tersedia: manusia terakhir yang memutuskan. Kontinuitas: brief
  lanjutan audit memakai `payload.fromAudit` (kini juga diterima `zBriefPayload`) + `branchFrom`
  `hanoman/<audit-id>`, tapi **TIDAK** melewati fase mana pun — beda sadar dari qa (ADR-0059), karena
  dokumen audit memuat temuan, bukan bentuk solusi. Sesi PRD menerima `branchFrom` **dan** `fromAudit`
  di `POST /terminal/sessions` (worktree lahir dari branch audit + isi dokumen audit disematkan ke
  `startPrdPrompt`); tanpa keduanya perilaku PRD lama utuh.
- **Model & effort per SESI** (SPEC-252/ADR-0061, mengamandemen ADR-0058): dipilih saat **Start** backlog lewat picker `StartSessionModal` (default = setelan global `model`/`effort`, `claude-opus-5` / `xhigh`), dikirim sebagai body opsional `model`/`effort` di `POST /terminal/sessions`, jadi argv `--model`/`--effort` saat sesi lahir → **andal penuh** (tak bergantung agen). Sesi tetap **satu proses, satu model seumur hidup**. Matrix per-fase lama (`phaseModels`, ADR-0058) **dicabut**: tak andal karena bergantung agen mengetik `/model`+`/effort` di batas fase, padahal agen menembus batas fase tanpa berhenti. Manusia tetap bisa `/model` manual di terminal. `steps` headless (ADR-0003) tetap usang.
- **Mode goal per sesi backlog** (SPEC-332/ADR-0073): sesi bisa lahir membawa gate `Stop` — `guardSettings` menyisipkan `hooks.Stop=[{type:"prompt",prompt:<kondisi>}]` ke `--settings` (mesin yang sama dipasang `/goal` Claude Code, tapi deterministik saat sesi lahir), plus keystroke `/goal` best-effort ke pane untuk visibilitas TUI. Kondisi default = DoD hanoman (semua fase tercatat di phase file, plan tak menyisakan `- [ ]`, push sukses) dan menuntut **bukti segar** karena evaluator hook `prompt` tak punya tool dan hanya membaca transkrip (yang bisa terpotong). Knob `Setting.goal` (default mati) + override `goal`/`goalCondition` saat Start; sesi scheduler mengikuti default global. **Bukan** guardrail deny — ADR-0037 tetap berlaku; interrupt manusia (`Esc`) bukan event Stop, jadi kendali tetap ada.
- **Mode goal codex memakai goal NATIVE codex** (SPEC-397/ADR-0085, mengamandemen ADR-0074 butir (b)):
  `armGoalInTui` **tak lagi khusus claude** — codex-cli **0.146.0** punya mode goal native
  (`codex features list` → `goals stable true`; `thread_goals` di `$CODEX_HOME/goals_1.sqlite`;
  status line `Pursuing goal` · `Goal achieved` · `Goal unmet`) dan codex **melanjutkan sendiri
  sesudah turn berakhir** sampai objektif tercapai. Premis ADR-0074 ("tak ada padanan terverifikasi")
  benar di 0.142.5, salah di 0.146.0. Gate sh **tetap terpasang** (masih menembak di 0.146): ia
  satu-satunya yang benar-benar **membaca** berkas fase & kotak `- [ ]` (cermin ADR-0029), sementara
  goal native menilai dengan prosa — jadi kondisi prosa bebas kini benar-benar dievaluasi di codex,
  batasan ADR-0074 itu dicabut. Harga yang diterima sadar: satu percobaan berhenti dievaluasi **dua
  kali**, keduanya berpagar (`GOAL_MAX_BLOCKS=25` / akunting budget codex). **Gotcha wajib:** TUI
  codex mengubah masukan yang datang dalam **satu burst ≥ 1024 karakter** jadi
  `[Pasted Content N chars]`, dan begitu itu terjadi slash-dispatch **tak jalan** — `/goal` terkirim
  sebagai **pesan chat biasa tanpa error, tanpa goal**. Deteksinya **per-burst PTY, bukan
  per-invokasi `send-keys`** (terukur: 4×500 char TANPA jeda → `[Pasted Content 1500 chars]`), jadi
  memotong keystroke tanpa jeda tak menyelesaikan apa pun → `goalChunks()` (runner, murni) mengirim
  potongan **500** ber-jeda 50 ms, dipakai **kedua** agen karena jebakan yang sama laten di claude
  (`GOAL_MAX` mengizinkan 4000 karakter). **Jebakan test:** verifikasi lama
  `paneText.includes("/goal")` **lulus palsu** persis untuk degradasi paste itu — pane memang memuat
  `/goal …`, sebagai pesan chat — jadi codex diverifikasi lewat penanda runtime goal-nya sendiri dan
  arming yang gagal boleh dikirim ulang (maks 3); verifikasi claude sengaja **tak disentuh**.
  Tanpa skema/migration/endpoint/knob baru.
- **Backlog goal — sesi dua fase tanpa perencanaan** (SPEC-407/ADR-0089, memperluas ADR-0073):
  source **`goal`** → flow **`goal`** = `PIPELINES.goal = ["Goal", "Verifikasi"]`. Sampai spec ini
  mode goal cuma **knob di atas pipeline `feature`**, jadi sesi "goal" tetap menulis design doc +
  plan berkotak sebelum menyentuh pekerjaannya. Kini prompt-nya builder terpisah
  **`startGoalPrompt`** (mengeja `Goal` / `Selesai bila` / `Batasan` dari payload; tanpa instruksi
  fase perencanaan, tanpa keputusan pasca-Audit, tanpa skill Brainstorm/Plan — fase `Goal` sengaja
  **tanpa skill**, hanya `Verifikasi` → `verification-before-completion`). Stage: `Goal` **aktif
  maupun tercatat** → `executing`, `Verifikasi` → `done` (nama fase wajib unik lintas `PIPELINES`
  — `REACHED` berkunci nama). Payload **bentuk ketiga** `zGoalPayload {goal, done, constraints,
  priority}`; `superRefine` `zCreateSpec` kini **tiga-arah** (`qa` ↔ `severity`, `goal` ↔ `goal`,
  selain itu brief) dan `Spec.objective` diturunkan dari `payload.goal`. **Mode goal dipaksa
  menyala** untuk flow ini (`opts.goal:false` diabaikan) dan **template global
  `Setting.goal.condition` DILEWATI** — ia generik untuk semua sesi sedangkan item goal membawa
  kondisinya sendiri; override per-sesi tetap paling tinggi. **Dua gotcha wajib:** (1) gerbang
  klausa scope verifikasi pindah dari "pipeline punya fase `Execute`" ke predikat
  **`writesCode(flow)`** — sesi goal menulis kode meski tanpa fase `Execute`, dan melewatkannya
  membuatnya jatuh ke DoD repo target alias suite penuh (lubang yang ditutup ADR-0080); (2)
  `resumeClause` hanya menyebut plan `docs/superpowers/plans/**` untuk pipeline ber-fase `Plan` —
  menyuruh sesi goal mencari plan justru mengundangnya membuat satu. Dua pintu masuk: tab **Goal**
  di modal backlog baru, dan tombol **"Take ke backlog"** di preview PRD yang kini **pemilih**
  (brief / goal, keduanya ber-`branchFrom = prd/<slug>`). Tanpa migration, tanpa endpoint baru;
  ADR-0029 (gerbang plan) & ADR-0037 tetap utuh.
- **Scope verifikasi per sesi** (SPEC-376/ADR-0080): `verifyScope` (`changed` default | `full`) —
  knob `Setting.verifyScope` (kolom `Json`, **tanpa migration**) + override saat Start. Sesi
  `changed` menguji **berkas yang berubah saja**: `pnpm vitest --run --changed "$HANOMAN_BASE_SHA"`
  atau `vitest related`, typecheck **per paket** (bukan `pnpm -r typecheck`), lint per berkas, dan
  build penuh / boot-server+curl hanya bila memang relevan. Alasannya sumber daya: beberapa sesi
  berjalan bersamaan di satu mesin (di repo ini satu suite penuh = 258 berkas test + 6 proses `tsc`).
  Akarnya **lubang di kontrak prompt** — `runner/src/prompt.ts` tak pernah menyebut scope verifikasi,
  jadi agen jatuh ke DoD repo target. Mewujud lewat **klausa prompt** (hanya flow ber-fase `Execute`
  — flow dokumen tak punya test) + **env** `HANOMAN_BASE_SHA`/`HANOMAN_VERIFY_SCOPE`; `baseSha`
  wajib lewat env karena worktree lahir `--detach` (tak ada `main`, `HEAD~1` salah). **Bukan**
  guardrail deny — ADR-0037 tetap utuh, dan agen boleh memperluas scope untuk perubahan berdampak
  luas asal menyebut alasannya. **Empat gotcha:** `--changed` menyalakan `passWithNoTests` sehingga
  nol test **terlihat hijau**; `--changed` di tingkat root WAJIB disertai **`--no-file-parallelism`**
  bila set-nya menyentuh test server — run root tak menghormati `fileParallelism: false` milik project
  server dan test server berbagi **satu berkas DB** (`<db>.test.db` per checkout sejak ADR-0086 —
  aman dari worktree tetangga, tapi tetap satu berkas untuk semua berkas test di paket itu), terukur
  di SPEC-397 set yang SAMA memberi **181 gagal
  palsu** paralel vs **736 lulus** serial, dengan bentuk kegagalan yang menyesatkan seperti regresi
  sync; env sesi dipasang sebagai **prefix shell** di depan argv sehingga
  tak pernah tercetak `/bin/echo` — buktinya harus dibaca dari DALAM proses (`fake-agent-env.sh`);
  dan untuk perubahan di modul INTI `--changed` memang mendekati suite penuh (terukur di SPEC-376
  sendiri: menyentuh `shared/src/{enums,entities,dto}.ts` → 217 berkas / 1589 test / 177 dtk) —
  itu blast radius yang sebenarnya, penghematan datang dari perubahan berdaun. `sync-ws.test.ts`
  terbukti **non-deterministik** (gagal 2× di run campur-project, lulus sendirian, lulus bersama
  tetangga server, lulus saat set yang sama diulang) — jalankan ulang terisolasi DAN ulangi
  set yang sama sebelum menyalahkan perubahanmu.
- Stage bergerak **maju** hanya lewat fase yang dilaporkan sesi; **mundur** hanya lewat aksi human eksplisit `PATCH /specs/:id { stage }` (backward-only, ADR-0027). `executing` **tertahan** (tak jadi `done`) selama plan `docs/superpowers/plans/**` masih punya `- [ ]` (ADR-0029).
- Biaya bersifat **estimasi dan tidak menggerakkan apa pun** (ADR-0012): tak ada `dailyBudget`/budget flag. Indikator limit dibaca dari OAuth usage API Anthropic (`services/limits.ts`), bukan parsing output terminal.
- **Jangan pernah menjalankan run/sesi di working tree utama** — selalu worktree terpisah. Jangan menyentuh worktree sesi lain.

## Aturan Keamanan

- Auth (ADR-0028): login email/password menggerbangi **seluruh `/api`** (gate `onRequest`, 401 tanpa sesi, termasuk upgrade WebSocket `/api/terminal`). Publik hanya `GET /health`, `GET /auth/status`, `POST /auth/login`, `POST /auth/setup`.
- **Agent token — akses AI agent** (SPEC-257/ADR-0065): jalur auth **kedua** ke `/api` untuk AI agent eksternal (`Authorization: Bearer` / `?agent_token=` di WS), ditegakkan **capability per-domain read/write** (write⊇read; katalog `@hanoman/shared`). `AgentToken` server-local (hash-at-rest, cermin DeviceToken); master switch `Setting.agentAccessEnabled` (default off) menolak semua. Tak-boleh-didelegasikan (agent → 403): `/auth`, `/agent-tokens`, `/device-tokens`, `/sync`. Cookie = akses penuh (tak ada RBAC). Bukan perluasan permukaan eksekusi — `sessions:write` RCE tetap dibatasi isolasi worktree.
- Password: `crypto.scrypt` (stdlib) + salt acak + `timingSafeEqual`; tak pernah dikembalikan ke client. Sesi: token opaque 256-bit di cookie `httpOnly`; DB menyimpan `sha256(token)`, revocable. Login di-throttle per IP; error selalu generic.
- Tanpa RBAC — semua user setara; `DELETE /auth/users/:id` menolak menghapus user terakhir. Bootstrap: `POST /auth/setup` membuat akun pertama lalu tertutup (409). Lakukan `setup` segera pada deploy pertama.
- **Guardrail perintah berbahaya DICABUT sepenuhnya** (SPEC-197, ADR-0037): sesi jalan `--dangerously-skip-permissions` tanpa hook deny apa pun — agen dipercaya penuh, setara developer yang menjalankan `claude` di mesinnya sendiri. `runner/src/safety.ts` sudah dihapus. **Jangan hidupkan kembali tanpa ADR baru.**
- **Isolasi worktree adalah satu-satunya batas keamanan yang tersisa** (ADR-0037): sesi di `.worktrees/<id>`, tak ada akses ke working tree utama.
- Kredensial Claude (Keychain macOS / `~/.claude/.credentials.json` / env `CLAUDE_CODE_OAUTH_TOKEN`|`ANTHROPIC_API_KEY`) dan private key VPS (`Vps.keyPath`, file di server) **tak pernah ke client maupun DB**.

## Aturan Data & Skema

- **Tujuh model inti** (SQLite via Prisma 6, ADR-0086): `Project`, `Spec`, `Setting`, `Notification`, `User`, `Session`, `Vps`. Tidak ada `Run` maupun `Trigger` — di-drop saat pindah ke sesi interaktif (ADR-0024). Model pendukung mencakup `DeviceToken`, **`AgentToken`** (kredensial AI agent + capability, SPEC-257/ADR-0065, server-local), `SessionResult`, sync (`SyncLog`/`SyncOutbox`/`SyncState`/`LocalBinding`/`RuntimeConfig`), error monitoring (`ErrorGroup`/`ErrorEvent`), Help Center (`Ticket`/`TicketAttachment`), VPS compliance (`VpsAuditSnapshot`/`VpsItemState`).
- Enum stage/source/priority disimpan sebagai **`String` + divalidasi zod** di `@hanoman/shared` (`enums.ts`), bukan enum Prisma.
- `Project.id` (slug) **kekal**, tak ada endpoint rename; `repoDir` OPSIONAL & tak disync. **`LocalBinding`** (`projectId → repoDir`, per-mesin, LOCAL-ONLY) meng-override path; `resolveRepoDir = binding ?? Project.repoDir` dipakai **seluruh** jalur baca (spawn/IDE/coverage/branches/specs/docs).
- `docStatus`/`coverage`/**Docs**/**PRD** **bukan kolom & tidak dipersist** — docs live dari disk via `git ls-files`, coverage diturunkan tiap `toProjectView` (ADR-0018), PRD = dokumen `docs/prd/<slug>.md` (ADR-0041). Tabel `DocFile` sudah di-drop (ADR-0011).
- **Jangan ubah skema tanpa migration + ADR.** Menambah model: hand-write `migration.sql` + `migrate deploy` (bukan `migrate dev` yang me-reset). Jalankan `prisma generate` sesudah merge yang membawa model baru. **DB test tak perlu disiapkan manual** sejak ADR-0086 — `server/test/global-setup.ts` menghapus `<db>.test.db` lalu `migrate deploy` tiap run. Model baru **wajib** ikut `PG_ORDER` di `cli/src/commands/migrate-pg.ts`; test DMMF akan merah kalau lupa.
- **Fitur yang tak didukung SQLite dan karena itu tak boleh masuk skema:** scalar list (`String[]` non-relasi), tipe native `@db.*`, `Decimal`, `Bytes`, `mode: "insensitive"` pada filter (`LIKE` SQLite sudah case-insensitive ASCII). Skema juga **tak memakai `@map`** sama sekali — properti itu yang membuat tool migrasi Postgres bisa memakai baris `SELECT *` langsung sebagai data `createMany`; jangan merusaknya.
- DB dijaga kosong untuk pemakaian nyata (tanpa demo seed). Test memakai berkas `<db>.test.db`, bukan `DATABASE_URL` dev — vitest **menolak jalan** bila keduanya sama.

## Aturan Dokumentasi & Alur

- **SoT sebagai konvensi** (ADR-0023, supersedes ADR-0001): `internal/docs/**` tetap Source of Truth — diperbarui dalam **commit yang sama** & ter-link di index (`internal/docs/README.md`). Tapi guardrail/Stop hook/gate Execute yang menegakkannya **dicabut** (SPEC-160). `hanoman docs scan` tetap ada sebagai laporan coverage read-only. **Jangan menambahkan gate kembali tanpa ADR baru.**
- **Nomor SPEC & ADR unik & imutable**; ADR usang tidak dihapus — ditandai statusnya. Sibling worktree bisa mereservasi nomor yang sama — **enumerasi lintas semua branch** sebelum mengklaim nomor (ADR-0021).
- **Dokumen audit berumur, ADR tidak** (SPEC-386/ADR-0083): laporan
  `internal/docs/research/audit-<spec>-<slug>.md` hidup sampai eskalasinya diputuskan (ADR-0076) dan
  spec turunannya tuntas, lalu **dihapus berikut entri indexnya**. Tiga syarat: temuannya sudah punya
  **jejak permanen** (ADR, baris di doc SoT, atau kode ter-commit); **rujukan masuk ikut dibereskan** —
  doc permanen kerap menaut dokumen auditnya, dan melewatkannya meninggalkan link mati di doc yang
  justru dimaksudkan abadi (di SPEC-386 ada empat: ADR-0062/0064/0081 + `frontend-implementation.md`);
  dan index **tidak** menyimpan abstrak audit. **Struktur index sejak SPEC-386:**
  `internal/docs/README.md` memuat **satu baris per ADR** (nomor · judul · penanda status), sedangkan
  **narasi** tiap keputusan hidup di sub-index `internal/docs/adr/README.md` — ADR baru wajib ditaut di
  **keduanya**. Reachability aman karena coverage memakai BFS graf link (`linkedSetFrom`), bukan daftar
  datar, jadi doc yang hanya ter-link lewat sub-index tetap terhitung `linked`. Alasan pemisahan: index
  dibaca **setiap** sesi agen; sebelum SPEC-386 94% isinya (46,6 KB) adalah changelog ADR + abstrak
  audit, sekarang ±9 KB.
- **Alur fitur:** spec → plan → execute. **Alur QA:** audit → keputusan → (spec → plan)? → execute — temuan kecil langsung execute, Spec & Plan ditandai `skipped`; keputusan dielicit lewat prompt & diambil agen (ADR-0020/0040). **Alur audit-only** (SPEC-237/ADR-0057): audit → laporan (dokumen), berhenti; tanpa perbaikan, promotable ke Finding QA.
- Prompt sesi memetakan fase → skill superpowers: Brainstorm→brainstorming, Audit→systematic-debugging, Plan→writing-plans, Execute→executing-plans + TDD + verification-before-completion.
- Ikuti design system di `internal/docs/design-system/**` (editorial, bone paper, brass accent).
- **Unduh dokumen** (SPEC-361/ADR-0078): setiap pratinjau Markdown (`SpecDocsModal` — dipakai Backlog **dan** Terminal, PRD, Docs SoT, IDE) punya tombol `.md` & `.pdf`. Mekanismenya query `?download=md|pdf` pada endpoint dokumen yang **sudah ada** — jangan bikin endpoint ekspor baru; nilai lain/absen mengembalikan JSON lama utuh. PDF dirender `server/src/services/doc-export.ts` (`marked.lexer` → `pdfkit`, standard-14 font, `--external:pdfkit` di esbuild). **Gotcha wajib:** pdfkit **tidak melempar** untuk glyph di luar WinAnsi — ia mencetak mojibake senyap (`→` jadi `!'`, emoji jadi `Ø<ß‰`), jadi semua teks harus lewat `toWinAnsi()`; dan pdfkit **mewariskan opsi** di sepanjang rantai `continued`, jadi flag seperti `strike` wajib eksplisit boolean atau satu `~~coret~~` mencoret sisa paragraf.
- **Pratinjau dokumen tak menggulir ke samping & setinggi ruang yang ada** (SPEC-363, tanpa ADR — memperbaiki SPEC-361/ADR-0078): `.hn-md` memasang `overflow-wrap: anywhere` (**bukan** `break-word` — hanya `anywhere` yang mengecilkan *min-content*, dan min-content itulah yang membuat rantai inline `code` tanpa spasi & tabel lebar mendorong container), `table-layout: fixed`, dan `pre` ber-`white-space: pre-wrap`. Terukur atas **353 `.md` nyata**: 33 dokumen menggulir horizontal → 0, 187 dokumen ber-`pre` → 0 (harga +12,5% tinggi konten). Tinggi pane diturunkan dari viewport lewat rantai flex (`Modal fillHeight` opt-in + `flex: 1 1 0` di root layar Docs/IDE), bukan `62vh`/`620` tetap. **Dua gotcha wajib.** (1) `flex-basis` di item terluar **harus `0`**, bukan `auto`: pembungkus `<main>` memakai `min-height: 100%` (SPEC-351), jadi basis `auto` membuat item memakai tinggi ISI-nya dan justru menumbuhkan halaman — terukur pane 6000 px + halaman ikut menggulir; `LIST_SCREEN_STYLE` (basis `auto`) karena itu **tak** bisa dipakai apa adanya di sini. (2) di pdfkit, `doc.text(str, x, y, { width })` **menyalakan pembungkus baris yang memanggil `addPage()` sendiri — walau `lineBreak: false`**; karena renderer menaruh teks di koordinat eksplisit sambil membukukan `doc.y` sendiri, setiap pemakaian `width` di posisi eksplisit melahirkan halaman kosong (footer bernomor → satu halaman kosong PER halaman, dan nomornya ikut tercetak di halaman kosong itu; penanda butir daftar → `doc.y = top` jadi koordinat halaman basi, 5 dari 12 halaman PRD kosong — rantai DUA mata, dan matriks 2×2 membuktikan memutus salah satu saja sudah cukup). Blok kode digambar **bersegmen** — satu `rect` latar per halaman — dan hanya pindah halaman bila bloknya memang muat di halaman kosong (dulu satu `rect` 2126,6 pt menabrak footer). Hasil: `api-contract.md` 42→18 halaman, PRD hardening-vps 12→7 tanpa halaman kosong.
- **Kartu yang berisi pane bergulir wajib `<Card fill>`, bukan `style`** (SPEC-393, tanpa ADR —
  memperbaiki SPEC-363): `Card` **selalu** menyisipkan satu pembungkus `<div>` di sekitar
  `children`, dan pembungkus itu `display: block` kecuali prop **`fill`** dipasang — `fill` yang
  menyetel `display:flex`+`flexDirection:column`+`flex:1 1 auto`+`minHeight:0` pada **dua-duanya**
  (div terluar *dan* pembungkus anak). SPEC-363 memasang rantainya lewat `style`, yang hanya
  mengenai div terluar, jadi pembungkus anak memutus rantai: `flex`/`minHeight` di pane jadi
  **inert**, pane tumbuh setinggi isinya, dan karena `Card` ber-`overflow: hidden` isinya
  **terpotong tanpa scroller mana pun** — Docs & IDE Explorer tak bisa digulir sama sekali.
  Terukur di Chrome (viewport 1512×813, `<main>` 757 px): pane 11 830 px di dalam kartu 701 px →
  **11 184 px hilang**, dan `clientHeight === scrollHeight` di pane membuktikan ia tak pernah
  menggulir melainkan hanya tumbuh. **Jebakan test:** kontrak style SPEC-363 memeriksa PANE-nya
  (`flex: 1 1 auto`, `overflow: auto`, tanpa px/vh) dan itu tetap benar sepanjang bug — yang salah
  induknya. Karena itu `src/test/scroll-chain.test.tsx` **menaiki rantai leluhur** pane dan
  menuntut tiap mata rantai meneruskan tinggi (`display` flex/grid + `minHeight: 0`); jsdom tak
  melayout, jadi hanya kontrak itu yang bisa dijaga di test. Kontrol kerjanya sejak 2026-07-10:
  `ProjectsScreen.tsx` `<Card padding={0} fill>`; `DocPreviewModal` aman karena rantainya
  `Modal fillHeight` → `modal-body`, tanpa `Card`. **Sweep dua lapis** (54 `Card` dienumerasi →
  9 kandidat → 4 tanpa `fill`, lalu detektor gejala "terpotong & tak terjangkau" di Chrome)
  menemukan korban keempat yang **tak dikeluhkan**: modal berkas Git Graph (kartu ber-`maxHeight:
  86vh`), 11 162 px hilang. `ReviewScreen`/`BranchesPanel` aman **justru karena** pane-nya masih
  ber-`maxHeight` tetap — pane berbatas sendiri tak bergantung pada rantai. **Gotcha kedua:**
  `fill` juga menyetel `flex: 1 1 auto`, jadi di modal yang dipusatkan overlay flex ber-arah
  **baris** ia melebarkan panel (terukur 900 → 1464 px) — kembalikan `flex: "0 1 auto"` lewat
  `style` (di-spread sesudah `fill`); kartu grid item (Docs/IDE) tak kena.
- **Aksi preview `.md` di IDE & Review** (SPEC-385, tanpa ADR — memperluas ADR-0078 + preseden
  SPEC-240/363): empat permukaan yang dulu menampilkan `.md` sebagai `<pre>` mentah kini punya aksi
  preview — pane **diff** Explorer, modal berkas **Git Graph**, dan **Review** (backlog *dan* sesi
  PRD) — sementara IDE mode file mendapat ruang baca lebar di samping toggle inline SPEC-240 yang
  **tetap ada**. Satu komponen DS `ds/DocPreviewModal.tsx` (`Modal fillHeight` + `MarkdownView` +
  `DocDownload` opsional) yang **tak menyentuh api client**; gerbang seragam `isMarkdownPath(path)`
  (predikat pindah dari const lokal `IdeScreen` ke `ds/markdown.tsx`) **dan** non-biner **dan**
  `content !== null`. **Git Graph memakai TAB `preview`, bukan modal** — permukaannya sudah modal,
  jadi modal bertumpuk membuat Escape ambigu. Tombol IDE berlabel **"Preview lebar"** karena toggle
  SPEC-240 sudah memakai kata "Preview". Parity unduh ADR-0078 diwujudkan dengan menempelkan
  `?download=md|pdf` ke **lima endpoint yang sudah ada** (`/specs/:id/review/*`,
  `/terminal/sessions/:id/review/*`, `/projects/:id/file-diff`, `/projects/:id/commit/:sha/file`,
  `/projects/:id/compare/file`) lewat `sendReviewDownload` — **tanpa endpoint/skema/migration/ADR
  baru**; yang dikirim `ReviewFile.content` (isi **sesudah** perubahan, sama dengan yang dirender),
  dan biner atau `content === null` → **404** (bukan PDF kosong yang menyesatkan). `shared/src/api.ts`
  sengaja **tak** disentuh — `paths.download()` sudah generik, dan menyentuh modul inti meledakkan
  blast radius `vitest --changed` (ADR-0080).
- **Setiap task execute selesai:** centang checklist di file plan (`docs/superpowers/plans/**`, `- [ ]` → `- [x]`), lalu jalankan **test yang tersentuh perubahan itu** (`pnpm vitest --run --changed "$HANOMAN_BASE_SHA"` atau sebut path test-nya). Bila task menyentuh endpoint, **test API-nya secara nyata di local** sekali di akhir — boot server (`pnpm dev` atau `node server/dist/server.js`) dan curl endpoint yang tersentuh, jangan hanya andalkan unit test. Fix sampai hijau sebelum lanjut.
- TypeScript strict; test untuk setiap logika orchestrasi (trigger, queue, worktree, guardrail). Sesi menjalankan test **yang tersentuh perubahannya** dan typecheck **paket yang tersentuh** (`pnpm --filter ./server typecheck`) — bukan suite penuh, bukan `pnpm -r typecheck` (SPEC-376/ADR-0080). Suite penuh (`vitest run --no-file-parallelism`) adalah langkah **manusia** sebelum merge. Hindari env prod bocor (`env -u NODE_ENV -u DATABASE_URL`).
- Definition of done: test yang tersentuh hijau · docs tersentuh diperbarui + ter-link · diff bersih di worktree, siap push ke target branch.
