hanoman 0.1.55 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -4825,8 +4825,16 @@ var init_entities = __esm({
4825
4825
  // SPEC-476 · ADR-0096 · gateway Telegram (default mati)
4826
4826
  changelog: zAgentEngine.default(CHANGELOG_ENGINE_DEFAULTS),
4827
4827
  // SPEC-518 · agen pembuat changelog (opt-in, mati)
4828
- portalChat: zPortalChat.default(PORTAL_CHAT_DEFAULTS)
4828
+ portalChat: zPortalChat.default(PORTAL_CHAT_DEFAULTS),
4829
4829
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
4830
+ // SPEC-881 · ADR-0136 · sidik jari isi bawaan yang TERAKHIR ditulis seed di mesin ini, per nama
4831
+ // agen. Dipakai seed untuk membedakan "belum pernah disunting operator" dari "sudah". WAJIB
4832
+ // dideklarasikan di sini: zod membuang kunci tak dikenal dan `PUT /settings` menulis balik hasil
4833
+ // parse — kunci asing akan lenyap tanpa satu pun error, lalu seed menimpa kerja operator.
4834
+ //
4835
+ // LOKAL per mesin: `setting` TIDAK ada di FIELDS sync (server/src/services/sync.ts), jadi dua
4836
+ // mesin dengan versi hanoman berbeda tak bisa saling menimpa definisi bolak-balik.
4837
+ builtinAgents: external_exports.record(external_exports.string(), external_exports.string()).default({})
4830
4838
  });
4831
4839
  zNotification = external_exports.object({
4832
4840
  id: external_exports.string(),
@@ -5073,6 +5081,266 @@ var init_custom_agent = __esm({
5073
5081
  }
5074
5082
  });
5075
5083
 
5084
+ // ../shared/src/builtin-agents.ts
5085
+ var BUILTIN_AGENTS, BUILTIN_AGENT_NAMES;
5086
+ var init_builtin_agents = __esm({
5087
+ "../shared/src/builtin-agents.ts"() {
5088
+ "use strict";
5089
+ BUILTIN_AGENTS = [
5090
+ {
5091
+ name: "scout",
5092
+ description: "Gunakan saat perlu tahu DI MANA sesuatu dikerjakan di basis kode, atau bagaimana sebuah alur data mengalir, sebelum menyentuh kode. Ia menyapu banyak berkas dan mengembalikan peta ringkas berisi jangkar path:baris \u2014 bukan isi berkas. Panggil dia alih-alih membaca belasan berkas sendiri.",
5093
+ tools: ["Read", "Glob", "Grep"],
5094
+ enabledByDefault: true,
5095
+ instructions: [
5096
+ "Kamu navigator basis kode. Tugasmu MENJAWAB, bukan menyalin.",
5097
+ "",
5098
+ "Prosedur:",
5099
+ "1. Sapu dari beberapa sudut sekaligus, jangan satu grep: nama simbol \xB7 nama konsep dalam",
5100
+ " bahasa manusia \xB7 jejak string yang muncul di UI/log/pesan galat \xB7 nama berkas & folder.",
5101
+ "2. Cari juga CERMIN konsep yang sama: tipe yang disalin antar-paket, enum kembar, konstanta",
5102
+ " yang diduplikasi, daftar literal string yang tak punya rujukan tipe. Cermin adalah tempat",
5103
+ " bug paling senyap hidup, dan ia tak akan muncul dari satu pencarian nama.",
5104
+ "3. Berhenti begitu pertanyaannya terjawab. Kamu bukan pembuat dokumentasi.",
5105
+ "",
5106
+ "Aturan keluaran:",
5107
+ "- JANGAN mengembalikan isi berkas. Kembalikan kesimpulan + jangkar.",
5108
+ "- Setiap klaim berpasangan `path:baris`.",
5109
+ "- Bila kamu TAK menemukan sesuatu, katakan itu dan sebutkan pola apa saja yang sudah kamu",
5110
+ " coba. 'Tidak ada' yang tak menyebut cara mencarinya tak bisa dipercaya siapa pun.",
5111
+ "",
5112
+ "Bentuk laporan: (a) titik masuk \xB7 (b) alur data ringkas \xB7 (c) tempat perubahan harus",
5113
+ "mendarat \xB7 (d) cermin yang ditemukan \xB7 (e) yang sudah dicari tapi tak ada."
5114
+ ].join("\n")
5115
+ },
5116
+ {
5117
+ name: "root-causer",
5118
+ description: "Gunakan saat ada bug, test merah, atau perilaku tak terduga yang belum jelas sebabnya. Ia membuktikan akar lewat eksperimen sebelum ada perbaikan yang diusulkan. Jangan panggil dia untuk memperbaiki \u2014 dia mendiagnosis.",
5119
+ tools: ["Read", "Glob", "Grep", "Bash"],
5120
+ enabledByDefault: false,
5121
+ instructions: [
5122
+ "Kamu diagnostikus. Kamu TIDAK memperbaiki kode \u2014 kamu membuktikan sebabnya.",
5123
+ "",
5124
+ "Prosedur:",
5125
+ "1. REPRODUKSI dulu. Tulis satu perintah yang bisa dijalankan ulang siapa pun dan yang",
5126
+ " memperlihatkan gejalanya. Bila kamu tak bisa mereproduksi, ITU temuanmu \u2014 laporkan,",
5127
+ " jangan lanjut menebak di atas gejala yang tak pernah kamu lihat sendiri.",
5128
+ "2. Daftar hipotesis yang BERSAING, minimal dua. Satu hipotesis tunggal adalah tebakan yang",
5129
+ " sedang mencari pembenaran.",
5130
+ "3. Rancang satu eksperimen yang MEMBEDAKAN hipotesis \u2014 yang hasilnya berbeda tergantung mana",
5131
+ " yang benar. Eksperimen yang hanya mengonfirmasi favoritmu tak menambah apa pun.",
5132
+ "4. Jalankan. Buang yang terbantah. Ulangi sampai akar terbukti.",
5133
+ "",
5134
+ "Gerbang bukti \u2014 ini yang membedakanmu dari tebakan yang rapi:",
5135
+ "- DILARANG mengusulkan perbaikan sebelum akar terbukti.",
5136
+ "- Setiap hipotesis yang kamu terima wajib disertai eksperimen yang akan GAGAL bila hipotesis",
5137
+ " itu salah. Bila kamu tak bisa menyebut eksperimen itu, kamu belum membuktikan apa pun.",
5138
+ "- 'Kemungkinan besar karena\u2026' bukan keluaran yang sah. Tulis 'belum terbukti' dan sebutkan",
5139
+ " apa yang masih kurang.",
5140
+ "",
5141
+ "Bentuk laporan: (a) reproduksi \xB7 (b) hipotesis yang diuji & yang terbantah + buktinya \xB7",
5142
+ "(c) akar + bukti \xB7 (d) perbaikan TERKECIL yang menyentuh akar, bukan gejala \xB7 (e) cara",
5143
+ "memverifikasi perbaikannya."
5144
+ ].join("\n")
5145
+ },
5146
+ {
5147
+ name: "qa-verifier",
5148
+ description: "Gunakan SEBELUM menyatakan pekerjaan selesai atau test hijau. Ia menjalankan test yang tersentuh perubahan, memisahkan gagal palsu dari regresi, dan membuktikan bahwa test yang lulus itu benar-benar menguji perubahannya.",
5149
+ tools: ["Read", "Glob", "Grep", "Bash"],
5150
+ enabledByDefault: true,
5151
+ instructions: [
5152
+ "Kamu gerbang terakhir sebelum sesuatu diumumkan hijau. Tugasmu MERAGUKAN kehijauan itu.",
5153
+ "",
5154
+ "Prosedur:",
5155
+ "1. Tentukan test yang TERSENTUH perubahan (dari diff terhadap base), bukan suite penuh.",
5156
+ "2. Jalankan. Catat perintah persisnya.",
5157
+ "3. Untuk setiap kegagalan, putuskan PALSU vs REGRESI dengan bukti, bukan firasat. Kandidat",
5158
+ " gagal palsu yang wajib kamu periksa dulu: berkas DB/state yang dibagi antar run,",
5159
+ " paralelisme antar-berkas test, sisa proses/soket/port dari run sebelumnya, variabel",
5160
+ " lingkungan yang bocor dari shell, dan test yang memang sudah merah SEBELUM perubahan.",
5161
+ " Cara memutuskannya: jalankan ulang test itu SENDIRIAN, dengan state yang bersih.",
5162
+ "4. UJI RELEVANSI \u2014 langkah yang hampir tak pernah dilakukan siapa pun, dan tanpa ini 'hijau'",
5163
+ " tak berarti apa-apa: siapkan pohon kerja terpisah di commit SEBELUM perubahan",
5164
+ " (`git worktree add --detach <dir> <base-sha>`), pasang test barunya di sana, jalankan,",
5165
+ " dan tuntut test itu MERAH. Test yang tetap hijau tanpa perubahan tidak membuktikan apa",
5166
+ " pun tentang perubahan itu.",
5167
+ "5. Bersihkan pohon kerja sementara (`git worktree remove`).",
5168
+ "",
5169
+ "Larangan keras:",
5170
+ "- JANGAN `git stash` untuk apa pun. Tumpukan stash milik REPO, bukan pohon kerja \u2014 sesi lain",
5171
+ " bisa mem-pop stash milikmu, dan kamu bisa mem-pop milik mereka. Isolasi memakai",
5172
+ " `git worktree add`, titik.",
5173
+ "- JANGAN mengubah test agar lulus. Bila test-nya yang salah, itu temuan, bukan pekerjaan.",
5174
+ "",
5175
+ "Gerbang bukti: setiap klaim membawa perintah DAN potongan keluarannya. Tanpa keluaran, tanpa",
5176
+ "klaim. 'Semua test lulus' tanpa keluaran adalah kegagalanmu, bukan laporan.",
5177
+ "",
5178
+ "Bentuk laporan: satu baris per test \u2014 lulus-dan-relevan \xB7 lulus-tapi-tak-membuktikan-apa-pun",
5179
+ "\xB7 regresi \xB7 gagal-palsu (+ sebabnya) \u2014 lalu satu putusan akhir: layak diumumkan selesai atau",
5180
+ "belum, dan apa yang kurang."
5181
+ ].join("\n")
5182
+ },
5183
+ {
5184
+ name: "edge-case-hunter",
5185
+ description: "Gunakan saat test yang ada hanya menguji jalur mulus dan kamu ingin batas-batas kontrak benar-benar tertutup. Ia menulis test yang hilang dan membuktikan tiap test baru merah dulu sebelum menyimpannya.",
5186
+ tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
5187
+ enabledByDefault: false,
5188
+ instructions: [
5189
+ "Kamu penambal jalur bahagia. Cakupan yang terlihat baik bukan urusanmu \u2014 kontrak yang tak",
5190
+ "pernah diuji itu urusanmu.",
5191
+ "",
5192
+ "Prosedur:",
5193
+ "1. Baca kontrak unit yang berubah: apa yang ia janjikan, apa yang ia terima, apa yang ia",
5194
+ " lakukan saat janji itu tak bisa dipenuhi.",
5195
+ "2. Enumerasi batas SECARA SISTEMATIS, jangan mengandalkan ingatan: kosong \xB7 null/undefined \xB7",
5196
+ " nol & negatif \xB7 unicode & string sangat panjang \xB7 urutan terbalik \xB7 kedatangan ganda",
5197
+ " (idempotensi) \xB7 kegagalan separuh jalan \xB7 timeout & retry \xB7 nilai asing dari luar batas",
5198
+ " kepercayaan (input pengguna, berkas konfigurasi, data dari mesin lain).",
5199
+ "3. Adu daftar itu dengan test yang sudah ada. Tandai yang belum tertutup.",
5200
+ "4. Tulis test yang hilang. Ikuti gaya berkas test tetangga \u2014 nama, struktur, helper.",
5201
+ "5. Jalankan.",
5202
+ "",
5203
+ "Gerbang bukti: setiap test baru WAJIB kamu tunjukkan MERAH dulu terhadap kode yang belum",
5204
+ "diperbaiki. Test yang lahir langsung hijau kamu laporkan sebagai 'tak membuktikan apa-apa' \u2014",
5205
+ "jangan disimpan diam-diam, karena ia akan tetap hijau saat kodenya kelak rusak.",
5206
+ "",
5207
+ "Batas: kamu menulis TEST. Jangan mengubah kode produksi agar test lulus \u2014 bila test barumu",
5208
+ "menemukan bug sungguhan, laporkan bugnya, biarkan test itu merah, dan katakan dengan jelas",
5209
+ "bahwa ia merah karena bug, bukan karena test-nya salah.",
5210
+ "",
5211
+ "Bentuk laporan: (a) batas yang kini tertutup \xB7 (b) batas yang sengaja dilewati + alasannya \xB7",
5212
+ "(c) bug yang ditemukan test baru."
5213
+ ].join("\n")
5214
+ },
5215
+ {
5216
+ name: "blast-radius",
5217
+ description: "Gunakan sesudah perubahan selesai untuk menemukan tempat LAIN yang seharusnya ikut berubah tapi tidak: daftar kolom, cermin tipe antar-paket, enum kembar, dokumen kontrak, tabel konstanta. Ia mencari kegagalan senyap \u2014 yang tak memunculkan satu pun error.",
5218
+ tools: ["Read", "Glob", "Grep", "Bash"],
5219
+ enabledByDefault: true,
5220
+ instructions: [
5221
+ "Kamu pencari cermin yang hanyut. Kelas bug yang kamu buru punya satu ciri: TIDAK ADA yang",
5222
+ "error. Satu kontrak hidup di beberapa tempat, satu tempat diperbarui, sisanya diam.",
5223
+ "",
5224
+ "Prosedur:",
5225
+ "1. Baca diff terhadap base. Tarik daftar yang berubah: simbol, kolom, nilai enum, kunci",
5226
+ " konfigurasi, nama berkas, bentuk payload.",
5227
+ "2. Untuk TIAP satu, sapu seluruh repo untuk semua tempat lain yang menyebutnya \u2014 ATAU yang",
5228
+ " seharusnya menyebutnya. Yang kedua ini yang penting, dan ia tak akan muncul dari pencarian",
5229
+ " nama saja. Tempat yang wajib kamu periksa:",
5230
+ " - daftar/array literal yang mencacah field atau kolom secara manual;",
5231
+ " - tabel konstanta & peta yang kuncinya harus lengkap tapi tak punya rujukan tipe;",
5232
+ " - tipe yang disalin (bukan diimpor) antar-paket, dan enum kembar;",
5233
+ " - skema validasi di batas HTTP vs bentuk yang benar-benar disimpan;",
5234
+ " - dokumen kontrak (API, data model) dan berkas contoh/konfigurasi;",
5235
+ " - berkas test yang mengunci bentuk lama.",
5236
+ "3. Laporkan yang belum ikut berubah.",
5237
+ "",
5238
+ "Gerbang bukti: tiap temuan menyebut `path:baris` DAN apa yang terjadi bila dibiarkan. Bila",
5239
+ "konsekuensinya 'gagal senyap' \u2014 nilai default palsu, kolom yang hilang tanpa error, cabang",
5240
+ "yang tak pernah dijalankan \u2014 NAIKKAN prioritasnya, jangan turunkan. Yang berteriak akan",
5241
+ "ketahuan sendiri; yang diam tidak.",
5242
+ "",
5243
+ "Bentuk laporan: daftar cermin yang hanyut, diurut dari yang paling senyap, tiap baris:",
5244
+ "jangkar \xB7 apa yang hanyut \xB7 akibat bila dibiarkan."
5245
+ ].join("\n")
5246
+ },
5247
+ {
5248
+ name: "spec-auditor",
5249
+ description: "Gunakan sebelum menutup pekerjaan untuk mengadu apa yang DIMINTA dengan apa yang benar-benar ada di diff. Ia menolak 'sepertinya sudah' dan memperlakukan kriteria tanpa jejak sebagai tak terpenuhi, walau kotaknya sudah tercentang.",
5250
+ tools: ["Read", "Glob", "Grep", "Bash"],
5251
+ enabledByDefault: false,
5252
+ instructions: [
5253
+ "Kamu pengadu janji. Kamu tak menilai bagus atau tidaknya kode \u2014 kamu menilai apakah yang",
5254
+ "diminta benar-benar ada.",
5255
+ "",
5256
+ "Prosedur:",
5257
+ "1. Baca sumber permintaannya: spec, plan, issue, atau deskripsi tugas. Bila ada beberapa,",
5258
+ " baca semuanya \u2014 plan bisa menyimpang dari spec, dan penyimpangan itu sendiri temuan.",
5259
+ "2. Ubah jadi daftar kriteria yang bisa diperiksa SATU PER SATU. Kalimat yang tak bisa",
5260
+ " diperiksa ('lebih baik', 'rapi') kamu tandai sebagai tak terukur, bukan kamu tafsirkan.",
5261
+ "3. Untuk tiap kriteria, cari JEJAKNYA di diff. Bukan di niat, bukan di komentar kode.",
5262
+ "4. Putuskan: terpenuhi (+jangkar) \xB7 tak terpenuhi \xB7 terpenuhi BERBEDA dari yang diminta \xB7",
5263
+ " dikerjakan TANPA diminta.",
5264
+ "",
5265
+ "Gerbang bukti:",
5266
+ "- Kriteria tanpa jangkar di diff = TAK TERPENUHI. Kotak yang sudah tercentang di berkas plan",
5267
+ " bukan bukti \u2014 ia klaim, dan klaim itu justru yang sedang kamu periksa.",
5268
+ "- Pekerjaan yang dikerjakan tanpa diminta dilaporkan TERPISAH, bukan dipuji. Ia menambah",
5269
+ " permukaan yang tak pernah diminta siapa pun untuk dipelihara.",
5270
+ "",
5271
+ "Bentuk laporan: tabel \u2014 kriteria \xB7 putusan \xB7 jangkar; lalu daftar pekerjaan di luar minta;",
5272
+ "lalu satu putusan akhir: boleh ditutup atau belum, dan apa yang kurang."
5273
+ ].join("\n")
5274
+ },
5275
+ {
5276
+ name: "security-reviewer",
5277
+ description: "Gunakan sebelum menggabungkan perubahan yang menyentuh route, handler, job, CLI, atau apa pun yang menerima input dari luar. Ia menelusuri jalur konkret dari input tak terpercaya sampai ke tempat ia melukai, dan menolak melaporkan kekhawatiran yang tak bisa ia buktikan jalurnya.",
5278
+ tools: ["Read", "Glob", "Grep", "Bash"],
5279
+ enabledByDefault: true,
5280
+ instructions: [
5281
+ "Kamu penelusur sumber-ke-sink. Daftar kekhawatiran umum tak mengubah apa pun; yang mengubah",
5282
+ "adalah satu jalur konkret dari input yang tak dipercaya sampai ke tempat ia melukai.",
5283
+ "",
5284
+ "Prosedur:",
5285
+ "1. Enumerasi TITIK MASUK yang tersentuh diff: route HTTP, handler pesan/webhook, job",
5286
+ " terjadwal, perintah CLI, pembaca berkas konfigurasi, dan apa pun yang membaca input",
5287
+ " pengguna atau data dari mesin lain.",
5288
+ "2. Untuk tiap titik masuk, telusuri input tak terpercaya sampai SINK: query basis data,",
5289
+ " `exec`/shell, path berkas, template/render, deserialisasi, permintaan keluar, redirect,",
5290
+ " dan apa pun yang ditulis ke log atau dikembalikan ke pemanggil.",
5291
+ "3. Di sepanjang jalur itu, periksa gerbang yang seharusnya ada:",
5292
+ " - autentikasi \u2014 siapa dia;",
5293
+ " - OTORISASI KEPEMILIKAN OBJEK \u2014 apakah dia berhak atas objek INI. Ini yang paling sering",
5294
+ " hilang, dan justru hilangnya di endpoint yang autentikasinya sudah benar;",
5295
+ " - validasi bentuk di batas, bukan di dalam;",
5296
+ " - batas ukuran & jumlah (payload, unggahan, paginasi, perulangan);",
5297
+ " - kredensial: bocor ke log, ke response, ke pesan galat, atau ikut ter-commit.",
5298
+ "",
5299
+ "Gerbang bukti \u2014 ini yang membedakanmu dari daftar kekhawatiran:",
5300
+ "- Temuan TANPA jalur konkret input \u2192 dampak TIDAK kamu laporkan. Tahan.",
5301
+ "- Sebutkan juga jalur mana saja yang sudah kamu telusuri dan BERSIH. Tanpa itu, diammu tak",
5302
+ " bisa dibedakan dari tidak memeriksa.",
5303
+ "- Jangan menilai dari nama fungsi atau kecocokan pola. Baca jalurnya.",
5304
+ "",
5305
+ "Bentuk laporan: per temuan \u2014 jalur (dengan jangkar) \xB7 dampak \xB7 perbaikan TERKECIL; lalu",
5306
+ "daftar titik masuk yang dinyatakan bersih."
5307
+ ].join("\n")
5308
+ },
5309
+ {
5310
+ name: "dep-auditor",
5311
+ description: "Gunakan saat diff menambah atau menaikkan versi dependensi. Ia memeriksa advisory, lisensi, tanda pemeliharaan, dan \u2014 yang paling sering terlewat \u2014 apakah fungsinya sudah tersedia tanpa dependensi baru itu.",
5312
+ tools: ["Read", "Glob", "Grep", "Bash", "WebSearch", "WebFetch"],
5313
+ enabledByDefault: false,
5314
+ instructions: [
5315
+ "Kamu gerbang rantai pasok. Satu dependensi masuk lewat satu baris diff dan tak pernah",
5316
+ "diperiksa lagi seumur hidup proyek \u2014 pemeriksaan itu terjadi sekarang atau tidak sama sekali.",
5317
+ "",
5318
+ "Prosedur:",
5319
+ "1. Dari diff, ambil dependensi yang BERTAMBAH atau NAIK VERSI (termasuk devDependencies).",
5320
+ " Lockfile ikut dibaca: dependensi transitif baru yang besar juga temuan.",
5321
+ "2. Untuk tiap satu, periksa dan sebutkan sumbernya:",
5322
+ " - advisory/CVE yang diketahui untuk rentang versi itu;",
5323
+ " - tanggal rilis terakhir & tanda pemeliharaan (isu terbuka menumpuk, maintainer tunggal);",
5324
+ " - lisensi, dan apakah ia cocok dengan lisensi proyek ini;",
5325
+ " - ukuran pohon transitifnya;",
5326
+ " - apakah paket menjalankan skrip saat instalasi.",
5327
+ "3. Pertanyaan yang paling sering dilewati, dan tanyakan SELALU: apakah fungsi yang dipakai",
5328
+ " sudah tersedia di dependensi yang SUDAH ada di proyek ini, atau di runtime-nya? Cek dulu",
5329
+ " sebelum menerima. Satu dependensi yang tak jadi masuk lebih berharga daripada sepuluh",
5330
+ " yang diaudit.",
5331
+ "",
5332
+ "Gerbang bukti: klaim CVE atau lisensi WAJIB membawa URL sumbernya. Tanpa sumber, tulis 'tak",
5333
+ "terverifikasi' \u2014 jangan hilangkan, dan jangan naikkan jadi fakta.",
5334
+ "",
5335
+ "Bentuk laporan: per dependensi \u2014 aman \xB7 aman dengan catatan \xB7 tolak (+ penggantinya, atau",
5336
+ "cara mengerjakannya tanpa dependensi itu)."
5337
+ ].join("\n")
5338
+ }
5339
+ ];
5340
+ BUILTIN_AGENT_NAMES = BUILTIN_AGENTS.map((a) => a.name);
5341
+ }
5342
+ });
5343
+
5076
5344
  // ../shared/src/agent-catalog.ts
5077
5345
  function modelsForRuntime(rt) {
5078
5346
  const claude = MODELS.map((m) => ({ id: m.id, label: m.label, runtime: "claude" }));
@@ -5355,6 +5623,36 @@ var init_lead = __esm({
5355
5623
  }
5356
5624
  });
5357
5625
 
5626
+ // ../shared/src/handled-by.ts
5627
+ function handledByOf(raw2) {
5628
+ if (raw2 === null || raw2 === void 0) return [];
5629
+ const p = zHandledBy.safeParse(raw2);
5630
+ return p.success ? p.data : [];
5631
+ }
5632
+ var zHandledByEntry, HANDLED_BY_MAX, zHandledBy, zHandledByView;
5633
+ var init_handled_by = __esm({
5634
+ "../shared/src/handled-by.ts"() {
5635
+ "use strict";
5636
+ init_zod();
5637
+ zHandledByEntry = external_exports.object({
5638
+ deviceId: external_exports.string().min(1),
5639
+ name: external_exports.string().min(1)
5640
+ });
5641
+ HANDLED_BY_MAX = 32;
5642
+ zHandledBy = external_exports.array(zHandledByEntry).max(HANDLED_BY_MAX).superRefine((list2, ctx) => {
5643
+ const seen = /* @__PURE__ */ new Set();
5644
+ for (const e of list2) {
5645
+ if (seen.has(e.deviceId)) {
5646
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `deviceId duplikat: ${e.deviceId}` });
5647
+ return;
5648
+ }
5649
+ seen.add(e.deviceId);
5650
+ }
5651
+ });
5652
+ zHandledByView = zHandledByEntry.extend({ revoked: external_exports.boolean().default(false) });
5653
+ }
5654
+ });
5655
+
5358
5656
  // ../shared/src/prd-status.ts
5359
5657
  function prdBranchFor(prdPath) {
5360
5658
  if (!prdPath.startsWith(PRD_DIR) || !prdPath.endsWith(".md")) return null;
@@ -5476,7 +5774,7 @@ var init_cron_expr = __esm({
5476
5774
  function flowForSource(source) {
5477
5775
  return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : source === "no_effort" ? "no_effort" : "feature";
5478
5776
  }
5479
- var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
5777
+ var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zSetupApply, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, PROVISION_PROFILES, COMPONENT_IDS, zProvision, UPDATE_RESTART_EXIT, CONFIG_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
5480
5778
  var init_dto = __esm({
5481
5779
  "../shared/src/dto.ts"() {
5482
5780
  "use strict";
@@ -5484,6 +5782,7 @@ var init_dto = __esm({
5484
5782
  init_entities();
5485
5783
  init_lead();
5486
5784
  init_auto_merge();
5785
+ init_handled_by();
5487
5786
  init_prd_status();
5488
5787
  init_enums();
5489
5788
  init_spec_source();
@@ -5530,6 +5829,8 @@ var init_dto = __esm({
5530
5829
  kind: zProjectKind,
5531
5830
  repoDir: external_exports.string().optional(),
5532
5831
  gitRemote: external_exports.string().optional(),
5832
+ // SPEC-880 · ADR-0135 · penanda "ditangani oleh" boleh di-set sejak awal (disync).
5833
+ handledBy: zHandledBy.optional(),
5533
5834
  desc: external_exports.string().default("")
5534
5835
  });
5535
5836
  zProjectId = external_exports.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "slug tak sah");
@@ -5539,6 +5840,10 @@ var init_dto = __esm({
5539
5840
  desc: external_exports.string().optional(),
5540
5841
  gitRemote: external_exports.string().optional(),
5541
5842
  // SPEC-213 · set git remote resmi project
5843
+ // SPEC-880 · ADR-0135 · daftar hanoman client pemegang project (DISYNC — beda dari repoDir &
5844
+ // opt-in di bawah yang lokal per-instance). `null` maupun `[]` = kosongkan. Digerbangi
5845
+ // `checkHandledBy`: deviceId wajib dikenal HANYA bila instance ini punya katalog device.
5846
+ handledBy: zHandledBy.nullable().optional(),
5542
5847
  repoDir: external_exports.string().nullable().optional(),
5543
5848
  // SPEC-217 · path default/server editable (null = kosongkan)
5544
5849
  schedulerOptIn: external_exports.boolean().optional(),
@@ -5614,6 +5919,10 @@ var init_dto = __esm({
5614
5919
  // SPEC-294 · opt-in scheduler otonom
5615
5920
  leadOptIn: external_exports.boolean().default(false),
5616
5921
  // SPEC-409 · ADR-0091 · opt-in hanoman-lead
5922
+ // SPEC-880 · ADR-0135 · penanda "ditangani oleh" yang SUDAH diperkaya: `name` = nama hidup bila
5923
+ // baris DeviceToken-nya ada di instance ini, else snapshot tersimpan; `revoked` diturunkan.
5924
+ // `[]` = belum ditetapkan — kolom NULL tak pernah bocor ke UI sebagai bentuk kedua "kosong".
5925
+ handledBy: zHandledByView.array().default([]),
5617
5926
  autoMerge: zAutoMerge.nullable().default(null)
5618
5927
  });
5619
5928
  zSchedulerQueueItem = external_exports.object({
@@ -5917,6 +6226,11 @@ var init_dto = __esm({
5917
6226
  zLogin = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(1) });
5918
6227
  zSignup = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(8) });
5919
6228
  zSetup = zSignup.extend({ setupToken: external_exports.string().min(1) });
6229
+ zSetupApply = external_exports.object({
6230
+ deployment: external_exports.enum(["local", "public"]),
6231
+ hardening: external_exports.boolean(),
6232
+ acknowledgedUnhardened: external_exports.boolean().optional()
6233
+ });
5920
6234
  zChangePassword = external_exports.object({
5921
6235
  currentPassword: external_exports.string().min(1),
5922
6236
  newPassword: external_exports.string().min(8)
@@ -5944,7 +6258,27 @@ var init_dto = __esm({
5944
6258
  });
5945
6259
  zAttest = external_exports.object({ note: external_exports.string().max(500).optional() });
5946
6260
  zRemediate = external_exports.object({ items: external_exports.array(external_exports.string()).min(1).max(64) });
6261
+ PROVISION_PROFILES = ["lab", "production"];
6262
+ COMPONENT_IDS = [
6263
+ "base",
6264
+ "node",
6265
+ "hanoman",
6266
+ "caddy",
6267
+ "podman",
6268
+ "agent-image",
6269
+ "claude",
6270
+ "codex",
6271
+ "gh"
6272
+ ];
6273
+ zProvision = external_exports.object({
6274
+ items: external_exports.array(external_exports.enum(COMPONENT_IDS)).min(1).max(16),
6275
+ profile: external_exports.enum(PROVISION_PROFILES),
6276
+ domain: external_exports.string().min(1).regex(HOST_RE).optional(),
6277
+ confirm: external_exports.boolean().optional(),
6278
+ force: external_exports.boolean().optional()
6279
+ });
5947
6280
  UPDATE_RESTART_EXIT = 75;
6281
+ CONFIG_RESTART_EXIT = 76;
5948
6282
  zUpdateApplyBody = external_exports.object({ confirm: external_exports.boolean().optional() });
5949
6283
  zTicketView = external_exports.object({
5950
6284
  id: external_exports.string(),
@@ -6160,6 +6494,11 @@ var init_api = __esm({
6160
6494
  terminalSession: (id) => `${API}/terminal/sessions/${id}`,
6161
6495
  terminalSteer: (id) => `${API}/terminal/sessions/${id}/steer`,
6162
6496
  terminalInterrupt: (id) => `${API}/terminal/sessions/${id}/interrupt`,
6497
+ // SPEC-899 · ADR-0142 · inbox keputusan. Di bawah prefix /terminal supaya ikut capability
6498
+ // `sessions` yang sudah ada — GET menurunkan cabang bacanya, POST cabang tulisnya, tanpa satu
6499
+ // baris pun perubahan di services/agent-capabilities.ts.
6500
+ terminalDialog: (id) => `${API}/terminal/sessions/${id}/dialog`,
6501
+ terminalDialogAnswer: (id) => `${API}/terminal/sessions/${id}/dialog/answer`,
6163
6502
  terminalPhases: (id) => `${API}/terminal/sessions/${id}/phases`,
6164
6503
  // SPEC-230 · review + integrate ber-skop sesi (sesi project-level PRD, tanpa Spec).
6165
6504
  sessionReview: (id) => `${API}/terminal/sessions/${id}/review`,
@@ -6191,6 +6530,11 @@ var init_api = __esm({
6191
6530
  vpsItemAttest: (id, itemId) => `${API}/vps/${id}/items/${itemId}/attest`,
6192
6531
  vpsRemediatePreview: (id) => `${API}/vps/${id}/remediate/preview`,
6193
6532
  vpsRemediate: (id) => `${API}/vps/${id}/remediate`,
6533
+ // SPEC-883 · provisioning berbasis katalog
6534
+ vpsComponents: () => `${API}/vps/components`,
6535
+ vpsProbe: (id) => `${API}/vps/${id}/probe`,
6536
+ vpsProvisionPreview: (id) => `${API}/vps/${id}/provision/preview`,
6537
+ vpsProvision: (id) => `${API}/vps/${id}/provision`,
6194
6538
  // SPEC-169 · auth
6195
6539
  authStatus: `${API}/auth/status`,
6196
6540
  authSetup: `${API}/auth/setup`,
@@ -6199,6 +6543,9 @@ var init_api = __esm({
6199
6543
  authUsers: `${API}/auth/users`,
6200
6544
  authUser: (id) => `${API}/auth/users/${id}`,
6201
6545
  authChangePassword: `${API}/auth/change-password`,
6546
+ // SPEC-884 · ADR-0139 · wizard setup awal
6547
+ setupStatus: `${API}/setup/status`,
6548
+ setupApply: `${API}/setup`,
6202
6549
  // SPEC-213 · device token + activity log
6203
6550
  deviceTokens: `${API}/device-tokens`,
6204
6551
  deviceToken: (id) => `${API}/device-tokens/${id}`,
@@ -6918,6 +7265,9 @@ var init_webhook = __esm({
6918
7265
  "gitRemote",
6919
7266
  "stack",
6920
7267
  "helpEnabled",
7268
+ // SPEC-880 · ADR-0135 · penerima harus bisa tahu mesin mana yang memegang project ini
7269
+ // tanpa mendiff dua amplop. Entrinya snapshot: [{deviceId,name}].
7270
+ "handledBy",
6921
7271
  "schedulerOptIn",
6922
7272
  "leadOptIn",
6923
7273
  "autoMerge",
@@ -6928,7 +7278,7 @@ var init_webhook = __esm({
6928
7278
  cascade: ["spec", "ticket", "customAgent", "githubIssue"],
6929
7279
  events: {
6930
7280
  created: { type: "project.created", label: "Project ditambah", when: "Project baru terdaftar di workspace." },
6931
- updated: { type: "project.updated", label: "Project diubah", when: "Nama, deskripsi, stack, remote, atau opt-in scheduler/lead/Help Center berubah." },
7281
+ updated: { type: "project.updated", label: "Project diubah", when: 'Nama, deskripsi, stack, remote, penanda "ditangani oleh", atau opt-in scheduler/lead/Help Center berubah.' },
6932
7282
  deleted: { type: "project.deleted", label: "Project dihapus", when: "Project dihapus. data.cascade menyebut jumlah anak yang ikut terhapus \u2014 anaknya sendiri TIDAK memancarkan deleted, sebab cascade dieksekusi SQLite, di luar jangkauan tap." }
6933
7283
  },
6934
7284
  sample: {
@@ -6939,6 +7289,7 @@ var init_webhook = __esm({
6939
7289
  gitRemote: "git@github.com:nafanesia/hanoman.git",
6940
7290
  stack: "ts",
6941
7291
  helpEnabled: false,
7292
+ handledBy: [{ deviceId: "clq0device1", name: "hm-dena" }],
6942
7293
  schedulerOptIn: true,
6943
7294
  leadOptIn: false,
6944
7295
  autoMerge: null,
@@ -7912,6 +8263,27 @@ var init_terminal_io = __esm({
7912
8263
  }
7913
8264
  });
7914
8265
 
8266
+ // ../shared/src/session-dialog.ts
8267
+ var zSessionDialogAnswer;
8268
+ var init_session_dialog = __esm({
8269
+ "../shared/src/session-dialog.ts"() {
8270
+ "use strict";
8271
+ init_zod();
8272
+ zSessionDialogAnswer = external_exports.object({
8273
+ screenHash: external_exports.string().min(1),
8274
+ choice: external_exports.number().int().positive().optional(),
8275
+ choices: external_exports.array(external_exports.number().int().positive()).max(32).optional(),
8276
+ text: external_exports.string().trim().min(1).max(16e3).optional()
8277
+ }).refine(
8278
+ (v) => !(v.choice !== void 0 && v.choices !== void 0),
8279
+ { message: "choice dan choices tak boleh dikirim bersamaan" }
8280
+ ).refine(
8281
+ (v) => v.choice !== void 0 || v.choices !== void 0 || v.text !== void 0,
8282
+ { message: "jawaban kosong" }
8283
+ );
8284
+ }
8285
+ });
8286
+
7915
8287
  // ../shared/src/index.ts
7916
8288
  var init_src = __esm({
7917
8289
  "../shared/src/index.ts"() {
@@ -7924,6 +8296,7 @@ var init_src = __esm({
7924
8296
  init_entities();
7925
8297
  init_agent();
7926
8298
  init_custom_agent();
8299
+ init_builtin_agents();
7927
8300
  init_agent_catalog();
7928
8301
  init_github();
7929
8302
  init_lead();
@@ -7943,10 +8316,12 @@ var init_src = __esm({
7943
8316
  init_webhook();
7944
8317
  init_mcp();
7945
8318
  init_auto_merge();
8319
+ init_handled_by();
7946
8320
  init_changelog();
7947
8321
  init_cron_expr();
7948
8322
  init_terminal_workspace();
7949
8323
  init_terminal_io();
8324
+ init_session_dialog();
7950
8325
  }
7951
8326
  });
7952
8327
 
@@ -8618,7 +8993,7 @@ var init_settings = __esm({
8618
8993
  const f = `'${decisionFile.split("'").join("'\\''")}'`;
8619
8994
  hooks2.Notification = [{ hooks: [{
8620
8995
  type: "command",
8621
- command: `grep -qiE 'idle|permission|waiting for|needs.?input' && echo waiting >> ${f} || true`
8996
+ command: `grep -qiE 'idle|permission|waiting for|needs.?input' && { [ -s ${f} ] || date +%s > ${f}; } || true`
8622
8997
  }] }];
8623
8998
  hooks2.UserPromptSubmit = [{ hooks: [{ type: "command", command: `: > ${f}` }] }];
8624
8999
  }
@@ -8691,7 +9066,7 @@ function codexHookArgs(o) {
8691
9066
  const stop = [];
8692
9067
  const submit = [];
8693
9068
  if (o.decisionFile) {
8694
- stop.push(`echo waiting >> ${shq(o.decisionFile)}`);
9069
+ stop.push(`[ -s ${shq(o.decisionFile)} ] || date +%s > ${shq(o.decisionFile)}`);
8695
9070
  submit.push(`: > ${shq(o.decisionFile)}`);
8696
9071
  }
8697
9072
  if (o.goalGate) stop.push(`sh ${shq(o.goalGate)}`);
@@ -8852,6 +9227,24 @@ function agentRosterBlock(defs) {
8852
9227
  }
8853
9228
  return lines2.join("\n");
8854
9229
  }
9230
+ function agentDelegationClause(defs) {
9231
+ if (defs.length === 0) return "";
9232
+ return [
9233
+ "",
9234
+ "## Subagent yang tersedia",
9235
+ "",
9236
+ "Sesi ini punya subagent berikut. Delegasikan saat tugasnya cocok \u2014 konteks mereka TERPISAH",
9237
+ "dari milikmu, jadi menyerahkan penyapuan & verifikasi ke mereka MENGHEMAT konteksmu sendiri,",
9238
+ "bukan memboroskannya.",
9239
+ "",
9240
+ ...defs.map((d) => `- **${d.name}** \u2014 ${d.description}`),
9241
+ "",
9242
+ `Panggil lewat tool ${MENTION_TOOL} dengan nama agennya. Mereka tak bisa mendelegasikan lagi,`,
9243
+ "jadi tak ada rantai panggilan yang perlu kamu jaga. Laporan mereka adalah MASUKAN \u2014 kamu yang",
9244
+ "memutuskan, dan kamu yang bertanggung jawab atas hasilnya.",
9245
+ ""
9246
+ ].join("\n");
9247
+ }
8855
9248
  var liveMentions;
8856
9249
  var init_custom_agents = __esm({
8857
9250
  "../runner/src/custom-agents.ts"() {
@@ -9228,6 +9621,164 @@ var init_spawn_helper = __esm({
9228
9621
  }
9229
9622
  });
9230
9623
 
9624
+ // ../runner/src/config-env.ts
9625
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
9626
+ import { join as join4 } from "node:path";
9627
+ function configEnvPath(home3) {
9628
+ return join4(home3, CONFIG_ENV_FILE);
9629
+ }
9630
+ function parseConfigEnv(text) {
9631
+ const out4 = {};
9632
+ for (const raw2 of text.split("\n")) {
9633
+ const line = raw2.trim();
9634
+ if (!line || line.startsWith("#")) continue;
9635
+ const eq = line.indexOf("=");
9636
+ if (eq <= 0) continue;
9637
+ out4[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
9638
+ }
9639
+ return out4;
9640
+ }
9641
+ function formatConfigEnv(values) {
9642
+ const lines2 = Object.entries(values).filter(([, v]) => v.trim() !== "").map(([k, v]) => `${k}=${v.trim()}`);
9643
+ return lines2.length ? `${lines2.join("\n")}
9644
+ ` : "";
9645
+ }
9646
+ function readConfigEnv(home3) {
9647
+ try {
9648
+ return parseConfigEnv(readFileSync3(configEnvPath(home3), "utf8"));
9649
+ } catch {
9650
+ return {};
9651
+ }
9652
+ }
9653
+ function writeConfigEnv(home3, values) {
9654
+ mkdirSync2(home3, { recursive: true, mode: 448 });
9655
+ const path2 = configEnvPath(home3);
9656
+ writeFileSync(path2, formatConfigEnv(values), { mode: 384 });
9657
+ chmodSync2(path2, 384);
9658
+ }
9659
+ var CONFIG_ENV_FILE;
9660
+ var init_config_env = __esm({
9661
+ "../runner/src/config-env.ts"() {
9662
+ "use strict";
9663
+ CONFIG_ENV_FILE = "config.env";
9664
+ }
9665
+ });
9666
+
9667
+ // ../runner/src/runtime-profile.ts
9668
+ function resolveHardening(env) {
9669
+ if (env.HANOMAN_HARDENING === "1") return true;
9670
+ return env.HANOMAN_SESSION_SANDBOX === "podman" || filled(env.HANOMAN_PUBLIC_ORIGINS) || filled(env.HANOMAN_TRUST_PROXY);
9671
+ }
9672
+ function resolveDeployment(env) {
9673
+ if (resolveHardening(env)) return "public";
9674
+ return env.HANOMAN_DEPLOYMENT === "public" ? "public" : "local";
9675
+ }
9676
+ var filled;
9677
+ var init_runtime_profile = __esm({
9678
+ "../runner/src/runtime-profile.ts"() {
9679
+ "use strict";
9680
+ filled = (v) => !!v && v.trim() !== "";
9681
+ }
9682
+ });
9683
+
9684
+ // ../runner/src/sandbox-probe.ts
9685
+ import { execFileSync } from "node:child_process";
9686
+ import { accessSync, constants } from "node:fs";
9687
+ import { isAbsolute as isAbsolute3 } from "node:path";
9688
+ function trustProxyOk(raw2) {
9689
+ const v = raw2?.trim();
9690
+ if (!v) return false;
9691
+ if (/^[1-9]\d*$/.test(v)) return true;
9692
+ const values = v.split(",").map((s2) => s2.trim()).filter(Boolean);
9693
+ if (!values.length) return false;
9694
+ return !values.some((s2) => s2 === "true" || !s2.includes("/") && s2 !== "loopback");
9695
+ }
9696
+ function prerequisites(env, facts) {
9697
+ const network = env.HANOMAN_SESSION_NETWORK ?? "hanoman-egress";
9698
+ const credentialDir = env.HANOMAN_AGENT_CREDENTIAL_DIR;
9699
+ const scanner = env.HANOMAN_UPLOAD_SCANNER?.trim();
9700
+ return [
9701
+ {
9702
+ id: "podman",
9703
+ label: "Podman rootless",
9704
+ ok: !!facts.podman && facts.rootless,
9705
+ detail: facts.podman ? facts.rootless ? facts.podman : `${facts.podman} \u2014 berjalan rootful, butuh rootless` : "podman tak ada"
9706
+ },
9707
+ {
9708
+ id: "network",
9709
+ label: `Network "${network}"`,
9710
+ ok: facts.networkExists,
9711
+ detail: facts.networkExists ? network : `network ${network} belum dibuat`
9712
+ },
9713
+ {
9714
+ id: "egress-proxy",
9715
+ label: "Egress proxy",
9716
+ ok: filled2(env.HANOMAN_EGRESS_PROXY),
9717
+ detail: env.HANOMAN_EGRESS_PROXY ?? "HANOMAN_EGRESS_PROXY belum disetel"
9718
+ },
9719
+ {
9720
+ id: "credential-dir",
9721
+ label: "Dir credential agen",
9722
+ ok: filled2(credentialDir) && facts.credentialDirReadable,
9723
+ detail: !filled2(credentialDir) ? "HANOMAN_AGENT_CREDENTIAL_DIR belum disetel" : facts.credentialDirReadable ? credentialDir : `${credentialDir} \u2014 tak terbaca`
9724
+ },
9725
+ {
9726
+ id: "control-origin",
9727
+ label: "Control origin",
9728
+ ok: filled2(env.HANOMAN_CONTROL_ORIGINS),
9729
+ detail: env.HANOMAN_CONTROL_ORIGINS ?? "HANOMAN_CONTROL_ORIGINS belum disetel"
9730
+ },
9731
+ {
9732
+ id: "trust-proxy",
9733
+ label: "Trusted proxy hop/CIDR",
9734
+ ok: trustProxyOk(env.HANOMAN_TRUST_PROXY),
9735
+ detail: filled2(env.HANOMAN_TRUST_PROXY) ? trustProxyOk(env.HANOMAN_TRUST_PROXY) ? env.HANOMAN_TRUST_PROXY : "harus hop atau CIDR eksplisit" : "HANOMAN_TRUST_PROXY belum disetel"
9736
+ },
9737
+ {
9738
+ id: "upload-scanner",
9739
+ label: "Scanner upload",
9740
+ ok: !!scanner && isAbsolute3(scanner),
9741
+ detail: !scanner ? "HANOMAN_UPLOAD_SCANNER belum disetel" : isAbsolute3(scanner) ? scanner : `${scanner} \u2014 path harus absolut`
9742
+ }
9743
+ ];
9744
+ }
9745
+ function allReady(rows) {
9746
+ return rows.every((r) => r.ok);
9747
+ }
9748
+ function run(file, args) {
9749
+ try {
9750
+ return execFileSync(file, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9751
+ } catch {
9752
+ return null;
9753
+ }
9754
+ }
9755
+ function collectProbeFacts(env) {
9756
+ const podman = run("podman", ["--version"]);
9757
+ const network = env.HANOMAN_SESSION_NETWORK ?? "hanoman-egress";
9758
+ const dir2 = env.HANOMAN_AGENT_CREDENTIAL_DIR;
9759
+ let credentialDirReadable = false;
9760
+ if (dir2) {
9761
+ try {
9762
+ accessSync(dir2, constants.R_OK);
9763
+ credentialDirReadable = true;
9764
+ } catch {
9765
+ }
9766
+ }
9767
+ return {
9768
+ podman,
9769
+ rootless: podman ? run("podman", ["info", "--format", "{{.Host.Security.Rootless}}"]) === "true" : false,
9770
+ networkExists: podman ? run("podman", ["network", "exists", network]) !== null : false,
9771
+ credentialDirReadable
9772
+ };
9773
+ }
9774
+ var filled2;
9775
+ var init_sandbox_probe = __esm({
9776
+ "../runner/src/sandbox-probe.ts"() {
9777
+ "use strict";
9778
+ filled2 = (v) => !!v && v.trim() !== "";
9779
+ }
9780
+ });
9781
+
9231
9782
  // ../runner/src/index.ts
9232
9783
  var init_src2 = __esm({
9233
9784
  "../runner/src/index.ts"() {
@@ -9248,6 +9799,9 @@ var init_src2 = __esm({
9248
9799
  init_skills();
9249
9800
  init_telegram_operator();
9250
9801
  init_spawn_helper();
9802
+ init_config_env();
9803
+ init_runtime_profile();
9804
+ init_sandbox_probe();
9251
9805
  }
9252
9806
  });
9253
9807
 
@@ -9382,7 +9936,7 @@ var init_tap = __esm({
9382
9936
  });
9383
9937
 
9384
9938
  // src/db.ts
9385
- import { mkdirSync as mkdirSync2 } from "node:fs";
9939
+ import { mkdirSync as mkdirSync3 } from "node:fs";
9386
9940
  import { dirname as dirname5, resolve as resolve7 } from "node:path";
9387
9941
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9388
9942
  import { PrismaClient } from "@prisma/client";
@@ -9399,7 +9953,7 @@ var init_db = __esm({
9399
9953
  if (notice) console.warn(notice);
9400
9954
  url = resolveDbUrl(process.env, schemaDir);
9401
9955
  process.env.DATABASE_URL = url;
9402
- mkdirSync2(dirname5(dbFilePath(url)), { recursive: true, mode: 448 });
9956
+ mkdirSync3(dirname5(dbFilePath(url)), { recursive: true, mode: 448 });
9403
9957
  base = new PrismaClient();
9404
9958
  void base.$queryRawUnsafe("PRAGMA journal_mode=WAL").catch((e) => console.warn("tak bisa menyetel journal_mode=WAL:", e));
9405
9959
  prisma = base.$extends(webhookTap(base));
@@ -9416,12 +9970,12 @@ var init_stage_machine = __esm({
9416
9970
  });
9417
9971
 
9418
9972
  // src/services/session-phases.ts
9419
- import { readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
9973
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
9420
9974
  function recorded(file) {
9421
9975
  const out4 = /* @__PURE__ */ new Map();
9422
9976
  let raw2;
9423
9977
  try {
9424
- raw2 = readFileSync3(file, "utf8");
9978
+ raw2 = readFileSync4(file, "utf8");
9425
9979
  } catch {
9426
9980
  return out4;
9427
9981
  }
@@ -9471,7 +10025,7 @@ function planComplete(worktree, specId) {
9471
10025
  for (const n2 of names) {
9472
10026
  if (!re.test(n2.toLowerCase())) continue;
9473
10027
  try {
9474
- if (/^[ \t]*- \[ \]/m.test(readFileSync3(`${dir2}/${n2}`, "utf8"))) return false;
10028
+ if (/^[ \t]*- \[ \]/m.test(readFileSync4(`${dir2}/${n2}`, "utf8"))) return false;
9475
10029
  } catch {
9476
10030
  }
9477
10031
  }
@@ -9525,9 +10079,9 @@ var init_session_id = __esm({
9525
10079
  });
9526
10080
 
9527
10081
  // src/services/secret-box.ts
9528
- import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
10082
+ import { chmodSync as chmodSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
9529
10083
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
9530
- import { dirname as dirname6, join as join4 } from "node:path";
10084
+ import { dirname as dirname6, join as join5 } from "node:path";
9531
10085
  function isEncrypted(value) {
9532
10086
  return value.startsWith(ENC_PREFIX);
9533
10087
  }
@@ -9559,7 +10113,7 @@ function fromEnv(raw2) {
9559
10113
  return null;
9560
10114
  }
9561
10115
  function secretKeyPath() {
9562
- return join4(resolveHome(), "secret.key");
10116
+ return join5(resolveHome(), "secret.key");
9563
10117
  }
9564
10118
  function secretKey() {
9565
10119
  if (cached) return cached;
@@ -9572,15 +10126,15 @@ function secretKey() {
9572
10126
  }
9573
10127
  const path2 = secretKeyPath();
9574
10128
  if (existsSync5(path2)) {
9575
- const key2 = Buffer.from(readFileSync4(path2, "utf8").trim(), "base64url");
10129
+ const key2 = Buffer.from(readFileSync5(path2, "utf8").trim(), "base64url");
9576
10130
  if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path2} rusak \u2014 panjangnya bukan 32 byte`);
9577
10131
  cached = key2;
9578
10132
  return key2;
9579
10133
  }
9580
10134
  const key = randomBytes(KEY_BYTES);
9581
- mkdirSync3(dirname6(path2), { recursive: true });
9582
- writeFileSync(path2, key.toString("base64url"), { mode: 384 });
9583
- chmodSync2(path2, 384);
10135
+ mkdirSync4(dirname6(path2), { recursive: true });
10136
+ writeFileSync2(path2, key.toString("base64url"), { mode: 384 });
10137
+ chmodSync3(path2, 384);
9584
10138
  cached = key;
9585
10139
  return key;
9586
10140
  }
@@ -9771,6 +10325,7 @@ var init_ssrf = __esm({
9771
10325
  import { lookup as dnsLookup2 } from "node:dns/promises";
9772
10326
  import { request as httpRequest } from "node:http";
9773
10327
  import { request as httpsRequest } from "node:https";
10328
+ import { createGunzip } from "node:zlib";
9774
10329
  async function pinnedRequest(input) {
9775
10330
  return new Promise((resolve21, reject2) => {
9776
10331
  const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
@@ -9790,21 +10345,43 @@ async function pinnedRequest(input) {
9790
10345
  lookup: (_host, opts, callback) => opts.all ? callback(null, [{ address: input.address, family: input.family }]) : callback(null, input.address, input.family),
9791
10346
  timeout: input.connectMs
9792
10347
  }, (response) => {
10348
+ const dimampat = input.acceptEncoding === "gzip" && String(response.headers["content-encoding"] ?? "").toLowerCase() === "gzip";
10349
+ const capTerurai = input.maxDecodedBytes ?? input.maxResponseBytes;
9793
10350
  const chunks = [];
9794
- let bytes = 0;
10351
+ let kabel = 0, terurai = 0;
10352
+ const selesai = () => resolve21({
10353
+ status: response.statusCode ?? 0,
10354
+ headers: response.headers,
10355
+ body: Buffer.concat(chunks)
10356
+ });
10357
+ const sink2 = dimampat ? createGunzip() : null;
10358
+ if (sink2) {
10359
+ sink2.on("data", (chunk) => {
10360
+ terurai += chunk.length;
10361
+ if (terurai > capTerurai) {
10362
+ sink2.destroy(new Error("outbound response terurai terlalu besar"));
10363
+ response.destroy();
10364
+ return;
10365
+ }
10366
+ chunks.push(chunk);
10367
+ });
10368
+ sink2.on("end", selesai);
10369
+ sink2.on("error", reject2);
10370
+ }
9795
10371
  response.on("data", (chunk) => {
9796
- bytes += chunk.length;
9797
- if (bytes > input.maxResponseBytes) {
10372
+ kabel += chunk.length;
10373
+ if (kabel > input.maxResponseBytes) {
9798
10374
  response.destroy(new Error("outbound response terlalu besar"));
9799
10375
  return;
9800
10376
  }
9801
- chunks.push(chunk);
10377
+ if (sink2) sink2.write(chunk);
10378
+ else chunks.push(chunk);
9802
10379
  });
9803
- response.on("end", () => resolve21({
9804
- status: response.statusCode ?? 0,
9805
- headers: response.headers,
9806
- body: Buffer.concat(chunks)
9807
- }));
10380
+ response.on("end", () => {
10381
+ if (sink2) sink2.end();
10382
+ else selesai();
10383
+ });
10384
+ response.on("error", reject2);
9808
10385
  });
9809
10386
  const timer9 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
9810
10387
  timer9.unref?.();
@@ -9838,7 +10415,7 @@ var init_safe_outbound_request = __esm({
9838
10415
  // src/services/uploads.ts
9839
10416
  import { randomUUID } from "node:crypto";
9840
10417
  import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
9841
- import { join as join5, resolve as resolve8 } from "node:path";
10418
+ import { join as join6, resolve as resolve8 } from "node:path";
9842
10419
  function extFor(mimeType) {
9843
10420
  return EXT[mimeType] ?? ".bin";
9844
10421
  }
@@ -9847,12 +10424,12 @@ function uploadDir() {
9847
10424
  }
9848
10425
  function sessionUploadDir(sessionId2) {
9849
10426
  if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
9850
- return join5(uploadDir(), "terminal", sessionId2);
10427
+ return join6(uploadDir(), "terminal", sessionId2);
9851
10428
  }
9852
10429
  async function saveSessionUpload(sessionId2, buf, mimeType) {
9853
10430
  const dir2 = sessionUploadDir(sessionId2);
9854
10431
  await mkdir(dir2, { recursive: true, mode: 448 });
9855
- const path2 = join5(dir2, `${randomUUID()}${extFor(mimeType)}`);
10432
+ const path2 = join6(dir2, `${randomUUID()}${extFor(mimeType)}`);
9856
10433
  await writeFile(path2, buf, { mode: 384 });
9857
10434
  return { path: path2, size: buf.length };
9858
10435
  }
@@ -9868,11 +10445,11 @@ async function dropSessionUploads(sessionId2) {
9868
10445
  }
9869
10446
  async function readUpload(storageKey) {
9870
10447
  const safe = storageKey.replace(/[/\\]/g, "");
9871
- return readFile2(join5(uploadDir(), safe));
10448
+ return readFile2(join6(uploadDir(), safe));
9872
10449
  }
9873
10450
  async function readUploadOrFetch(storageKey) {
9874
10451
  const safe = storageKey.replace(/[/\\]/g, "");
9875
- const target2 = join5(uploadDir(), safe);
10452
+ const target2 = join6(uploadDir(), safe);
9876
10453
  try {
9877
10454
  return await readFile2(target2);
9878
10455
  } catch {
@@ -9899,7 +10476,7 @@ async function readUploadOrFetch(storageKey) {
9899
10476
  }
9900
10477
  async function deleteUpload(storageKey) {
9901
10478
  const safe = storageKey.replace(/[/\\]/g, "");
9902
- await unlink(join5(uploadDir(), safe)).catch(() => {
10479
+ await unlink(join6(uploadDir(), safe)).catch(() => {
9903
10480
  });
9904
10481
  }
9905
10482
  var EXT, SESSION_ID;
@@ -9935,16 +10512,16 @@ function readChoiceDialog(paneText2) {
9935
10512
  if (m) found.push({ n: Number(m[1]), label: cleanLabel(m[2] ?? "") });
9936
10513
  }
9937
10514
  if (found.length < 2) return null;
9938
- const run2 = [];
10515
+ const run3 = [];
9939
10516
  let expected = found[found.length - 1].n;
9940
10517
  for (let i = found.length - 1; i >= 0 && expected >= 1; i--) {
9941
10518
  const row = found[i];
9942
10519
  if (row.n !== expected) break;
9943
- run2.unshift(row);
10520
+ run3.unshift(row);
9944
10521
  expected -= 1;
9945
10522
  }
9946
- if (run2.length < 2 || run2[0].n !== 1) return null;
9947
- const rows = run2.map((r) => {
10523
+ if (run3.length < 2 || run3[0].n !== 1) return null;
10524
+ const rows = run3.map((r) => {
9948
10525
  const m = CHECK.exec(r.label);
9949
10526
  const label = m ? (m[2] ?? "").trim() : r.label;
9950
10527
  return {
@@ -10152,7 +10729,7 @@ var init_tui_dialog = __esm({
10152
10729
 
10153
10730
  // src/services/session-sandbox.ts
10154
10731
  function assertRuntimeBoundary(env, runtime) {
10155
- if (env.NODE_ENV !== "production") return;
10732
+ if (!resolveHardening(env)) return;
10156
10733
  if (runtime.uid === 0) throw new Error("production Hanoman harus berjalan sebagai user non-root");
10157
10734
  if (env.HANOMAN_SESSION_SANDBOX !== "podman")
10158
10735
  throw new Error("HANOMAN_SESSION_SANDBOX=podman wajib di production");
@@ -10207,7 +10784,7 @@ function sandboxArgv(input) {
10207
10784
  }
10208
10785
  function sandboxArgvFromEnv(input) {
10209
10786
  const env = input.env ?? process.env;
10210
- const mode = env.HANOMAN_SESSION_SANDBOX ?? (env.NODE_ENV === "production" ? "required" : "off");
10787
+ const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
10211
10788
  if (mode === "off") return null;
10212
10789
  if (mode !== "podman") throw new Error("session sandbox production tidak dikonfigurasi");
10213
10790
  const credentialDir = env.HANOMAN_AGENT_CREDENTIAL_DIR;
@@ -10230,27 +10807,28 @@ var quote;
10230
10807
  var init_session_sandbox = __esm({
10231
10808
  "src/services/session-sandbox.ts"() {
10232
10809
  "use strict";
10810
+ init_src2();
10233
10811
  quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
10234
10812
  }
10235
10813
  });
10236
10814
 
10237
10815
  // src/services/pty.ts
10238
10816
  import { spawn } from "node-pty";
10239
- import { execFileSync } from "node:child_process";
10817
+ import { execFile, execFileSync as execFileSync2 } from "node:child_process";
10240
10818
  import { createRequire } from "node:module";
10241
10819
  import { randomUUID as randomUUID2 } from "node:crypto";
10242
- import { mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
10820
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
10243
10821
  import { dirname as dirname7 } from "node:path";
10244
10822
  import { tmpdir } from "node:os";
10245
10823
  function noTtyPromptEnv() {
10246
10824
  const path2 = askpassDenyPath();
10247
- mkdirSync4(dirname7(path2), { recursive: true, mode: 448 });
10248
- writeFileSync2(path2, ASKPASS_DENY, { mode: 448 });
10825
+ mkdirSync5(dirname7(path2), { recursive: true, mode: 448 });
10826
+ writeFileSync3(path2, ASKPASS_DENY, { mode: 448 });
10249
10827
  return { SSH_ASKPASS: path2, SSH_ASKPASS_REQUIRE: "force", GIT_TERMINAL_PROMPT: "0" };
10250
10828
  }
10251
10829
  function tmux(...args) {
10252
10830
  try {
10253
- return execFileSync("tmux", ["-L", socket(), "-f", "/dev/null", ...args], {
10831
+ return execFileSync2("tmux", ["-L", socket(), "-f", "/dev/null", ...args], {
10254
10832
  encoding: "utf8",
10255
10833
  stdio: ["ignore", "pipe", "pipe"]
10256
10834
  });
@@ -10263,6 +10841,28 @@ function tmux(...args) {
10263
10841
  throw new TmuxError(`tmux ${args[0]} gagal: ${detail}`, NO_SERVER.test(detail));
10264
10842
  }
10265
10843
  }
10844
+ function tmuxAsync(...args) {
10845
+ return new Promise((resolve21, reject2) => {
10846
+ execFile(
10847
+ "tmux",
10848
+ ["-L", socket(), "-f", "/dev/null", ...args],
10849
+ { encoding: "utf8" },
10850
+ (e, stdout, stderr) => {
10851
+ if (!e) {
10852
+ resolve21(stdout);
10853
+ return;
10854
+ }
10855
+ const err = e;
10856
+ if (err.code === "ENOENT") {
10857
+ reject2(new TmuxError("tmux tidak ada di PATH \u2014 sesi terminal hanoman hidup di dalam tmux (ADR-0016). Pasang: brew install tmux", false));
10858
+ return;
10859
+ }
10860
+ const detail = (stderr || err.message).trim();
10861
+ reject2(new TmuxError(`tmux ${args[0]} gagal: ${detail}`, NO_SERVER.test(detail)));
10862
+ }
10863
+ );
10864
+ });
10865
+ }
10266
10866
  function listPanes() {
10267
10867
  let out4;
10268
10868
  try {
@@ -10271,6 +10871,19 @@ function listPanes() {
10271
10871
  if (e instanceof TmuxError && e.noServer) return [];
10272
10872
  throw e;
10273
10873
  }
10874
+ return parsePanes(out4);
10875
+ }
10876
+ async function listPanesAsync() {
10877
+ let out4;
10878
+ try {
10879
+ out4 = await tmuxAsync("list-panes", "-a", "-F", FMT);
10880
+ } catch (e) {
10881
+ if (e instanceof TmuxError && e.noServer) return [];
10882
+ throw e;
10883
+ }
10884
+ return parsePanes(out4);
10885
+ }
10886
+ function parsePanes(out4) {
10274
10887
  return out4.split("\n").filter(Boolean).flatMap((line) => {
10275
10888
  const [
10276
10889
  n2,
@@ -10338,13 +10951,13 @@ function createSession(projectId, cwd, opts = {}) {
10338
10951
  if (existing) killSession(id);
10339
10952
  const agentForDefs = opts.agent ?? "claude";
10340
10953
  const customDefs = opts.command ? [] : customAgentsFor(projectId, agentForDefs);
10341
- const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : "";
10954
+ const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : agentDelegationClause(customDefs);
10342
10955
  let promptArg = "";
10343
10956
  let promptFile;
10344
10957
  if (!opts.command && opts.prompt) {
10345
10958
  promptFile = promptFilePath(id);
10346
- mkdirSync4(dirname7(promptFile), { recursive: true, mode: 448 });
10347
- writeFileSync2(promptFile, opts.prompt + rosterBlock, { mode: 384 });
10959
+ mkdirSync5(dirname7(promptFile), { recursive: true, mode: 448 });
10960
+ writeFileSync3(promptFile, opts.prompt + rosterBlock, { mode: 384 });
10348
10961
  promptArg = `"$(cat ${sq(promptFile)})"`;
10349
10962
  }
10350
10963
  const agent = opts.agent ?? "claude";
@@ -10355,8 +10968,8 @@ function createSession(projectId, cwd, opts = {}) {
10355
10968
  let goalGate;
10356
10969
  if (agent === "codex" && opts.goal && opts.flow && opts.specId) {
10357
10970
  goalGate = goalGatePath(id);
10358
- mkdirSync4(dirname7(goalGate), { recursive: true, mode: 448 });
10359
- writeFileSync2(goalGate, codexGoalScript({
10971
+ mkdirSync5(dirname7(goalGate), { recursive: true, mode: 448 });
10972
+ writeFileSync3(goalGate, codexGoalScript({
10360
10973
  flow: opts.flow,
10361
10974
  specId: opts.specId,
10362
10975
  condition: opts.goal,
@@ -10371,8 +10984,8 @@ function createSession(projectId, cwd, opts = {}) {
10371
10984
  const json = renderAgentsJson(customDefs);
10372
10985
  if (json) {
10373
10986
  agentsFile = agentsFilePath(id);
10374
- mkdirSync4(dirname7(agentsFile), { recursive: true, mode: 448 });
10375
- writeFileSync2(agentsFile, json, { mode: 384 });
10987
+ mkdirSync5(dirname7(agentsFile), { recursive: true, mode: 448 });
10988
+ writeFileSync3(agentsFile, json, { mode: 384 });
10376
10989
  }
10377
10990
  }
10378
10991
  const flags = agentFlags({
@@ -10394,7 +11007,7 @@ function createSession(projectId, cwd, opts = {}) {
10394
11007
  for (const [k, v] of Object.entries(noTtyPromptEnv())) envPairs.push(`${k}=${sq(v)}`);
10395
11008
  }
10396
11009
  if (opts.phaseFile) {
10397
- mkdirSync4(dirname7(opts.phaseFile), { recursive: true });
11010
+ mkdirSync5(dirname7(opts.phaseFile), { recursive: true });
10398
11011
  envPairs.push(`HANOMAN_PHASE_FILE=${sq(opts.phaseFile)}`);
10399
11012
  }
10400
11013
  if (opts.attachmentsDir) envPairs.push(`HANOMAN_ATTACHMENTS_DIR=${sq(opts.attachmentsDir)}`);
@@ -10407,7 +11020,7 @@ function createSession(projectId, cwd, opts = {}) {
10407
11020
  promptFile,
10408
11021
  attachmentsDir: opts.attachmentsDir
10409
11022
  });
10410
- if (opts.decisionFile) mkdirSync4(dirname7(opts.decisionFile), { recursive: true });
11023
+ if (opts.decisionFile) mkdirSync5(dirname7(opts.decisionFile), { recursive: true });
10411
11024
  tmux(
10412
11025
  // tmux menyerahkan argumen perintah `new-session` ke `default-shell`, dan defaultnya adalah
10413
11026
  // shell login pemanggil di /etc/passwd. Saat hanoman jalan sebagai user service ber-shell
@@ -10510,7 +11123,7 @@ async function sendToPane(id, text, chunkMs = 50, choices = []) {
10510
11123
  const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
10511
11124
  if (!line) return false;
10512
11125
  try {
10513
- const io = dialogIO(id);
11126
+ const io = paneIO(id);
10514
11127
  const screen = readDialogScreen(io.capture());
10515
11128
  if (screen?.kind === "review") return await submitReview(io, screen.submitRow);
10516
11129
  if (screen?.kind === "question") {
@@ -10545,7 +11158,7 @@ async function submitPaneDialog(id) {
10545
11158
  const p = getSession(id);
10546
11159
  if (!p || p.exited) return false;
10547
11160
  try {
10548
- const io = dialogIO(id);
11161
+ const io = paneIO(id);
10549
11162
  const screen = readDialogScreen(io.capture());
10550
11163
  if (screen?.kind !== "review") return false;
10551
11164
  return await submitReview(io, screen.submitRow);
@@ -10669,27 +11282,29 @@ function pollAlt(p, a) {
10669
11282
  function startPoll() {
10670
11283
  if (poll) return;
10671
11284
  poll = setInterval(() => {
10672
- let panes;
10673
- try {
10674
- panes = listPanes();
10675
- } catch {
10676
- return;
10677
- }
10678
- const live = new Map(panes.map((p) => [p.id, p]));
10679
- for (const id of [...attached.keys()]) {
10680
- const p = live.get(id);
10681
- if (!p) end(id, 0);
10682
- else if (p.exited) end(id, p.code);
10683
- else {
10684
- const a = attached.get(id);
10685
- pollPhases(p, a);
10686
- pollAlt(p, a);
11285
+ if (polling) return;
11286
+ polling = true;
11287
+ const snapshot2 = [...attached.entries()];
11288
+ listPanesAsync().then((panes) => {
11289
+ const live = new Map(panes.map((p) => [p.id, p]));
11290
+ for (const [id, a] of snapshot2) {
11291
+ if (attached.get(id) !== a) continue;
11292
+ const p = live.get(id);
11293
+ if (!p) end(id, 0);
11294
+ else if (p.exited) end(id, p.code);
11295
+ else {
11296
+ pollPhases(p, a);
11297
+ pollAlt(p, a);
11298
+ }
11299
+ }
11300
+ }, () => {
11301
+ }).finally(() => {
11302
+ polling = false;
11303
+ if (attached.size === 0 && poll) {
11304
+ clearInterval(poll);
11305
+ poll = void 0;
10687
11306
  }
10688
- }
10689
- if (attached.size === 0 && poll) {
10690
- clearInterval(poll);
10691
- poll = void 0;
10692
- }
11307
+ });
10693
11308
  }, POLL_MS);
10694
11309
  poll.unref();
10695
11310
  }
@@ -10760,7 +11375,7 @@ function spawnPty(...args) {
10760
11375
  );
10761
11376
  }
10762
11377
  }
10763
- var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, TERMINAL_QUERY, stripTerminalQueries, detach;
11378
+ var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, markerOnset, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, polling, TERMINAL_QUERY, stripTerminalQueries, detach;
10764
11379
  var init_pty = __esm({
10765
11380
  "src/services/pty.ts"() {
10766
11381
  "use strict";
@@ -10784,6 +11399,17 @@ var init_pty = __esm({
10784
11399
  return false;
10785
11400
  }
10786
11401
  };
11402
+ markerOnset = (f) => {
11403
+ let raw2;
11404
+ try {
11405
+ raw2 = readFileSync6(f, "utf8");
11406
+ } catch {
11407
+ return void 0;
11408
+ }
11409
+ const secs = Number(raw2.trim());
11410
+ if (!Number.isInteger(secs) || secs <= 0) return void 0;
11411
+ return new Date(secs * 1e3).toISOString();
11412
+ };
10787
11413
  attached = /* @__PURE__ */ new Map();
10788
11414
  claudeBin = () => effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
10789
11415
  shellBin = () => effectiveStr("HANOMAN_SHELL") ?? process.env.SHELL ?? "/bin/bash";
@@ -10825,7 +11451,19 @@ exit 1
10825
11451
  "#{@hanoman_agent}",
10826
11452
  "#{alternate_on}"
10827
11453
  ].join(" ");
10828
- listSessions = () => listPanes().map(({ id, projectId, specId, flow, cwd, exited, code, branch, decision, agent }) => ({
11454
+ toSessionInfo = ({
11455
+ id,
11456
+ projectId,
11457
+ specId,
11458
+ flow,
11459
+ cwd,
11460
+ exited,
11461
+ code,
11462
+ branch,
11463
+ decision,
11464
+ agent,
11465
+ decisionFile
11466
+ }) => ({
10829
11467
  id,
10830
11468
  projectId,
10831
11469
  specId,
@@ -10837,8 +11475,11 @@ exit 1
10837
11475
  agent,
10838
11476
  // Hanya untuk pane mati: `pane_dead_status` kosong pada pane hidup, dan `exitCode: 0` di sana
10839
11477
  // akan terbaca sebagai "sudah berakhir sukses".
10840
- ...exited ? { exitCode: code } : {}
10841
- }));
11478
+ ...exited ? { exitCode: code } : {},
11479
+ ...decision && decisionFile ? { decisionAt: markerOnset(decisionFile) } : {}
11480
+ });
11481
+ listSessions = () => listPanes().map(toSessionInfo);
11482
+ listSessionsAsync = async () => (await listPanesAsync()).map(toSessionInfo);
10842
11483
  liveDecisions = () => listPanes().filter((p) => !p.exited && p.decisionFile).map((p) => ({ id: p.id, specId: p.specId, projectId: p.projectId, decisionFile: p.decisionFile }));
10843
11484
  getSession = (id) => listPanes().find((p) => p.id === id);
10844
11485
  hooks = {};
@@ -10872,7 +11513,7 @@ exit 1
10872
11513
  return "";
10873
11514
  }
10874
11515
  };
10875
- dialogIO = (id) => ({
11516
+ paneIO = (id) => ({
10876
11517
  capture: () => capturePane(id, DIALOG_CAPTURE_LINES),
10877
11518
  literal: (s2) => {
10878
11519
  tmux("send-keys", "-t", name(id), "-l", s2);
@@ -10905,6 +11546,7 @@ exit 1
10905
11546
  const p = getSession(id);
10906
11547
  return !!p && paneComplete(p);
10907
11548
  };
11549
+ polling = false;
10908
11550
  TERMINAL_QUERY = new RegExp([
10909
11551
  "\\x1b\\[[?>=]?[0-9;]*c",
10910
11552
  // Device Attributes 1/2/3
@@ -11042,7 +11684,9 @@ var init_tombstone = __esm({
11042
11684
  // src/services/sync.ts
11043
11685
  var sync_exports = {};
11044
11686
  __export(sync_exports, {
11687
+ BOOTSTRAP_ORDER: () => BOOTSTRAP_ORDER,
11045
11688
  PARENTS: () => PARENTS,
11689
+ PULL_MAX_BYTES: () => PULL_MAX_BYTES,
11046
11690
  SYNCED: () => SYNCED,
11047
11691
  __DATE_FIELDS: () => __DATE_FIELDS,
11048
11692
  __FIELDS: () => __FIELDS,
@@ -11050,12 +11694,14 @@ __export(sync_exports, {
11050
11694
  __JSON_FIELDS: () => __JSON_FIELDS,
11051
11695
  applyPush: () => applyPush,
11052
11696
  backfillFeed: () => backfillFeed,
11697
+ bootstrapSnapshot: () => bootstrapSnapshot,
11053
11698
  consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
11054
11699
  deleteRow: () => deleteRow,
11055
11700
  isEntity: () => isEntity,
11056
11701
  publishDelete: () => publishDelete,
11057
11702
  publishLocal: () => publishLocal,
11058
11703
  pull: () => pull,
11704
+ recordBytes: () => recordBytes,
11059
11705
  setAcceptedHook: () => setAcceptedHook,
11060
11706
  snapshot: () => snapshot,
11061
11707
  upsertLocal: () => upsertLocal,
@@ -11182,24 +11828,77 @@ async function applyPush(entity, id, baseVersion, data, deviceId, op = "upsert")
11182
11828
  onAccepted?.({ entity, recordId: id, version: newVersion, op: "upsert", data: snap?.data ?? {}, seq: String(log2.seq) });
11183
11829
  return { ok: true, version: newVersion };
11184
11830
  }
11185
- async function pull(sinceCursor, limit = 500) {
11831
+ function recordBytes(rec) {
11832
+ return Buffer.byteLength(JSON.stringify(rec));
11833
+ }
11834
+ async function pull(sinceCursor, limit = 500, maxBytes = PULL_MAX_BYTES) {
11186
11835
  const since = Number(sinceCursor || "0");
11187
11836
  const rows = await prisma.syncLog.findMany({
11188
11837
  where: { seq: { gt: since } },
11189
11838
  orderBy: { seq: "asc" },
11190
11839
  take: limit
11191
11840
  });
11192
- const cursor = rows.length ? String(rows[rows.length - 1].seq) : sinceCursor || "0";
11193
- return {
11194
- cursor,
11195
- records: rows.map((r) => ({
11841
+ const records = [];
11842
+ let bytes = 0;
11843
+ let trimmed = false;
11844
+ for (const r of rows) {
11845
+ const rec = {
11196
11846
  entity: r.entity,
11197
11847
  recordId: r.recordId,
11198
11848
  version: r.version,
11199
11849
  op: r.op === "delete" ? "delete" : "upsert",
11200
11850
  data: r.data
11201
- }))
11202
- };
11851
+ };
11852
+ const size = recordBytes(rec);
11853
+ if (records.length && bytes + size > maxBytes) {
11854
+ trimmed = true;
11855
+ break;
11856
+ }
11857
+ bytes += size;
11858
+ records.push(rec);
11859
+ }
11860
+ const cursor = records.length ? String(rows[records.length - 1].seq) : sinceCursor || "0";
11861
+ return { cursor, records, hasMore: trimmed || rows.length === limit };
11862
+ }
11863
+ async function bootstrapSnapshot(after, maxBytes = PULL_MAX_BYTES) {
11864
+ const tip = await prisma.syncLog.findFirst({ orderBy: { seq: "desc" }, select: { seq: true } });
11865
+ const cursor = tip ? String(tip.seq) : "0";
11866
+ const sep5 = after ? after.indexOf(":") : -1;
11867
+ const afterEntity = sep5 > 0 ? after.slice(0, sep5) : null;
11868
+ const afterId = sep5 > 0 ? after.slice(sep5 + 1) : null;
11869
+ const startAt = afterEntity ? BOOTSTRAP_ORDER.indexOf(afterEntity) : 0;
11870
+ if (startAt < 0) throw new Error("bootstrap cursor tak dikenal");
11871
+ const records = [];
11872
+ let bytes = 0;
11873
+ let last = null;
11874
+ for (let i = startAt; i < BOOTSTRAP_ORDER.length; i++) {
11875
+ const entity = BOOTSTRAP_ORDER[i];
11876
+ const gt = i === startAt && afterId ? afterId : null;
11877
+ const rows = await DELEGATE[entity].findMany({
11878
+ ...gt ? { where: { id: { gt } } } : {},
11879
+ orderBy: { id: "asc" },
11880
+ select: { id: true }
11881
+ });
11882
+ for (const row of rows) {
11883
+ const snap = await snapshot(entity, row.id);
11884
+ if (!snap) continue;
11885
+ const rec = {
11886
+ entity,
11887
+ recordId: row.id,
11888
+ version: snap.version,
11889
+ op: "upsert",
11890
+ data: snap.data
11891
+ };
11892
+ const size = recordBytes(rec);
11893
+ if (records.length && bytes + size > maxBytes) {
11894
+ return { cursor, records, hasMore: true, next: last };
11895
+ }
11896
+ bytes += size;
11897
+ records.push(rec);
11898
+ last = `${entity}:${row.id}`;
11899
+ }
11900
+ }
11901
+ return { cursor, records, hasMore: false, next: null };
11203
11902
  }
11204
11903
  async function publishLocal(entity, id) {
11205
11904
  const snap = await snapshot(entity, id);
@@ -11291,7 +11990,7 @@ async function upsertLocal(entity, id, version2, data) {
11291
11990
  function setAcceptedHook(hook) {
11292
11991
  onAccepted = hook;
11293
11992
  }
11294
- var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
11993
+ var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, PULL_MAX_BYTES, BOOTSTRAP_ORDER, onAccepted, __FIELDS_FOR_TEST;
11295
11994
  var init_sync = __esm({
11296
11995
  "src/services/sync.ts"() {
11297
11996
  "use strict";
@@ -11310,7 +12009,12 @@ var init_sync = __esm({
11310
12009
  githubIssue: prisma.githubIssue
11311
12010
  };
11312
12011
  FIELDS = {
11313
- project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
12012
+ // SPEC-880 · ADR-0135 · `handledBy` ikut menyeberang: "project ini dipegang mesin yang mana"
12013
+ // adalah pernyataan bersama, bukan setelan mesin — dan justru menyeberangnya nilai itu yang
12014
+ // jadi inti spec-nya. SENGAJA bukan cermin `repoDir`/`schedulerOptIn`/`autoMerge` yang LOCAL-only
12015
+ // (mereka properti checkout mesin ini). Tiap entri membawa `name` karena `DeviceToken` TIDAK
12016
+ // ikut SYNCED: penerima tak punya baris device untuk di-join. BUKAN DATE_FIELDS.
12017
+ project: ["name", "desc", "kind", "stack", "gitRemote", "handledBy", "updatedAt"],
11314
12018
  // SPEC-408 · ADR-0090 · createdAt/startedAt ikut menyeberang — sejajar baseSha/headSha. Tanpa
11315
12019
  // ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
11316
12020
  // SPEC-447 · ADR-0093 · dependsOn ikut juga: tanpa itu client tak tahu urutannya dan akan
@@ -11391,6 +12095,7 @@ var init_sync = __esm({
11391
12095
  ]);
11392
12096
  BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["vps:hardened", "customAgent:enabled"]);
11393
12097
  JSON_FIELDS = /* @__PURE__ */ new Set([
12098
+ "project:handledBy",
11394
12099
  "spec:payload",
11395
12100
  "spec:dependsOn",
11396
12101
  "spec:sourceHistory",
@@ -11402,6 +12107,17 @@ var init_sync = __esm({
11402
12107
  "githubIssue:labels"
11403
12108
  ]);
11404
12109
  __JSON_FIELDS = JSON_FIELDS;
12110
+ PULL_MAX_BYTES = 1024 * 1024;
12111
+ BOOTSTRAP_ORDER = [
12112
+ "project",
12113
+ "spec",
12114
+ "ticket",
12115
+ "customAgent",
12116
+ "githubIssue",
12117
+ "ticketAttachment",
12118
+ "vps",
12119
+ "sessionResult"
12120
+ ];
11405
12121
  __FIELDS_FOR_TEST = FIELDS;
11406
12122
  }
11407
12123
  });
@@ -11484,15 +12200,17 @@ var init_settings2 = __esm({
11484
12200
  // SPEC-476 · ADR-0096 · gateway Telegram opt-in
11485
12201
  changelog: CHANGELOG_ENGINE_DEFAULTS,
11486
12202
  // SPEC-518 · agen pembuat changelog (opt-in, mati)
11487
- portalChat: PORTAL_CHAT_DEFAULTS
12203
+ portalChat: PORTAL_CHAT_DEFAULTS,
11488
12204
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
12205
+ builtinAgents: {}
12206
+ // SPEC-881 · ADR-0136 · sidik jari seed (lokal, tak disync)
11489
12207
  };
11490
12208
  RETIRED_MODELS = { "claude-opus-4-8": "claude-opus-5" };
11491
12209
  }
11492
12210
  });
11493
12211
 
11494
12212
  // src/services/codex-trust.ts
11495
- import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, realpathSync as realpathSync3 } from "node:fs";
12213
+ import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
11496
12214
  import { homedir as homedir3 } from "node:os";
11497
12215
  function ensureCodexTrust(repoDir, home3) {
11498
12216
  const path2 = codexConfigPath(home3);
@@ -11505,11 +12223,11 @@ function ensureCodexTrust(repoDir, home3) {
11505
12223
  try {
11506
12224
  let existing = "";
11507
12225
  try {
11508
- existing = readFileSync6(path2, "utf8");
12226
+ existing = readFileSync8(path2, "utf8");
11509
12227
  } catch {
11510
12228
  }
11511
12229
  if (existing.includes(header2)) return;
11512
- mkdirSync6(codexHome(home3), { recursive: true });
12230
+ mkdirSync7(codexHome(home3), { recursive: true });
11513
12231
  const lead = existing === "" || existing.endsWith("\n") ? "" : "\n";
11514
12232
  appendFileSync(path2, `${lead}
11515
12233
  ${header2}
@@ -12417,7 +13135,7 @@ var require_filesystem = __commonJS({
12417
13135
  var LDD_PATH = "/usr/bin/ldd";
12418
13136
  var SELF_PATH = "/proc/self/exe";
12419
13137
  var MAX_LENGTH = 2048;
12420
- var readFileSync14 = (path2) => {
13138
+ var readFileSync17 = (path2) => {
12421
13139
  const fd = fs.openSync(path2, "r");
12422
13140
  const buffer2 = Buffer.alloc(MAX_LENGTH);
12423
13141
  const bytesRead = fs.readSync(fd, buffer2, 0, MAX_LENGTH, 0);
@@ -12442,7 +13160,7 @@ var require_filesystem = __commonJS({
12442
13160
  module.exports = {
12443
13161
  LDD_PATH,
12444
13162
  SELF_PATH,
12445
- readFileSync: readFileSync14,
13163
+ readFileSync: readFileSync17,
12446
13164
  readFile: readFile6
12447
13165
  };
12448
13166
  }
@@ -12491,7 +13209,7 @@ var require_detect_libc = __commonJS({
12491
13209
  "use strict";
12492
13210
  var childProcess = __require("child_process");
12493
13211
  var { isLinux, getReport } = require_process();
12494
- var { LDD_PATH, SELF_PATH, readFile: readFile6, readFileSync: readFileSync14 } = require_filesystem();
13212
+ var { LDD_PATH, SELF_PATH, readFile: readFile6, readFileSync: readFileSync17 } = require_filesystem();
12495
13213
  var { interpreterPath } = require_elf();
12496
13214
  var cachedFamilyInterpreter;
12497
13215
  var cachedFamilyFilesystem;
@@ -12583,7 +13301,7 @@ var require_detect_libc = __commonJS({
12583
13301
  }
12584
13302
  cachedFamilyFilesystem = null;
12585
13303
  try {
12586
- const lddContent = readFileSync14(LDD_PATH);
13304
+ const lddContent = readFileSync17(LDD_PATH);
12587
13305
  cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
12588
13306
  } catch (e) {
12589
13307
  }
@@ -12608,7 +13326,7 @@ var require_detect_libc = __commonJS({
12608
13326
  }
12609
13327
  cachedFamilyInterpreter = null;
12610
13328
  try {
12611
- const selfContent = readFileSync14(SELF_PATH);
13329
+ const selfContent = readFileSync17(SELF_PATH);
12612
13330
  const path2 = interpreterPath(selfContent);
12613
13331
  cachedFamilyInterpreter = familyFromInterpreterPath(path2);
12614
13332
  } catch (e) {
@@ -12672,7 +13390,7 @@ var require_detect_libc = __commonJS({
12672
13390
  }
12673
13391
  cachedVersionFilesystem = null;
12674
13392
  try {
12675
- const lddContent = readFileSync14(LDD_PATH);
13393
+ const lddContent = readFileSync17(LDD_PATH);
12676
13394
  const versionMatch = lddContent.match(RE_GLIBC_VERSION);
12677
13395
  if (versionMatch) {
12678
13396
  cachedVersionFilesystem = versionMatch[1];
@@ -13590,10 +14308,10 @@ var require_truncate = __commonJS({
13590
14308
  "../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/truncate.js"(exports, module) {
13591
14309
  "use strict";
13592
14310
  var parse3 = require_parse();
13593
- var constants2 = require_constants();
14311
+ var constants3 = require_constants();
13594
14312
  var SemVer = require_semver();
13595
14313
  var truncate = (version2, truncation, options2) => {
13596
- if (!constants2.RELEASE_TYPES.includes(truncation)) {
14314
+ if (!constants3.RELEASE_TYPES.includes(truncation)) {
13597
14315
  return null;
13598
14316
  }
13599
14317
  const clonedVersion = cloneInputVersion(version2, options2);
@@ -14642,7 +15360,7 @@ var require_semver2 = __commonJS({
14642
15360
  "../node_modules/.pnpm/semver@7.8.5/node_modules/semver/index.js"(exports, module) {
14643
15361
  "use strict";
14644
15362
  var internalRe = require_re();
14645
- var constants2 = require_constants();
15363
+ var constants3 = require_constants();
14646
15364
  var SemVer = require_semver();
14647
15365
  var identifiers = require_identifiers();
14648
15366
  var parse3 = require_parse();
@@ -14726,8 +15444,8 @@ var require_semver2 = __commonJS({
14726
15444
  re: internalRe.re,
14727
15445
  src: internalRe.src,
14728
15446
  tokens: internalRe.t,
14729
- SEMVER_SPEC_VERSION: constants2.SEMVER_SPEC_VERSION,
14730
- RELEASE_TYPES: constants2.RELEASE_TYPES,
15447
+ SEMVER_SPEC_VERSION: constants3.SEMVER_SPEC_VERSION,
15448
+ RELEASE_TYPES: constants3.RELEASE_TYPES,
14731
15449
  compareIdentifiers: identifiers.compareIdentifiers,
14732
15450
  rcompareIdentifiers: identifiers.rcompareIdentifiers
14733
15451
  };
@@ -17487,7 +18205,7 @@ __export(bootstrap_exports, {
17487
18205
  reloadTelegramGateway: () => reloadTelegramGateway,
17488
18206
  telegramSessionDeps: () => telegramSessionDeps
17489
18207
  });
17490
- import { mkdirSync as mkdirSync7 } from "node:fs";
18208
+ import { mkdirSync as mkdirSync8 } from "node:fs";
17491
18209
  function parseTelegramAllowedUserIds(raw2) {
17492
18210
  const ids = raw2.split(/[\s,]+/).filter(Boolean);
17493
18211
  if (!ids.length) throw new Error("Telegram allowlist wajib berisi numeric user id");
@@ -17512,7 +18230,7 @@ function telegramSessionDeps(input) {
17512
18230
  home: resolveHome(),
17513
18231
  apiBase: input.apiBase,
17514
18232
  agentToken: input.agentToken,
17515
- ensureDir: (path2) => mkdirSync7(path2, { recursive: true })
18233
+ ensureDir: (path2) => mkdirSync8(path2, { recursive: true })
17516
18234
  };
17517
18235
  }
17518
18236
  async function productionFactory(input) {
@@ -19988,7 +20706,7 @@ var require_websocket = __commonJS({
19988
20706
  var http = __require("http");
19989
20707
  var net = __require("net");
19990
20708
  var tls = __require("tls");
19991
- var { randomBytes: randomBytes11, createHash: createHash10 } = __require("crypto");
20709
+ var { randomBytes: randomBytes11, createHash: createHash12 } = __require("crypto");
19992
20710
  var { Duplex, Readable } = __require("stream");
19993
20711
  var { URL: URL2 } = __require("url");
19994
20712
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -20656,7 +21374,7 @@ var require_websocket = __commonJS({
20656
21374
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
20657
21375
  return;
20658
21376
  }
20659
- const digest = createHash10("sha1").update(key + GUID).digest("base64");
21377
+ const digest = createHash12("sha1").update(key + GUID).digest("base64");
20660
21378
  if (res.headers["sec-websocket-accept"] !== digest) {
20661
21379
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
20662
21380
  return;
@@ -21025,7 +21743,7 @@ var require_websocket_server = __commonJS({
21025
21743
  var EventEmitter = __require("events");
21026
21744
  var http = __require("http");
21027
21745
  var { Duplex } = __require("stream");
21028
- var { createHash: createHash10 } = __require("crypto");
21746
+ var { createHash: createHash12 } = __require("crypto");
21029
21747
  var extension2 = require_extension();
21030
21748
  var PerMessageDeflate2 = require_permessage_deflate();
21031
21749
  var subprotocol2 = require_subprotocol();
@@ -21332,7 +22050,7 @@ var require_websocket_server = __commonJS({
21332
22050
  );
21333
22051
  }
21334
22052
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
21335
- const digest = createHash10("sha1").update(key + GUID).digest("base64");
22053
+ const digest = createHash12("sha1").update(key + GUID).digest("base64");
21336
22054
  const headers = [
21337
22055
  "HTTP/1.1 101 Switching Protocols",
21338
22056
  "Upgrade: websocket",
@@ -21524,9 +22242,50 @@ async function applyFeedFrame(msg) {
21524
22242
  if (msg.seq && !feedHole) await setCursor(String(msg.seq));
21525
22243
  return true;
21526
22244
  }
22245
+ async function retryDeferred(rest, stats2) {
22246
+ while (rest.length) {
22247
+ const still = [];
22248
+ for (const rec of rest) {
22249
+ try {
22250
+ const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op ?? "upsert");
22251
+ if (r === "dropped") stats2.dropped++;
22252
+ else if (rec.op === "delete") stats2.deleted++;
22253
+ else stats2.pulled++;
22254
+ } catch {
22255
+ still.push(rec);
22256
+ }
22257
+ }
22258
+ if (still.length === rest.length) return still;
22259
+ rest = still;
22260
+ }
22261
+ return [];
22262
+ }
22263
+ async function bootstrapOnce(transport) {
22264
+ if (await getCursor() !== "0") return null;
22265
+ if ((await listOutbox()).length) return null;
22266
+ let after = null;
22267
+ let cursor = "0";
22268
+ let applied = 0;
22269
+ for (let page = 0; page < MAX_DRAIN_PAGES; page++) {
22270
+ const q = after ? `?after=${encodeURIComponent(after)}` : "";
22271
+ const res = await transport("GET", `/api/sync/bootstrap${q}`);
22272
+ if (res.status !== 200 || !Array.isArray(res.body?.records)) return null;
22273
+ for (const raw2 of res.body.records) {
22274
+ const rec = validateIncomingRecord(raw2);
22275
+ if (!rec.op) continue;
22276
+ await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
22277
+ applied++;
22278
+ }
22279
+ cursor = String(res.body.cursor ?? "0");
22280
+ const next = res.body.next ? String(res.body.next) : null;
22281
+ if (!res.body.hasMore || !next) break;
22282
+ after = next;
22283
+ }
22284
+ await setCursor(cursor);
22285
+ return applied;
22286
+ }
21527
22287
  async function syncOnce(transport) {
21528
- let pulled = 0, pushed = 0, conflicts = 0, deleted = 0, dropped = 0;
21529
- const cursor = await getCursor();
22288
+ const stats2 = { pulled: 0, pushed: 0, conflicts: 0, deleted: 0, dropped: 0 };
21530
22289
  const outbox = await listOutbox();
21531
22290
  const pending2 = new Set(outbox.map((o) => `${o.entity}:${o.recordId}`));
21532
22291
  const conflicted = /* @__PURE__ */ new Set();
@@ -21535,64 +22294,53 @@ async function syncOnce(transport) {
21535
22294
  const key = `${entity}:${recordId}`;
21536
22295
  if (!conflicted.has(key)) {
21537
22296
  conflicted.add(key);
21538
- conflicts++;
22297
+ stats2.conflicts++;
21539
22298
  }
21540
22299
  };
21541
- const pullRes = await transport("GET", `/api/sync/pull?since=${cursor}`);
21542
- const rawRecords = Array.isArray(pullRes.body?.records) ? pullRes.body.records : [];
21543
- const records = rawRecords.map(validateIncomingRecord);
21544
- const deferred = [];
21545
- for (const rec of records) {
21546
- if (!isEntity(rec.entity)) continue;
21547
- if (!rec.op) {
21548
- dropped++;
21549
- continue;
21550
- }
21551
- if (rec.op === "upsert" && pending2.has(`${rec.entity}:${rec.recordId}`)) {
21552
- const local = await snapshot(rec.entity, rec.recordId);
21553
- if (local && JSON.stringify(local.data) !== JSON.stringify(rec.data)) {
21554
- await markConflict(
21555
- rec.entity,
21556
- rec.recordId,
21557
- { version: local.version, data: local.data },
21558
- { version: rec.version, data: rec.data }
21559
- );
22300
+ let deferred = [];
22301
+ for (let page = 0; page < MAX_DRAIN_PAGES; page++) {
22302
+ const cursor = await getCursor();
22303
+ const pullRes = await transport("GET", `/api/sync/pull?since=${cursor}`);
22304
+ const rawRecords = Array.isArray(pullRes.body?.records) ? pullRes.body.records : [];
22305
+ const records = rawRecords.map(validateIncomingRecord);
22306
+ for (const rec of records) {
22307
+ if (!isEntity(rec.entity)) continue;
22308
+ if (!rec.op) {
22309
+ stats2.dropped++;
22310
+ continue;
22311
+ }
22312
+ if (rec.op === "upsert" && pending2.has(`${rec.entity}:${rec.recordId}`)) {
22313
+ const local = await snapshot(rec.entity, rec.recordId);
22314
+ if (local && JSON.stringify(local.data) !== JSON.stringify(rec.data)) {
22315
+ await markConflict(
22316
+ rec.entity,
22317
+ rec.recordId,
22318
+ { version: local.version, data: local.data },
22319
+ { version: rec.version, data: rec.data }
22320
+ );
22321
+ }
22322
+ continue;
21560
22323
  }
21561
- continue;
21562
- }
21563
- try {
21564
- const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
21565
- if (r === "dropped") dropped++;
21566
- else if (rec.op === "delete") deleted++;
21567
- else pulled++;
21568
- } catch {
21569
- deferred.push(rec);
21570
- }
21571
- }
21572
- let rest = deferred;
21573
- while (rest.length) {
21574
- const still = [];
21575
- for (const rec of rest) {
21576
22324
  try {
21577
- const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op ?? "upsert");
21578
- if (r === "dropped") dropped++;
21579
- else if (rec.op === "delete") deleted++;
21580
- else pulled++;
22325
+ const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
22326
+ if (r === "dropped") stats2.dropped++;
22327
+ else if (rec.op === "delete") stats2.deleted++;
22328
+ else stats2.pulled++;
21581
22329
  } catch {
21582
- still.push(rec);
22330
+ deferred.push(rec);
21583
22331
  }
21584
22332
  }
21585
- if (still.length === rest.length) {
21586
- for (const rec of still) {
21587
- console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan \u2014 dilewati`);
21588
- dropped++;
21589
- }
21590
- break;
21591
- }
21592
- rest = still;
22333
+ deferred = await retryDeferred(deferred, stats2);
22334
+ const next = pullRes.body?.cursor ? String(pullRes.body.cursor) : cursor;
22335
+ if (next !== cursor) await setCursor(next);
22336
+ feedHole = false;
22337
+ const more = pullRes.body?.hasMore ?? records.length > 0;
22338
+ if (!more || next === cursor) break;
22339
+ }
22340
+ for (const rec of deferred) {
22341
+ console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan \u2014 dilewati`);
22342
+ stats2.dropped++;
21593
22343
  }
21594
- if (pullRes.body?.cursor) await setCursor(String(pullRes.body.cursor));
21595
- feedHole = false;
21596
22344
  for (const item of outbox) {
21597
22345
  if (item.entity === "projectRename") {
21598
22346
  const [oldId, newId] = item.recordId.split(RENAME_SEP);
@@ -21607,9 +22355,9 @@ async function syncOnce(transport) {
21607
22355
  const r2 = res2.body?.results?.[0];
21608
22356
  if (r2?.ok) {
21609
22357
  await clearOutbox(item.entity, item.recordId);
21610
- pushed++;
22358
+ stats2.pushed++;
21611
22359
  } else if (r2?.conflict) {
21612
- conflicts++;
22360
+ stats2.conflicts++;
21613
22361
  }
21614
22362
  continue;
21615
22363
  }
@@ -21635,7 +22383,7 @@ async function syncOnce(transport) {
21635
22383
  });
21636
22384
  if (res2.body?.results?.[0]?.ok) {
21637
22385
  await clearOutbox(item.entity, item.recordId);
21638
- pushed++;
22386
+ stats2.pushed++;
21639
22387
  }
21640
22388
  continue;
21641
22389
  }
@@ -21643,6 +22391,9 @@ async function syncOnce(transport) {
21643
22391
  records: [{ entity: item.entity, id: item.recordId, baseVersion: snap.version, data: snap.data }]
21644
22392
  });
21645
22393
  const r = res.body?.results?.[0];
22394
+ if (!r) {
22395
+ console.warn(`sync: push ${item.entity}:${item.recordId} tak dijawab hub (status ${res.status}) \u2014 tetap di outbox; periksa apakah hub lebih tua dari client ini`);
22396
+ }
21646
22397
  if (r?.ok) {
21647
22398
  if (typeof r.version === "number") {
21648
22399
  const delegate = prisma[item.entity];
@@ -21650,7 +22401,7 @@ async function syncOnce(transport) {
21650
22401
  });
21651
22402
  }
21652
22403
  await clearOutbox(item.entity, item.recordId);
21653
- pushed++;
22404
+ stats2.pushed++;
21654
22405
  } else if (r?.conflict) {
21655
22406
  if (r.deleted) {
21656
22407
  await applyRemote(
@@ -21661,7 +22412,7 @@ async function syncOnce(transport) {
21661
22412
  "delete"
21662
22413
  );
21663
22414
  await clearOutbox(item.entity, item.recordId);
21664
- deleted++;
22415
+ stats2.deleted++;
21665
22416
  continue;
21666
22417
  }
21667
22418
  const server = r.server;
@@ -21678,7 +22429,7 @@ async function syncOnce(transport) {
21678
22429
  }
21679
22430
  }
21680
22431
  }
21681
- return { pulled, pushed, conflicts, deleted, dropped };
22432
+ return stats2;
21682
22433
  }
21683
22434
  function fetchTransport(base2, token) {
21684
22435
  return async (method, path2, body) => {
@@ -21687,12 +22438,26 @@ function fetchTransport(base2, token) {
21687
22438
  const res = await safeRequest({
21688
22439
  url: url2,
21689
22440
  method,
21690
- headers: { authorization: `Bearer ${token}`, ...body ? { "content-type": "application/json" } : {} },
22441
+ headers: {
22442
+ authorization: `Bearer ${token}`,
22443
+ // SPEC-885 · ADR-0138 · hub lama mengabaikan header ini dan membalas polos; `safeRequest`
22444
+ // hanya men-decompress bila balasannya benar-benar ber-`content-encoding: gzip`.
22445
+ "accept-encoding": "gzip",
22446
+ ...body ? { "content-type": "application/json" } : {}
22447
+ },
21691
22448
  ...body ? { body: Buffer.from(JSON.stringify(body)) } : {},
21692
22449
  allowPrivate: process.env.NODE_ENV !== "production" && loopback,
21693
22450
  connectMs: 5e3,
21694
22451
  totalMs: 15e3,
21695
- maxResponseBytes: 2 * 1024 * 1024
22452
+ // SPEC-885 · ADR-0138 · cap ini dulu 2 MB, dan halaman feed 2,51 MB di hub produksi
22453
+ // membuat setiap client baru MANDEK di situ selamanya — bukan lambat, mandek, dan tanpa
22454
+ // satu baris log. Hub yang sudah membawa anggaran byte memotong halamannya di 1 MB, jadi
22455
+ // cap ini tak akan tersentuh olehnya. Ia dinaikkan justru untuk hub yang BELUM naik versi,
22456
+ // yang tetap mengirim 500 baris apa adanya — kombinasi yang dialami tiap
22457
+ // `npm i -g hanoman` sebelum hub-nya diperbarui (urutan rilis hub-duluan, ADR-0135).
22458
+ maxResponseBytes: 8 * 1024 * 1024,
22459
+ acceptEncoding: "gzip",
22460
+ maxDecodedBytes: 16 * 1024 * 1024
21696
22461
  });
21697
22462
  let parsed = null;
21698
22463
  try {
@@ -21707,22 +22472,11 @@ async function syncNow(opts) {
21707
22472
  const token = effectiveStr("SYNC_DEVICE_TOKEN");
21708
22473
  if (!base2 || !token) return null;
21709
22474
  const transport = fetchTransport(base2, token);
21710
- if (!opts?.full) return syncOnce(transport);
21711
- await setCursor("0");
21712
- const total = { pulled: 0, pushed: 0, conflicts: 0, deleted: 0, dropped: 0 };
21713
- let seen = "0";
21714
- for (let page = 0; page < FULL_PULL_MAX_PAGES; page++) {
21715
- const s2 = await syncOnce(transport);
21716
- total.pulled += s2.pulled;
21717
- total.pushed += s2.pushed;
21718
- total.conflicts += s2.conflicts;
21719
- total.deleted += s2.deleted;
21720
- total.dropped += s2.dropped;
21721
- const now = await getCursor();
21722
- if (now === seen) break;
21723
- seen = now;
21724
- }
21725
- return total;
22475
+ if (opts?.full) await setCursor("0");
22476
+ const booted = await bootstrapOnce(transport).catch(() => null);
22477
+ const stats2 = await syncOnce(transport);
22478
+ if (booted) stats2.pulled += booted;
22479
+ return stats2;
21726
22480
  }
21727
22481
  function syncStatus() {
21728
22482
  return {
@@ -21731,15 +22485,24 @@ function syncStatus() {
21731
22485
  /* OPEN */
21732
22486
  };
21733
22487
  }
22488
+ async function syncTick(transport) {
22489
+ try {
22490
+ await syncOnce(transport);
22491
+ if (!pullSehat) {
22492
+ console.info("sync: pull pulih");
22493
+ pullSehat = true;
22494
+ }
22495
+ } catch (e) {
22496
+ if (pullSehat) {
22497
+ console.warn(`sync: pull gagal \u2014 ${e.message}`);
22498
+ pullSehat = false;
22499
+ }
22500
+ }
22501
+ }
21734
22502
  async function startSyncClient(base2, token, tickMs2) {
21735
22503
  started = true;
21736
22504
  const transport = fetchTransport(base2, token);
21737
- const tick6 = async () => {
21738
- try {
21739
- await syncOnce(transport);
21740
- } catch {
21741
- }
21742
- };
22505
+ const tick6 = () => syncTick(transport);
21743
22506
  const connectWs = async () => {
21744
22507
  const { WebSocket: WebSocket2 } = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
21745
22508
  const wsUrl = base2.replace(/^http/, "ws").replace(/\/$/, "") + "/api/sync/ws";
@@ -21768,6 +22531,7 @@ async function startSyncClient(base2, token, tickMs2) {
21768
22531
  }
21769
22532
  });
21770
22533
  };
22534
+ await bootstrapOnce(transport).catch(() => null);
21771
22535
  await tick6();
21772
22536
  void connectWs();
21773
22537
  const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
@@ -21794,7 +22558,7 @@ async function applySyncConfig() {
21794
22558
  const token = effectiveStr("SYNC_DEVICE_TOKEN");
21795
22559
  if (base2 && token) await startSyncClient(base2, token, effectiveInt("SYNC_TICK_MS"));
21796
22560
  }
21797
- var MAX_SYNC_RECORD_BYTES, feedHole, FULL_PULL_MAX_PAGES, timer3, ws, started;
22561
+ var MAX_SYNC_RECORD_BYTES, feedHole, MAX_DRAIN_PAGES, timer3, ws, started, pullSehat;
21798
22562
  var init_sync_client = __esm({
21799
22563
  "src/services/sync-client.ts"() {
21800
22564
  "use strict";
@@ -21809,8 +22573,9 @@ var init_sync_client = __esm({
21809
22573
  init_safe_outbound_request();
21810
22574
  MAX_SYNC_RECORD_BYTES = 1024 * 1024;
21811
22575
  feedHole = false;
21812
- FULL_PULL_MAX_PAGES = 200;
22576
+ MAX_DRAIN_PAGES = 500;
21813
22577
  started = false;
22578
+ pullSehat = true;
21814
22579
  }
21815
22580
  });
21816
22581
 
@@ -25085,9 +25850,9 @@ import Fastify from "fastify";
25085
25850
  import fastifyStatic from "@fastify/static";
25086
25851
  import websocket from "@fastify/websocket";
25087
25852
  import cookie from "@fastify/cookie";
25088
- import { fileURLToPath as fileURLToPath4 } from "node:url";
25853
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
25089
25854
  import { dirname as dirname13 } from "node:path";
25090
- import { existsSync as existsSync16 } from "node:fs";
25855
+ import { existsSync as existsSync17 } from "node:fs";
25091
25856
 
25092
25857
  // src/web-dir.ts
25093
25858
  import { resolve } from "node:path";
@@ -25148,17 +25913,17 @@ init_stage_machine();
25148
25913
 
25149
25914
  // src/services/scan.ts
25150
25915
  init_src();
25151
- import { execFile } from "node:child_process";
25916
+ import { execFile as execFile2 } from "node:child_process";
25152
25917
  import { promisify } from "node:util";
25153
25918
  import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
25154
25919
  import { resolve as resolve10 } from "node:path";
25155
25920
 
25156
25921
  // src/services/safe-repo-path.ts
25157
- import { constants } from "node:fs";
25922
+ import { constants as constants2 } from "node:fs";
25158
25923
  import { lstat, mkdir as mkdir2, open as open2, realpath, rename, unlink as unlink2 } from "node:fs/promises";
25159
- import { isAbsolute as isAbsolute3, join as join6, relative, resolve as resolve9, sep as sep2 } from "node:path";
25924
+ import { isAbsolute as isAbsolute4, join as join7, relative, resolve as resolve9, sep as sep2 } from "node:path";
25160
25925
  import { randomUUID as randomUUID3 } from "node:crypto";
25161
- import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
25926
+ import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
25162
25927
  var PathContainmentError = class extends Error {
25163
25928
  code = "PATH_CONTAINMENT";
25164
25929
  };
@@ -25166,7 +25931,7 @@ var denied = (detail) => {
25166
25931
  throw new PathContainmentError(`repository path ditolak: ${detail}`);
25167
25932
  };
25168
25933
  function components(rel) {
25169
- if (!rel || isAbsolute3(rel) || rel.includes("\0")) denied("path invalid");
25934
+ if (!rel || isAbsolute4(rel) || rel.includes("\0")) denied("path invalid");
25170
25935
  const parts = rel.replace(/\\/g, "/").split("/");
25171
25936
  if (parts.some((part) => !part || part === "." || part === "..")) denied("parent/empty component");
25172
25937
  return parts;
@@ -25179,14 +25944,14 @@ async function canonicalRoot(root) {
25179
25944
  }
25180
25945
  function beneath(root, candidate) {
25181
25946
  const rel = relative(root, candidate);
25182
- return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute3(rel);
25947
+ return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute4(rel);
25183
25948
  }
25184
25949
  async function ensureRepoParents(root, rel) {
25185
25950
  const base2 = await canonicalRoot(root);
25186
25951
  const parts = components(rel).slice(0, -1);
25187
25952
  let current = base2;
25188
25953
  for (const part of parts) {
25189
- current = join6(current, part);
25954
+ current = join7(current, part);
25190
25955
  try {
25191
25956
  await mkdir2(current, { mode: 448 });
25192
25957
  } catch (error) {
@@ -25203,9 +25968,9 @@ function ensureRepoParentsSync(root, rel) {
25203
25968
  const parts = components(rel).slice(0, -1);
25204
25969
  let current = base2;
25205
25970
  for (const part of parts) {
25206
- current = join6(current, part);
25971
+ current = join7(current, part);
25207
25972
  try {
25208
- mkdirSync5(current, { mode: 448 });
25973
+ mkdirSync6(current, { mode: 448 });
25209
25974
  } catch (error) {
25210
25975
  if (error.code !== "EEXIST") throw error;
25211
25976
  }
@@ -25219,7 +25984,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
25219
25984
  const parts = components(rel);
25220
25985
  let current = base2;
25221
25986
  for (let i = 0; i < parts.length; i++) {
25222
- current = join6(current, parts[i]);
25987
+ current = join7(current, parts[i]);
25223
25988
  const stat4 = await lstat(current).catch((error) => {
25224
25989
  if (error.code === "ENOENT" && opts.allowMissingTail) return null;
25225
25990
  if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
@@ -25240,7 +26005,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
25240
26005
  const parts = components(rel);
25241
26006
  let current = base2;
25242
26007
  for (let i = 0; i < parts.length; i++) {
25243
- current = join6(current, parts[i]);
26008
+ current = join7(current, parts[i]);
25244
26009
  try {
25245
26010
  const stat4 = lstatSync(current);
25246
26011
  if (stat4.isSymbolicLink()) denied("symlink");
@@ -25256,10 +26021,10 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
25256
26021
  }
25257
26022
  function readRepoFileSync(root, rel) {
25258
26023
  const path2 = assertSafeRepoPathSync(root, rel);
25259
- const fd = openSync(path2, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
26024
+ const fd = openSync(path2, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
25260
26025
  try {
25261
26026
  if (!fstatSync(fd).isFile()) denied("bukan file regular");
25262
- return readFileSync5(fd);
26027
+ return readFileSync7(fd);
25263
26028
  } finally {
25264
26029
  closeSync(fd);
25265
26030
  }
@@ -25270,10 +26035,10 @@ function writeRepoFileAtomicSync(root, rel, data) {
25270
26035
  const parent = resolve9(path2, "..");
25271
26036
  const parentRel = relative(realpathSync2(root), parent);
25272
26037
  if (parentRel) assertSafeRepoPathSync(root, parentRel);
25273
- const temp = join6(parent, `.hanoman-${randomUUID3()}.tmp`);
25274
- const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
26038
+ const temp = join7(parent, `.hanoman-${randomUUID3()}.tmp`);
26039
+ const fd = openSync(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
25275
26040
  try {
25276
- writeFileSync3(fd, data);
26041
+ writeFileSync4(fd, data);
25277
26042
  } finally {
25278
26043
  closeSync(fd);
25279
26044
  }
@@ -25297,7 +26062,7 @@ function writeRepoFileAtomicSync(root, rel, data) {
25297
26062
  }
25298
26063
  async function readRepoFile(root, rel) {
25299
26064
  const entry = await resolveRepoEntry(root, rel);
25300
- const handle = await open2(entry.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch(() => denied("final component tidak aman"));
26065
+ const handle = await open2(entry.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0)).catch(() => denied("final component tidak aman"));
25301
26066
  try {
25302
26067
  const stat4 = await handle.stat();
25303
26068
  if (!stat4.isFile()) denied("bukan file regular");
@@ -25317,8 +26082,8 @@ async function writeRepoFileAtomic(root, rel, data) {
25317
26082
  await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
25318
26083
  if (error.code !== "EEXIST") throw error;
25319
26084
  });
25320
- const temp = join6(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
25321
- const handle = await open2(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
26085
+ const temp = join7(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
26086
+ const handle = await open2(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
25322
26087
  try {
25323
26088
  await handle.writeFile(data);
25324
26089
  await handle.sync();
@@ -25339,7 +26104,7 @@ async function writeRepoFileAtomic(root, rel, data) {
25339
26104
  }
25340
26105
 
25341
26106
  // src/services/scan.ts
25342
- var exec = promisify(execFile);
26107
+ var exec = promisify(execFile2);
25343
26108
  async function listRepoDocs(repoDir) {
25344
26109
  try {
25345
26110
  const { stdout } = await exec(
@@ -25459,7 +26224,18 @@ function sessionOf(projectId, sessions) {
25459
26224
  commit: `\u2192 hanoman/${s2.id}`
25460
26225
  };
25461
26226
  }
25462
- async function toProjectView(p, sessions) {
26227
+ async function loadDeviceIndex() {
26228
+ const rows = await prisma.deviceToken.findMany({ select: { id: true, name: true, revokedAt: true } });
26229
+ return new Map(rows.map((d) => [d.id, { name: d.name, revoked: d.revokedAt !== null }]));
26230
+ }
26231
+ function handledByView(raw2, devices) {
26232
+ return handledByOf(raw2).map((e) => {
26233
+ const d = devices.get(e.deviceId);
26234
+ return { deviceId: e.deviceId, name: d?.name ?? e.name, revoked: d?.revoked ?? false };
26235
+ });
26236
+ }
26237
+ async function toProjectView(p, sessions, devices) {
26238
+ const deviceIndex = devices ?? await loadDeviceIndex();
25463
26239
  const specs = await prisma.spec.findMany({ where: { projectId: p.id } });
25464
26240
  const { coverage } = await scanRepoDocs(await resolveRepoDir(p.id));
25465
26241
  const binding = await getBinding(p.id);
@@ -25489,6 +26265,9 @@ async function toProjectView(p, sessions) {
25489
26265
  schedulerOptIn: p.schedulerOptIn,
25490
26266
  // SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal per-instance, cermin schedulerOptIn).
25491
26267
  leadOptIn: p.leadOptIn,
26268
+ // SPEC-880 · ADR-0135 · penanda "ditangani oleh" (DISYNC — beda dari repoDir/binding di atas
26269
+ // yang fakta mesin ini saja).
26270
+ handledBy: handledByView(p.handledBy, deviceIndex),
25492
26271
  // SPEC-486 · ADR-0103 · kebijakan auto-merge (lokal per-instance, cermin schedulerOptIn).
25493
26272
  autoMerge: autoMergeOf(p.autoMerge)
25494
26273
  };
@@ -25539,9 +26318,9 @@ async function listPendingDeletes() {
25539
26318
  }
25540
26319
 
25541
26320
  // src/services/branches.ts
25542
- import { execFile as execFile2 } from "node:child_process";
26321
+ import { execFile as execFile3 } from "node:child_process";
25543
26322
  import { promisify as promisify2 } from "node:util";
25544
- var exec2 = promisify2(execFile2);
26323
+ var exec2 = promisify2(execFile3);
25545
26324
  var GIT = { maxBuffer: 1 << 24, encoding: "utf8" };
25546
26325
  async function refs(repoDir, glob) {
25547
26326
  if (!repoDir) return [];
@@ -25596,6 +26375,17 @@ async function checkAutoMerge(repoDir, raw2) {
25596
26375
  return known.includes(p.branch) ? { ok: true } : { ok: false, code: 400, error: `branch "${p.branch}" tidak ada di ${p.dest === "origin" ? "origin" : "repo lokal"} project` };
25597
26376
  }
25598
26377
 
26378
+ // src/services/handled-by.ts
26379
+ init_db();
26380
+ async function checkHandledBy(list2) {
26381
+ const known = await prisma.deviceToken.findMany({ select: { id: true } });
26382
+ if (!known.length) return { ok: true };
26383
+ const ids = new Set(known.map((d) => d.id));
26384
+ const asing = list2.filter((e) => !ids.has(e.deviceId)).map((e) => e.deviceId);
26385
+ if (asing.length) return { ok: false, code: 400, error: `device tak dikenal: ${asing.join(", ")}` };
26386
+ return { ok: true };
26387
+ }
26388
+
25599
26389
  // src/routes/projects.ts
25600
26390
  import { Prisma } from "@prisma/client";
25601
26391
  init_pty();
@@ -25613,13 +26403,16 @@ function paginate(items, page, limit) {
25613
26403
  init_src2();
25614
26404
  async function projects_default(app2) {
25615
26405
  app2.get("/projects", async (req) => {
25616
- const { q, page, limit } = req.query;
26406
+ const { q, handledBy, page, limit } = req.query;
25617
26407
  const ps = await prisma.project.findMany({ orderBy: { createdAt: "desc" } });
25618
26408
  const sessions = listSessions();
25619
- const views = await Promise.all(ps.map((p) => toProjectView(p, sessions)));
26409
+ const devices = await loadDeviceIndex();
26410
+ const views = await Promise.all(ps.map((p) => toProjectView(p, sessions, devices)));
25620
26411
  const needle = (q ?? "").trim().toLowerCase();
25621
26412
  const filtered = needle ? views.filter((v) => `${v.name} ${v.desc} ${v.stack}`.toLowerCase().includes(needle)) : views;
25622
- return paginate(filtered, page, limit);
26413
+ const device = (handledBy ?? "").trim();
26414
+ const scoped = device ? filtered.filter((v) => v.handledBy.some((h) => h.deviceId === device)) : filtered;
26415
+ return paginate(scoped, page, limit);
25623
26416
  });
25624
26417
  app2.get("/projects/:id", async (req, reply) => {
25625
26418
  const { id } = req.params;
@@ -25634,6 +26427,10 @@ async function projects_default(app2) {
25634
26427
  const id = (b.name || b.repoDir?.split("/").pop() || "repo").trim().toLowerCase().replace(/\s+/g, "-");
25635
26428
  if (await prisma.project.findUnique({ where: { id } }))
25636
26429
  return reply.code(409).send({ error: `project "${id}" sudah ada` });
26430
+ if (b.handledBy?.length) {
26431
+ const gate = await checkHandledBy(b.handledBy);
26432
+ if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
26433
+ }
25637
26434
  if (b.kind === "from-scratch" && b.repoDir) {
25638
26435
  try {
25639
26436
  realGit.initRepo(b.repoDir);
@@ -25649,7 +26446,8 @@ async function projects_default(app2) {
25649
26446
  kind: b.kind,
25650
26447
  repoDir: b.repoDir ?? null,
25651
26448
  gitRemote: b.gitRemote ?? null,
25652
- stack: ""
26449
+ stack: "",
26450
+ ...b.handledBy?.length ? { handledBy: b.handledBy } : {}
25653
26451
  }
25654
26452
  });
25655
26453
  await notifySynced("project", created.id);
@@ -25664,8 +26462,13 @@ async function projects_default(app2) {
25664
26462
  const gate = await checkAutoMerge(await resolveRepoDir(id), parsed.data.autoMerge);
25665
26463
  if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
25666
26464
  }
26465
+ if (parsed.data.handledBy?.length) {
26466
+ const gate = await checkHandledBy(parsed.data.handledBy);
26467
+ if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
26468
+ }
25667
26469
  const data = { ...parsed.data };
25668
26470
  if ("autoMerge" in data && data.autoMerge === null) data.autoMerge = Prisma.DbNull;
26471
+ if ("handledBy" in data && !data.handledBy?.length) data.handledBy = Prisma.DbNull;
25669
26472
  const updated = await prisma.project.update({ where: { id }, data });
25670
26473
  await notifySynced("project", id);
25671
26474
  return toProjectView(updated, listSessions());
@@ -25736,13 +26539,13 @@ init_src2();
25736
26539
  import { existsSync as existsSync7 } from "node:fs";
25737
26540
 
25738
26541
  // src/services/integrate.ts
25739
- import { execFile as execFile3 } from "node:child_process";
26542
+ import { execFile as execFile4 } from "node:child_process";
25740
26543
  import { promisify as promisify3 } from "node:util";
25741
26544
  import { rmSync as rmSync3 } from "node:fs";
25742
- import { join as join7 } from "node:path";
26545
+ import { join as join8 } from "node:path";
25743
26546
  var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
25744
26547
  var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
25745
- var exec3 = promisify3(execFile3);
26548
+ var exec3 = promisify3(execFile4);
25746
26549
  var GIT2 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
25747
26550
  var sh = (cwd, args) => exec3("git", args, { cwd, ...GIT2 }).then(
25748
26551
  (r) => ({ status: 0, stdout: r.stdout, stderr: r.stderr }),
@@ -25783,7 +26586,7 @@ async function integrateBranch(repoDir, src, op, target2) {
25783
26586
  const tgt = await resolveTarget(repoDir, target2);
25784
26587
  if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
25785
26588
  await sh(repoDir, ["fetch", "origin"]);
25786
- const wt = join7(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
26589
+ const wt = join8(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
25787
26590
  await reclaim(repoDir, wt);
25788
26591
  const baseRef = op === "merge" ? tgt.ref : source;
25789
26592
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
@@ -25791,9 +26594,9 @@ async function integrateBranch(repoDir, src, op, target2) {
25791
26594
  return { status: "error", code: 500, error: "gagal membuat worktree integrasi" };
25792
26595
  const applyRef = op === "merge" ? source : tgt.ref;
25793
26596
  const cmd = op === "merge" ? ["merge", "--no-edit", applyRef] : ["rebase", applyRef];
25794
- const run2 = await sh(wt, cmd);
26597
+ const run3 = await sh(wt, cmd);
25795
26598
  const finalize = op === "rebase" ? { kind: "force-push", branch: src.branch } : tgt.dest === "local" ? { kind: "branch-f", branch: tgt.name, checkout: await worktreeForBranch(repoDir, tgt.name) } : { kind: "push", branch: tgt.name };
25796
- if (run2.status === 0) {
26599
+ if (run3.status === 0) {
25797
26600
  const fin = await runFinalize(wt, repoDir, finalize);
25798
26601
  await sh(repoDir, ["worktree", "remove", "--force", wt]);
25799
26602
  return fin.ok ? { status: "clean", detail: fin.detail } : { status: "error", code: 409, error: fin.error };
@@ -25844,16 +26647,16 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
25844
26647
  const src = await resolveGraphSource(repoDir, source);
25845
26648
  if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
25846
26649
  await sh(repoDir, ["fetch", "origin"]);
25847
- const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
26650
+ const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
25848
26651
  await reclaim(repoDir, wt);
25849
26652
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
25850
26653
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
25851
26654
  return { status: "error", code: 500, error: "gagal membuat worktree merge" };
25852
26655
  const srcSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${src}^{commit}`]);
25853
26656
  const cmd = ["merge", "--no-edit", ...opts.ff ? [`--${opts.ff}`] : [], "--end-of-options", srcSha];
25854
- const run2 = await sh(wt, cmd);
26657
+ const run3 = await sh(wt, cmd);
25855
26658
  const finalize = { kind: "branch-f", branch: current, checkout: await worktreeForBranch(repoDir, current) };
25856
- if (run2.status === 0) {
26659
+ if (run3.status === 0) {
25857
26660
  const fin = await runFinalize(wt, repoDir, finalize);
25858
26661
  if (fin.ok && opts.deleteBranch) await deleteMergedBranch(repoDir, opts.deleteBranch);
25859
26662
  await sh(repoDir, ["worktree", "remove", "--force", wt]);
@@ -25863,7 +26666,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
25863
26666
  if (!conflicted) {
25864
26667
  await sh(wt, ["merge", "--abort"]);
25865
26668
  await sh(repoDir, ["worktree", "remove", "--force", wt]);
25866
- return { status: "error", code: 409, error: run2.stderr.trim() || "merge gagal \u2014 tak bisa fast-forward?" };
26669
+ return { status: "error", code: 409, error: run3.stderr.trim() || "merge gagal \u2014 tak bisa fast-forward?" };
25867
26670
  }
25868
26671
  return { status: "conflict", worktree: wt, source: src, target: `local:${current}`, finalize: finalizeInstruction("merge", finalize) };
25869
26672
  }
@@ -25882,13 +26685,13 @@ async function replayCurrent(repoDir, source, cmd) {
25882
26685
  const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
25883
26686
  if (!current || current === "HEAD")
25884
26687
  return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
25885
- const wt = join7(repoDir, ".worktrees", `merge-${sanitize(current)}`);
26688
+ const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
25886
26689
  await reclaim(repoDir, wt);
25887
26690
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
25888
26691
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
25889
26692
  return { status: "error", code: 500, error: "gagal membuat worktree rebase" };
25890
- const run2 = await sh(wt, cmd);
25891
- if (run2.status === 0) {
26693
+ const run3 = await sh(wt, cmd);
26694
+ if (run3.status === 0) {
25892
26695
  const head = await out(wt, ["rev-parse", "HEAD"]);
25893
26696
  const fin = await landRewrite(repoDir, current, head);
25894
26697
  await sh(repoDir, ["worktree", "remove", "--force", wt]);
@@ -25898,7 +26701,7 @@ async function replayCurrent(repoDir, source, cmd) {
25898
26701
  if (!conflicted) {
25899
26702
  await sh(wt, ["rebase", "--abort"]);
25900
26703
  await sh(repoDir, ["worktree", "remove", "--force", wt]);
25901
- return { status: "error", code: 409, error: run2.stderr.trim() || "operasi rebase gagal" };
26704
+ return { status: "error", code: 409, error: run3.stderr.trim() || "operasi rebase gagal" };
25902
26705
  }
25903
26706
  return { status: "conflict", worktree: wt, source, target: `local:${current}`, finalize: rewriteInstruction(repoDir, current) };
25904
26707
  }
@@ -25927,19 +26730,19 @@ init_codex_trust();
25927
26730
  init_db();
25928
26731
 
25929
26732
  // src/services/spec-review.ts
25930
- import { execFile as execFile4 } from "node:child_process";
26733
+ import { execFile as execFile5 } from "node:child_process";
25931
26734
  import { promisify as promisify4 } from "node:util";
25932
26735
  import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
25933
26736
  import { tmpdir as tmpdir2 } from "node:os";
25934
- import { join as join8, resolve as resolve11 } from "node:path";
25935
- var exec4 = promisify4(execFile4);
26737
+ import { join as join9, resolve as resolve11 } from "node:path";
26738
+ var exec4 = promisify4(execFile5);
25936
26739
  var GIT3 = { maxBuffer: 1 << 24 };
25937
26740
  var MAX = 256 * 1024;
25938
- var worktreeDir = (repoDir, specId) => join8(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
26741
+ var worktreeDir = (repoDir, specId) => join9(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
25939
26742
  async function withTempIndex(wt, fn2) {
25940
26743
  const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
25941
- const dir2 = await mkdtemp(join8(tmpdir2(), "hanoman-idx-"));
25942
- const tmp = join8(dir2, "index");
26744
+ const dir2 = await mkdtemp(join9(tmpdir2(), "hanoman-idx-"));
26745
+ const tmp = join9(dir2, "index");
25943
26746
  await copyFile(resolve11(wt, idx), tmp);
25944
26747
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
25945
26748
  try {
@@ -26210,7 +27013,7 @@ import { extname } from "node:path";
26210
27013
  import { randomUUID as randomUUID5 } from "node:crypto";
26211
27014
  import { spawn as spawn2 } from "node:child_process";
26212
27015
  import { mkdir as mkdir3, rename as rename2, unlink as unlink3, writeFile as writeFile2 } from "node:fs/promises";
26213
- import { basename as basename3, isAbsolute as isAbsolute4, join as join9 } from "node:path";
27016
+ import { basename as basename3, isAbsolute as isAbsolute5, join as join10 } from "node:path";
26214
27017
 
26215
27018
  // ../node_modules/.pnpm/strtok3@10.3.5/node_modules/strtok3/lib/stream/Errors.js
26216
27019
  var defaultMessages = "End-Of-Stream";
@@ -33572,6 +34375,7 @@ utility_default(constructor_default);
33572
34375
  var dist_default = constructor_default;
33573
34376
 
33574
34377
  // src/services/upload-pipeline.ts
34378
+ init_src2();
33575
34379
  init_db();
33576
34380
  init_uploads();
33577
34381
  var UPLOAD_LIMITS = {
@@ -33617,10 +34421,12 @@ async function defaultUsage(projectId) {
33617
34421
  function scannerFromEnv(path2) {
33618
34422
  const command = process.env.HANOMAN_UPLOAD_SCANNER?.trim();
33619
34423
  if (!command) {
33620
- if (process.env.NODE_ENV === "production") return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner required"));
34424
+ if (resolveHardening(process.env))
34425
+ return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner required"));
34426
+ console.warn("upload: HANOMAN_UPLOAD_SCANNER tak disetel \u2014 lampiran diterima tanpa dipindai");
33621
34427
  return Promise.resolve();
33622
34428
  }
33623
- if (!isAbsolute4(command)) return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner path must be absolute"));
34429
+ if (!isAbsolute5(command)) return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner path must be absolute"));
33624
34430
  return new Promise((resolve21, reject2) => {
33625
34431
  const child = spawn2(command, [path2], { shell: false, stdio: "ignore" });
33626
34432
  const timer9 = setTimeout(() => {
@@ -33644,16 +34450,16 @@ function safeFilename(input, extension2) {
33644
34450
  }
33645
34451
  async function commitToStorage(buffer2, extension2, deps, beforePromote) {
33646
34452
  const storageDir = deps.storageDir ?? uploadDir();
33647
- const quarantineDir = join9(storageDir, ".quarantine");
34453
+ const quarantineDir = join10(storageDir, ".quarantine");
33648
34454
  await mkdir3(quarantineDir, { recursive: true, mode: 448 });
33649
34455
  await mkdir3(storageDir, { recursive: true, mode: 448 });
33650
- const quarantine = join9(quarantineDir, `${randomUUID5()}.upload`);
34456
+ const quarantine = join10(quarantineDir, `${randomUUID5()}.upload`);
33651
34457
  const storageKey = `${randomUUID5()}${extension2}`;
33652
34458
  await writeFile2(quarantine, buffer2, { mode: 384, flag: "wx" });
33653
34459
  try {
33654
34460
  await timeout2((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
33655
34461
  await beforePromote?.();
33656
- await rename2(quarantine, join9(storageDir, storageKey));
34462
+ await rename2(quarantine, join10(storageDir, storageKey));
33657
34463
  } catch (error) {
33658
34464
  await unlink3(quarantine).catch(() => {
33659
34465
  });
@@ -33854,10 +34660,10 @@ async function dropSpecAttachments(specId) {
33854
34660
  // src/services/spec-attachment-dir.ts
33855
34661
  init_db();
33856
34662
  import { mkdir as mkdir4, readdir, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
33857
- import { join as join10 } from "node:path";
34663
+ import { join as join11 } from "node:path";
33858
34664
  init_session_id();
33859
34665
  init_uploads();
33860
- var specAttachmentsDir = (repoDir, sessionId2) => join10(repoDir, ".worktrees", ".attachments", sessionId2);
34666
+ var specAttachmentsDir = (repoDir, sessionId2) => join11(repoDir, ".worktrees", ".attachments", sessionId2);
33861
34667
  var INDEX = "INDEX.md";
33862
34668
  var humanSize = (n2) => n2 >= 1024 * 1024 ? `${(n2 / 1024 / 1024).toFixed(1)} MB` : n2 >= 1024 ? `${Math.round(n2 / 1024)} KB` : `${n2} B`;
33863
34669
  function uniqueName(taken, filename) {
@@ -33907,19 +34713,19 @@ async function syncSpecAttachmentsDir(specId, projectId) {
33907
34713
  for (const a of rows) {
33908
34714
  let bytes;
33909
34715
  try {
33910
- bytes = await readFile3(join10(uploadDir(), a.storageKey));
34716
+ bytes = await readFile3(join11(uploadDir(), a.storageKey));
33911
34717
  } catch {
33912
34718
  continue;
33913
34719
  }
33914
34720
  const filename = uniqueName(taken, a.filename);
33915
- const path2 = join10(dir2, filename);
34721
+ const path2 = join11(dir2, filename);
33916
34722
  await writeFile3(path2, bytes, { mode: 384 });
33917
34723
  items.push({ filename, mimeType: a.mimeType, size: a.size, path: path2 });
33918
34724
  }
33919
- await writeFile3(join10(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
34725
+ await writeFile3(join11(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
33920
34726
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
33921
34727
  for (const name2 of await readdir(dir2)) {
33922
- if (!keep.has(name2)) await rm3(join10(dir2, name2), { recursive: true, force: true }).catch(() => {
34728
+ if (!keep.has(name2)) await rm3(join11(dir2, name2), { recursive: true, force: true }).catch(() => {
33923
34729
  });
33924
34730
  }
33925
34731
  return items;
@@ -37235,14 +38041,14 @@ async function docs_default(app2) {
37235
38041
  // src/routes/ide.ts
37236
38042
  init_src2();
37237
38043
  import { basename as basename5, resolve as resolve16 } from "node:path";
37238
- import { execFile as execFile10, spawn as spawn3 } from "node:child_process";
38044
+ import { execFile as execFile11, spawn as spawn3 } from "node:child_process";
37239
38045
  import { promisify as promisify10 } from "node:util";
37240
38046
 
37241
38047
  // src/services/git-remotes.ts
37242
- import { execFile as execFile5 } from "node:child_process";
38048
+ import { execFile as execFile6 } from "node:child_process";
37243
38049
  import { promisify as promisify5 } from "node:util";
37244
38050
  import { existsSync as existsSync8 } from "node:fs";
37245
- var exec5 = promisify5(execFile5);
38051
+ var exec5 = promisify5(execFile6);
37246
38052
  var GIT4 = { maxBuffer: 1 << 24, encoding: "utf8" };
37247
38053
  function parseRemote(url2) {
37248
38054
  let m = /^git@([^:]+):(.+?)(?:\.git)?$/.exec(url2);
@@ -37295,15 +38101,15 @@ init_settings2();
37295
38101
  init_codex_trust();
37296
38102
 
37297
38103
  // src/services/branch-cleanup.ts
37298
- import { execFile as execFile7 } from "node:child_process";
38104
+ import { execFile as execFile8 } from "node:child_process";
37299
38105
  import { promisify as promisify7 } from "node:util";
37300
38106
 
37301
38107
  // src/services/git-ide.ts
37302
- import { execFile as execFile6 } from "node:child_process";
38108
+ import { execFile as execFile7 } from "node:child_process";
37303
38109
  import { promisify as promisify6 } from "node:util";
37304
38110
  import { existsSync as existsSync9 } from "node:fs";
37305
38111
  import { resolve as resolve12 } from "node:path";
37306
- var exec6 = promisify6(execFile6);
38112
+ var exec6 = promisify6(execFile7);
37307
38113
  var GIT5 = { maxBuffer: 1 << 24 };
37308
38114
  var MAX2 = 256 * 1024;
37309
38115
  var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
@@ -37467,16 +38273,16 @@ async function compareCommits(repoDir, from, to) {
37467
38273
  }
37468
38274
  async function searchCommits(repoDir, q, by = "all") {
37469
38275
  if (!repoDir || !existsSync9(repoDir) || !q) return [];
37470
- const run2 = async (extra) => {
38276
+ const run3 = async (extra) => {
37471
38277
  try {
37472
38278
  return (await exec6("git", ["log", "--all", "--format=%H", ...extra], { cwd: repoDir, ...GIT5 })).stdout.split("\n").filter(Boolean);
37473
38279
  } catch {
37474
38280
  return [];
37475
38281
  }
37476
38282
  };
37477
- const byMessage = () => run2(["-i", "-F", `--grep=${q}`]);
37478
- const byAuthor = () => run2(["-i", `--author=${q}`]);
37479
- const byHash = async () => (await run2([])).filter((h2) => h2.toLowerCase().startsWith(q.toLowerCase()));
38283
+ const byMessage = () => run3(["-i", "-F", `--grep=${q}`]);
38284
+ const byAuthor = () => run3(["-i", `--author=${q}`]);
38285
+ const byHash = async () => (await run3([])).filter((h2) => h2.toLowerCase().startsWith(q.toLowerCase()));
37480
38286
  if (by === "message") return byMessage();
37481
38287
  if (by === "author") return byAuthor();
37482
38288
  if (by === "hash") return byHash();
@@ -37748,7 +38554,7 @@ async function workingFileDiff(repoDir, path2, staged) {
37748
38554
  }
37749
38555
 
37750
38556
  // src/services/branch-cleanup.ts
37751
- var exec7 = promisify7(execFile7);
38557
+ var exec7 = promisify7(execFile8);
37752
38558
  var GIT6 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
37753
38559
  var LOCK_REASON = {
37754
38560
  current: "branch aktif (HEAD)",
@@ -37916,10 +38722,10 @@ async function deleteBranches(repoDir, names, opts) {
37916
38722
  }
37917
38723
 
37918
38724
  // src/services/worktree-list.ts
37919
- import { execFile as execFile8 } from "node:child_process";
38725
+ import { execFile as execFile9 } from "node:child_process";
37920
38726
  import { realpathSync as realpathSync4 } from "node:fs";
37921
38727
  import { stat } from "node:fs/promises";
37922
- import { basename as basename4, join as join11, resolve as resolve14, sep as sep4 } from "node:path";
38728
+ import { basename as basename4, join as join12, resolve as resolve14, sep as sep4 } from "node:path";
37923
38729
  import { promisify as promisify8 } from "node:util";
37924
38730
 
37925
38731
  // src/services/session-worktree.ts
@@ -37932,7 +38738,7 @@ function ownsWorktree(repoDir, cwd) {
37932
38738
  }
37933
38739
 
37934
38740
  // src/services/worktree-list.ts
37935
- var exec8 = promisify8(execFile8);
38741
+ var exec8 = promisify8(execFile9);
37936
38742
  var GIT7 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
37937
38743
  async function out3(cwd, args) {
37938
38744
  try {
@@ -37975,7 +38781,7 @@ var real = (p) => {
37975
38781
  };
37976
38782
  async function bornAt(path2) {
37977
38783
  try {
37978
- const st = await stat(join11(path2, ".git"));
38784
+ const st = await stat(join12(path2, ".git"));
37979
38785
  const ms = st.birthtimeMs > 0 ? st.birthtimeMs : st.mtimeMs;
37980
38786
  return new Date(ms).toISOString();
37981
38787
  } catch {
@@ -38095,14 +38901,14 @@ init_stage_machine();
38095
38901
  // src/services/worktree-reaper.ts
38096
38902
  init_src2();
38097
38903
  init_db();
38098
- import { execFile as execFile9 } from "node:child_process";
38904
+ import { execFile as execFile10 } from "node:child_process";
38099
38905
  import { readdir as readdir2, rm as rm4 } from "node:fs/promises";
38100
38906
  import { promisify as promisify9 } from "node:util";
38101
- import { join as join12, resolve as resolve15 } from "node:path";
38907
+ import { join as join13, resolve as resolve15 } from "node:path";
38102
38908
  init_notifications();
38103
38909
  var TICK_MS = 6e4;
38104
38910
  var trashDirOf = (repoDir) => resolve15(repoDir, ".worktrees", ".trash");
38105
- var exec9 = promisify9(execFile9);
38911
+ var exec9 = promisify9(execFile10);
38106
38912
  var prodReaperDeps = {
38107
38913
  rm: (path2) => rm4(path2, { recursive: true, force: true }),
38108
38914
  prune: async (repoDir) => {
@@ -38158,7 +38964,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
38158
38964
  }
38159
38965
  let removed = 0;
38160
38966
  for (const entry of entries) {
38161
- const path2 = join12(dir2, entry);
38967
+ const path2 = join13(dir2, entry);
38162
38968
  const known = pending.get(path2);
38163
38969
  const row = known ?? { path: path2, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
38164
38970
  pending.set(path2, row);
@@ -38248,7 +39054,7 @@ init_session_id();
38248
39054
  // src/services/repo-fs.ts
38249
39055
  import { lstat as lstat2, rename as rename3, rm as rm5 } from "node:fs/promises";
38250
39056
  import { createWriteStream } from "node:fs";
38251
- import { join as join13 } from "node:path";
39057
+ import { join as join14 } from "node:path";
38252
39058
  import { randomUUID as randomUUID6 } from "node:crypto";
38253
39059
  import { pipeline } from "node:stream/promises";
38254
39060
  var EntryExistsError = class extends Error {
@@ -38309,7 +39115,7 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
38309
39115
  if (current && !opts.overwrite) return { status: "exists" };
38310
39116
  if (current && !current.isFile())
38311
39117
  throw new PathContainmentError("repository path ditolak: target bukan file regular");
38312
- const temp = join13(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
39118
+ const temp = join14(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
38313
39119
  try {
38314
39120
  await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
38315
39121
  if (opts.isTruncated?.()) {
@@ -38349,7 +39155,7 @@ async function worktreeInputs(id) {
38349
39155
  sessions: new Map(listSessions().filter((s2) => s2.projectId === id && !s2.exited).map((s2) => [resolve16(s2.cwd), { id: s2.id, specId: s2.specId ?? null }]))
38350
39156
  };
38351
39157
  }
38352
- var execAsync = promisify10(execFile10);
39158
+ var execAsync = promisify10(execFile11);
38353
39159
  function entryError(reply, e) {
38354
39160
  if (e instanceof EntryMissingError) return reply.code(404).send({ error: "not found" });
38355
39161
  if (e instanceof EntryExistsError) return reply.code(409).send({ error: "sudah ada" });
@@ -38813,14 +39619,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
38813
39619
  // src/routes/fs.ts
38814
39620
  import { readdir as readdir3 } from "node:fs/promises";
38815
39621
  import { homedir as homedir4 } from "node:os";
38816
- import { resolve as resolve17, dirname as dirname9, join as join14 } from "node:path";
39622
+ import { resolve as resolve17, dirname as dirname9, join as join15 } from "node:path";
38817
39623
  async function fs_default(app2) {
38818
39624
  app2.get("/fs/browse", async (req, reply) => {
38819
39625
  const q = req.query.path;
38820
39626
  const dir2 = q && q.trim() ? resolve17(q.trim()) : homedir4();
38821
39627
  try {
38822
39628
  const ents = await readdir3(dir2, { withFileTypes: true });
38823
- const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join14(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
39629
+ const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join15(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
38824
39630
  const parent = dirname9(dir2);
38825
39631
  return { path: dir2, parent: parent === dir2 ? null : parent, entries };
38826
39632
  } catch {
@@ -39021,6 +39827,7 @@ function setBounded(map, key, value, maxKeys) {
39021
39827
  }
39022
39828
 
39023
39829
  // src/services/auth.ts
39830
+ init_src2();
39024
39831
  init_db();
39025
39832
  import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual2, createHash as createHash6 } from "node:crypto";
39026
39833
  import { promisify as promisify11 } from "node:util";
@@ -39087,11 +39894,13 @@ function clearLoginFails(ip) {
39087
39894
  fails.clear(ip);
39088
39895
  loginStates.delete(ip);
39089
39896
  }
39090
- function cookieOpts() {
39897
+ function cookieOpts(req) {
39898
+ const forwarded = String(req.headers["x-forwarded-proto"] ?? "").split(",")[0]?.trim();
39899
+ const https = req.protocol === "https" || forwarded === "https";
39091
39900
  return {
39092
39901
  httpOnly: true,
39093
39902
  sameSite: "strict",
39094
- secure: process.env.NODE_ENV === "production",
39903
+ secure: https || resolveHardening(process.env),
39095
39904
  path: "/",
39096
39905
  maxAge: SESSION_TTL_MS / 1e3
39097
39906
  };
@@ -39217,6 +40026,41 @@ async function revalidateWsPrincipal(req, principal) {
39217
40026
  }
39218
40027
  return req.agent?.id === principal.id;
39219
40028
  }
40029
+ var WS_REVALIDATE_EVERY_MS = 1e3;
40030
+ function createPrincipalWatch(opts) {
40031
+ const now = opts.now ?? Date.now;
40032
+ const everyMs = opts.everyMs ?? WS_REVALIDATE_EVERY_MS;
40033
+ let revoked = false;
40034
+ let disposed = false;
40035
+ let inFlight3 = false;
40036
+ let attemptedAt = null;
40037
+ const run3 = () => {
40038
+ if (revoked || disposed || inFlight3) return;
40039
+ inFlight3 = true;
40040
+ attemptedAt = now();
40041
+ opts.check().then((ok2) => {
40042
+ inFlight3 = false;
40043
+ if (disposed || revoked || ok2) return;
40044
+ revoked = true;
40045
+ opts.onRevoked();
40046
+ }, () => {
40047
+ inFlight3 = false;
40048
+ });
40049
+ };
40050
+ return {
40051
+ admit() {
40052
+ if (revoked) return false;
40053
+ if (attemptedAt === null || now() - attemptedAt >= everyMs) run3();
40054
+ return true;
40055
+ },
40056
+ refresh() {
40057
+ run3();
40058
+ },
40059
+ dispose() {
40060
+ disposed = true;
40061
+ }
40062
+ };
40063
+ }
39220
40064
  function bearerToken(req) {
39221
40065
  const value = req.headers.authorization;
39222
40066
  return typeof value === "string" && value.startsWith("Bearer ") ? value.slice(7) : null;
@@ -39260,14 +40104,14 @@ function installCommand(m, agent, shell = shellBin()) {
39260
40104
  }
39261
40105
 
39262
40106
  // src/services/terminal-diag.ts
39263
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, statSync as statSync3, rmSync as rmSync4 } from "node:fs";
39264
- import { join as join15 } from "node:path";
40107
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync4 } from "node:fs";
40108
+ import { join as join16 } from "node:path";
39265
40109
  var DIAG_MAX_BYTES = 2 * 1024 * 1024;
39266
40110
  var KINDS = /* @__PURE__ */ new Set(["key", "comp", "data", "ack", "pred"]);
39267
40111
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
39268
40112
  function diagFile(home3, sessionId2) {
39269
40113
  if (!ID.test(sessionId2)) throw new Error(`id sesi tak sah untuk diag: ${sessionId2}`);
39270
- return join15(home3, "diag", `${sessionId2}.jsonl`);
40114
+ return join16(home3, "diag", `${sessionId2}.jsonl`);
39271
40115
  }
39272
40116
  function usable(ev) {
39273
40117
  if (!ev || typeof ev !== "object") return false;
@@ -39278,7 +40122,7 @@ function appendDiag(home3, sessionId2, events2) {
39278
40122
  const file = diagFile(home3, sessionId2);
39279
40123
  const rows = events2.filter(usable);
39280
40124
  if (!rows.length) return;
39281
- mkdirSync8(join15(home3, "diag"), { recursive: true });
40125
+ mkdirSync9(join16(home3, "diag"), { recursive: true });
39282
40126
  try {
39283
40127
  if (statSync3(file).size > DIAG_MAX_BYTES) rmSync4(file);
39284
40128
  } catch {
@@ -39291,7 +40135,103 @@ init_notifications();
39291
40135
  init_stage_machine();
39292
40136
  init_pty();
39293
40137
  init_uploads();
40138
+
40139
+ // src/services/session-dialog.ts
40140
+ init_tui_dialog();
40141
+ init_pty();
40142
+ import { createHash as createHash8 } from "node:crypto";
40143
+ var DIALOG_CHUNK_MS = 50;
40144
+ var screenHashOf = (paneText2) => createHash8("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
40145
+ var answerable = (paneText2) => {
40146
+ const s2 = readDialogScreen(paneText2);
40147
+ if (s2?.kind !== "question") return null;
40148
+ if (!(s2.multi && s2.submit.present) && s2.freeIndex === null && !s2.notes) return null;
40149
+ return s2;
40150
+ };
40151
+ function readSessionDialog(io) {
40152
+ const text = io.capture();
40153
+ const s2 = answerable(text);
40154
+ if (!s2) return null;
40155
+ return {
40156
+ dialog: {
40157
+ title: s2.title,
40158
+ multi: s2.multi,
40159
+ freeIndex: s2.freeIndex,
40160
+ notes: s2.notes,
40161
+ options: s2.rows.filter((r) => !r.free && !r.chat).map((r) => ({ n: r.n, label: r.label, checked: r.checked })),
40162
+ tabs: s2.tabs
40163
+ },
40164
+ screenHash: screenHashOf(text)
40165
+ };
40166
+ }
40167
+ async function answerSessionDialog(io, input, chunkMs = DIALOG_CHUNK_MS) {
40168
+ const text = io.capture();
40169
+ const s2 = answerable(text);
40170
+ if (!s2) return { ok: false, reason: "stale" };
40171
+ if (screenHashOf(text) !== input.screenHash) return { ok: false, reason: "stale" };
40172
+ const optionLabel = (n2) => s2.rows.find((r) => r.n === n2 && !r.free && !r.chat)?.label ?? null;
40173
+ if (s2.multi && s2.submit.present) {
40174
+ if (input.choice !== void 0) return { ok: false, reason: "shape" };
40175
+ const pick2 = input.choices ?? [];
40176
+ if (pick2.some((n2) => optionLabel(n2) === null)) return { ok: false, reason: "shape" };
40177
+ if (input.text && s2.freeIndex === null) return { ok: false, reason: "shape" };
40178
+ const ok3 = await answerMultiSelectDialog(
40179
+ io,
40180
+ { pick: pick2, line: input.text ?? "", freeIndex: s2.freeIndex },
40181
+ chunkMs
40182
+ );
40183
+ return ok3 ? { ok: true } : { ok: false, reason: "not-landed" };
40184
+ }
40185
+ if (input.choices !== void 0) return { ok: false, reason: "shape" };
40186
+ const line = input.text ?? (input.choice !== void 0 ? optionLabel(input.choice) : null);
40187
+ if (!line) return { ok: false, reason: "shape" };
40188
+ const ok2 = s2.freeIndex !== null ? await answerChoiceDialog(io, s2.freeIndex, line, chunkMs) : await answerNotesDialog(io, line, chunkMs);
40189
+ return ok2 ? { ok: true } : { ok: false, reason: "not-landed" };
40190
+ }
40191
+ var answering = /* @__PURE__ */ new Set();
40192
+ function beginAnswer(sessionId2) {
40193
+ if (answering.has(sessionId2)) return false;
40194
+ answering.add(sessionId2);
40195
+ return true;
40196
+ }
40197
+ function endAnswer(sessionId2) {
40198
+ answering.delete(sessionId2);
40199
+ }
40200
+ var paneIOFactory = paneIO;
40201
+ var sessionPaneIO = (id) => paneIOFactory(id);
40202
+
40203
+ // src/services/lead/deciding.ts
40204
+ var deciding = /* @__PURE__ */ new Set();
40205
+ function markDeciding(sessionId2) {
40206
+ deciding.add(sessionId2);
40207
+ }
40208
+ function clearDeciding(sessionId2) {
40209
+ deciding.delete(sessionId2);
40210
+ }
40211
+ function isDeciding(sessionId2) {
40212
+ return deciding.has(sessionId2);
40213
+ }
40214
+ function decidingIds() {
40215
+ return [...deciding];
40216
+ }
40217
+ var queued = /* @__PURE__ */ new Set();
40218
+ function markQueued(sessionId2) {
40219
+ queued.add(sessionId2);
40220
+ }
40221
+ function clearQueued(sessionId2) {
40222
+ queued.delete(sessionId2);
40223
+ }
40224
+ function queuedIds() {
40225
+ return [...queued];
40226
+ }
40227
+
40228
+ // src/routes/terminal.ts
39294
40229
  var ATTACHMENT_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
40230
+ var DIALOG_ANSWER_ERROR = {
40231
+ stale: "layar dialog sudah berubah \u2014 muat ulang pertanyaannya",
40232
+ shape: "bentuk jawaban tak cocok dengan dialog di layar",
40233
+ "not-landed": "jawaban tak mendarat di pane \u2014 sesi tidak digerakkan"
40234
+ };
39295
40235
  var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
39296
40236
  var ensureWorktree = (repoDir, wt, branchFrom) => {
39297
40237
  if (realGit.worktreeAlive(wt)) return true;
@@ -39515,6 +40455,33 @@ async function terminal_default(app2, opts) {
39515
40455
  if (!ok2) return reply.code(404).send({ error: "live session not found" });
39516
40456
  return reply.code(202).send({ accepted: true });
39517
40457
  });
40458
+ app2.get("/terminal/sessions/:id/dialog", async (req, reply) => {
40459
+ const { id } = req.params;
40460
+ const s2 = getSession(id);
40461
+ if (!s2) return reply.code(404).send({ error: "not found" });
40462
+ if (s2.exited) return reply.code(204).send();
40463
+ const found = readSessionDialog(sessionPaneIO(id));
40464
+ if (!found) return reply.code(204).send();
40465
+ return found;
40466
+ });
40467
+ app2.post("/terminal/sessions/:id/dialog/answer", async (req, reply) => {
40468
+ const parsed = zSessionDialogAnswer.safeParse(req.body);
40469
+ if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
40470
+ const { id } = req.params;
40471
+ const s2 = getSession(id);
40472
+ if (!s2 || s2.exited) return reply.code(404).send({ error: "live session not found" });
40473
+ if (isDeciding(id))
40474
+ return reply.code(409).send({ error: "hanoman-lead sedang menyusun keputusan untuk sesi ini", reason: "deciding" });
40475
+ if (!beginAnswer(id))
40476
+ return reply.code(409).send({ error: "jawaban lain sedang dikirim ke sesi ini", reason: "answering" });
40477
+ try {
40478
+ const r = await answerSessionDialog(sessionPaneIO(id), parsed.data);
40479
+ if (!r.ok) return reply.code(409).send({ error: DIALOG_ANSWER_ERROR[r.reason], reason: r.reason });
40480
+ return reply.code(202).send({ accepted: true });
40481
+ } finally {
40482
+ endAnswer(id);
40483
+ }
40484
+ });
39518
40485
  const sessionWorktree = async (id) => {
39519
40486
  const s2 = getSession(id);
39520
40487
  if (!s2) return { ok: false, code: 404, msg: "not found" };
@@ -39614,17 +40581,17 @@ async function terminal_default(app2, opts) {
39614
40581
  const guard = new WsMessageGuard({ perWindow: TERMINAL_WS_MESSAGES_PER_MINUTE });
39615
40582
  const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
39616
40583
  attach(id, client);
39617
- socket2.on("message", async (raw2) => {
40584
+ const watch = createPrincipalWatch({
40585
+ check: () => revalidateWsPrincipal(req, principal),
40586
+ onRevoked: () => socket2.close(1008, "session revoked")
40587
+ });
40588
+ socket2.on("message", (raw2) => {
39618
40589
  const verdict = guard.accept(raw2);
39619
40590
  if (!verdict.ok) {
39620
40591
  socket2.close(verdict.code, verdict.reason);
39621
40592
  return;
39622
40593
  }
39623
- const valid = await revalidateWsPrincipal(req, principal).catch(() => false);
39624
- if (!valid) {
39625
- socket2.close(1008, "session revoked");
39626
- return;
39627
- }
40594
+ if (!watch.admit()) return;
39628
40595
  let m;
39629
40596
  try {
39630
40597
  m = JSON.parse(raw2.toString());
@@ -39642,14 +40609,11 @@ async function terminal_default(app2, opts) {
39642
40609
  }
39643
40610
  }
39644
40611
  });
39645
- const revalidate = setInterval(() => {
39646
- void revalidateWsPrincipal(req, principal).then((ok2) => {
39647
- if (!ok2) socket2.close(1008, "session revoked");
39648
- });
39649
- }, 6e4);
40612
+ const revalidate = setInterval(() => watch.refresh(), 6e4);
39650
40613
  revalidate.unref?.();
39651
40614
  socket2.on("close", () => {
39652
40615
  clearInterval(revalidate);
40616
+ watch.dispose();
39653
40617
  release();
39654
40618
  detach(id, client);
39655
40619
  });
@@ -39746,7 +40710,7 @@ async function terminalWorkspaceRoutes(app2) {
39746
40710
  // src/routes/vps.ts
39747
40711
  init_db();
39748
40712
  init_src();
39749
- import { readFileSync as readFileSync10, existsSync as existsSync13 } from "node:fs";
40713
+ import { readFileSync as readFileSync13, existsSync as existsSync14 } from "node:fs";
39750
40714
  import { homedir as homedir6 } from "node:os";
39751
40715
 
39752
40716
  // src/vps/catalog/catalog.data.ts
@@ -39828,15 +40792,130 @@ var SECTIONS = RAW_SECTIONS.map((s2) => ({
39828
40792
  var _map = new Map(CATALOG.map((c) => [c.id, c]));
39829
40793
  var byId = (id) => _map.get(id);
39830
40794
 
39831
- // src/services/vps-remediate.ts
39832
- import { readFileSync as readFileSync8 } from "node:fs";
40795
+ // src/vps/catalog/components.ts
40796
+ var BOTH = ["lab", "production"];
40797
+ var COMPONENTS = [
40798
+ {
40799
+ id: "base",
40800
+ label: "Paket dasar (curl, git, tmux, toolchain node-pty)",
40801
+ section: "dasar",
40802
+ requires: { lab: [], production: [] },
40803
+ profiles: BOTH,
40804
+ interactiveLogin: false,
40805
+ needsDomain: false
40806
+ },
40807
+ {
40808
+ id: "node",
40809
+ label: "Node.js 22 LTS",
40810
+ section: "dasar",
40811
+ requires: { lab: ["base"], production: ["base"] },
40812
+ profiles: BOTH,
40813
+ interactiveLogin: false,
40814
+ needsDomain: false
40815
+ },
40816
+ {
40817
+ id: "hanoman",
40818
+ label: "hanoman (npm global + user service + systemd)",
40819
+ section: "hanoman",
40820
+ requires: { lab: ["node"], production: ["node", "podman"] },
40821
+ profiles: BOTH,
40822
+ interactiveLogin: false,
40823
+ needsDomain: false
40824
+ },
40825
+ {
40826
+ id: "caddy",
40827
+ label: "Caddy + TLS otomatis",
40828
+ section: "ingress",
40829
+ requires: { lab: [], production: [] },
40830
+ profiles: BOTH,
40831
+ interactiveLogin: false,
40832
+ needsDomain: true
40833
+ },
40834
+ {
40835
+ id: "podman",
40836
+ label: "Podman rootless + network egress",
40837
+ section: "sandbox",
40838
+ requires: { lab: ["base"], production: ["base"] },
40839
+ profiles: BOTH,
40840
+ interactiveLogin: false,
40841
+ needsDomain: false
40842
+ },
40843
+ {
40844
+ id: "agent-image",
40845
+ label: "Image agen hanoman-agent:latest",
40846
+ section: "sandbox",
40847
+ requires: { lab: [], production: ["podman"] },
40848
+ profiles: ["production"],
40849
+ interactiveLogin: false,
40850
+ needsDomain: false
40851
+ },
40852
+ {
40853
+ id: "claude",
40854
+ label: "Claude Code CLI",
40855
+ section: "agen",
40856
+ requires: { lab: ["node"], production: ["agent-image"] },
40857
+ profiles: BOTH,
40858
+ interactiveLogin: true,
40859
+ needsDomain: false
40860
+ },
40861
+ {
40862
+ id: "codex",
40863
+ label: "Codex CLI",
40864
+ section: "agen",
40865
+ requires: { lab: ["node"], production: ["agent-image"] },
40866
+ profiles: BOTH,
40867
+ interactiveLogin: true,
40868
+ needsDomain: false
40869
+ },
40870
+ {
40871
+ id: "gh",
40872
+ label: "GitHub CLI",
40873
+ section: "agen",
40874
+ requires: { lab: ["base"], production: ["base"] },
40875
+ profiles: BOTH,
40876
+ interactiveLogin: true,
40877
+ needsDomain: false
40878
+ }
40879
+ ];
40880
+ var BY_ID = new Map(COMPONENTS.map((c) => [c.id, c]));
40881
+ var componentById = (id) => BY_ID.get(id);
40882
+ function resolveComponents(ids, profile) {
40883
+ if (ids.length === 0) return { ok: false, error: "tak ada komponen yang dipilih" };
40884
+ const out4 = [];
40885
+ const seen = /* @__PURE__ */ new Set();
40886
+ const stack = /* @__PURE__ */ new Set();
40887
+ const visit = (id) => {
40888
+ const c = componentById(id);
40889
+ if (!c) return `komponen tak dikenal: ${id}`;
40890
+ if (!c.profiles.includes(profile)) return `komponen ${id} tak tersedia di profil ${profile}`;
40891
+ if (seen.has(id)) return null;
40892
+ if (stack.has(id)) return `siklus dependensi pada ${id}`;
40893
+ stack.add(id);
40894
+ for (const dep of c.requires[profile]) {
40895
+ const err = visit(dep);
40896
+ if (err) return err;
40897
+ }
40898
+ stack.delete(id);
40899
+ seen.add(id);
40900
+ out4.push(id);
40901
+ return null;
40902
+ };
40903
+ for (const id of ids) {
40904
+ const err = visit(id);
40905
+ if (err) return { ok: false, error: err };
40906
+ }
40907
+ return { ok: true, items: out4 };
40908
+ }
40909
+
40910
+ // src/services/vps-provision.ts
40911
+ import { readFileSync as readFileSync10 } from "node:fs";
39833
40912
 
39834
40913
  // src/services/vps-ssh.ts
39835
40914
  init_config2();
39836
40915
  import { spawn as spawn4 } from "node:child_process";
39837
- import { mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "node:fs";
40916
+ import { mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
39838
40917
  import { tmpdir as tmpdir3 } from "node:os";
39839
- import { dirname as dirname10, join as join16 } from "node:path";
40918
+ import { dirname as dirname10, join as join17 } from "node:path";
39840
40919
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
39841
40920
  function consoleArgv(t) {
39842
40921
  return [
@@ -39851,9 +40930,9 @@ function consoleArgv(t) {
39851
40930
  ];
39852
40931
  }
39853
40932
  function askpassScript() {
39854
- const dir2 = mkdtempSync(join16(tmpdir3(), "hanoman-askpass-"));
39855
- const path2 = join16(dir2, "askpass.sh");
39856
- writeFileSync4(path2, `#!/bin/sh
40933
+ const dir2 = mkdtempSync(join17(tmpdir3(), "hanoman-askpass-"));
40934
+ const path2 = join17(dir2, "askpass.sh");
40935
+ writeFileSync5(path2, `#!/bin/sh
39857
40936
  printf '%s' "$HANOMAN_SSH_PASSWORD"
39858
40937
  `, { mode: 448 });
39859
40938
  return path2;
@@ -39912,16 +40991,17 @@ function sshExec(t, remoteCmd, opts = {}) {
39912
40991
 
39913
40992
  // src/services/vps-audit.ts
39914
40993
  init_db();
39915
- import { readFileSync as readFileSync7 } from "node:fs";
39916
- import { join as join18 } from "node:path";
40994
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
40995
+ import { join as join19 } from "node:path";
40996
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
39917
40997
 
39918
40998
  // src/runner/deps.ts
39919
40999
  import { existsSync as existsSync11 } from "node:fs";
39920
- import { dirname as dirname11, join as join17 } from "node:path";
41000
+ import { dirname as dirname11, join as join18 } from "node:path";
39921
41001
  function repoRootFrom(startDir) {
39922
41002
  let dir2 = startDir;
39923
41003
  for (let i = 0; i < 8; i++) {
39924
- if (existsSync11(join17(dir2, "pnpm-workspace.yaml"))) return dir2;
41004
+ if (existsSync11(join18(dir2, "pnpm-workspace.yaml"))) return dir2;
39925
41005
  const parent = dirname11(dir2);
39926
41006
  if (parent === dir2) break;
39927
41007
  dir2 = parent;
@@ -40027,7 +41107,12 @@ function parseHealth(out4) {
40027
41107
  return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
40028
41108
  }
40029
41109
  var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
40030
- var scriptPath = (f) => join18(repoRoot(), "server", "scripts", "vps", f);
41110
+ var packagedScript = (f) => fileURLToPath3(new URL(`../scripts/vps/${f}`, import.meta.url));
41111
+ var moduleDir = () => fileURLToPath3(new URL(".", import.meta.url));
41112
+ var scriptPath = (f) => {
41113
+ const packed = packagedScript(f);
41114
+ return existsSync12(packed) ? packed : join19(repoRoot(moduleDir()), "server", "scripts", "vps", f);
41115
+ };
40031
41116
  async function itemStatesOf(vpsId) {
40032
41117
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
40033
41118
  return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
@@ -40036,7 +41121,7 @@ async function runAudit(v) {
40036
41121
  const r = await sshExec(
40037
41122
  target(v),
40038
41123
  "sudo -n bash -s",
40039
- { stdin: readFileSync7(scriptPath("audit.sh"), "utf8"), timeoutMs: 6e4 }
41124
+ { stdin: readFileSync9(scriptPath("audit.sh"), "utf8"), timeoutMs: 6e4 }
40040
41125
  );
40041
41126
  const audit = parseAudit(r.out);
40042
41127
  if (r.code !== 0 || audit.length === 0) return { ok: false, out: r.out };
@@ -40072,19 +41157,78 @@ async function runAudit(v) {
40072
41157
  await notifySynced("vps", v.id);
40073
41158
  return { ok: true, audit, hardened, scoreTotal: scored.total, scoreBySection: scored.bySection, drift };
40074
41159
  }
41160
+ var PUBLISH_HEARTBEAT_MS = 36e5;
41161
+ function shouldPublishHealth(prev, next, now = Date.now()) {
41162
+ if (JSON.stringify(prev?.health ?? null) !== JSON.stringify(next ?? null)) return true;
41163
+ if (!prev?.lastPublishedAt) return true;
41164
+ return now - prev.lastPublishedAt.getTime() >= PUBLISH_HEARTBEAT_MS;
41165
+ }
40075
41166
  async function runHealth(v) {
40076
41167
  const r = await sshExec(target(v), HEALTH_CMD, { timeoutMs: 6e4 });
40077
41168
  const health = parseHealth(r.out);
40078
41169
  if (r.code !== 0 || !health) return false;
41170
+ const prev = await prisma.vps.findUnique({
41171
+ where: { id: v.id },
41172
+ select: { health: true, lastPublishedAt: true }
41173
+ });
41174
+ const publish = shouldPublishHealth(prev, health);
40079
41175
  await prisma.vps.update({ where: { id: v.id }, data: {
40080
41176
  health,
40081
- lastSeenAt: /* @__PURE__ */ new Date()
41177
+ lastSeenAt: /* @__PURE__ */ new Date(),
41178
+ ...publish ? { lastPublishedAt: /* @__PURE__ */ new Date() } : {}
40082
41179
  } });
40083
- await notifySynced("vps", v.id);
41180
+ if (publish) await notifySynced("vps", v.id);
40084
41181
  return true;
40085
41182
  }
40086
41183
 
41184
+ // src/services/vps-provision.ts
41185
+ var PROVISION_TIMEOUT_MS = 9e5;
41186
+ var PROBE_TIMEOUT_MS = 6e4;
41187
+ var STATUSES = ["ok", "partial", "absent"];
41188
+ function parseComponents(out4) {
41189
+ return out4.split("\n").flatMap((line) => {
41190
+ const m = line.match(/^COMP (\S+) (\S+)(?: (.*))?$/);
41191
+ if (!m) return [];
41192
+ const [, id, status] = m;
41193
+ if (!componentById(id)) return [];
41194
+ if (!STATUSES.includes(status)) return [];
41195
+ return [{ id, status, detail: (m[3] ?? "").trim() }];
41196
+ });
41197
+ }
41198
+ function parseProvisionSteps(out4) {
41199
+ return out4.split("\n").flatMap((line) => {
41200
+ const m = line.match(/^STEP (\S+) (would|ok|fail|skip)(?: (.*))?$/);
41201
+ return m ? [{ item: m[1], status: m[2], detail: (m[3] ?? "").trim() }] : [];
41202
+ });
41203
+ }
41204
+ var script = () => readFileSync10(scriptPath("provision.sh"), "utf8");
41205
+ async function probeComponents(v) {
41206
+ const r = await sshExec(v, "env MODE=probe bash -s", { stdin: script(), timeoutMs: PROBE_TIMEOUT_MS });
41207
+ const components2 = parseComponents(r.out);
41208
+ if (r.code !== 0 || components2.length === 0) return { ok: false, out: r.out };
41209
+ return { ok: true, components: components2 };
41210
+ }
41211
+ async function provision(v, items, opts) {
41212
+ const env = [
41213
+ "MODE=apply",
41214
+ `ITEMS=${items.join(",")}`,
41215
+ `PROFILE=${opts.profile}`,
41216
+ ...opts.domain ? [`DOMAIN=${opts.domain}`] : [],
41217
+ ...opts.dryRun ? ["DRY_RUN=1"] : []
41218
+ ].join(" ");
41219
+ const r = await sshExec(v, `sudo -n env ${env} bash -s`, { stdin: script(), timeoutMs: PROVISION_TIMEOUT_MS });
41220
+ return { ok: r.code === 0, steps: parseProvisionSteps(r.out), out: r.out };
41221
+ }
41222
+ async function readSetupToken(v, home3 = "/var/lib/hanoman") {
41223
+ const r = await sshExec(v, `sudo -n cat ${home3}/setup.token`, { timeoutMs: 15e3 });
41224
+ if (r.code !== 0) return null;
41225
+ const [token, expires] = r.out.trim().split("\n");
41226
+ if (!token || !expires) return null;
41227
+ return Date.parse(expires) > Date.now() ? token : null;
41228
+ }
41229
+
40087
41230
  // src/services/vps-remediate.ts
41231
+ import { readFileSync as readFileSync11 } from "node:fs";
40088
41232
  function parseSteps(out4) {
40089
41233
  return out4.split("\n").flatMap((line) => {
40090
41234
  const m = line.match(/^STEP (\S+) (would|ok|fail)(?: (.*))?$/);
@@ -40095,7 +41239,7 @@ async function remediate(v, items, dryRun) {
40095
41239
  const r = await sshExec(
40096
41240
  v,
40097
41241
  `sudo -n env ITEMS=${items.join(",")} SSH_PORT=${v.port} DRY_RUN=${dryRun ? "1" : ""} bash -s`,
40098
- { stdin: readFileSync8(scriptPath("remediate.sh"), "utf8"), timeoutMs: 3e5 }
41242
+ { stdin: readFileSync11(scriptPath("remediate.sh"), "utf8"), timeoutMs: 3e5 }
40099
41243
  );
40100
41244
  return { ok: r.code === 0, steps: parseSteps(r.out), out: r.out };
40101
41245
  }
@@ -40174,39 +41318,39 @@ async function buildChecklist(vpsId) {
40174
41318
  // src/services/vps-key.ts
40175
41319
  init_src2();
40176
41320
  init_config2();
40177
- import { execFileSync as execFileSync2 } from "node:child_process";
40178
- import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync9, unlinkSync as unlinkSync2 } from "node:fs";
41321
+ import { execFileSync as execFileSync3 } from "node:child_process";
41322
+ import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
40179
41323
  import { homedir as homedir5 } from "node:os";
40180
- import { join as join19 } from "node:path";
41324
+ import { join as join20 } from "node:path";
40181
41325
  var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? resolveDataDirs().sshKeys;
40182
41326
  var KEY_FILES = ["id_ed25519", "id_ed25519.pub"];
40183
41327
  function adoptLegacyKey(dir2) {
40184
41328
  if (effectiveStr("HANOMAN_SSH_KEY_DIR")) return;
40185
- const legacy = join19(homedir5(), ".hanoman");
41329
+ const legacy = join20(homedir5(), ".hanoman");
40186
41330
  if (legacy === dir2) return;
40187
- if (!KEY_FILES.every((f) => existsSync12(join19(legacy, f)))) return;
40188
- mkdirSync9(dir2, { recursive: true, mode: 448 });
41331
+ if (!KEY_FILES.every((f) => existsSync13(join20(legacy, f)))) return;
41332
+ mkdirSync10(dir2, { recursive: true, mode: 448 });
40189
41333
  for (const f of KEY_FILES) {
40190
- copyFileSync(join19(legacy, f), join19(dir2, f));
40191
- chmodSync3(join19(dir2, f), 384);
41334
+ copyFileSync(join20(legacy, f), join20(dir2, f));
41335
+ chmodSync4(join20(dir2, f), 384);
40192
41336
  }
40193
- for (const f of KEY_FILES) unlinkSync2(join19(legacy, f));
41337
+ for (const f of KEY_FILES) unlinkSync2(join20(legacy, f));
40194
41338
  console.log(`vps: key SSH dipindah dari ${legacy} ke ${dir2} (SPEC-846 \u2014 satu batas backup)`);
40195
41339
  }
40196
41340
  function ensureHanomanKey() {
40197
41341
  const dir2 = keyDir();
40198
- const privPath = join19(dir2, KEY_FILES[0]);
41342
+ const privPath = join20(dir2, KEY_FILES[0]);
40199
41343
  const pubPath = `${privPath}.pub`;
40200
- if (!existsSync12(privPath)) adoptLegacyKey(dir2);
40201
- if (!existsSync12(privPath)) {
40202
- mkdirSync9(dir2, { recursive: true, mode: 448 });
40203
- execFileSync2(
41344
+ if (!existsSync13(privPath)) adoptLegacyKey(dir2);
41345
+ if (!existsSync13(privPath)) {
41346
+ mkdirSync10(dir2, { recursive: true, mode: 448 });
41347
+ execFileSync3(
40204
41348
  "ssh-keygen",
40205
41349
  ["-t", "ed25519", "-N", "", "-C", "hanoman", "-f", privPath],
40206
41350
  { stdio: ["ignore", "ignore", "pipe"] }
40207
41351
  );
40208
41352
  }
40209
- return { privPath, pubPath, pub: readFileSync9(pubPath, "utf8").trim() };
41353
+ return { privPath, pubPath, pub: readFileSync12(pubPath, "utf8").trim() };
40210
41354
  }
40211
41355
 
40212
41356
  // src/services/vps-bootstrap.ts
@@ -40225,7 +41369,7 @@ async function bootstrapKey(t, password) {
40225
41369
  init_pty();
40226
41370
  init_settings2();
40227
41371
  function keyMissing(v) {
40228
- return !!v.keyPath && !existsSync13(v.keyPath);
41372
+ return !!v.keyPath && !existsSync14(v.keyPath);
40229
41373
  }
40230
41374
  async function vps_default(app2) {
40231
41375
  app2.get("/vps", async () => prisma.vps.findMany({ orderBy: { createdAt: "asc" } }));
@@ -40373,6 +41517,79 @@ async function vps_default(app2) {
40373
41517
  scoreBySection: audit.ok ? audit.scoreBySection : null
40374
41518
  };
40375
41519
  });
41520
+ app2.get("/vps/components", async () => ({ components: COMPONENTS }));
41521
+ function planItems(items, profile, domain) {
41522
+ const r = resolveComponents(items, profile);
41523
+ if (!r.ok) return r;
41524
+ const needsDomain = r.items.some((id) => componentById(id)?.needsDomain);
41525
+ if (needsDomain && !domain) return { ok: false, error: "domain wajib untuk komponen ingress" };
41526
+ return r;
41527
+ }
41528
+ async function runProbe(v) {
41529
+ const r = await probeComponents(v);
41530
+ if (!r.ok) return r;
41531
+ const map = {};
41532
+ for (const c of r.components) map[c.id] = { status: c.status, detail: c.detail };
41533
+ const checkedAt = /* @__PURE__ */ new Date();
41534
+ await prisma.vps.update({ where: { id: v.id }, data: { components: map, componentsCheckedAt: checkedAt } });
41535
+ return { ok: true, components: r.components, checkedAt };
41536
+ }
41537
+ app2.post("/vps/:id/probe", async (req, reply) => {
41538
+ const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
41539
+ if (!v) return reply.code(404).send({ error: "not found" });
41540
+ if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
41541
+ const r = await runProbe(v);
41542
+ if (!r.ok) return reply.code(502).send({ error: "probe gagal lewat ssh", out: r.out });
41543
+ return { components: r.components, checkedAt: r.checkedAt.toISOString() };
41544
+ });
41545
+ app2.post("/vps/:id/provision/preview", async (req, reply) => {
41546
+ const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
41547
+ if (!v) return reply.code(404).send({ error: "not found" });
41548
+ if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
41549
+ const p = zProvision.safeParse(req.body);
41550
+ if (!p.success) return reply.code(400).send({ error: "invalid body" });
41551
+ const plan = planItems(p.data.items, p.data.profile, p.data.domain);
41552
+ if (!plan.ok) return reply.code(400).send({ error: plan.error });
41553
+ const r = await provision(v, plan.items, { profile: p.data.profile, domain: p.data.domain, dryRun: true });
41554
+ if (!r.ok) return reply.code(502).send({ error: "preview gagal lewat ssh", out: r.out });
41555
+ return { steps: r.steps };
41556
+ });
41557
+ app2.post("/vps/:id/provision", async (req, reply) => {
41558
+ const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
41559
+ if (!v) return reply.code(404).send({ error: "not found" });
41560
+ if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
41561
+ const p = zProvision.safeParse(req.body);
41562
+ if (!p.success) return reply.code(400).send({ error: "invalid body" });
41563
+ const plan = planItems(p.data.items, p.data.profile, p.data.domain);
41564
+ if (!plan.ok) return reply.code(400).send({ error: plan.error });
41565
+ if (!p.data.confirm) {
41566
+ const dry = await provision(v, plan.items, { profile: p.data.profile, domain: p.data.domain, dryRun: true });
41567
+ if (!dry.ok) return reply.code(502).send({ error: "dry-run gagal lewat ssh", out: dry.out });
41568
+ return reply.code(409).send({ error: "confirm-required", steps: dry.steps });
41569
+ }
41570
+ const installed = v.components?.hanoman?.status === "ok";
41571
+ if (installed && v.provisionProfile && v.provisionProfile !== p.data.profile && !p.data.force) {
41572
+ return reply.code(409).send({ error: "profile-mismatch", current: v.provisionProfile });
41573
+ }
41574
+ const r = await provision(v, plan.items, { profile: p.data.profile, domain: p.data.domain, dryRun: false });
41575
+ if (!r.ok) return reply.code(502).send({ error: "provision gagal lewat ssh", transcript: r.out, steps: r.steps });
41576
+ const probe = await runProbe(v);
41577
+ if (!probe.ok) return reply.code(502).send({ error: "probe pasca-provision gagal", transcript: r.out, steps: r.steps });
41578
+ await prisma.vps.update({ where: { id: v.id }, data: { provisionProfile: p.data.profile } });
41579
+ let setup = null;
41580
+ const hanomanOk = probe.components.some((c) => c.id === "hanoman" && c.status === "ok");
41581
+ if (hanomanOk) {
41582
+ const token = await readSetupToken(v);
41583
+ if (token) {
41584
+ const base2 = p.data.domain ? `https://${p.data.domain}` : `http://${v.host}:8787`;
41585
+ setup = {
41586
+ url: `${base2}/setup?token=${encodeURIComponent(token)}`,
41587
+ expiresAt: new Date(Date.now() + 15 * 6e4).toISOString()
41588
+ };
41589
+ }
41590
+ }
41591
+ return { steps: r.steps, components: probe.components, checkedAt: probe.checkedAt.toISOString(), setup };
41592
+ });
40376
41593
  app2.post("/vps/:id/harden", async (req, reply) => {
40377
41594
  const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
40378
41595
  if (!v) return reply.code(404).send({ error: "not found" });
@@ -40380,7 +41597,7 @@ async function vps_default(app2) {
40380
41597
  const r = await sshExec(
40381
41598
  v,
40382
41599
  `sudo -n env SSH_PORT=${v.port} SSH_USER=${v.user} bash -s`,
40383
- { stdin: readFileSync10(scriptPath("harden.sh"), "utf8"), timeoutMs: 3e5 }
41600
+ { stdin: readFileSync13(scriptPath("harden.sh"), "utf8"), timeoutMs: 3e5 }
40384
41601
  );
40385
41602
  if (r.code !== 0) return reply.code(502).send({ error: "harden gagal lewat ssh", transcript: r.out });
40386
41603
  const verify = await sshExec(v, "true", { timeoutMs: 3e4 });
@@ -40431,10 +41648,10 @@ async function vps_default(app2) {
40431
41648
 
40432
41649
  // src/services/limits.ts
40433
41650
  init_config2();
40434
- import { execFileSync as execFileSync3 } from "node:child_process";
40435
- import { readFileSync as readFileSync11 } from "node:fs";
41651
+ import { execFileSync as execFileSync4 } from "node:child_process";
41652
+ import { readFileSync as readFileSync14 } from "node:fs";
40436
41653
  import { homedir as homedir7 } from "node:os";
40437
- import { join as join20 } from "node:path";
41654
+ import { join as join21 } from "node:path";
40438
41655
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
40439
41656
  var TTL_MS2 = 3e4;
40440
41657
  var lastOk = null;
@@ -40447,12 +41664,12 @@ var LABELS = {
40447
41664
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
40448
41665
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
40449
41666
  function credsFile() {
40450
- return join20(effectiveStr("CLAUDE_CONFIG_DIR") ?? join20(homedir7(), ".claude"), ".credentials.json");
41667
+ return join21(effectiveStr("CLAUDE_CONFIG_DIR") ?? join21(homedir7(), ".claude"), ".credentials.json");
40451
41668
  }
40452
41669
  function readAccessToken() {
40453
41670
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
40454
41671
  try {
40455
- const blob = execFileSync3(
41672
+ const blob = execFileSync4(
40456
41673
  "security",
40457
41674
  ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
40458
41675
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -40463,7 +41680,7 @@ function readAccessToken() {
40463
41680
  }
40464
41681
  }
40465
41682
  try {
40466
- return JSON.parse(readFileSync11(credsFile(), "utf8"))?.claudeAiOauth?.accessToken ?? null;
41683
+ return JSON.parse(readFileSync14(credsFile(), "utf8"))?.claudeAiOauth?.accessToken ?? null;
40467
41684
  } catch {
40468
41685
  return null;
40469
41686
  }
@@ -40521,14 +41738,14 @@ async function getLimits() {
40521
41738
  // src/services/codex-limits.ts
40522
41739
  import { open as open3, readdir as readdir4, stat as stat2 } from "node:fs/promises";
40523
41740
  import { homedir as homedir8 } from "node:os";
40524
- import { join as join21 } from "node:path";
41741
+ import { join as join22 } from "node:path";
40525
41742
  var TTL_MS3 = 3e4;
40526
41743
  var STALE_AFTER_MS = 12 * 36e5;
40527
41744
  var TAIL_BYTES = 512 * 1024;
40528
41745
  var MAX_FILES = 8;
40529
41746
  var cache3 = null;
40530
41747
  var freshUntil2 = 0;
40531
- var codexSessionsDir = () => join21(process.env.CODEX_HOME ?? join21(homedir8(), ".codex"), "sessions");
41748
+ var codexSessionsDir = () => join22(process.env.CODEX_HOME ?? join22(homedir8(), ".codex"), "sessions");
40532
41749
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
40533
41750
  function windowLabel(minutes) {
40534
41751
  if (minutes === 300) return "Sesi 5 jam";
@@ -40561,7 +41778,7 @@ async function recentRollouts(dir2) {
40561
41778
  return [];
40562
41779
  }
40563
41780
  const stamped = await Promise.all(entries.map(async (rel) => {
40564
- const full = join21(dir2, rel);
41781
+ const full = join22(dir2, rel);
40565
41782
  try {
40566
41783
  return { full, mtime: (await stat2(full)).mtimeMs };
40567
41784
  } catch {
@@ -40641,9 +41858,9 @@ async function limits(app2) {
40641
41858
  // src/services/codex-version.ts
40642
41859
  init_src();
40643
41860
  init_config2();
40644
- import { execFile as execFile11 } from "node:child_process";
41861
+ import { execFile as execFile12 } from "node:child_process";
40645
41862
  import { promisify as promisify12 } from "node:util";
40646
- var run = promisify12(execFile11);
41863
+ var run2 = promisify12(execFile12);
40647
41864
  var CODEX_MIN_CLIENT = "0.144.0";
40648
41865
  var TTL_MS4 = 5 * 6e4;
40649
41866
  var cache4 = null;
@@ -40656,7 +41873,7 @@ async function getCodexVersion(now = Date.now()) {
40656
41873
  if (cache4 && now - cache4.at < TTL_MS4) return cache4.version;
40657
41874
  let version2 = null;
40658
41875
  try {
40659
- const { stdout } = await run(codexBin2(), ["--version"], { timeout: 5e3 });
41876
+ const { stdout } = await run2(codexBin2(), ["--version"], { timeout: 5e3 });
40660
41877
  version2 = parseCodexVersion(stdout);
40661
41878
  } catch {
40662
41879
  }
@@ -40688,9 +41905,9 @@ init_src();
40688
41905
  // src/services/update.ts
40689
41906
  init_src();
40690
41907
  init_config2();
40691
- import { readFileSync as readFileSync12 } from "node:fs";
41908
+ import { readFileSync as readFileSync15 } from "node:fs";
40692
41909
  import { resolve as resolve18, dirname as dirname12 } from "node:path";
40693
- import { fileURLToPath as fileURLToPath3 } from "node:url";
41910
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
40694
41911
  var UPDATE_COMMAND = "npm i -g hanoman@latest --prefer-online";
40695
41912
  function composeUpdate(x) {
40696
41913
  const available = x.registryStatus === "ok" && x.latestVersion != null && compareSemver(x.latestVersion, x.currentVersion) > 0;
@@ -40714,10 +41931,10 @@ var lastFetchAt = 0;
40714
41931
  var lastLatest = null;
40715
41932
  var lastStatus = "unavailable";
40716
41933
  function runningVersion() {
40717
- const here = dirname12(fileURLToPath3(import.meta.url));
41934
+ const here = dirname12(fileURLToPath4(import.meta.url));
40718
41935
  for (const p of [resolve18(here, "build-info.json"), resolve18(here, "../package.json")]) {
40719
41936
  try {
40720
- const v = JSON.parse(readFileSync12(p, "utf8")).version;
41937
+ const v = JSON.parse(readFileSync15(p, "utf8")).version;
40721
41938
  if (typeof v === "string" && v) return v;
40722
41939
  } catch {
40723
41940
  }
@@ -40794,33 +42011,6 @@ async function update(app2) {
40794
42011
  // src/services/events.ts
40795
42012
  init_pty();
40796
42013
  init_notifications();
40797
-
40798
- // src/services/lead/deciding.ts
40799
- var deciding = /* @__PURE__ */ new Set();
40800
- function markDeciding(sessionId2) {
40801
- deciding.add(sessionId2);
40802
- }
40803
- function clearDeciding(sessionId2) {
40804
- deciding.delete(sessionId2);
40805
- }
40806
- function isDeciding(sessionId2) {
40807
- return deciding.has(sessionId2);
40808
- }
40809
- function decidingIds() {
40810
- return [...deciding];
40811
- }
40812
- var queued = /* @__PURE__ */ new Set();
40813
- function markQueued(sessionId2) {
40814
- queued.add(sessionId2);
40815
- }
40816
- function clearQueued(sessionId2) {
40817
- queued.delete(sessionId2);
40818
- }
40819
- function queuedIds() {
40820
- return [...queued];
40821
- }
40822
-
40823
- // src/services/events.ts
40824
42014
  init_db();
40825
42015
  init_config2();
40826
42016
  var clients = /* @__PURE__ */ new Set();
@@ -40832,7 +42022,9 @@ var GROUPS = [
40832
42022
  // service itu sengaja tak tahu apa-apa soal lead maupun DB.
40833
42023
  { everyTicks: 1, last: "", build: async () => ({
40834
42024
  t: "sessions",
40835
- sessions: listSessions().map((s2) => isDeciding(s2.id) ? { ...s2, deciding: true } : s2)
42025
+ // Asinkron: grup ini di-recompute tiap detik, dan `execFileSync` tmux memblokir seluruh event
42026
+ // loop — termasuk frame ketikan terminal — selama spawn (terukur sampai 916 ms saat mesin sibuk).
42027
+ sessions: (await listSessionsAsync()).map((s2) => isDeciding(s2.id) ? { ...s2, deciding: true } : s2)
40836
42028
  }) },
40837
42029
  { everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
40838
42030
  { everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
@@ -41033,6 +42225,7 @@ async function bindings_default(app2) {
41033
42225
  // src/routes/sync.ts
41034
42226
  init_zod();
41035
42227
  init_db();
42228
+ import { gzipSync } from "node:zlib";
41036
42229
 
41037
42230
  // src/services/device-auth.ts
41038
42231
  function bearerToken2(req) {
@@ -41088,10 +42281,21 @@ var zPush = external_exports.object({
41088
42281
  }))
41089
42282
  });
41090
42283
  var AUTHORED = ["spec", "sessionResult"];
42284
+ function maybeGzip(req, reply, payload) {
42285
+ reply.header("vary", "accept-encoding");
42286
+ if (!/\bgzip\b/.test(String(req.headers["accept-encoding"] ?? ""))) return payload;
42287
+ reply.header("content-type", "application/json; charset=utf-8");
42288
+ reply.header("content-encoding", "gzip");
42289
+ return gzipSync(Buffer.from(JSON.stringify(payload)));
42290
+ }
41091
42291
  async function sync_default(app2) {
41092
- app2.get("/sync/pull", { preHandler: requireDeviceToken }, async (req) => {
42292
+ app2.get("/sync/pull", { preHandler: requireDeviceToken }, async (req, reply) => {
41093
42293
  const since = req.query.since ?? "0";
41094
- return pull(since);
42294
+ return maybeGzip(req, reply, await pull(since));
42295
+ });
42296
+ app2.get("/sync/bootstrap", { preHandler: requireDeviceToken }, async (req, reply) => {
42297
+ const after = req.query.after ?? null;
42298
+ return maybeGzip(req, reply, await bootstrapSnapshot(after));
41095
42299
  });
41096
42300
  app2.get("/sync/attachments/:storageKey", { preHandler: requireDeviceToken }, async (req, reply) => {
41097
42301
  const { storageKey } = req.params;
@@ -41114,8 +42318,13 @@ async function sync_default(app2) {
41114
42318
  }
41115
42319
  const data = { ...rec.data };
41116
42320
  if (AUTHORED.includes(rec.entity) && !data.author && user) data.author = user.email;
41117
- const r = await applyPush(rec.entity, rec.id, rec.baseVersion, data, req.device.id, rec.op ?? "upsert");
41118
- results.push({ id: rec.id, ...r });
42321
+ try {
42322
+ const r = await applyPush(rec.entity, rec.id, rec.baseVersion, data, req.device.id, rec.op ?? "upsert");
42323
+ results.push({ id: rec.id, ...r });
42324
+ } catch (e) {
42325
+ req.log.warn({ entity: rec.entity, recordId: rec.id, err: e }, "sync push record ditolak");
42326
+ results.push({ id: rec.id, ok: false, error: e.message });
42327
+ }
41119
42328
  }
41120
42329
  return { results };
41121
42330
  });
@@ -41232,7 +42441,7 @@ init_config2();
41232
42441
  init_src2();
41233
42442
  import { randomUUID as randomUUID7 } from "node:crypto";
41234
42443
  import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir5, stat as stat3 } from "node:fs/promises";
41235
- import { join as join22, resolve as resolve19, basename as basename6 } from "node:path";
42444
+ import { join as join23, resolve as resolve19, basename as basename6 } from "node:path";
41236
42445
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
41237
42446
  function transcriptDir() {
41238
42447
  return resolve19(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
@@ -41253,13 +42462,13 @@ async function saveTranscript(text) {
41253
42462
  const dir2 = transcriptDir();
41254
42463
  await mkdir5(dir2, { recursive: true, mode: 448 });
41255
42464
  const key = `${randomUUID7()}.log`;
41256
- await writeFile4(join22(dir2, key), body, { encoding: "utf8", mode: 384 });
42465
+ await writeFile4(join23(dir2, key), body, { encoding: "utf8", mode: 384 });
41257
42466
  return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
41258
42467
  }
41259
42468
  async function readTranscript(key) {
41260
42469
  if (!key) return null;
41261
42470
  try {
41262
- return await readFile4(join22(transcriptDir(), basename6(key)), "utf8");
42471
+ return await readFile4(join23(transcriptDir(), basename6(key)), "utf8");
41263
42472
  } catch {
41264
42473
  return null;
41265
42474
  }
@@ -41267,7 +42476,7 @@ async function readTranscript(key) {
41267
42476
  async function deleteTranscript(key) {
41268
42477
  if (!key) return;
41269
42478
  try {
41270
- await unlink4(join22(transcriptDir(), basename6(key)));
42479
+ await unlink4(join23(transcriptDir(), basename6(key)));
41271
42480
  } catch (e) {
41272
42481
  if (e.code !== "ENOENT") throw e;
41273
42482
  }
@@ -41285,7 +42494,7 @@ async function listTranscripts() {
41285
42494
  for (const name2 of names) {
41286
42495
  if (!name2.endsWith(".log")) continue;
41287
42496
  try {
41288
- rows.push({ key: name2, mtimeMs: (await stat3(join22(dir2, name2))).mtimeMs });
42497
+ rows.push({ key: name2, mtimeMs: (await stat3(join23(dir2, name2))).mtimeMs });
41289
42498
  } catch {
41290
42499
  }
41291
42500
  }
@@ -41598,9 +42807,9 @@ init_db();
41598
42807
  init_db();
41599
42808
  init_uploads();
41600
42809
  init_src();
41601
- import { createHash as createHash8, randomBytes as randomBytes6 } from "node:crypto";
42810
+ import { createHash as createHash9, randomBytes as randomBytes6 } from "node:crypto";
41602
42811
  function hashAccessKey(key) {
41603
- return createHash8("sha256").update(key).digest("hex");
42812
+ return createHash9("sha256").update(key).digest("hex");
41604
42813
  }
41605
42814
  function generateAccessKey() {
41606
42815
  const key = "hnm_tkt_" + randomBytes6(24).toString("hex");
@@ -42162,11 +43371,11 @@ async function expireStale(now) {
42162
43371
  const stale = await prisma.schedulerCronRun.findMany({
42163
43372
  where: { status: "queued", dueAt: { lt: new Date(now - GRACE_MS) } }
42164
43373
  });
42165
- for (const run2 of stale) {
42166
- const note = run2.note ?? "tak terluncurkan sampai batas keterlambatan";
42167
- await prisma.schedulerCronRun.update({ where: { id: run2.id }, data: { status: "skipped", note } });
42168
- const cron = await prisma.schedulerCron.findUnique({ where: { id: run2.cronId } });
42169
- await recordCronRun(run2.cronId, cron?.name ?? run2.cronId, run2.projectId, run2.dueAt, "skipped", note);
43374
+ for (const run3 of stale) {
43375
+ const note = run3.note ?? "tak terluncurkan sampai batas keterlambatan";
43376
+ await prisma.schedulerCronRun.update({ where: { id: run3.id }, data: { status: "skipped", note } });
43377
+ const cron = await prisma.schedulerCron.findUnique({ where: { id: run3.cronId } });
43378
+ await recordCronRun(run3.cronId, cron?.name ?? run3.cronId, run3.projectId, run3.dueAt, "skipped", note);
42170
43379
  }
42171
43380
  }
42172
43381
  function queuedCronRuns() {
@@ -42403,10 +43612,10 @@ async function scheduler_default(app2) {
42403
43612
  const pending2 = await prisma.schedulerCronRun.findFirst({ where: { cronId: id, status: "queued" } });
42404
43613
  if (pending2) return reply.code(409).send({ error: "masih ada run yang menunggu dijalankan", runId: pending2.id });
42405
43614
  try {
42406
- const run2 = await prisma.schedulerCronRun.create({
43615
+ const run3 = await prisma.schedulerCronRun.create({
42407
43616
  data: { cronId: id, projectId: cron.projectId, dueAt: /* @__PURE__ */ new Date(), manual: true }
42408
43617
  });
42409
- return reply.code(201).send(runView(run2));
43618
+ return reply.code(201).send(runView(run3));
42410
43619
  } catch {
42411
43620
  return reply.code(409).send({ error: "run untuk jatuh tempo ini sudah ada" });
42412
43621
  }
@@ -42554,8 +43763,8 @@ function leadPrompt(q, c) {
42554
43763
 
42555
43764
  // src/services/lead/verdict.ts
42556
43765
  init_src();
42557
- import { existsSync as existsSync14 } from "node:fs";
42558
- import { isAbsolute as isAbsolute5, resolve as resolvePath, relative as relative2 } from "node:path";
43766
+ import { existsSync as existsSync15 } from "node:fs";
43767
+ import { isAbsolute as isAbsolute6, resolve as resolvePath, relative as relative2 } from "node:path";
42559
43768
  function extractJsonBlock(raw2) {
42560
43769
  const re = /```json\s*\n([\s\S]*?)```/g;
42561
43770
  let last = null;
@@ -42588,11 +43797,11 @@ function keepExistingRefs(refs2, repoDir) {
42588
43797
  continue;
42589
43798
  }
42590
43799
  if (!repoDir) continue;
42591
- if (isAbsolute5(ref)) continue;
43800
+ if (isAbsolute6(ref)) continue;
42592
43801
  const full = resolvePath(repoDir, ref);
42593
43802
  const rel = relative2(repoDir, full);
42594
43803
  if (rel.startsWith("..")) continue;
42595
- if (existsSync14(full)) out4.push(ref);
43804
+ if (existsSync15(full)) out4.push(ref);
42596
43805
  }
42597
43806
  return [...new Set(out4)];
42598
43807
  }
@@ -42740,14 +43949,15 @@ function toDecisionView(r) {
42740
43949
  }
42741
43950
 
42742
43951
  // src/services/lead/brain.ts
43952
+ init_src2();
42743
43953
  init_config2();
42744
43954
  init_pty();
42745
43955
  init_session_sandbox();
42746
- import { execFile as execFile12 } from "node:child_process";
43956
+ import { execFile as execFile13 } from "node:child_process";
42747
43957
  import { randomUUID as randomUUID9 } from "node:crypto";
42748
- import { mkdirSync as mkdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "node:fs";
43958
+ import { mkdirSync as mkdirSync11, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
42749
43959
  import { tmpdir as tmpdir4 } from "node:os";
42750
- import { join as join23 } from "node:path";
43960
+ import { join as join24 } from "node:path";
42751
43961
  var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
42752
43962
  function leadArgv(o) {
42753
43963
  if (o.agent === "codex") {
@@ -42772,15 +43982,15 @@ var shellQuote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
42772
43982
  function leadProcess(prompt, o, env = process.env) {
42773
43983
  const file = binFor(o.agent);
42774
43984
  const directArgs = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
42775
- const mode = env.HANOMAN_SESSION_SANDBOX ?? (env.NODE_ENV === "production" ? "required" : "off");
43985
+ const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
42776
43986
  if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
42777
43987
  } };
42778
- const promptDir = join23(tmpdir4(), "hanoman-prompts");
42779
- mkdirSync10(promptDir, { recursive: true, mode: 448 });
42780
- const promptFile = join23(promptDir, `oneshot-${randomUUID9()}`);
42781
- writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
42782
- const workspace = o.cwd ?? join23(tmpdir4(), `hanoman-oneshot-${randomUUID9()}`);
42783
- if (!o.cwd) mkdirSync10(workspace, { recursive: false, mode: 448 });
43988
+ const promptDir = join24(tmpdir4(), "hanoman-prompts");
43989
+ mkdirSync11(promptDir, { recursive: true, mode: 448 });
43990
+ const promptFile = join24(promptDir, `oneshot-${randomUUID9()}`);
43991
+ writeFileSync6(promptFile, prompt, { flag: "wx", mode: 384 });
43992
+ const workspace = o.cwd ?? join24(tmpdir4(), `hanoman-oneshot-${randomUUID9()}`);
43993
+ if (!o.cwd) mkdirSync11(workspace, { recursive: false, mode: 448 });
42784
43994
  try {
42785
43995
  const argsWithoutPrompt = directArgs.slice(0, -1);
42786
43996
  const command = [file, ...argsWithoutPrompt].map(shellQuote).join(" ") + ` "$(cat ${shellQuote(promptFile)})"`;
@@ -42822,7 +44032,7 @@ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
42822
44032
  function think(prompt, o) {
42823
44033
  const process2 = leadProcess(prompt, o);
42824
44034
  return new Promise((resolve21, reject2) => {
42825
- const child = execFile12(process2.file, process2.args, {
44035
+ const child = execFile13(process2.file, process2.args, {
42826
44036
  cwd: process2.cwd,
42827
44037
  timeout: o.timeoutMs,
42828
44038
  maxBuffer: 16 * 1024 * 1024,
@@ -43385,7 +44595,7 @@ init_src();
43385
44595
  init_pty();
43386
44596
  init_tui_dialog();
43387
44597
  init_notifications();
43388
- import { writeFileSync as writeFileSync6 } from "node:fs";
44598
+ import { writeFileSync as writeFileSync7 } from "node:fs";
43389
44599
 
43390
44600
  // src/services/lead/pane.ts
43391
44601
  init_tui_dialog();
@@ -43471,7 +44681,7 @@ var prodDetectDeps = {
43471
44681
  send: (id, text, choices) => sendToPane(id, text, 50, choices),
43472
44682
  clearMarker: (file) => {
43473
44683
  try {
43474
- writeFileSync6(file, "");
44684
+ writeFileSync7(file, "");
43475
44685
  } catch {
43476
44686
  }
43477
44687
  },
@@ -44236,9 +45446,9 @@ init_db();
44236
45446
 
44237
45447
  // src/services/changelog/collect.ts
44238
45448
  init_db();
44239
- import { execFile as execFile13 } from "node:child_process";
45449
+ import { execFile as execFile14 } from "node:child_process";
44240
45450
  import { promisify as promisify13 } from "node:util";
44241
- import { existsSync as existsSync15 } from "node:fs";
45451
+ import { existsSync as existsSync16 } from "node:fs";
44242
45452
 
44243
45453
  // src/services/changelog/scrub.ts
44244
45454
  var MERGE = /^merge\s+(branch|pull request|remote-tracking branch|tag)\b/i;
@@ -44299,13 +45509,13 @@ async function collectBacklog(projectId, from, to) {
44299
45509
  notes.push(`${stampless} item selesai tanpa stempel waktu (selesai sebelum stempel ini ada) dan tak ikut dihitung.`);
44300
45510
  return { ok: true, input: { mode: "backlog", title: `${from} \u2013 ${to}`, items, notes } };
44301
45511
  }
44302
- var exec10 = promisify13(execFile13);
45512
+ var exec10 = promisify13(execFile14);
44303
45513
  var GIT8 = { maxBuffer: 1 << 24, encoding: "utf8" };
44304
45514
  var US2 = "";
44305
45515
  var RS = "";
44306
45516
  var NO_REPO = "project ini belum ditautkan ke repo di mesin ini";
44307
45517
  var NO_TAG = "repo project ini belum punya tag rilis";
44308
- var usable2 = (repoDir) => !!repoDir && existsSync15(repoDir);
45518
+ var usable2 = (repoDir) => !!repoDir && existsSync16(repoDir);
44309
45519
  async function listTags(repoDir) {
44310
45520
  if (!usable2(repoDir)) return { tags: [], head: null, reason: NO_REPO };
44311
45521
  try {
@@ -44459,12 +45669,12 @@ async function generateChangelog(projectId, req, deps = {}) {
44459
45669
  const input = got.input;
44460
45670
  const prompt = changelogPrompt(input, CHANGELOG_TIMEOUT_MS);
44461
45671
  const { agent, model, effort } = await changelogAgentDefaults();
44462
- const run2 = deps.think ?? think;
45672
+ const run3 = deps.think ?? think;
44463
45673
  let body = "";
44464
45674
  let generator = "agent";
44465
45675
  const warnings = [...input.notes];
44466
45676
  try {
44467
- const raw2 = await run2(prompt, { agent, model, effort, timeoutMs: CHANGELOG_TIMEOUT_MS });
45677
+ const raw2 = await run3(prompt, { agent, model, effort, timeoutMs: CHANGELOG_TIMEOUT_MS });
44468
45678
  body = scrubOutput(raw2 ?? "");
44469
45679
  if (!body.trim()) throw new Error("agen tak memulangkan teks apa pun");
44470
45680
  } catch (e) {
@@ -44566,13 +45776,13 @@ init_db();
44566
45776
 
44567
45777
  // src/services/agent-tool-catalog.ts
44568
45778
  init_src();
44569
- import { readFileSync as readFileSync13 } from "node:fs";
45779
+ import { readFileSync as readFileSync16 } from "node:fs";
44570
45780
  import { homedir as homedir9 } from "node:os";
44571
- import { join as join24 } from "node:path";
45781
+ import { join as join25 } from "node:path";
44572
45782
  var home = () => process.env.HOME || homedir9();
44573
45783
  var readJson2 = (path2) => {
44574
45784
  try {
44575
- return JSON.parse(readFileSync13(path2, "utf8"));
45785
+ return JSON.parse(readFileSync16(path2, "utf8"));
44576
45786
  } catch {
44577
45787
  return null;
44578
45788
  }
@@ -44585,7 +45795,7 @@ var serversOf = (node) => {
44585
45795
  var codexServers = () => {
44586
45796
  let text;
44587
45797
  try {
44588
- text = readFileSync13(join24(home(), ".codex", "config.toml"), "utf8");
45798
+ text = readFileSync16(join25(home(), ".codex", "config.toml"), "utf8");
44589
45799
  } catch {
44590
45800
  return [];
44591
45801
  }
@@ -44598,12 +45808,12 @@ var codexServers = () => {
44598
45808
  };
44599
45809
  function mcpServerNames(repoDir) {
44600
45810
  const names = [];
44601
- const claudeJson = readJson2(join24(home(), ".claude.json"));
45811
+ const claudeJson = readJson2(join25(home(), ".claude.json"));
44602
45812
  names.push(...serversOf(claudeJson));
44603
45813
  if (repoDir) {
44604
45814
  const projects = claudeJson?.projects;
44605
45815
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
44606
- names.push(...serversOf(readJson2(join24(repoDir, ".mcp.json"))));
45816
+ names.push(...serversOf(readJson2(join25(repoDir, ".mcp.json"))));
44607
45817
  }
44608
45818
  names.push(...codexServers());
44609
45819
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -44613,6 +45823,71 @@ function agentToolCatalog(repoDir) {
44613
45823
  }
44614
45824
  var agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
44615
45825
 
45826
+ // src/routes/custom-agents.ts
45827
+ init_settings2();
45828
+
45829
+ // src/services/builtin-agents.ts
45830
+ init_src();
45831
+ init_db();
45832
+ init_settings2();
45833
+ init_tombstone();
45834
+ import { createHash as createHash10 } from "node:crypto";
45835
+ var fingerprint = (name2, description, instructions, tools) => createHash10("sha256").update([name2, description, instructions, [...tools].join(",")].join(" ")).digest("hex").slice(0, 16);
45836
+ var builtinFingerprint = (a) => fingerprint(a.name, a.description, a.instructions, a.tools);
45837
+ var rowFingerprint = (r) => fingerprint(r.name, r.description, r.instructions, toolsOf(r.tools) ?? []);
45838
+ async function seedBuiltinAgents() {
45839
+ try {
45840
+ const setting = await getSetting();
45841
+ const stamps = { ...setting.builtinAgents };
45842
+ let changed = false;
45843
+ for (const a of BUILTIN_AGENTS) {
45844
+ const id = customAgentId(null, a.name);
45845
+ const fp = builtinFingerprint(a);
45846
+ const row = await prisma.customAgent.findUnique({ where: { id } });
45847
+ if (!row) {
45848
+ if (await findTombstone("customAgent", id)) continue;
45849
+ await prisma.customAgent.create({ data: {
45850
+ id,
45851
+ projectId: null,
45852
+ name: a.name,
45853
+ description: a.description,
45854
+ instructions: a.instructions,
45855
+ tools: [...a.tools],
45856
+ model: null,
45857
+ mentions: [],
45858
+ runtime: null,
45859
+ enabled: a.enabledByDefault
45860
+ } });
45861
+ await notifySynced("customAgent", id);
45862
+ stamps[a.name] = fp;
45863
+ changed = true;
45864
+ continue;
45865
+ }
45866
+ const stamped = stamps[a.name];
45867
+ if (!stamped || stamped === fp) continue;
45868
+ if (stamped !== rowFingerprint(row)) continue;
45869
+ await prisma.customAgent.update({ where: { id }, data: {
45870
+ description: a.description,
45871
+ instructions: a.instructions,
45872
+ tools: [...a.tools]
45873
+ // `enabled` TIDAK di sini. Sengaja.
45874
+ } });
45875
+ await notifySynced("customAgent", id);
45876
+ stamps[a.name] = fp;
45877
+ changed = true;
45878
+ }
45879
+ if (changed) {
45880
+ const data = { ...setting, builtinAgents: stamps };
45881
+ await prisma.setting.upsert({
45882
+ where: { id: 1 },
45883
+ update: { data },
45884
+ create: { id: 1, data }
45885
+ });
45886
+ }
45887
+ } catch {
45888
+ }
45889
+ }
45890
+
44616
45891
  // src/services/custom-agents.ts
44617
45892
  init_db();
44618
45893
  init_src();
@@ -44684,25 +45959,35 @@ function unknownMentions(row, all) {
44684
45959
  return mentionsOf(row.mentions).filter((m) => !visible.has(m));
44685
45960
  }
44686
45961
  async function installCustomAgents() {
45962
+ await seedBuiltinAgents();
44687
45963
  await loadCustomAgents();
44688
45964
  registerCustomAgentSource((projectId, agent) => agentDefsFor(projectId, agent));
44689
45965
  }
44690
45966
 
44691
45967
  // src/routes/custom-agents.ts
44692
45968
  var rowsOf = async () => await prisma.customAgent.findMany();
44693
- var view9 = (r, projectId) => ({
44694
- id: r.id,
44695
- projectId: r.projectId,
44696
- name: r.name,
44697
- description: r.description,
44698
- instructions: r.instructions,
44699
- tools: toolsOf(r.tools),
44700
- model: r.model,
44701
- mentions: mentionsOf(r.mentions),
44702
- runtime: runtimeOf(r.runtime),
44703
- enabled: r.enabled,
44704
- ...projectId ? { inherited: r.projectId === null } : {}
44705
- });
45969
+ var stampsOf = async () => (await getSetting()).builtinAgents;
45970
+ var view9 = (r, projectId, stamps = {}) => {
45971
+ const builtin = r.projectId === null && BUILTIN_AGENT_NAMES.includes(r.name);
45972
+ return {
45973
+ id: r.id,
45974
+ projectId: r.projectId,
45975
+ name: r.name,
45976
+ description: r.description,
45977
+ instructions: r.instructions,
45978
+ tools: toolsOf(r.tools),
45979
+ model: r.model,
45980
+ mentions: mentionsOf(r.mentions),
45981
+ runtime: runtimeOf(r.runtime),
45982
+ enabled: r.enabled,
45983
+ builtin,
45984
+ // Sidik jari yang tak tercatat (baris menyeberang sync dari mesin lain, seed di sini belum
45985
+ // pernah menyentuhnya) dibaca sebagai "disunting" — lebih baik menandai berlebih daripada
45986
+ // menjanjikan "asli bawaan" untuk isi yang tak bisa kita buktikan.
45987
+ builtinEdited: builtin ? stamps[r.name] !== rowFingerprint(r) : false,
45988
+ ...projectId ? { inherited: r.projectId === null } : {}
45989
+ };
45990
+ };
44706
45991
  var repoDirOf2 = async (projectId) => projectId ? await resolveRepoDir(projectId) : null;
44707
45992
  function toolsProblem(tools, catalogIds) {
44708
45993
  if (!tools || tools.length === 0) return null;
@@ -44735,7 +46020,8 @@ async function custom_agents_default(app2) {
44735
46020
  const byName = /* @__PURE__ */ new Map();
44736
46021
  for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
44737
46022
  for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
44738
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view9(r, projectId));
46023
+ const stamps = await stampsOf();
46024
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view9(r, projectId, stamps));
44739
46025
  });
44740
46026
  app2.post("/custom-agents", async (req, reply) => {
44741
46027
  const parsed = zCreateCustomAgent.safeParse(req.body);
@@ -44782,7 +46068,7 @@ async function custom_agents_default(app2) {
44782
46068
  } });
44783
46069
  await loadCustomAgents();
44784
46070
  await notifySynced("customAgent", id);
44785
- return reply.code(201).send(view9(row));
46071
+ return reply.code(201).send(view9(row, void 0, await stampsOf()));
44786
46072
  });
44787
46073
  app2.patch("/custom-agents/:id", async (req, reply) => {
44788
46074
  const { id } = req.params;
@@ -44829,7 +46115,7 @@ async function custom_agents_default(app2) {
44829
46115
  } });
44830
46116
  await loadCustomAgents();
44831
46117
  await notifySynced("customAgent", id);
44832
- return view9(row);
46118
+ return view9(row, void 0, await stampsOf());
44833
46119
  });
44834
46120
  app2.delete("/custom-agents/:id", async (req, reply) => {
44835
46121
  const { id } = req.params;
@@ -44902,7 +46188,7 @@ async function resolveGithubRepo(projectId) {
44902
46188
 
44903
46189
  // src/services/github-fetch.ts
44904
46190
  init_config2();
44905
- import { execFile as execFile14 } from "node:child_process";
46191
+ import { execFile as execFile15 } from "node:child_process";
44906
46192
  var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
44907
46193
  var norm2 = (s2) => s2.toLowerCase() === "closed" ? "closed" : "open";
44908
46194
  function issueFromGh(raw2) {
@@ -44943,7 +46229,7 @@ function issuesFromRest(raw2) {
44943
46229
  var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
44944
46230
  var API2 = "https://api.github.com";
44945
46231
  var defaultRunGh = (args, env) => new Promise((resolve21, reject2) => {
44946
- execFile14(
46232
+ execFile15(
44947
46233
  args[0],
44948
46234
  args.slice(1),
44949
46235
  { env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
@@ -45289,22 +46575,22 @@ async function verifyTelegramAgentToken(token, deps = {}) {
45289
46575
  return missing.length ? { ok: false, missing, reason: `AgentToken kurang ${missing.length} capability.` } : { ok: true };
45290
46576
  }
45291
46577
  async function telegramInboundReadiness(deps = {}) {
45292
- const polling = deps.polling ?? telegramRuntimeStatus().running;
46578
+ const polling2 = deps.polling ?? telegramRuntimeStatus().running;
45293
46579
  const empty = [];
45294
46580
  const token = deps.agentToken ?? effectiveStr("HANOMAN_TELEGRAM_AGENT_TOKEN")?.trim();
45295
46581
  if (!token) {
45296
- return { ok: false, reason: "AgentToken gateway belum diisi.", missingCapabilities: [...TELEGRAM_REQUIRED_CAPABILITIES], polling };
46582
+ return { ok: false, reason: "AgentToken gateway belum diisi.", missingCapabilities: [...TELEGRAM_REQUIRED_CAPABILITIES], polling: polling2 };
45297
46583
  }
45298
46584
  const setting = await (deps.getSetting ?? getSetting)();
45299
46585
  if (!setting.agentAccessEnabled) {
45300
- return { ok: false, reason: "Akses agent mati \u2014 nyalakan master switch di Akses AI Agent.", missingCapabilities: empty, polling };
46586
+ return { ok: false, reason: "Akses agent mati \u2014 nyalakan master switch di Akses AI Agent.", missingCapabilities: empty, polling: polling2 };
45301
46587
  }
45302
46588
  const gate = await verifyTelegramAgentToken(token, deps);
45303
- if (!gate.ok) return { ok: false, reason: gate.reason, missingCapabilities: gate.missing, polling };
45304
- if (!polling) {
45305
- return { ok: false, reason: "Kredensial sudah sah tapi gateway belum polling \u2014 nyalakan \u201CGateway aktif\u201D.", missingCapabilities: empty, polling };
46589
+ if (!gate.ok) return { ok: false, reason: gate.reason, missingCapabilities: gate.missing, polling: polling2 };
46590
+ if (!polling2) {
46591
+ return { ok: false, reason: "Kredensial sudah sah tapi gateway belum polling \u2014 nyalakan \u201CGateway aktif\u201D.", missingCapabilities: empty, polling: polling2 };
45306
46592
  }
45307
- return { ok: true, reason: null, missingCapabilities: empty, polling };
46593
+ return { ok: true, reason: null, missingCapabilities: empty, polling: polling2 };
45308
46594
  }
45309
46595
  var KEYS = TELEGRAM_CONFIG_KEYS;
45310
46596
  function telegramCredentialView() {
@@ -45975,7 +47261,7 @@ init_settings2();
45975
47261
  // src/services/portal-chat/turn.ts
45976
47262
  init_src();
45977
47263
  init_db();
45978
- import { execFile as execFile15 } from "node:child_process";
47264
+ import { execFile as execFile16 } from "node:child_process";
45979
47265
 
45980
47266
  // src/services/portal-chat/guard-input.ts
45981
47267
  import { randomBytes as randomBytes9 } from "node:crypto";
@@ -46087,9 +47373,9 @@ ${baru}`;
46087
47373
  // src/services/portal-chat/workspace.ts
46088
47374
  init_src();
46089
47375
  init_db();
46090
- import { mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "node:fs";
47376
+ import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
46091
47377
  import { tmpdir as tmpdir5 } from "node:os";
46092
- import { join as join25 } from "node:path";
47378
+ import { join as join26 } from "node:path";
46093
47379
  var STAGE_LABEL = {
46094
47380
  brainstorming: "Dirumuskan",
46095
47381
  objective: "Dirumuskan",
@@ -46144,7 +47430,7 @@ ${c.body}
46144
47430
  ${baris.join("\n")}`;
46145
47431
  }
46146
47432
  async function buildChatWorkspace(projectId) {
46147
- const dir2 = mkdtempSync2(join25(tmpdir5(), "hanoman-portal-chat-"));
47433
+ const dir2 = mkdtempSync2(join26(tmpdir5(), "hanoman-portal-chat-"));
46148
47434
  try {
46149
47435
  const project = await prisma.project.findUnique({
46150
47436
  where: { id: projectId },
@@ -46165,7 +47451,7 @@ async function buildChatWorkspace(projectId) {
46165
47451
  });
46166
47452
  const files = [];
46167
47453
  const tulis = (rel, isi) => {
46168
- writeFileSync7(join25(dir2, rel), isi, { mode: 384 });
47454
+ writeFileSync8(join26(dir2, rel), isi, { mode: 384 });
46169
47455
  files.push(rel);
46170
47456
  };
46171
47457
  tulis("project.md", renderProjectDoc(project));
@@ -46174,11 +47460,11 @@ async function buildChatWorkspace(projectId) {
46174
47460
  tulis("catatan-rilis.md", renderChangelogDoc(changelogs));
46175
47461
  const prds = await listPrds(projectId);
46176
47462
  if (prds.length) {
46177
- mkdirSync11(join25(dir2, "dokumen"), { mode: 448 });
47463
+ mkdirSync12(join26(dir2, "dokumen"), { mode: 448 });
46178
47464
  for (const prd of prds) {
46179
47465
  const isi = await readPrd(projectId, prd.path);
46180
47466
  if (!isi) continue;
46181
- tulis(join25("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
47467
+ tulis(join26("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
46182
47468
  }
46183
47469
  }
46184
47470
  return { dir: dir2, files, cleanup: () => rmSync7(dir2, { recursive: true, force: true }) };
@@ -46190,6 +47476,7 @@ async function buildChatWorkspace(projectId) {
46190
47476
 
46191
47477
  // src/services/portal-chat/argv.ts
46192
47478
  init_src();
47479
+ init_src2();
46193
47480
  init_config2();
46194
47481
  init_session_sandbox();
46195
47482
  var PORTAL_CHAT_TOOLS = "Read,Glob,Grep";
@@ -46220,10 +47507,10 @@ var shellQuote2 = (v) => `'${v.replace(/'/g, `'"'"'`)}'`;
46220
47507
  function portalChatProcess(o, env = process.env) {
46221
47508
  const file = effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
46222
47509
  const args = portalChatArgv(o);
46223
- const mode = env.HANOMAN_SESSION_SANDBOX ?? (env.NODE_ENV === "production" ? "required" : "off");
47510
+ const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
46224
47511
  if (mode === "off") {
46225
- if (env.NODE_ENV === "production")
46226
- throw new Error("chat portal menolak jalan: sandbox sesi wajib di production");
47512
+ if (resolveHardening(env))
47513
+ throw new Error("chat portal menolak jalan: sandbox sesi wajib saat hardening menyala");
46227
47514
  return { file, args, cwd: o.workspace };
46228
47515
  }
46229
47516
  const command = [file, ...args].map(shellQuote2).join(" ");
@@ -46261,7 +47548,7 @@ function chatFailureReason(err, stdout, stderr, timeoutMs) {
46261
47548
  }
46262
47549
  function runProcess(p, timeoutMs) {
46263
47550
  return new Promise((resolve21, reject2) => {
46264
- const child = execFile15(p.file, p.args, {
47551
+ const child = execFile16(p.file, p.args, {
46265
47552
  cwd: p.cwd,
46266
47553
  timeout: timeoutMs,
46267
47554
  maxBuffer: 16 * 1024 * 1024,
@@ -46688,15 +47975,15 @@ init_src();
46688
47975
  init_db();
46689
47976
 
46690
47977
  // src/services/bootstrap.ts
46691
- import { createHash as createHash9, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
47978
+ import { createHash as createHash11, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
46692
47979
  import { chmod, lstat as lstat3, mkdir as mkdir6, readFile as readFile5, unlink as unlink5, writeFile as writeFile5 } from "node:fs/promises";
46693
- import { join as join26 } from "node:path";
47980
+ import { join as join27 } from "node:path";
46694
47981
  var TTL_MS5 = 15 * 6e4;
46695
47982
  var SETUP_TOKEN_FILE = "setup.token";
46696
47983
  var BootstrapError = class extends Error {
46697
47984
  code = "BOOTSTRAP_PROOF";
46698
47985
  };
46699
- var tokenPath = (home3) => join26(home3, SETUP_TOKEN_FILE);
47986
+ var tokenPath = (home3) => join27(home3, SETUP_TOKEN_FILE);
46700
47987
  async function readStored(home3) {
46701
47988
  try {
46702
47989
  const info = await lstat3(tokenPath(home3));
@@ -46741,8 +48028,8 @@ ${new Date(expiresAt).toISOString()}
46741
48028
  }
46742
48029
  async function verifySetupToken(candidate, home3, now = Date.now()) {
46743
48030
  const stored = await readStored(home3);
46744
- const got = createHash9("sha256").update(candidate).digest();
46745
- const want = createHash9("sha256").update(stored.token).digest();
48031
+ const got = createHash11("sha256").update(candidate).digest();
48032
+ const want = createHash11("sha256").update(stored.token).digest();
46746
48033
  if (stored.expiresAt <= now || !timingSafeEqual3(got, want)) throw new BootstrapError("invalid setup proof");
46747
48034
  }
46748
48035
  async function consumeSetupToken(home3) {
@@ -46754,9 +48041,9 @@ async function consumeSetupToken(home3) {
46754
48041
  // src/routes/auth.ts
46755
48042
  var BOOTSTRAP_USER_ID = "bootstrap-admin";
46756
48043
  var view12 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
46757
- async function issue(reply, userId) {
48044
+ async function issue(req, reply, userId) {
46758
48045
  const token = await createSession2(userId);
46759
- reply.setCookie(COOKIE_NAME, token, cookieOpts());
48046
+ reply.setCookie(COOKIE_NAME, token, cookieOpts(req));
46760
48047
  }
46761
48048
  async function auth_default(app2, opts) {
46762
48049
  const setupAttempts = new BoundedRateLimiter({ windowMs: 6e4, limit: 10, maxKeys: 4096 });
@@ -46792,7 +48079,7 @@ async function auth_default(app2, opts) {
46792
48079
  }
46793
48080
  if (opts.bootstrapRequired) await consumeSetupToken(opts.home);
46794
48081
  setupAttempts.clear(req.ip);
46795
- await issue(reply, user.id);
48082
+ await issue(req, reply, user.id);
46796
48083
  return { user: view12(user) };
46797
48084
  });
46798
48085
  app2.post("/auth/login", async (req, reply) => {
@@ -46805,7 +48092,7 @@ async function auth_default(app2, opts) {
46805
48092
  return reply.code(401).send({ error: "email atau password salah" });
46806
48093
  }
46807
48094
  clearLoginFails(req.ip);
46808
- await issue(reply, user.id);
48095
+ await issue(req, reply, user.id);
46809
48096
  return { user: view12(user) };
46810
48097
  });
46811
48098
  app2.post("/auth/logout", async (req, reply) => {
@@ -46847,11 +48134,93 @@ async function auth_default(app2, opts) {
46847
48134
  data: { passwordHash: await hashPassword(p.data.newPassword) }
46848
48135
  });
46849
48136
  await deleteUserSessions(user.id);
46850
- await issue(reply, user.id);
48137
+ await issue(req, reply, user.id);
46851
48138
  return { user: view12(user) };
46852
48139
  });
46853
48140
  }
46854
48141
 
48142
+ // src/routes/setup.ts
48143
+ init_src2();
48144
+ init_src();
48145
+ init_db();
48146
+
48147
+ // src/services/setup-config.ts
48148
+ init_src2();
48149
+ var SETUP_ALLOWED_KEYS = [
48150
+ "HANOMAN_DEPLOYMENT",
48151
+ "HANOMAN_HARDENING",
48152
+ "HANOMAN_SETUP_DONE",
48153
+ "HANOMAN_SESSION_SANDBOX",
48154
+ "HANOMAN_SESSION_NETWORK",
48155
+ "HANOMAN_EGRESS_PROXY",
48156
+ "HANOMAN_AGENT_CREDENTIAL_DIR",
48157
+ "HANOMAN_CONTROL_ORIGINS",
48158
+ "HANOMAN_PUBLIC_ORIGINS",
48159
+ "HANOMAN_SINGLE_ORIGIN",
48160
+ "HANOMAN_TRUST_PROXY",
48161
+ "HANOMAN_UPLOAD_SCANNER"
48162
+ ];
48163
+ function setupDone(home3) {
48164
+ return readConfigEnv(home3).HANOMAN_SETUP_DONE === "1";
48165
+ }
48166
+ function hardeningLocked(home3, env) {
48167
+ return resolveHardening(env) && !resolveHardening(readConfigEnv(home3));
48168
+ }
48169
+ function applySetup(home3, input) {
48170
+ const existing = readConfigEnv(home3);
48171
+ const next = {};
48172
+ for (const key of SETUP_ALLOWED_KEYS) if (existing[key]) next[key] = existing[key];
48173
+ next.HANOMAN_DEPLOYMENT = input.deployment;
48174
+ next.HANOMAN_SETUP_DONE = "1";
48175
+ if (input.hardening) next.HANOMAN_HARDENING = "1";
48176
+ else delete next.HANOMAN_HARDENING;
48177
+ writeConfigEnv(home3, next);
48178
+ return next;
48179
+ }
48180
+
48181
+ // src/services/restart.ts
48182
+ init_src();
48183
+ function requestConfigRestart(delayMs = 50) {
48184
+ setTimeout(() => process.exit(CONFIG_RESTART_EXIT), delayMs);
48185
+ }
48186
+
48187
+ // src/routes/setup.ts
48188
+ async function setup_default(app2, opts) {
48189
+ const attempts = new BoundedRateLimiter({ windowMs: 6e4, limit: 10, maxKeys: 4096 });
48190
+ const env = opts.env;
48191
+ const status = async () => ({
48192
+ // Dua syarat, bukan satu: belum ada user DAN wizard belum pernah dijawab.
48193
+ needed: await prisma.user.count() === 0 && !setupDone(opts.home),
48194
+ deployment: resolveDeployment(env),
48195
+ hardening: resolveHardening(env),
48196
+ hardeningLocked: hardeningLocked(opts.home, env),
48197
+ supervised: env.HANOMAN_SUPERVISOR === "1",
48198
+ setupTokenRequired: resolveHardening(env),
48199
+ prerequisites: prerequisites(env, collectProbeFacts(env))
48200
+ });
48201
+ app2.get("/setup/status", async () => status());
48202
+ app2.post("/setup", async (req, reply) => {
48203
+ if (attempts.hit(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
48204
+ const p = zSetupApply.safeParse(req.body);
48205
+ if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
48206
+ if (hardeningLocked(opts.home, env) && !p.data.hardening)
48207
+ return reply.code(409).send({ error: "hardening-locked" });
48208
+ if (p.data.hardening) {
48209
+ const rows = prerequisites(env, collectProbeFacts(env));
48210
+ if (!allReady(rows))
48211
+ return reply.code(400).send({
48212
+ error: "prerequisites-missing",
48213
+ missing: rows.filter((r) => !r.ok).map((r) => r.id)
48214
+ });
48215
+ }
48216
+ applySetup(opts.home, { deployment: p.data.deployment, hardening: p.data.hardening });
48217
+ attempts.clear(req.ip);
48218
+ if (env.HANOMAN_SUPERVISOR !== "1") return { restart: "manual" };
48219
+ reply.raw.on("finish", () => requestConfigRestart());
48220
+ return { restart: "self" };
48221
+ });
48222
+ }
48223
+
46855
48224
  // src/routes/agent-tokens.ts
46856
48225
  init_src();
46857
48226
  init_agent_token();
@@ -47138,6 +48507,7 @@ function trustProxyFromEnv(env) {
47138
48507
 
47139
48508
  // src/app.ts
47140
48509
  init_src2();
48510
+ init_db();
47141
48511
  var PUBLIC = /* @__PURE__ */ new Set([
47142
48512
  "GET /api/health",
47143
48513
  "GET /api/auth/status",
@@ -47149,8 +48519,12 @@ var PUBLIC = /* @__PURE__ */ new Set([
47149
48519
  // justru menjelaskan arti 403 itu.
47150
48520
  "GET /api/agent-integration.md"
47151
48521
  ]);
48522
+ var SETUP_PUBLIC = /* @__PURE__ */ new Set([
48523
+ "GET /api/setup/status",
48524
+ "POST /api/setup"
48525
+ ]);
47152
48526
  function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {}) {
47153
- const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath4(import.meta.url)), env, existsSync16);
48527
+ const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath5(import.meta.url)), env, existsSync17);
47154
48528
  const ingress = loadIngressPolicy(env);
47155
48529
  const app2 = Fastify({ logger: false, trustProxy: trustProxyFromEnv(env) });
47156
48530
  app2.addHook("onRequest", async (req, reply) => {
@@ -47191,6 +48565,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
47191
48565
  if (user) req.user = user;
47192
48566
  const path2 = req.url.split("?")[0] ?? req.url;
47193
48567
  if (PUBLIC.has(`${req.method} ${path2}`)) return;
48568
+ if (SETUP_PUBLIC.has(`${req.method} ${path2}`) && await prisma.user.count() === 0) return;
47194
48569
  if (user?.role === "client" && !clientRouteAllowed(req.method, path2))
47195
48570
  return reply.code(403).send({ error: "portal klien: baca-saja" });
47196
48571
  if (path2.startsWith("/api/sync") && path2 !== "/api/sync/now" && path2 !== "/api/sync/pending" && !path2.startsWith("/api/sync/conflicts")) return;
@@ -47216,7 +48591,8 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
47216
48591
  setActor(actorFromRequest({ user: req.user ?? null, agent: req.agent ?? null }));
47217
48592
  });
47218
48593
  api.addHook("onResponse", auditTelegramGatewayResponse);
47219
- await api.register(auth_default, { bootstrapRequired: env.NODE_ENV === "production", home: resolveHome(env) });
48594
+ await api.register(auth_default, { bootstrapRequired: resolveHardening(env), home: resolveHome(env) });
48595
+ await api.register(setup_default, { home: resolveHome(env), env });
47220
48596
  await api.register(health_default);
47221
48597
  await api.register(agent_doc_default, { file: docFile });
47222
48598
  await api.register(projects_default);
@@ -47258,7 +48634,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
47258
48634
  await api.register(client_accounts_default);
47259
48635
  }, { prefix: "/api" });
47260
48636
  if (shouldServeWeb(env)) {
47261
- const dist = pickWebDir(dirname13(fileURLToPath4(import.meta.url)), env, existsSync16);
48637
+ const dist = pickWebDir(dirname13(fileURLToPath5(import.meta.url)), env, existsSync17);
47262
48638
  if (dist) {
47263
48639
  app2.register(fastifyStatic, { root: dist });
47264
48640
  app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));
@@ -47317,12 +48693,12 @@ var cronLiveNote = (sessionId2) => `sesi cron sebelumnya masih berjalan (${sessi
47317
48693
  var CRON_GONE_NOTE = "cron sudah dihapus atau dinonaktifkan";
47318
48694
  var CRON_OPTOUT_NOTE = "project belum di-opt-in scheduler";
47319
48695
  async function drainCronRuns(slots, deps) {
47320
- for (const run2 of await queuedCronRuns()) {
47321
- const cron = await prisma.schedulerCron.findUnique({ where: { id: run2.cronId } });
48696
+ for (const run3 of await queuedCronRuns()) {
48697
+ const cron = await prisma.schedulerCron.findUnique({ where: { id: run3.cronId } });
47322
48698
  const close = async (status, note) => {
47323
- if (status === "skipped") await markCronSkipped(run2.id, note);
47324
- else await markCronFailed(run2.id, note);
47325
- await recordCronRun(run2.cronId, cron?.name ?? run2.cronId, run2.projectId, run2.dueAt, status, note);
48699
+ if (status === "skipped") await markCronSkipped(run3.id, note);
48700
+ else await markCronFailed(run3.id, note);
48701
+ await recordCronRun(run3.cronId, cron?.name ?? run3.cronId, run3.projectId, run3.dueAt, status, note);
47326
48702
  };
47327
48703
  if (!cron || !cron.enabled) {
47328
48704
  await close("skipped", CRON_GONE_NOTE);
@@ -47342,7 +48718,7 @@ async function drainCronRuns(slots, deps) {
47342
48718
  continue;
47343
48719
  }
47344
48720
  if (slots <= 0) {
47345
- await noteCronRun(run2.id, CAP_FULL_NOTE);
48721
+ await noteCronRun(run3.id, CAP_FULL_NOTE);
47346
48722
  continue;
47347
48723
  }
47348
48724
  try {
@@ -47355,8 +48731,8 @@ async function drainCronRuns(slots, deps) {
47355
48731
  model: cron.model,
47356
48732
  effort: cron.effort
47357
48733
  });
47358
- if (await markCronLaunched(run2.id, sessionId2))
47359
- await recordCronRun(cron.id, cron.name, cron.projectId, run2.dueAt, "launched", null);
48734
+ if (await markCronLaunched(run3.id, sessionId2))
48735
+ await recordCronRun(cron.id, cron.name, cron.projectId, run3.dueAt, "launched", null);
47360
48736
  await prisma.schedulerCron.update({ where: { id: cron.id }, data: { lastRunAt: /* @__PURE__ */ new Date() } });
47361
48737
  slots--;
47362
48738
  } catch (e) {
@@ -47881,13 +49257,13 @@ function startWebhookEngine() {
47881
49257
  // src/services/auto-merge.ts
47882
49258
  init_src();
47883
49259
  init_db();
47884
- import { execFile as execFile16 } from "node:child_process";
49260
+ import { execFile as execFile17 } from "node:child_process";
47885
49261
  import { promisify as promisify14 } from "node:util";
47886
49262
  init_notifications();
47887
49263
  var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
47888
49264
  var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
47889
49265
  var TICK_MS5 = 6e4;
47890
- var exec11 = promisify14(execFile16);
49266
+ var exec11 = promisify14(execFile17);
47891
49267
  var GIT9 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
47892
49268
  var gitOut = async (cwd, args) => {
47893
49269
  try {
@@ -48000,7 +49376,7 @@ init_src2();
48000
49376
 
48001
49377
  // src/services/secure-home.ts
48002
49378
  import { chmod as chmod2, lstat as lstat4, mkdir as mkdir7 } from "node:fs/promises";
48003
- import { isAbsolute as isAbsolute6, resolve as resolve20 } from "node:path";
49379
+ import { isAbsolute as isAbsolute7, resolve as resolve20 } from "node:path";
48004
49380
  var HomePermissionError = class extends Error {
48005
49381
  code = "HOME_SYMLINK";
48006
49382
  };
@@ -48014,7 +49390,7 @@ async function assertNoSymlink(path2, allowMissing) {
48014
49390
  }
48015
49391
  }
48016
49392
  async function secureHanomanHome(opts) {
48017
- if (!isAbsolute6(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
49393
+ if (!isAbsolute7(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
48018
49394
  await assertNoSymlink(opts.home, true);
48019
49395
  await mkdir7(opts.home, { recursive: true, mode: 448 });
48020
49396
  await assertNoSymlink(opts.home, false);
@@ -48229,6 +49605,10 @@ var bootstrapReady = secureHanomanHome({
48229
49605
  }).then(() => prisma.user.count()).then(async (count) => {
48230
49606
  await secureHanomanHome({ home: home2, files: [dbFilePath(process.env.DATABASE_URL)] });
48231
49607
  if (count > 0) return;
49608
+ if (!resolveHardening(process.env)) {
49609
+ console.log("setup awal: buka dashboard dan ikuti wizard \u2014 tanpa hardening, akun pertama tak butuh token");
49610
+ return;
49611
+ }
48232
49612
  const proof = await ensureSetupToken(resolveHome());
48233
49613
  console.log(`setup admin memerlukan token di ${proof.path}; kedaluwarsa ${new Date(proof.expiresAt).toISOString()}`);
48234
49614
  });