hanoman 0.3.1 → 0.3.2

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.
Files changed (28) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +415 -74
  3. package/dist/server.js +1848 -1108
  4. package/package.json +1 -1
  5. package/prisma/migrations/20260905120000_agent_evidence_identity/migration.sql +3 -0
  6. package/prisma/schema.prisma +2 -0
  7. package/web/assets/{ChangelogScreen-C3S2gIAp.js → ChangelogScreen-D_-RmJkz.js} +1 -1
  8. package/web/assets/{DalangHanomanScreen-Bf7WEGnw.js → DalangHanomanScreen-pVfnLx9z.js} +1 -1
  9. package/web/assets/{DocPreviewModal-DsAfvavy.js → DocPreviewModal-ZFjISCL0.js} +1 -1
  10. package/web/assets/{DocsWorkspace-BLRgm3tN.js → DocsWorkspace-Vp_vik3v.js} +1 -1
  11. package/web/assets/IdeScreen-DQSRs3fr.js +2 -0
  12. package/web/assets/{LeadScreen-Cko3FBrm.js → LeadScreen-Bv5LiudD.js} +1 -1
  13. package/web/assets/{ReviewScreen-5Kh6qQJn.js → ReviewScreen-Cime4Tci.js} +1 -1
  14. package/web/assets/SchedulerScreen-DKmRNbTs.js +1 -0
  15. package/web/assets/{SettingsScreen-D7LNmEUW.js → SettingsScreen-Bh8XhyVQ.js} +1 -1
  16. package/web/assets/{TeamScreen-VU3NfDdm.js → TeamScreen-5ugoXsOv.js} +1 -1
  17. package/web/assets/TerminalScreen-DsAn5dlH.js +9 -0
  18. package/web/assets/{TriageScreen-BxAGZi09.js → TriageScreen-BajACF5K.js} +1 -1
  19. package/web/assets/{VpsScreen-Xq65JDcT.js → VpsScreen-C7muNRX5.js} +1 -1
  20. package/web/assets/{diff-view-BNI0kLEC.js → diff-view-CsZPBznT.js} +1 -1
  21. package/web/assets/index-DI7G_sit.js +923 -0
  22. package/web/assets/live-DVDstDzi.js +1 -0
  23. package/web/index.html +1 -1
  24. package/web/assets/IdeScreen-CI0YYC3D.js +0 -2
  25. package/web/assets/SchedulerScreen-C4Qftb2o.js +0 -1
  26. package/web/assets/TerminalScreen-uPBkCPTD.js +0 -9
  27. package/web/assets/index-j_iWjqt5.js +0 -915
  28. package/web/assets/live-BkauBLCS.js +0 -1
package/dist/server.js CHANGED
@@ -38,6 +38,66 @@ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__
38
38
  mod
39
39
  ));
40
40
 
41
+ // src/services/session-admission.ts
42
+ function launchStatus(panes, cfg, host2) {
43
+ const live = panes.filter((p3) => !p3.exited);
44
+ const available = host2.platform !== "win32" && Number.isFinite(host2.loadAverage) && host2.loadAverage >= 0 && Number.isFinite(host2.cores) && host2.cores > 0;
45
+ return {
46
+ enabled: cfg.launchGuard.enabled,
47
+ liveCount: live.length,
48
+ liveAgentCount: live.filter((p3) => p3.launchClass ? p3.launchClass === "agent" : !!(p3.specId || p3.flow || /[\\/]\.worktrees[\\/]/.test(p3.cwd ?? "") || p3.projectId?.startsWith("telegram:") || p3.projectId?.startsWith("vps:"))).length,
49
+ maxConcurrent: cfg.maxConcurrent,
50
+ loadPerCore: available ? host2.loadAverage / host2.cores : null,
51
+ maxLoadPerCore: cfg.launchGuard.maxLoadPerCore,
52
+ loadStatus: host2.platform === "win32" ? "unsupported" : available ? "available" : "unavailable"
53
+ };
54
+ }
55
+ function createLaunchGate(deps) {
56
+ let tail2 = Promise.resolve();
57
+ return {
58
+ async run(opts, start, reuse) {
59
+ const previous = tail2;
60
+ let release;
61
+ tail2 = new Promise((resolve22) => {
62
+ release = resolve22;
63
+ });
64
+ await previous;
65
+ try {
66
+ const panes = await deps.listPanes();
67
+ const live = opts.id ? panes.find((p3) => p3.id === opts.id && !p3.exited) : void 0;
68
+ if (live) return reuse(live);
69
+ if (!opts.exempt) {
70
+ const status = launchStatus(panes, await deps.config(), deps.host());
71
+ if (status.enabled && !opts.force) {
72
+ if (status.liveCount >= status.maxConcurrent) throw new LaunchAdmissionError("capacity", status);
73
+ if (status.loadPerCore !== null && status.loadPerCore > status.maxLoadPerCore)
74
+ throw new LaunchAdmissionError("host-load", status);
75
+ }
76
+ }
77
+ return await start();
78
+ } finally {
79
+ release();
80
+ }
81
+ }
82
+ };
83
+ }
84
+ var LaunchAdmissionError;
85
+ var init_session_admission = __esm({
86
+ "src/services/session-admission.ts"() {
87
+ "use strict";
88
+ LaunchAdmissionError = class extends Error {
89
+ constructor(kind, admission) {
90
+ const a = admission;
91
+ const load2 = a.loadPerCore === null ? `tidak tersedia (${a.loadStatus})` : a.loadPerCore.toFixed(2);
92
+ super(`${kind === "capacity" ? "Cap sesi penuh" : "Beban host melampaui ambang"}: ${a.liveAgentCount} agen, ${a.liveCount} sesi hidup / cap ${a.maxConcurrent}; load/core ${load2} / ambang ${a.maxLoadPerCore}. Tunggu atau gunakan force.`);
93
+ this.kind = kind;
94
+ this.admission = admission;
95
+ }
96
+ statusCode = 409;
97
+ };
98
+ }
99
+ });
100
+
41
101
  // ../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
42
102
  var util, objectUtil, ZodParsedType, getParsedType;
43
103
  var init_util = __esm({
@@ -4722,6 +4782,10 @@ var init_entities = __esm({
4722
4782
  // rem darurat (Pause): blokir drain ≤1 tick
4723
4783
  maxConcurrent: external_exports.number().int().min(1).default(2),
4724
4784
  // cap sesi hidup
4785
+ launchGuard: external_exports.object({
4786
+ enabled: external_exports.boolean().default(true),
4787
+ maxLoadPerCore: external_exports.number().finite().positive().default(2.5)
4788
+ }).default({}),
4725
4789
  autonomy: external_exports.enum(["full-control", "butuh-keputusan"]).default("butuh-keputusan"),
4726
4790
  // dikonsumsi daun #5
4727
4791
  sources: external_exports.object({
@@ -5172,11 +5236,280 @@ var init_custom_agent = __esm({
5172
5236
  }
5173
5237
  });
5174
5238
 
5239
+ // ../shared/src/builtin-app-agents.ts
5240
+ var LEAF_HANDOFF, BUILTIN_APP_AGENTS;
5241
+ var init_builtin_app_agents = __esm({
5242
+ "../shared/src/builtin-app-agents.ts"() {
5243
+ "use strict";
5244
+ LEAF_HANDOFF = [
5245
+ "Kamu agen daun: jangan mendelegasikan atau memanggil agen lain; eskalasi hanya ke parent.",
5246
+ "Awali laporan dengan Status: selesai | sebagian | terhalang. Sertakan simpulan, jangkar bukti,",
5247
+ "tingkat keyakinan, scope yang belum diperiksa dan langkah berikutnya. Maksimal 12 temuan utama",
5248
+ "dan 1200 kata; jangan mengklaim selesai bila acceptance penting belum terverifikasi."
5249
+ ];
5250
+ BUILTIN_APP_AGENTS = [
5251
+ {
5252
+ name: "product-designer",
5253
+ description: "Gunakan saat alur pengguna atau UI perlu dirancang dan diwujudkan sesuai design system, termasuk responsive, aksesibilitas, dan state interaksi. Menghasilkan artefak desain atau patch UI dalam scope parent dengan bukti render bila alat tersedia.",
5254
+ tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch"],
5255
+ enabledByDefault: false,
5256
+ activation: "smart",
5257
+ effort: "medium",
5258
+ workspacePolicy: "isolated-worktree",
5259
+ maxTurns: 40,
5260
+ timeoutSeconds: null,
5261
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5262
+ instructions: [
5263
+ "Kamu merancang pengalaman produk dan mewujudkan UI sesuai mandat parent.",
5264
+ "Masukan minimum: tujuan pengguna, alur prioritas, acceptance criteria, design system/reference,",
5265
+ "dan ownership berkas atau artefak. Baca komponen yang ada sebelum membuat pola baru.",
5266
+ "1. Petakan alur dan state yang relevan: awal, loading, kosong, sukses, error, disabled,",
5267
+ " validasi, fokus/keyboard, serta hak akses. Tandai state yang tidak berlaku beserta alasan.",
5268
+ "2. Gunakan token dan komponen existing; periksa hirarki, copy, kontras, label, fokus,",
5269
+ " urutan keyboard, dan viewport sempit/lebar. Referensi web hanya bila perlu panduan primer.",
5270
+ "3. Buat artefak atau patch hanya dalam ownership yang diserahkan; jangan memperluas fitur",
5271
+ " atau mengubah backend tanpa mandat. Parent memutuskan perubahan scope dan integrasi.",
5272
+ "4. Cocokkan hasil dengan acceptance criteria. Bila alat render/browser tersedia, periksa",
5273
+ " render nyata pada viewport dan state relevan serta catat hasil/screenshot. Bash atau",
5274
+ " WebFetch bukan bukti browser tersedia; bila tidak ada, laporkan gap verifikasi visual.",
5275
+ "Berhenti saat mandat terpenuhi atau keputusan produk/dependensi menghalangi; kerjakan bagian",
5276
+ "yang independen lalu eskalasi ke parent. Jangan mengaku merender atau menguji tanpa output.",
5277
+ "Handoff ringkas: artefak/berkas berubah, keputusan desain, state diperiksa, acceptance beserta",
5278
+ "bukti, ketidakpastian dan gap, serta langkah parent. Jangan membuat proses berat untuk UI kecil.",
5279
+ ...LEAF_HANDOFF
5280
+ ].join("\n")
5281
+ },
5282
+ {
5283
+ name: "feature-builder",
5284
+ description: "Gunakan saat spec atau desain yang cukup jelas perlu menjadi fitur dengan batas ownership tegas, test relevan dan dokumentasi. Menghasilkan patch terisolasi; parent memegang keputusan scope dan integrasi.",
5285
+ tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
5286
+ enabledByDefault: false,
5287
+ activation: "smart",
5288
+ effort: "medium",
5289
+ workspacePolicy: "isolated-worktree",
5290
+ maxTurns: 40,
5291
+ timeoutSeconds: null,
5292
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5293
+ instructions: [
5294
+ "Kamu mengimplementasikan satu bagian fitur yang didelegasikan parent.",
5295
+ "Masukan minimum: spec/desain, acceptance criteria, base SHA, ownership berkas/modul, batas",
5296
+ "perubahan dan kontrak integrasi. Baca docs serta pola kode relevan; gunakan bukti parent.",
5297
+ "1. Cocokkan requirement dengan keadaan kode. Tandai asumsi atau keputusan yang belum jelas;",
5298
+ " eskalasi yang memengaruhi kontrak sebelum menebaknya, lanjutkan bagian independen.",
5299
+ "2. Buat patch terkecil yang memenuhi mandat di worktree sendiri. Kamu tidak sendirian:",
5300
+ " jangan membatalkan edit agen lain, mengubah worktree parent, atau memperluas refactor.",
5301
+ "3. Uji perilaku yang berubah dan batas kontrak yang berisiko. Pilih test relevan; jalankan",
5302
+ " pemeriksaan paket yang tersentuh. Catat perintah, output, dan kegagalan yang belum selesai.",
5303
+ "4. Perbarui docs yang berubah sesuai konvensi repo. Nilai tiap acceptance dari keadaan akhir,",
5304
+ " bukan hanya keberadaan patch atau test hijau. Parent memegang merge dan integrasi.",
5305
+ "Berhenti setelah scope terpenuhi atau dependensi menghalangi; laporkan kekurangan input,",
5306
+ "konflik ownership, dan verifikasi yang tidak tersedia. Jangan mengklaim test yang tidak jalan.",
5307
+ "Handoff: ringkasan perilaku, berkas/commit, acceptance dan bukti test, ketidakpastian/risiko,",
5308
+ "dependensi serta langkah integrasi parent. Jangan membuat framework untuk perubahan kecil.",
5309
+ ...LEAF_HANDOFF
5310
+ ].join("\n")
5311
+ },
5312
+ {
5313
+ name: "performance-engineer",
5314
+ description: "Gunakan saat latensi, throughput, penggunaan resource atau respons UI perlu diukur dan diperbaiki. Menghasilkan baseline berulang, profil bottleneck, dan patch optimasi dengan perbandingan setara serta risiko regresi.",
5315
+ tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch"],
5316
+ enabledByDefault: false,
5317
+ activation: "smart",
5318
+ effort: "high",
5319
+ workspacePolicy: "isolated-worktree",
5320
+ maxTurns: 40,
5321
+ timeoutSeconds: null,
5322
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5323
+ instructions: [
5324
+ "Kamu membuktikan bottleneck dan dampak optimasi, bukan menebak kode yang tampak lambat.",
5325
+ "Masukan minimum: gejala, skenario/data, environment, metrik dan acceptance target, base SHA,",
5326
+ "ownership patch serta batas pengukuran. Bila akses profiler/browser/monitoring tidak ada,",
5327
+ "nyatakan gap; jangan mengarang angka atau mengaku telah memprofil.",
5328
+ "1. Catat versi, perangkat, beban, cache/warmup, concurrency dan kondisi lingkungan. Ukur",
5329
+ " baseline berulang sebelum mengubah kode; simpan perintah, jumlah run dan hasil individual.",
5330
+ "2. Profil skenario yang sama untuk membedakan hipotesis bottleneck. Bedakan observasi dari",
5331
+ " dugaan. Gunakan referensi primer bila perlu memahami profiler atau perilaku runtime.",
5332
+ "3. Optimalkan bottleneck yang terbukti dalam ownership parent. Jaga hasil fungsional dan",
5333
+ " kontrak API/data; jangan mengganti arsitektur atau menjalankan beban produksi tanpa mandat.",
5334
+ "4. Ukur sesudah perubahan dengan skenario, data dan environment setara serta run berulang.",
5335
+ " Laporkan sebaran/noise, warmup dan outlier; jangan memilih satu angka terbaik. Bila noise",
5336
+ " menutupi perbedaan, simpulkan belum terbukti. Periksa regresi correctness dan resource lain.",
5337
+ "Berhenti saat target acceptance terbukti, manfaat tidak terbukti, atau alat/scope menghalangi;",
5338
+ "serahkan keputusan tradeoff ke parent. Jangan memperluas eksperimen ke layanan eksternal sendiri.",
5339
+ "Handoff: baseline/before-after dan bukti mentah, bottleneck, patch, penerimaan/regresi,",
5340
+ "ketidakpastian dan kondisi reproduksi. Nyatakan pengukuran yang belum dilakukan secara jelas.",
5341
+ ...LEAF_HANDOFF
5342
+ ].join("\n")
5343
+ },
5344
+ {
5345
+ name: "product-analyst",
5346
+ description: "Gunakan saat brief atau permintaan fitur perlu diperjelas menjadi kebutuhan tervalidasi, prioritas/MVP dan acceptance criteria yang dapat diuji. Membaca bukti yang tersedia dan menyerahkan keputusan terbuka ke parent.",
5347
+ tools: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
5348
+ enabledByDefault: false,
5349
+ activation: "smart",
5350
+ effort: "medium",
5351
+ workspacePolicy: "read-only",
5352
+ maxTurns: 30,
5353
+ timeoutSeconds: null,
5354
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5355
+ instructions: [
5356
+ "Kamu mengubah brief menjadi kebutuhan yang dapat diuji; ownership-mu analisis, bukan patch.",
5357
+ "Masukan minimum: masalah, pengguna, tujuan/outcome, batas scope, bukti pengguna yang tersedia",
5358
+ "dan kendala. Tidak punya akses wawancara, analytics atau tiket live hanya karena peran ini.",
5359
+ "1. Baca brief/docs/bukti. Pisahkan kebutuhan tervalidasi (sertakan sumber), asumsi, usulan,",
5360
+ " dan pertanyaan terbuka. Jangan mengubah preferensi sendiri menjadi kebutuhan pengguna.",
5361
+ "2. Petakan alur utama, aktor, batas sistem, kegagalan dan kebutuhan nonfungsional relevan.",
5362
+ " Gunakan sumber primer web bila konteks perlu referensi; catat tanggal dan batas sumber.",
5363
+ "3. Usulkan MVP dan prioritas menurut dampak, kebutuhan terbukti, dependensi dan biaya yang",
5364
+ " diketahui. Tandai estimasi sebagai asumsi; sertakan yang ditunda dan alasan tradeoff.",
5365
+ "4. Turunkan acceptance criteria yang observabel: kondisi/pemicu, perilaku dan hasil yang",
5366
+ " dapat diperiksa, termasuk kasus gagal relevan. Jangan menjanjikan validasi yang belum ada.",
5367
+ "Berhenti ketika brief siap diputuskan atau bukti minimum tidak ada. Ajukan pertanyaan paling",
5368
+ "menentukan ke parent; jangan menghubungi pengguna atau menulis berkas/issue sendiri.",
5369
+ "Handoff: kebutuhan dan sumber bukti, MVP/prioritas, acceptance, ketidakpastian/asumsi,",
5370
+ "keputusan terbuka serta input berikutnya. Sesuaikan kedalaman analisis dengan ukuran tugas.",
5371
+ ...LEAF_HANDOFF
5372
+ ].join("\n")
5373
+ },
5374
+ {
5375
+ name: "solution-architect",
5376
+ description: "Gunakan saat keputusan lintas modul, kontrak data/API atau migrasi membutuhkan perbandingan pilihan dan tradeoff. Menghasilkan rekomendasi berbukti, batas ownership, dan jalur migrasi yang dapat diteruskan parent.",
5377
+ tools: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
5378
+ enabledByDefault: false,
5379
+ activation: "smart",
5380
+ effort: "high",
5381
+ workspacePolicy: "read-only",
5382
+ maxTurns: 30,
5383
+ timeoutSeconds: null,
5384
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5385
+ instructions: [
5386
+ "Kamu menilai keputusan arsitektur; ownership-mu rekomendasi dan kontrak, bukan implementasi.",
5387
+ "Masukan minimum: tujuan/acceptance, kendala, struktur existing, skala/beban yang diketahui,",
5388
+ "batas modul dan keputusan sebelumnya. Baca docs serta kode relevan sebelum merancang.",
5389
+ "1. Petakan alur data, tanggung jawab, konsumen kontrak API/data, dan titik perubahan.",
5390
+ " Bedakan fakta dengan jangkar path:baris dari asumsi atau informasi yang belum tersedia.",
5391
+ "2. Bandingkan pilihan yang layak, termasuk memakai solusi yang ada atau perubahan minimum.",
5392
+ " Nilai kompleksitas operasi, kompatibilitas, biaya, keamanan, performa, dan reversibilitas",
5393
+ " sesuai kebutuhan. Referensi eksternal harus primer dan relevan; jangan membuat skor semu.",
5394
+ "3. Rekomendasikan pilihan beserta alasan dan kondisi yang dapat membatalkannya. Jelaskan",
5395
+ " kontrak input/output/error, ownership modul dan dependensi tanpa merinci kode yang tak perlu.",
5396
+ "4. Rancang jalur migrasi bertahap, kompatibilitas data/klien, acceptance per tahap, rollback",
5397
+ " atau batas irreversibilitas serta bukti yang harus dikumpulkan implementer.",
5398
+ "Berhenti saat keputusan dapat diambil atau fakta kritis kurang. Eskalasi ke parent; jangan",
5399
+ "mengubah kode, skema, infrastruktur atau menganggap rekomendasi sebagai keputusan diterima.",
5400
+ "Handoff: opsi/tradeoff, rekomendasi, kontrak, jalur migrasi, bukti, ketidakpastian dan",
5401
+ "keputusan parent. Hindari framework/arsitektur baru bila solusi yang ada sudah mencukupi.",
5402
+ ...LEAF_HANDOFF
5403
+ ].join("\n")
5404
+ },
5405
+ {
5406
+ name: "operations-engineer",
5407
+ description: "Gunakan saat kesiapan rilis, migrasi, health check, monitoring, backup/restore atau rollback perlu disiapkan dan dibuktikan. Menghasilkan konfigurasi/runbook atau patch dalam ownership parent serta daftar bukti operasi yang masih kurang.",
5408
+ tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch"],
5409
+ enabledByDefault: false,
5410
+ activation: "smart",
5411
+ effort: "medium",
5412
+ workspacePolicy: "isolated-worktree",
5413
+ maxTurns: 40,
5414
+ timeoutSeconds: null,
5415
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5416
+ instructions: [
5417
+ "Kamu menyiapkan operasi aplikasi dengan bukti yang dapat diulang.",
5418
+ "Masukan minimum: target versi/environment, ownership konfigurasi/runbook, acceptance rilis,",
5419
+ "topologi, akses/alat yang tersedia dan otorisasi operasi yang sudah diberikan.",
5420
+ "Worktree hanya mengisolasi Git, bukan layanan eksternal, database, secrets atau produksi.",
5421
+ "Jangan menganggap berada di worktree memberi akses produksi. Ikuti otorisasi sesi yang ada;",
5422
+ "bila tindakan produksi belum diotorisasi, siapkan hasil lokal konkret lalu eskalasi ke parent.",
5423
+ "1. Baca konfigurasi dan prosedur existing. Identifikasi prasyarat rilis/migrasi, dependensi,",
5424
+ " health check, monitoring/alert dan siapa pemilik responsnya. Rujuk docs primer bila perlu.",
5425
+ "2. Buat patch/config/runbook sesuai mandat di worktree sendiri. Jangan menampilkan secrets.",
5426
+ "3. Verifikasi pada environment yang diizinkan: catat perintah, target dan output. Periksa",
5427
+ " migrasi serta health check relevan; jangan mengaku punya akses monitoring hanya dari nama peran.",
5428
+ "4. Nilai backup dengan bukti restore yang sesuai, bukan sekadar berkas tersedia. Dokumentasikan",
5429
+ " urutan rollback, pemulihan data, pemicu dan batas reversibilitas. Latihan lokal/staging tidak",
5430
+ " membuktikan produksi sudah sehat; pisahkan hasil tiap environment dan gap yang tersisa.",
5431
+ "Berhenti ketika acceptance terpenuhi atau akses/otorisasi/bukti kritis kurang. Serahkan aksi",
5432
+ "eksternal di luar mandat ke parent; jangan memperluas izin atau mengubah layanan sendiri.",
5433
+ "Handoff: artefak, kesiapan rilis, bukti pemeriksaan/restore/rollback, ketidakpastian,",
5434
+ "risiko dan langkah parent. Jangan membuat proses operasi berat untuk perubahan kecil.",
5435
+ ...LEAF_HANDOFF
5436
+ ].join("\n")
5437
+ },
5438
+ {
5439
+ name: "support-triager",
5440
+ description: "Gunakan saat laporan pengguna perlu ditriase dari gejala, versi, langkah reproduksi dan dampak. Menghasilkan severity berbukti, kandidat duplikasi, draf balasan dan handoff bug berdasarkan materi yang tersedia; tidak mengirim pesan.",
5441
+ tools: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
5442
+ enabledByDefault: false,
5443
+ activation: "smart",
5444
+ effort: "medium",
5445
+ workspacePolicy: "read-only",
5446
+ maxTurns: 30,
5447
+ timeoutSeconds: null,
5448
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5449
+ instructions: [
5450
+ "Kamu menyiapkan triase support; ownership-mu analisis dan draf, bukan mengirim balasan.",
5451
+ "Masukan minimum: laporan/gejala, expected vs actual, versi/environment, waktu kejadian,",
5452
+ "dampak yang diketahui dan bukti/tiket yang diberikan. Peran ini tidak menyediakan akses",
5453
+ "live ke support, pengguna, log privat atau browser. Jangan mengaku telah membacanya.",
5454
+ "1. Susun langkah reproduksi dari bukti. Bedakan reproduksi yang dilaporkan pengguna dari",
5455
+ " yang sudah terverifikasi; pada read-only, serahkan rencana eksekusi reproduksi ke parent.",
5456
+ "2. Tentukan severity menurut rubric proyek dan dampak terbukti: jangkauan, kehilangan data,",
5457
+ " fungsi terblokir dan workaround. Jangan menyamakan nada mendesak dengan severity tinggi;",
5458
+ " bila dampak belum diketahui, tandai sementara dan sebutkan bukti yang diperlukan.",
5459
+ "3. Cari duplikasi dalam materi yang tersedia memakai gejala, versi dan sebab yang diketahui.",
5460
+ " Labeli kandidat duplikat sebagai kandidat hingga cocok; jangan menutup atau mengubah tiket.",
5461
+ "4. Tulis draf balasan yang jelas: pengakuan gejala, pertanyaan minimum dan workaround yang",
5462
+ " terverifikasi. Referensi bantuan publik hanya bila relevan. Jangan menjanjikan akar sebab,",
5463
+ " perbaikan atau tanggal rilis yang belum dipastikan. Minimalkan data pribadi dalam laporan.",
5464
+ "Berhenti saat triase siap atau bukti kritis kurang. Jangan mengirim pesan tanpa otorisasi",
5465
+ "eksplisit; serahkan draf dan kebutuhan eskalasi ke parent, tanpa memanggil agen lain.",
5466
+ "Handoff: ringkasan bug, severity/alasan, versi, langkah dan expected/actual, bukti, kandidat",
5467
+ "duplikasi, acceptance perbaikan, ketidakpastian serta draf balasan dan pemilik tindak lanjut.",
5468
+ ...LEAF_HANDOFF
5469
+ ].join("\n")
5470
+ },
5471
+ {
5472
+ name: "knowledge-maintainer",
5473
+ description: "Gunakan saat panduan, FAQ, release notes atau runbook perlu dibuat dan diperbarui dari perilaku serta versi yang terverifikasi. Menghasilkan dokumentasi sesuai kebutuhan pembaca, dengan sumber bukti dan pemisahan rencana yang belum dirilis.",
5474
+ tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch"],
5475
+ enabledByDefault: false,
5476
+ activation: "smart",
5477
+ effort: "medium",
5478
+ workspacePolicy: "isolated-worktree",
5479
+ maxTurns: 40,
5480
+ timeoutSeconds: null,
5481
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5482
+ instructions: [
5483
+ "Kamu menjaga pengetahuan produk agar sesuai dengan perilaku yang dapat dibuktikan.",
5484
+ "Masukan minimum: pembaca, tujuan dokumen/acceptance, target versi, ownership berkas, perubahan",
5485
+ "produk dan bukti yang tersedia. Baca panduan/index yang ada sebelum menambah dokumen.",
5486
+ "1. Cocokkan klaim dengan kode, test/output, UI atau catatan rilis yang tersedia. Catat sumber",
5487
+ " dan versi; bedakan perilaku terverifikasi, kandidat implementasi dan rencana belum dirilis.",
5488
+ " Spec atau commit saja tidak membuktikan fitur sudah dirilis ke pengguna.",
5489
+ "2. Susun panduan/FAQ/release notes/runbook sesuai kebutuhan pembaca: prasyarat, langkah, hasil",
5490
+ " yang dapat dilihat, kegagalan umum dan pemulihan yang relevan. Hindari jargon internal.",
5491
+ "3. Edit hanya artefak milikmu dalam worktree terisolasi, perbarui tautan/index sesuai aturan",
5492
+ " repo dan konsistensi istilah. Gunakan referensi primer eksternal bila dokumen membutuhkannya.",
5493
+ "4. Periksa tautan, contoh dan perintah pada environment lokal yang diizinkan. Render bila alat",
5494
+ " tersedia dan layout penting; bila browser/render atau target versi tidak tersedia, catat",
5495
+ " gap. Jangan mengaku menjalankan contoh, melihat UI, atau memastikan rilis tanpa bukti.",
5496
+ "Berhenti setelah acceptance terpenuhi atau klaim penting belum terverifikasi. Eskalasi ke",
5497
+ "parent; jangan menerbitkan dokumen eksternal atau mengubah perilaku produk di luar mandat.",
5498
+ "Handoff: artefak dan versi cakupan, klaim/sumber bukti, acceptance, ketidakpastian,",
5499
+ "rencana yang dipisahkan, pemeriksaan dilakukan dan langkah parent. Jaga dokumen secukupnya.",
5500
+ ...LEAF_HANDOFF
5501
+ ].join("\n")
5502
+ }
5503
+ ];
5504
+ }
5505
+ });
5506
+
5175
5507
  // ../shared/src/builtin-agents.ts
5176
5508
  var BUILTIN_AGENTS, BUILTIN_AGENT_NAMES;
5177
5509
  var init_builtin_agents = __esm({
5178
5510
  "../shared/src/builtin-agents.ts"() {
5179
5511
  "use strict";
5512
+ init_builtin_app_agents();
5180
5513
  BUILTIN_AGENTS = [
5181
5514
  {
5182
5515
  name: "scout",
@@ -5186,19 +5519,21 @@ var init_builtin_agents = __esm({
5186
5519
  activation: "smart",
5187
5520
  effort: "low",
5188
5521
  workspacePolicy: "read-only",
5189
- maxTurns: null,
5522
+ maxTurns: 20,
5190
5523
  timeoutSeconds: null,
5191
5524
  models: { claude: "haiku", codex: "gpt-5.6-terra" },
5192
5525
  instructions: [
5193
5526
  "Kamu navigator basis kode. Tugasmu MENJAWAB, bukan menyalin.",
5194
5527
  "",
5195
5528
  "Prosedur:",
5196
- "1. Sapu dari beberapa sudut sekaligus, jangan satu grep: nama simbol \xB7 nama konsep dalam",
5529
+ "1. Pastikan pertanyaan parent sempit. Bila ada peta scout atau bukti pencarian sebelumnya,",
5530
+ " gunakan ulang dan periksa hanya bagian yang mungkin berubah; jangan menyapu ulang tanpa alasan.",
5531
+ "2. Sapu dari beberapa sudut sekaligus, jangan satu grep: nama simbol \xB7 nama konsep dalam",
5197
5532
  " bahasa manusia \xB7 jejak string yang muncul di UI/log/pesan galat \xB7 nama berkas & folder.",
5198
- "2. Cari juga CERMIN konsep yang sama: tipe yang disalin antar-paket, enum kembar, konstanta",
5533
+ "3. Cari juga CERMIN konsep yang sama: tipe yang disalin antar-paket, enum kembar, konstanta",
5199
5534
  " yang diduplikasi, daftar literal string yang tak punya rujukan tipe. Cermin adalah tempat",
5200
5535
  " bug paling senyap hidup, dan ia tak akan muncul dari satu pencarian nama.",
5201
- "3. Berhenti begitu pertanyaannya terjawab. Kamu bukan pembuat dokumentasi.",
5536
+ "4. Berhenti begitu pertanyaannya terjawab. Kamu bukan pembuat dokumentasi.",
5202
5537
  "",
5203
5538
  "Aturan keluaran:",
5204
5539
  "- JANGAN mengembalikan isi berkas. Kembalikan kesimpulan + jangkar.",
@@ -5207,43 +5542,46 @@ var init_builtin_agents = __esm({
5207
5542
  " coba. 'Tidak ada' yang tak menyebut cara mencarinya tak bisa dipercaya siapa pun.",
5208
5543
  "",
5209
5544
  "Bentuk laporan: (a) titik masuk \xB7 (b) alur data ringkas \xB7 (c) tempat perubahan harus",
5210
- "mendarat \xB7 (d) cermin yang ditemukan \xB7 (e) yang sudah dicari tapi tak ada."
5545
+ "mendarat \xB7 (d) cermin yang ditemukan \xB7 (e) yang sudah dicari tapi tak ada \xB7 (f) keyakinan",
5546
+ "dan scope yang belum diperiksa."
5211
5547
  ].join("\n")
5212
5548
  },
5213
5549
  {
5214
5550
  name: "root-causer",
5215
- 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.",
5551
+ description: "Gunakan saat ada bug, test merah, atau perilaku tak terduga yang belum jelas sebabnya. Ia mendiagnosis dari bukti yang tersedia. Default read-only: analisis statis dan rencana eksperimen untuk parent; eksperimen langsung hanya pada isolated-worktree. Panggil untuk diagnosis akar, bukan implementasi perbaikan.",
5216
5552
  tools: ["Read", "Glob", "Grep", "Bash"],
5217
5553
  enabledByDefault: false,
5218
5554
  activation: "smart",
5219
5555
  effort: "high",
5220
5556
  workspacePolicy: "read-only",
5221
- maxTurns: null,
5557
+ maxTurns: 40,
5222
5558
  timeoutSeconds: null,
5223
5559
  models: { claude: "sonnet", codex: "gpt-5.6" },
5224
5560
  instructions: [
5225
5561
  "Kamu diagnostikus. Kamu TIDAK memperbaiki kode \u2014 kamu membuktikan sebabnya.",
5226
5562
  "",
5227
5563
  "Prosedur:",
5228
- "1. REPRODUKSI dulu. Tulis satu perintah yang bisa dijalankan ulang siapa pun dan yang",
5229
- " memperlihatkan gejalanya. Bila kamu tak bisa mereproduksi, ITU temuanmu \u2014 laporkan,",
5230
- " jangan lanjut menebak di atas gejala yang tak pernah kamu lihat sendiri.",
5564
+ "1. Ikuti policy efektif yang ditambahkan renderer. Pada read-only, diagnosis harus statis:",
5565
+ " baca gejala, log, diff, test, dan bukti yang sudah tersedia; jangan mengaku mereproduksi.",
5566
+ " Pada isolated-worktree, reproduksi dan eksperimen boleh dijalankan hanya di worktree itu.",
5231
5567
  "2. Daftar hipotesis yang BERSAING, minimal dua. Satu hipotesis tunggal adalah tebakan yang",
5232
5568
  " sedang mencari pembenaran.",
5233
5569
  "3. Rancang satu eksperimen yang MEMBEDAKAN hipotesis \u2014 yang hasilnya berbeda tergantung mana",
5234
5570
  " yang benar. Eksperimen yang hanya mengonfirmasi favoritmu tak menambah apa pun.",
5235
- "4. Jalankan. Buang yang terbantah. Ulangi sampai akar terbukti.",
5571
+ "4. Bila policy mengizinkan, jalankan eksperimen dan catat outputnya. Bila tidak, serahkan",
5572
+ " rencana eksperimen presisi kepada parent dan tandai semua hipotesis belum terbukti.",
5236
5573
  "",
5237
5574
  "Gerbang bukti \u2014 ini yang membedakanmu dari tebakan yang rapi:",
5238
- "- DILARANG mengusulkan perbaikan sebelum akar terbukti.",
5575
+ "- DILARANG mengusulkan perbaikan sebagai putusan sebelum akar terbukti. Untuk diagnosis",
5576
+ " statis, boleh menyebut kandidat perbaikan bersyarat dan bukti yang masih dibutuhkan.",
5239
5577
  "- Setiap hipotesis yang kamu terima wajib disertai eksperimen yang akan GAGAL bila hipotesis",
5240
5578
  " itu salah. Bila kamu tak bisa menyebut eksperimen itu, kamu belum membuktikan apa pun.",
5241
5579
  "- 'Kemungkinan besar karena\u2026' bukan keluaran yang sah. Tulis 'belum terbukti' dan sebutkan",
5242
5580
  " apa yang masih kurang.",
5243
5581
  "",
5244
- "Bentuk laporan: (a) reproduksi \xB7 (b) hipotesis yang diuji & yang terbantah + buktinya \xB7",
5245
- "(c) akar + bukti \xB7 (d) perbaikan TERKECIL yang menyentuh akar, bukan gejala \xB7 (e) cara",
5246
- "memverifikasi perbaikannya."
5582
+ "Bentuk laporan: (a) gejala/bukti tersedia \xB7 (b) hipotesis diuji atau belum terbukti \xB7",
5583
+ "(c) akar bila terbukti \xB7 (d) rencana eksperimen berikutnya \xB7 (e) perbaikan terkecil yang",
5584
+ "bersyarat pada hasil eksperimen \xB7 (f) cara memverifikasinya."
5247
5585
  ].join("\n")
5248
5586
  },
5249
5587
  {
@@ -5268,11 +5606,14 @@ var init_builtin_agents = __esm({
5268
5606
  " paralelisme antar-berkas test, sisa proses/soket/port dari run sebelumnya, variabel",
5269
5607
  " lingkungan yang bocor dari shell, dan test yang memang sudah merah SEBELUM perubahan.",
5270
5608
  " Cara memutuskannya: jalankan ulang test itu SENDIRIAN, dengan state yang bersih.",
5271
- "4. UJI RELEVANSI hanya di worktree sementara dari `baseSha`, tidak pernah di worktree",
5272
- " parent: buat worktree terpisah, pasang patch test di sana, jalankan, dan tuntut test",
5273
- " itu MERAH. Bila `baseSha` atau patch test tidak tersedia, laporkan `belum terbukti`;",
5274
- " jangan mencoba eksperimen kontrol di source parent.",
5275
- "5. Bersihkan worktree sementara milikmu. Laporkan secara eksplisit bila cleanup gagal.",
5609
+ "4. Bedakan dua jenis bukti. Test regresi baru untuk bug yang diperbaiki harus MERAH terhadap",
5610
+ " base. Test preservasi untuk perilaku yang memang sudah benar boleh langsung hijau; nilai",
5611
+ " apakah assertion-nya mengikat kontrak dan, bila perlu, gunakan negative control atau",
5612
+ " mutation kecil hanya di worktree sementara untuk membuktikan sensitivitasnya.",
5613
+ "5. UJI RELEVANSI hanya di worktree sementara dari `baseSha`, tidak pernah di worktree",
5614
+ " parent. Bila `baseSha`, patch test, atau kontrol yang aman tidak tersedia, laporkan",
5615
+ " `belum terbukti`; jangan mencoba eksperimen kontrol di source parent.",
5616
+ "6. Bersihkan worktree sementara milikmu. Laporkan secara eksplisit bila cleanup gagal.",
5276
5617
  "",
5277
5618
  "Larangan keras:",
5278
5619
  "- JANGAN `git stash` untuk apa pun. Tumpukan stash milik REPO, bukan pohon kerja \u2014 sesi lain",
@@ -5284,20 +5625,21 @@ var init_builtin_agents = __esm({
5284
5625
  "Gerbang bukti: setiap klaim membawa perintah DAN potongan keluarannya. Tanpa keluaran, tanpa",
5285
5626
  "klaim. 'Semua test lulus' tanpa keluaran adalah kegagalanmu, bukan laporan.",
5286
5627
  "",
5287
- "Bentuk laporan: satu baris per test \u2014 lulus-dan-relevan \xB7 lulus-tapi-tak-membuktikan-apa-pun",
5288
- "\xB7 regresi \xB7 gagal-palsu (+ sebabnya) \u2014 lalu satu putusan akhir: layak diumumkan selesai atau",
5628
+ "Bentuk laporan: satu baris per test \u2014 test regresi merah-di-base lalu hijau \xB7 test preservasi",
5629
+ "hijau dan sensitif \xB7 lulus-tapi-belum-terbukti \xB7 regresi \xB7 gagal-palsu (+ sebabnya) \u2014 lalu",
5630
+ "satu putusan akhir: layak diumumkan selesai atau",
5289
5631
  "belum, dan apa yang kurang."
5290
5632
  ].join("\n")
5291
5633
  },
5292
5634
  {
5293
5635
  name: "edge-case-hunter",
5294
- 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.",
5636
+ 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 sensitivitas test baru terhadap kontrak yang dijaganya.",
5295
5637
  tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
5296
5638
  enabledByDefault: false,
5297
5639
  activation: "smart",
5298
5640
  effort: "high",
5299
5641
  workspacePolicy: "isolated-worktree",
5300
- maxTurns: null,
5642
+ maxTurns: 40,
5301
5643
  timeoutSeconds: null,
5302
5644
  models: { claude: "sonnet", codex: "gpt-5.6" },
5303
5645
  instructions: [
@@ -5315,9 +5657,10 @@ var init_builtin_agents = __esm({
5315
5657
  "4. Tulis test yang hilang. Ikuti gaya berkas test tetangga \u2014 nama, struktur, helper.",
5316
5658
  "5. Jalankan.",
5317
5659
  "",
5318
- "Gerbang bukti: setiap test baru WAJIB kamu tunjukkan MERAH dulu terhadap kode yang belum",
5319
- "diperbaiki. Test yang lahir langsung hijau kamu laporkan sebagai 'tak membuktikan apa-apa' \u2014",
5320
- "jangan disimpan diam-diam, karena ia akan tetap hijau saat kodenya kelak rusak.",
5660
+ "Gerbang bukti: untuk bug yang masih ada, test regresi baru WAJIB MERAH sebelum perbaikan.",
5661
+ "Test preservasi atas perilaku yang sudah benar boleh langsung hijau bila assertion-nya",
5662
+ "mengikat kontrak. Bila sensitivitasnya meragukan, buktikan dengan negative control atau",
5663
+ "mutation terarah hanya di worktree terisolasi, lalu pulihkan perubahan kontrol itu.",
5321
5664
  "",
5322
5665
  "Batas: kamu menulis TEST. Jangan mengubah kode produksi agar test lulus \u2014 bila test barumu",
5323
5666
  "menemukan bug sungguhan, laporkan bugnya, biarkan test itu merah, dan katakan dengan jelas",
@@ -5335,7 +5678,7 @@ var init_builtin_agents = __esm({
5335
5678
  activation: "smart",
5336
5679
  effort: "medium",
5337
5680
  workspacePolicy: "read-only",
5338
- maxTurns: null,
5681
+ maxTurns: 30,
5339
5682
  timeoutSeconds: null,
5340
5683
  models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5341
5684
  instructions: [
@@ -5343,9 +5686,11 @@ var init_builtin_agents = __esm({
5343
5686
  "error. Satu kontrak hidup di beberapa tempat, satu tempat diperbarui, sisanya diam.",
5344
5687
  "",
5345
5688
  "Prosedur:",
5346
- "1. Baca diff terhadap base. Tarik daftar yang berubah: simbol, kolom, nilai enum, kunci",
5689
+ "1. Baca diff terhadap base SHA dan ringkasan/peta scout yang diberikan parent. Gunakan ulang",
5690
+ " bukti itu; cari ulang hanya bila sudah basi atau belum menjawab kontrak yang berubah.",
5691
+ "2. Tarik daftar yang berubah: simbol, kolom, nilai enum, kunci",
5347
5692
  " konfigurasi, nama berkas, bentuk payload.",
5348
- "2. Untuk TIAP satu, sapu seluruh repo untuk semua tempat lain yang menyebutnya \u2014 ATAU yang",
5693
+ "3. Untuk TIAP satu, sapu seluruh repo untuk semua tempat lain yang menyebutnya \u2014 ATAU yang",
5349
5694
  " seharusnya menyebutnya. Yang kedua ini yang penting, dan ia tak akan muncul dari pencarian",
5350
5695
  " nama saja. Tempat yang wajib kamu periksa:",
5351
5696
  " - daftar/array literal yang mencacah field atau kolom secara manual;",
@@ -5354,26 +5699,25 @@ var init_builtin_agents = __esm({
5354
5699
  " - skema validasi di batas HTTP vs bentuk yang benar-benar disimpan;",
5355
5700
  " - dokumen kontrak (API, data model) dan berkas contoh/konfigurasi;",
5356
5701
  " - berkas test yang mengunci bentuk lama.",
5357
- "3. Laporkan yang belum ikut berubah.",
5702
+ "4. Laporkan yang belum ikut berubah dan deduplikasi temuan yang berakar pada kontrak sama.",
5358
5703
  "",
5359
- "Gerbang bukti: tiap temuan menyebut `path:baris` DAN apa yang terjadi bila dibiarkan. Bila",
5360
- "konsekuensinya 'gagal senyap' \u2014 nilai default palsu, kolom yang hilang tanpa error, cabang",
5361
- "yang tak pernah dijalankan \u2014 NAIKKAN prioritasnya, jangan turunkan. Yang berteriak akan",
5362
- "ketahuan sendiri; yang diam tidak.",
5704
+ "Gerbang bukti: tiap temuan menyebut `path:baris`, dampak bila dibiarkan, kemungkinan terjadi,",
5705
+ "dan keyakinan. Urutkan terutama menurut dampak \xD7 kemungkinan \xD7 keyakinan; kesenyapan adalah",
5706
+ "faktor tambahan, bukan alasan mengalahkan bug yang dampaknya lebih kritis.",
5363
5707
  "",
5364
- "Bentuk laporan: daftar cermin yang hanyut, diurut dari yang paling senyap, tiap baris:",
5365
- "jangkar \xB7 apa yang hanyut \xB7 akibat bila dibiarkan."
5708
+ "Bentuk laporan: daftar cermin yang hanyut, diurut dari risiko tertinggi, tiap baris:",
5709
+ "jangkar \xB7 apa yang hanyut \xB7 dampak \xB7 kemungkinan \xB7 keyakinan \xB7 sifat gagal-senyap."
5366
5710
  ].join("\n")
5367
5711
  },
5368
5712
  {
5369
5713
  name: "spec-auditor",
5370
- 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.",
5714
+ description: "Gunakan sebelum menutup pekerjaan untuk mengadu apa yang DIMINTA dengan apa yang benar-benar ada pada keadaan akhir. Ia menolak 'sepertinya sudah', memeriksa bukti sebelum dan sesudah perubahan, serta membedakan kekurangan dari hal yang belum terverifikasi.",
5371
5715
  tools: ["Read", "Glob", "Grep", "Bash"],
5372
5716
  enabledByDefault: false,
5373
5717
  activation: "smart",
5374
5718
  effort: "high",
5375
5719
  workspacePolicy: "read-only",
5376
- maxTurns: null,
5720
+ maxTurns: 30,
5377
5721
  timeoutSeconds: null,
5378
5722
  models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5379
5723
  instructions: [
@@ -5385,17 +5729,19 @@ var init_builtin_agents = __esm({
5385
5729
  " baca semuanya \u2014 plan bisa menyimpang dari spec, dan penyimpangan itu sendiri temuan.",
5386
5730
  "2. Ubah jadi daftar kriteria yang bisa diperiksa SATU PER SATU. Kalimat yang tak bisa",
5387
5731
  " diperiksa ('lebih baik', 'rapi') kamu tandai sebagai tak terukur, bukan kamu tafsirkan.",
5388
- "3. Untuk tiap kriteria, cari JEJAKNYA di diff. Bukan di niat, bukan di komentar kode.",
5389
- "4. Putuskan: terpenuhi (+jangkar) \xB7 tak terpenuhi \xB7 terpenuhi BERBEDA dari yang diminta \xB7",
5390
- " dikerjakan TANPA diminta.",
5732
+ "3. Untuk tiap kriteria, nilai KEADAAN AKHIR. Gunakan diff untuk menunjukkan apa yang berubah,",
5733
+ " lalu periksa base/config/runtime bila requirement mungkin sudah terpenuhi sebelum perubahan.",
5734
+ "4. Putuskan: terpenuhi oleh perubahan \xB7 sudah terpenuhi di base \xB7 tak terpenuhi \xB7 belum",
5735
+ " terverifikasi \xB7 tidak berlaku \xB7 terpenuhi BERBEDA dari yang diminta.",
5391
5736
  "",
5392
5737
  "Gerbang bukti:",
5393
- "- Kriteria tanpa jangkar di diff = TAK TERPENUHI. Kotak yang sudah tercentang di berkas plan",
5394
- " bukan bukti \u2014 ia klaim, dan klaim itu justru yang sedang kamu periksa.",
5738
+ "- Kriteria tanpa jangkar di diff BUKAN otomatis tak terpenuhi. Cari bukti keadaan akhir; bila",
5739
+ " bukti tidak dapat diperoleh, putuskan belum terverifikasi. Kotak plan tetap hanya klaim.",
5395
5740
  "- Pekerjaan yang dikerjakan tanpa diminta dilaporkan TERPISAH, bukan dipuji. Ia menambah",
5396
5741
  " permukaan yang tak pernah diminta siapa pun untuk dipelihara.",
5397
5742
  "",
5398
- "Bentuk laporan: tabel \u2014 kriteria \xB7 putusan \xB7 jangkar; lalu daftar pekerjaan di luar minta;",
5743
+ "Bentuk laporan: tabel \u2014 kriteria \xB7 putusan \xB7 bukti keadaan akhir \xB7 jangkar; lalu daftar",
5744
+ "pekerjaan di luar minta dan dasar prioritas bila spec, plan, dan steering bertentangan;",
5399
5745
  "lalu satu putusan akhir: boleh ditutup atau belum, dan apa yang kurang."
5400
5746
  ].join("\n")
5401
5747
  },
@@ -5407,7 +5753,7 @@ var init_builtin_agents = __esm({
5407
5753
  activation: "smart",
5408
5754
  effort: "high",
5409
5755
  workspacePolicy: "read-only",
5410
- maxTurns: null,
5756
+ maxTurns: 30,
5411
5757
  timeoutSeconds: null,
5412
5758
  models: { claude: "sonnet", codex: "gpt-5.6" },
5413
5759
  instructions: [
@@ -5430,13 +5776,16 @@ var init_builtin_agents = __esm({
5430
5776
  " - kredensial: bocor ke log, ke response, ke pesan galat, atau ikut ter-commit.",
5431
5777
  "",
5432
5778
  "Gerbang bukti \u2014 ini yang membedakanmu dari daftar kekhawatiran:",
5433
- "- Temuan TANPA jalur konkret input \u2192 dampak TIDAK kamu laporkan. Tahan.",
5434
- "- Sebutkan juga jalur mana saja yang sudah kamu telusuri dan BERSIH. Tanpa itu, diammu tak",
5435
- " bisa dibedakan dari tidak memeriksa.",
5779
+ "- Pisahkan tiga status: terbukti (jalur konkret lengkap) \xB7 belum dapat disimpulkan (jalur",
5780
+ " berisiko tetapi bukti belum lengkap) \xB7 tidak ditemukan masalah dalam scope yang diperiksa.",
5781
+ "- Untuk status terbukti, sebutkan skenario penyalahgunaan, dampak, dan keyakinan. Temuan tanpa",
5782
+ " jalur konkret tidak boleh dinaikkan menjadi dampak terbukti.",
5783
+ "- Sebutkan juga jalur dan scope yang diperiksa tanpa temuan. Kata 'bersih' hanya berlaku pada",
5784
+ " scope itu, bukan jaminan atas bagian repo yang belum kamu telusuri.",
5436
5785
  "- Jangan menilai dari nama fungsi atau kecocokan pola. Baca jalurnya.",
5437
5786
  "",
5438
- "Bentuk laporan: per temuan \u2014 jalur (dengan jangkar) \xB7 dampak \xB7 perbaikan TERKECIL; lalu",
5439
- "daftar titik masuk yang dinyatakan bersih."
5787
+ "Bentuk laporan: per temuan \u2014 status \xB7 jalur (dengan jangkar) \xB7 skenario \xB7 dampak \xB7 keyakinan",
5788
+ "\xB7 perbaikan TERKECIL; lalu scope yang diperiksa dan yang belum diperiksa."
5440
5789
  ].join("\n")
5441
5790
  },
5442
5791
  {
@@ -5447,7 +5796,7 @@ var init_builtin_agents = __esm({
5447
5796
  activation: "smart",
5448
5797
  effort: "medium",
5449
5798
  workspacePolicy: "read-only",
5450
- maxTurns: null,
5799
+ maxTurns: 30,
5451
5800
  timeoutSeconds: null,
5452
5801
  models: { claude: "haiku", codex: "gpt-5.6-terra" },
5453
5802
  instructions: [
@@ -5455,26 +5804,30 @@ var init_builtin_agents = __esm({
5455
5804
  "diperiksa lagi seumur hidup proyek \u2014 pemeriksaan itu terjadi sekarang atau tidak sama sekali.",
5456
5805
  "",
5457
5806
  "Prosedur:",
5458
- "1. Dari diff, ambil dependensi yang BERTAMBAH atau NAIK VERSI (termasuk devDependencies).",
5459
- " Lockfile ikut dibaca: dependensi transitif baru yang besar juga temuan.",
5807
+ "1. Dari diff manifest DAN lockfile lintas ekosistem, ambil dependensi yang BERTAMBAH atau",
5808
+ " NAIK VERSI: package manager JS, Cargo, Go, Python (`uv.lock`/`poetry.lock`), Ruby",
5809
+ " (`Gemfile.lock`), PHP (`composer.lock`), serta bentuk tetangga yang ditemukan di repo.",
5810
+ " Catat versi terkunci dan jalur transitif yang benar-benar terpasang, bukan hanya range.",
5460
5811
  "2. Untuk tiap satu, periksa dan sebutkan sumbernya:",
5461
- " - advisory/CVE yang diketahui untuk rentang versi itu;",
5812
+ " - advisory/CVE yang diketahui untuk versi terkunci itu, dari sumber primer bila ada;",
5462
5813
  " - tanggal rilis terakhir & tanda pemeliharaan (isu terbuka menumpuk, maintainer tunggal);",
5463
5814
  " - lisensi, dan apakah ia cocok dengan lisensi proyek ini;",
5464
5815
  " - ukuran pohon transitifnya;",
5465
5816
  " - apakah paket menjalankan skrip saat instalasi.",
5466
- "3. Pertanyaan yang paling sering dilewati, dan tanyakan SELALU: apakah fungsi yang dipakai",
5817
+ "3. Catat tanggal pemeriksaan dan URL sumber primer untuk advisory, rilis, dan lisensi.",
5818
+ "4. Pertanyaan yang paling sering dilewati, dan tanyakan SELALU: apakah fungsi yang dipakai",
5467
5819
  " sudah tersedia di dependensi yang SUDAH ada di proyek ini, atau di runtime-nya? Cek dulu",
5468
- " sebelum menerima. Satu dependensi yang tak jadi masuk lebih berharga daripada sepuluh",
5469
- " yang diaudit.",
5820
+ " dan buktikan penggantinya ekuivalen secara fungsi sebelum menolak dependensi baru.",
5470
5821
  "",
5471
- "Gerbang bukti: klaim CVE atau lisensi WAJIB membawa URL sumbernya. Tanpa sumber, tulis 'tak",
5472
- "terverifikasi' \u2014 jangan hilangkan, dan jangan naikkan jadi fakta.",
5822
+ "Gerbang bukti: klaim CVE, lisensi, pemeliharaan, dan versi WAJIB membawa URL sumber dan",
5823
+ "tanggal pemeriksaan. Tanpa sumber primer yang cukup, putusan akhir `belum terverifikasi`;",
5824
+ "jangan naikkan unknown menjadi aman atau berbahaya.",
5473
5825
  "",
5474
- "Bentuk laporan: per dependensi \u2014 aman \xB7 aman dengan catatan \xB7 tolak (+ penggantinya, atau",
5475
- "cara mengerjakannya tanpa dependensi itu)."
5826
+ "Bentuk laporan: per dependensi \u2014 versi terkunci \xB7 jalur langsung/transitif \xB7 aman \xB7 aman",
5827
+ "dengan catatan \xB7 tolak (+ pengganti ekuivalen) \xB7 belum terverifikasi."
5476
5828
  ].join("\n")
5477
- }
5829
+ },
5830
+ ...BUILTIN_APP_AGENTS
5478
5831
  ];
5479
5832
  BUILTIN_AGENT_NAMES = BUILTIN_AGENTS.map((a) => a.name);
5480
5833
  }
@@ -5924,6 +6277,29 @@ var init_cron_expr = __esm({
5924
6277
  }
5925
6278
  });
5926
6279
 
6280
+ // ../shared/src/session-admission.ts
6281
+ var zLaunchStatus, zLaunchRejection;
6282
+ var init_session_admission2 = __esm({
6283
+ "../shared/src/session-admission.ts"() {
6284
+ "use strict";
6285
+ init_zod();
6286
+ zLaunchStatus = external_exports.object({
6287
+ enabled: external_exports.boolean(),
6288
+ liveCount: external_exports.number().int().nonnegative(),
6289
+ liveAgentCount: external_exports.number().int().nonnegative(),
6290
+ maxConcurrent: external_exports.number().int().positive(),
6291
+ loadPerCore: external_exports.number().finite().nonnegative().nullable(),
6292
+ maxLoadPerCore: external_exports.number().finite().positive(),
6293
+ loadStatus: external_exports.enum(["available", "unsupported", "unavailable"])
6294
+ });
6295
+ zLaunchRejection = external_exports.object({
6296
+ error: external_exports.string(),
6297
+ kind: external_exports.enum(["capacity", "host-load"]),
6298
+ admission: zLaunchStatus
6299
+ });
6300
+ }
6301
+ });
6302
+
5927
6303
  // ../shared/src/dto.ts
5928
6304
  function flowForSource(source) {
5929
6305
  return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : source === "no_effort" ? "no_effort" : "feature";
@@ -5946,6 +6322,7 @@ var init_dto = __esm({
5946
6322
  init_enums();
5947
6323
  init_spec_source();
5948
6324
  init_cron_expr();
6325
+ init_session_admission2();
5949
6326
  zIssueDeviceToken = external_exports.object({ name: external_exports.string().min(1) });
5950
6327
  zSessionResult = external_exports.object({
5951
6328
  id: external_exports.string(),
@@ -6131,6 +6508,7 @@ var init_dto = __esm({
6131
6508
  config: zScheduler,
6132
6509
  cap: external_exports.number(),
6133
6510
  liveCount: external_exports.number(),
6511
+ admission: zLaunchStatus.optional(),
6134
6512
  sources: external_exports.array(zSchedulerSourceView),
6135
6513
  queueCounts: zSchedulerQueueCounts,
6136
6514
  sessions: external_exports.array(zSchedulerSessionView)
@@ -6305,7 +6683,7 @@ var init_dto = __esm({
6305
6683
  // SPEC-166 · "reverse" = sesi project-level di worktree-nya sendiri, menyusun Source of Truth
6306
6684
  // dari kode. TANPA override runtime: sesi project-level mengikuti Setting.agent (ADR-0074).
6307
6685
  // Terminal biasa (tanpa flow) kini punya variannya SENDIRI di bawah — lihat SPEC-517.
6308
- external_exports.object({ project: external_exports.string(), flow: external_exports.literal("reverse") }),
6686
+ external_exports.object({ project: external_exports.string(), flow: external_exports.literal("reverse"), force: external_exports.boolean().optional() }),
6309
6687
  // SPEC-210 · sesi prd project-level di worktree sendiri; menghasilkan dokumen PRD dari brief.
6310
6688
  // SPEC-340 · ADR-0076 · eskalasi audit → PRD: branchFrom = branch audit (worktree lahir dari sana,
6311
6689
  // resolveCommit + fallback origin/<rev>), fromAudit = id spec audit (isi dokumennya disematkan ke
@@ -6315,13 +6693,14 @@ var init_dto = __esm({
6315
6693
  flow: external_exports.literal("prd"),
6316
6694
  brief: zPrdBrief,
6317
6695
  branchFrom: external_exports.string().min(1).optional(),
6318
- fromAudit: external_exports.string().min(1).optional()
6696
+ fromAudit: external_exports.string().min(1).optional(),
6697
+ force: external_exports.boolean().optional()
6319
6698
  }),
6320
6699
  // SPEC-273 · sesi breakdown project-level: pecah SATU PRD (prdPath) → manifest N backlog.
6321
- external_exports.object({ project: external_exports.string(), flow: external_exports.literal("breakdown"), prdPath: external_exports.string().min(1) }),
6700
+ external_exports.object({ project: external_exports.string(), flow: external_exports.literal("breakdown"), prdPath: external_exports.string().min(1), force: external_exports.boolean().optional() }),
6322
6701
  // SPEC-222 · scaffold: sesi project-level from-scratch, menyusun SoT dari ide. Tanpa brief
6323
6702
  // (diseed dari Project.desc), tanpa Spec — cermin reverse.
6324
- external_exports.object({ project: external_exports.string(), flow: external_exports.literal("scaffold") }),
6703
+ external_exports.object({ project: external_exports.string(), flow: external_exports.literal("scaffold"), force: external_exports.boolean().optional() }),
6325
6704
  // SPEC-517 · terminal agen biasa: agen (claude|codex) + model + effort boleh dipilih PER SESI,
6326
6705
  // seperti picker Start backlog (ADR-0061/0074). Kosong → default global (Setting).
6327
6706
  // `flow: z.undefined()` BUKAN hiasan: varian ini permisif dan diletakkan SESUDAH semua varian
@@ -11837,6 +12216,7 @@ var init_src = __esm({
11837
12216
  init_portal_chat();
11838
12217
  init_prd_status();
11839
12218
  init_session_kind();
12219
+ init_session_admission2();
11840
12220
  init_session_end();
11841
12221
  init_config();
11842
12222
  init_config_registry();
@@ -12732,14 +13112,20 @@ var init_agent_cli = __esm({
12732
13112
  });
12733
13113
 
12734
13114
  // ../runner/src/custom-agents.ts
12735
- function agentPromptOf(def2, roster) {
13115
+ function agentPromptOf(def2, roster, runtime = "claude") {
12736
13116
  const can = liveMentions(def2, roster);
12737
- const instructions = def2.timeoutSeconds ? `${def2.instructions}
12738
-
12739
- Batas waktu Hanoman untuk pekerjaan ini ${def2.timeoutSeconds} detik. Prioritaskan putusan dan bukti sebelum batas itu.` : def2.instructions;
13117
+ const contract = [
13118
+ def2.instructions,
13119
+ "",
13120
+ "---",
13121
+ ...policyClause(def2),
13122
+ ...workLimitClause(def2, runtime),
13123
+ "",
13124
+ ...handoffClause()
13125
+ ];
12740
13126
  if (can.length === 0) {
12741
13127
  return [
12742
- instructions,
13128
+ ...contract,
12743
13129
  "",
12744
13130
  "---",
12745
13131
  "Kamu TIDAK boleh mendelegasikan ke agen lain. Selesaikan sendiri lalu laporkan hasilnya.",
@@ -12749,7 +13135,7 @@ Batas waktu Hanoman untuk pekerjaan ini ${def2.timeoutSeconds} detik. Prioritask
12749
13135
  }
12750
13136
  const list2 = can.map((m) => `@${m}`).join(", ");
12751
13137
  return [
12752
- instructions,
13138
+ ...contract,
12753
13139
  "",
12754
13140
  "---",
12755
13141
  `Kamu boleh mendelegasikan HANYA ke: ${list2}. Panggil lewat ${MENTION_TOOL} dengan nama agennya.`,
@@ -12767,7 +13153,7 @@ function renderAgentsJson(defs, options2 = {}) {
12767
13153
  const readOnly = d.workspacePolicy === "read-only";
12768
13154
  out4[d.name] = {
12769
13155
  description: d.description,
12770
- prompt: agentPromptOf(d, defs),
13156
+ prompt: agentPromptOf(d, defs, "claude") + (options2.promptSuffix ?? ""),
12771
13157
  tools: readOnly ? resolvedTools.filter((tool) => READ_ONLY_TOOLS.has(tool)) : resolvedTools,
12772
13158
  ...d.model ? { model: d.model } : {},
12773
13159
  ...d.effort ? { effort: d.effort } : {},
@@ -12793,22 +13179,12 @@ function agentDelegationClause(defs, runtime = "claude") {
12793
13179
  if (defs.length === 0) return "";
12794
13180
  return [
12795
13181
  "",
12796
- "## Subagent yang tersedia",
12797
13182
  "",
12798
- "Sesi ini punya subagent berikut. Delegasikan saat tugasnya cocok \u2014 konteks mereka TERPISAH",
12799
- "dari milikmu, jadi menyerahkan penyapuan & verifikasi ke mereka MENGHEMAT konteksmu sendiri,",
12800
- "bukan memboroskannya.",
12801
- "",
12802
- ...defs.map((d) => `- **${d.name}** \u2014 ${d.description}`),
12803
- "",
12804
- runtime === "codex" ? "Panggil target bernama persis lewat `spawn_agent`." : `Panggil lewat tool ${MENTION_TOOL} dengan nama agennya.`,
12805
- "Mereka tak bisa mendelegasikan lagi,",
12806
- "jadi tak ada rantai panggilan yang perlu kamu jaga. Laporan mereka adalah MASUKAN \u2014 kamu yang",
12807
- "memutuskan, dan kamu yang bertanggung jawab atas hasilnya.",
13183
+ `Delegasikan tugas yang relevan melalui ${runtime === "codex" ? "spawn_agent" : MENTION_TOOL}. Sertakan tujuan, scope, base SHA, kandidat termasuk dirty changes, bukti sebelumnya, dan aturan verifikasi. Tinjau hasil subagent sebelum digunakan.`,
12808
13184
  ""
12809
13185
  ].join("\n");
12810
13186
  }
12811
- var liveMentions, READ_ONLY_TOOLS;
13187
+ var liveMentions, policyClause, workLimitClause, handoffClause, READ_ONLY_TOOLS;
12812
13188
  var init_custom_agents = __esm({
12813
13189
  "../runner/src/custom-agents.ts"() {
12814
13190
  "use strict";
@@ -12819,154 +13195,56 @@ var init_custom_agents = __esm({
12819
13195
  const names = new Set(roster.map((r) => r.name));
12820
13196
  return def2.mentions.filter((m) => names.has(m) && m !== def2.name);
12821
13197
  };
12822
- READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "Bash", "WebFetch", "WebSearch"]);
12823
- }
12824
- });
12825
-
12826
- // ../runner/src/agent-readonly.ts
12827
- import { chmodSync, writeFileSync } from "node:fs";
12828
- import { join } from "node:path";
12829
- function denyReadOnly(detail) {
12830
- return { allowed: false, reason: `Hanoman read-only policy: ${detail}` };
12831
- }
12832
- function tokenizeReadOnlyCommand(command) {
12833
- const tokens = [];
12834
- let token = "";
12835
- let quote3 = "";
12836
- let escaped = false;
12837
- let active = false;
12838
- for (let index = 0; index < command.length; index++) {
12839
- const char = command[index];
12840
- if (escaped) {
12841
- token += char;
12842
- escaped = false;
12843
- active = true;
12844
- continue;
12845
- }
12846
- if (quote3) {
12847
- if (char === quote3) quote3 = "";
12848
- else if (char === "\\" && quote3 === '"') escaped = true;
12849
- else token += char;
12850
- active = true;
12851
- continue;
12852
- }
12853
- if (char === "'" || char === '"') {
12854
- quote3 = char;
12855
- active = true;
12856
- } else if (char === "\\") {
12857
- escaped = true;
12858
- active = true;
12859
- } else if (/\s/.test(char)) {
12860
- if (active) {
12861
- tokens.push(token);
12862
- token = "";
12863
- active = false;
13198
+ policyClause = (def2) => {
13199
+ const policy = def2.workspacePolicy ?? "inherit";
13200
+ if (policy === "read-only") {
13201
+ const root = def2.name === "root-causer" ? [
13202
+ "Untuk root-causer, lakukan diagnosis statis dari bukti yang sudah tersedia. Labeli hipotesis",
13203
+ "yang belum terbukti dan berikan rencana eksperimen untuk parent; jangan menjalankan reproduksi",
13204
+ "yang memerlukan eksekusi atau mutasi di workspace ini."
13205
+ ] : [];
13206
+ return [
13207
+ "Policy efektif: read-only. Inspeksi statis saja; jangan mengubah workspace atau menjalankan",
13208
+ "operasi yang ditolak validator read-only.",
13209
+ ...root,
13210
+ "Jangan mengklaim eksperimen telah dijalankan tanpa output yang benar-benar kamu terima."
13211
+ ];
12864
13212
  }
12865
- } else {
12866
- token += char;
12867
- active = true;
12868
- }
12869
- }
12870
- if (quote3 || escaped) return null;
12871
- if (active) tokens.push(token);
12872
- return tokens;
12873
- }
12874
- function evaluateReadOnlyPayload(payload, policy, environment) {
12875
- if (!payload || typeof payload !== "object") return denyReadOnly("payload hook tidak sah");
12876
- const event = payload;
12877
- const tool = typeof event.tool_name === "string" ? event.tool_name : typeof event.toolName === "string" ? event.toolName : "";
12878
- if (!tool) return denyReadOnly("nama tool tidak tersedia");
12879
- if (policy.directTools.includes(tool)) return { allowed: true };
12880
- if (policy.deniedTools.includes(tool) || tool.startsWith("mcp__")) {
12881
- return denyReadOnly(`tool ${tool} dapat mengubah state`);
12882
- }
12883
- if (!policy.shellTools.includes(tool)) return denyReadOnly(`tool ${tool} tidak terbukti read-only`);
12884
- const rawInput = event.tool_input;
12885
- const input = rawInput && typeof rawInput === "object" ? rawInput : {};
12886
- const command = typeof input.command === "string" ? input.command : typeof input.cmd === "string" ? input.cmd : "";
12887
- const trimmed = command.trim();
12888
- if (!trimmed) return denyReadOnly("perintah shell kosong atau tidak dikenal");
12889
- if (/\r|\n|;|&&|\|\||\||[<>]|\$|`/.test(trimmed)) {
12890
- return denyReadOnly("operator shell yang dapat merangkai atau menulis dilarang");
12891
- }
12892
- const tokens = tokenizeReadOnlyCommand(trimmed);
12893
- if (!tokens?.length) return denyReadOnly("perintah shell tidak dapat diparse dengan aman");
12894
- const first = tokens[0] ?? "";
12895
- const commandName = first.split("/").pop() ?? "";
12896
- if (first !== commandName) {
12897
- return denyReadOnly("executable ber-path tidak diizinkan; gunakan command allowlist dari PATH");
12898
- }
12899
- if (commandName === "git") {
12900
- const subcommand = tokens[1] ?? "";
12901
- if (!policy.gitCommands.includes(subcommand)) {
12902
- return denyReadOnly(`git ${subcommand || "<kosong>"} bukan operasi baca yang diizinkan`);
12903
- }
12904
- const args = tokens.slice(2);
12905
- if (args.some((arg) => arg === "--output" || arg.startsWith("--output=") || arg === "--ext-diff" || arg === "--textconv")) {
12906
- return denyReadOnly("opsi git dapat menulis atau menjalankan helper eksternal");
12907
- }
12908
- if (subcommand !== "status" && (!args.includes("--no-ext-diff") || !args.includes("--no-textconv"))) {
12909
- return denyReadOnly("git diff/show/log wajib menonaktifkan helper eksternal dan textconv");
12910
- }
12911
- return { allowed: true };
12912
- }
12913
- if (!policy.shellCommands.includes(commandName)) {
12914
- return denyReadOnly(`perintah ${commandName || "<kosong>"} tidak terbukti read-only`);
12915
- }
12916
- if (commandName === "rg" && environment.RIPGREP_CONFIG_PATH?.trim()) {
12917
- return denyReadOnly("RIPGREP_CONFIG_PATH dapat menyuntikkan preprocessor eksternal");
12918
- }
12919
- if (commandName === "rg" && tokens.slice(1).some((arg) => arg === "--pre" || arg.startsWith("--pre=") || arg === "--pre-glob" || arg.startsWith("--pre-glob=") || arg === "--hostname-bin" || arg.startsWith("--hostname-bin="))) {
12920
- return denyReadOnly("opsi rg dapat menjalankan helper eksternal");
12921
- }
12922
- if (commandName === "sed") {
12923
- const quiet = tokens[1] === "-n" || tokens[1] === "--quiet" || tokens[1] === "--silent";
12924
- const printOnly = /^\d+(?:,\d+)?p$/.test(tokens[2] ?? "");
12925
- const files = tokens.slice(3);
12926
- if (!quiet || !printOnly || files.length === 0 || files.some((arg) => arg.startsWith("-"))) {
12927
- return denyReadOnly("hanya sed -n '<baris>[,<baris>]p' <berkas> yang diizinkan");
12928
- }
12929
- }
12930
- return { allowed: true };
12931
- }
12932
- function readOnlyHookSource() {
12933
- return [
12934
- '"use strict";',
12935
- `const denyReadOnly = ${denyReadOnly.toString()};`,
12936
- `const tokenizeReadOnlyCommand = ${tokenizeReadOnlyCommand.toString()};`,
12937
- `const evaluate = ${evaluateReadOnlyPayload.toString()};`,
12938
- `const policy = ${JSON.stringify(POLICY)};`,
12939
- "let input = '';",
12940
- "process.stdin.setEncoding('utf8');",
12941
- "process.stdin.on('data', chunk => { input += chunk; });",
12942
- "process.stdin.on('end', () => {",
12943
- " let payload;",
12944
- " try { payload = JSON.parse(input); } catch { payload = null; }",
12945
- " const decision = evaluate(payload, policy, process.env);",
12946
- " if (!decision.allowed) { process.stderr.write(decision.reason + '\\n'); process.exitCode = 2; }",
12947
- "});",
12948
- "process.stdin.resume();",
12949
- ""
12950
- ].join("\n");
12951
- }
12952
- function writeReadOnlyHook(dir2) {
12953
- const path = join(dir2, "custom-agent-readonly.cjs");
12954
- writeFileSync(path, readOnlyHookSource(), { mode: 384 });
12955
- chmodSync(path, 384);
12956
- return { path, command: `node ${shellQuote(path)}` };
12957
- }
12958
- var POLICY, shellQuote;
12959
- var init_agent_readonly = __esm({
12960
- "../runner/src/agent-readonly.ts"() {
12961
- "use strict";
12962
- POLICY = {
12963
- directTools: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
12964
- shellTools: ["Bash", "local_shell", "exec_command"],
12965
- deniedTools: ["Write", "Edit", "Task", "apply_patch", "spawn_agent"],
12966
- shellCommands: ["rg", "sed", "head", "tail", "wc", "ls"],
12967
- gitCommands: ["diff", "show", "status", "log"]
13213
+ if (policy === "isolated-worktree") {
13214
+ const root = def2.name === "root-causer" ? ["Untuk root-causer, kamu boleh mereproduksi dan menjalankan eksperimen hanya di worktree terisolasi ini."] : [];
13215
+ return [
13216
+ "Policy efektif: isolated-worktree. Semua tulisan, test patch, reproduksi, dan eksperimen",
13217
+ "harus tetap di worktree terisolasi yang diberikan; jangan menyentuh worktree parent.",
13218
+ ...root,
13219
+ "Jangan mengklaim eksperimen telah dijalankan tanpa output yang benar-benar kamu terima."
13220
+ ];
13221
+ }
13222
+ return [
13223
+ "Policy efektif: inherit. Ikuti izin workspace sesi parent dan jangan memperluas scope sendiri.",
13224
+ "Jangan mengklaim eksperimen telah dijalankan tanpa output yang benar-benar kamu terima."
13225
+ ];
13226
+ };
13227
+ workLimitClause = (def2, runtime) => {
13228
+ const lines2 = [];
13229
+ if (typeof def2.maxTurns === "number") {
13230
+ lines2.push(runtime === "claude" ? `Batas awal pekerjaan ${def2.maxTurns} turn. Renderer juga mengirim maxTurns native ke Claude; ini batas turn, bukan hard kill wall-clock.` : `Batas awal pekerjaan ${def2.maxTurns} turn adalah batas instruksional di Codex; bukan hard kill.`);
13231
+ }
13232
+ if (typeof def2.timeoutSeconds === "number") {
13233
+ lines2.push(
13234
+ `Batas waktu ${def2.timeoutSeconds} detik adalah batas instruksional. Prioritaskan putusan dan bukti; ini bukan jaminan hard kill server.`
13235
+ );
13236
+ }
13237
+ return lines2;
12968
13238
  };
12969
- shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
13239
+ handoffClause = () => [
13240
+ "Kontrak serah-terima:",
13241
+ "- Masukan yang harus kamu gunakan: tujuan, scope, base SHA, kandidat yang diperiksa termasuk",
13242
+ " dirty changes, bukti sebelumnya, dan aturan verifikasi. Bila ada yang hilang, nyatakan batasnya.",
13243
+ "- Awali laporan dengan `Status: selesai | sebagian | terhalang`.",
13244
+ "- Laporkan simpulan, jangkar bukti, tingkat keyakinan, scope yang belum diperiksa, dan langkah",
13245
+ " berikutnya. Batas laporan: maksimal 12 temuan utama dan maksimal 1200 kata."
13246
+ ];
13247
+ READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "Bash", "WebFetch", "WebSearch"]);
12970
13248
  }
12971
13249
  });
12972
13250
 
@@ -12988,8 +13266,8 @@ var init_runtime_profile = __esm({
12988
13266
  });
12989
13267
 
12990
13268
  // ../runner/src/codex-agent-config.ts
12991
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync2 } from "node:fs";
12992
- import { join as join2 } from "node:path";
13269
+ import { chmodSync, writeFileSync } from "node:fs";
13270
+ import { join } from "node:path";
12993
13271
  function codexNativeAgentsSupported(version) {
12994
13272
  const parsed = version ? /(\d+)\.(\d+)\.(\d+)/.exec(version)?.[0] : null;
12995
13273
  return parsed ? cmpVersion(parsed, CODEX_NATIVE_AGENTS_MIN_CLIENT) >= 0 : false;
@@ -13010,7 +13288,7 @@ function codexNativeVersionProbe(env, codexBin3 = env.HANOMAN_CODEX_BIN ?? "code
13010
13288
  env.HANOMAN_SESSION_IMAGE ?? "hanoman-agent:latest",
13011
13289
  "/bin/sh",
13012
13290
  "-lc",
13013
- `${shellQuote2(codexBin3)} --version`
13291
+ `${shellQuote(codexBin3)} --version`
13014
13292
  ]
13015
13293
  };
13016
13294
  }
@@ -13018,7 +13296,7 @@ function renderCodexAgentToml(def2, roster, options2 = {}) {
13018
13296
  const lines2 = [
13019
13297
  `name = ${tomlString(def2.name)}`,
13020
13298
  `description = ${tomlString(def2.description)}`,
13021
- `developer_instructions = ${tomlString(agentPromptOf(def2, roster))}`,
13299
+ `developer_instructions = ${tomlString(agentPromptOf(def2, roster, "codex") + (options2.promptSuffix ?? ""))}`,
13022
13300
  ...def2.model ? [`model = ${tomlString(def2.model)}`] : [],
13023
13301
  ...def2.effort ? [`model_reasoning_effort = ${tomlString(def2.effort)}`] : [],
13024
13302
  ...def2.workspacePolicy === "read-only" ? ['sandbox_mode = "read-only"'] : []
@@ -13054,8 +13332,8 @@ function materializeCodexAgents(defs, tempDir, options2 = {}) {
13054
13332
  }))
13055
13333
  };
13056
13334
  }
13057
- const write2 = options2.writeFile ?? ((path, content) => writeFileSync2(path, content, { mode: 384 }));
13058
- const chmod3 = options2.chmod ?? chmodSync2;
13335
+ const write2 = options2.writeFile ?? ((path, content) => writeFileSync(path, content, { mode: 384 }));
13336
+ const chmod3 = options2.chmod ?? chmodSync;
13059
13337
  const successful = [];
13060
13338
  const warnings = [];
13061
13339
  for (const [index, def2] of defs.entries()) {
@@ -13066,7 +13344,7 @@ function materializeCodexAgents(defs, tempDir, options2 = {}) {
13066
13344
  });
13067
13345
  continue;
13068
13346
  }
13069
- const path = join2(tempDir, `${String(index).padStart(2, "0")}-${safeFilename(def2.name)}.toml`);
13347
+ const path = join(tempDir, `${String(index).padStart(2, "0")}-${safeFilename(def2.name)}.toml`);
13070
13348
  try {
13071
13349
  write2(path, renderCodexAgentToml(def2, defs, options2));
13072
13350
  chmod3(path, 384);
@@ -13101,7 +13379,7 @@ function materializeCodexAgents(defs, tempDir, options2 = {}) {
13101
13379
  liveDefs
13102
13380
  };
13103
13381
  }
13104
- var CODEX_NATIVE_AGENTS_MIN_CLIENT, shellQuote2, tomlString, tomlKey, safeFilename;
13382
+ var CODEX_NATIVE_AGENTS_MIN_CLIENT, shellQuote, tomlString, tomlKey, safeFilename;
13105
13383
  var init_codex_agent_config = __esm({
13106
13384
  "../runner/src/codex-agent-config.ts"() {
13107
13385
  "use strict";
@@ -13109,13 +13387,193 @@ var init_codex_agent_config = __esm({
13109
13387
  init_custom_agents();
13110
13388
  init_runtime_profile();
13111
13389
  CODEX_NATIVE_AGENTS_MIN_CLIENT = "0.151.0";
13112
- shellQuote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`;
13390
+ shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
13113
13391
  tomlString = (value) => JSON.stringify(value);
13114
13392
  tomlKey = (value) => JSON.stringify(value);
13115
13393
  safeFilename = (name2) => name2.replace(/[^a-z0-9-]/gi, "-");
13116
13394
  }
13117
13395
  });
13118
13396
 
13397
+ // ../runner/src/agent-readonly.ts
13398
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync2 } from "node:fs";
13399
+ import { join as join2 } from "node:path";
13400
+ function denyReadOnly(detail) {
13401
+ return { allowed: false, reason: `Hanoman read-only policy: ${detail}` };
13402
+ }
13403
+ function tokenizeReadOnlyCommand(command) {
13404
+ const tokens = [];
13405
+ let token = "";
13406
+ let quote3 = "";
13407
+ let escaped = false;
13408
+ let active = false;
13409
+ for (let index = 0; index < command.length; index++) {
13410
+ const char = command[index];
13411
+ if (escaped) {
13412
+ token += char;
13413
+ escaped = false;
13414
+ active = true;
13415
+ continue;
13416
+ }
13417
+ if (quote3) {
13418
+ if (char === quote3) quote3 = "";
13419
+ else if (char === "\\" && quote3 === '"') escaped = true;
13420
+ else token += char;
13421
+ active = true;
13422
+ continue;
13423
+ }
13424
+ if (char === "'" || char === '"') {
13425
+ quote3 = char;
13426
+ active = true;
13427
+ } else if (char === "\\") {
13428
+ escaped = true;
13429
+ active = true;
13430
+ } else if (/\s/.test(char)) {
13431
+ if (active) {
13432
+ tokens.push(token);
13433
+ token = "";
13434
+ active = false;
13435
+ }
13436
+ } else {
13437
+ token += char;
13438
+ active = true;
13439
+ }
13440
+ }
13441
+ if (quote3 || escaped) return null;
13442
+ if (active) tokens.push(token);
13443
+ return tokens;
13444
+ }
13445
+ function evaluateReadOnlyPayload(payload, policy, environment) {
13446
+ if (!payload || typeof payload !== "object") return denyReadOnly("payload hook tidak sah");
13447
+ const event = payload;
13448
+ const tool = typeof event.tool_name === "string" ? event.tool_name : typeof event.toolName === "string" ? event.toolName : "";
13449
+ if (!tool) return denyReadOnly("nama tool tidak tersedia");
13450
+ if (policy.directTools.includes(tool)) return { allowed: true };
13451
+ if (policy.deniedTools.includes(tool) || tool.startsWith("mcp__")) {
13452
+ return denyReadOnly(`tool ${tool} dapat mengubah state`);
13453
+ }
13454
+ if (!policy.shellTools.includes(tool)) return denyReadOnly(`tool ${tool} tidak terbukti read-only`);
13455
+ const rawInput = event.tool_input;
13456
+ const input = rawInput && typeof rawInput === "object" ? rawInput : {};
13457
+ const command = typeof input.command === "string" ? input.command : typeof input.cmd === "string" ? input.cmd : "";
13458
+ const trimmed = command.trim();
13459
+ if (!trimmed) return denyReadOnly("perintah shell kosong atau tidak dikenal");
13460
+ if (/\r|\n|;|&&|\|\||\||[<>]|\$|`/.test(trimmed)) {
13461
+ return denyReadOnly("operator shell yang dapat merangkai atau menulis dilarang");
13462
+ }
13463
+ const tokens = tokenizeReadOnlyCommand(trimmed);
13464
+ if (!tokens?.length) return denyReadOnly("perintah shell tidak dapat diparse dengan aman");
13465
+ const first = tokens[0] ?? "";
13466
+ const commandName = first.split("/").pop() ?? "";
13467
+ if (first !== commandName) {
13468
+ return denyReadOnly("executable ber-path tidak diizinkan; gunakan command allowlist dari PATH");
13469
+ }
13470
+ if (commandName === "git") {
13471
+ const subcommand = tokens[1] ?? "";
13472
+ if (!policy.gitCommands.includes(subcommand)) {
13473
+ return denyReadOnly(`git ${subcommand || "<kosong>"} bukan operasi baca yang diizinkan`);
13474
+ }
13475
+ const args = tokens.slice(2);
13476
+ if (args.some((arg) => arg === "--output" || arg.startsWith("--output=") || arg === "--ext-diff" || arg === "--textconv")) {
13477
+ return denyReadOnly("opsi git dapat menulis atau menjalankan helper eksternal");
13478
+ }
13479
+ if (subcommand !== "status" && (!args.includes("--no-ext-diff") || !args.includes("--no-textconv"))) {
13480
+ return denyReadOnly("git diff/show/log wajib menonaktifkan helper eksternal dan textconv");
13481
+ }
13482
+ return { allowed: true };
13483
+ }
13484
+ if (!policy.shellCommands.includes(commandName)) {
13485
+ return denyReadOnly(`perintah ${commandName || "<kosong>"} tidak terbukti read-only`);
13486
+ }
13487
+ if (commandName === "rg" && environment.RIPGREP_CONFIG_PATH?.trim()) {
13488
+ return denyReadOnly("RIPGREP_CONFIG_PATH dapat menyuntikkan preprocessor eksternal");
13489
+ }
13490
+ if (commandName === "rg" && tokens.slice(1).some((arg) => arg === "--pre" || arg.startsWith("--pre=") || arg === "--pre-glob" || arg.startsWith("--pre-glob=") || arg === "--hostname-bin" || arg.startsWith("--hostname-bin="))) {
13491
+ return denyReadOnly("opsi rg dapat menjalankan helper eksternal");
13492
+ }
13493
+ if (commandName === "sed") {
13494
+ const quiet = tokens[1] === "-n" || tokens[1] === "--quiet" || tokens[1] === "--silent";
13495
+ const printOnly = /^\d+(?:,\d+)?p$/.test(tokens[2] ?? "");
13496
+ const files = tokens.slice(3);
13497
+ if (!quiet || !printOnly || files.length === 0 || files.some((arg) => arg.startsWith("-"))) {
13498
+ return denyReadOnly("hanya sed -n '<baris>[,<baris>]p' <berkas> yang diizinkan");
13499
+ }
13500
+ }
13501
+ return { allowed: true };
13502
+ }
13503
+ function readOnlyHookSource() {
13504
+ return [
13505
+ '"use strict";',
13506
+ `const denyReadOnly = ${denyReadOnly.toString()};`,
13507
+ `const tokenizeReadOnlyCommand = ${tokenizeReadOnlyCommand.toString()};`,
13508
+ `const evaluate = ${evaluateReadOnlyPayload.toString()};`,
13509
+ `const policy = ${JSON.stringify(POLICY)};`,
13510
+ "let input = '';",
13511
+ "process.stdin.setEncoding('utf8');",
13512
+ "process.stdin.on('data', chunk => { input += chunk; });",
13513
+ "process.stdin.on('end', () => {",
13514
+ " let payload;",
13515
+ " try { payload = JSON.parse(input); } catch { payload = null; }",
13516
+ " const decision = evaluate(payload, policy, process.env);",
13517
+ " if (!decision.allowed) { process.stderr.write(decision.reason + '\\n'); process.exitCode = 2; }",
13518
+ "});",
13519
+ "process.stdin.resume();",
13520
+ ""
13521
+ ].join("\n");
13522
+ }
13523
+ function writeReadOnlyHook(dir2) {
13524
+ const path = join2(dir2, "custom-agent-readonly.cjs");
13525
+ writeFileSync2(path, readOnlyHookSource(), { mode: 384 });
13526
+ chmodSync2(path, 384);
13527
+ return { path, command: `node ${shellQuote2(path)}` };
13528
+ }
13529
+ var POLICY, shellQuote2;
13530
+ var init_agent_readonly = __esm({
13531
+ "../runner/src/agent-readonly.ts"() {
13532
+ "use strict";
13533
+ POLICY = {
13534
+ directTools: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
13535
+ shellTools: ["Bash", "local_shell", "exec_command"],
13536
+ deniedTools: ["Write", "Edit", "Task", "apply_patch", "spawn_agent"],
13537
+ shellCommands: ["rg", "sed", "head", "tail", "wc", "ls"],
13538
+ gitCommands: ["diff", "show", "status", "log"]
13539
+ };
13540
+ shellQuote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`;
13541
+ }
13542
+ });
13543
+
13544
+ // ../runner/src/agent-definition.ts
13545
+ import { createHash } from "node:crypto";
13546
+ function agentDefinitionHash(def2, roster, runtime, inherited = {}) {
13547
+ const options2 = { readOnlyHookCommand: "<hanoman-read-only-hook>", promptSuffix: inherited.promptSuffix };
13548
+ const liveRoster = [def2, ...roster.filter((entry) => entry.name !== def2.name)];
13549
+ const native = runtime === "codex" ? renderCodexAgentToml(def2, liveRoster, options2) : JSON.parse(renderAgentsJson(liveRoster, options2))[def2.name];
13550
+ if (typeof native !== "string") native.tools.sort();
13551
+ return createHash("sha256").update(JSON.stringify({
13552
+ version: 1,
13553
+ runtime,
13554
+ native,
13555
+ activation: def2.activation ?? "always",
13556
+ model: def2.model ?? inherited.model ?? null,
13557
+ effort: def2.effort ?? inherited.effort ?? null,
13558
+ readOnlyPolicy: def2.workspacePolicy === "read-only" ? readOnlyHookSource() : null
13559
+ })).digest("hex");
13560
+ }
13561
+ var init_agent_definition = __esm({
13562
+ "../runner/src/agent-definition.ts"() {
13563
+ "use strict";
13564
+ init_custom_agents();
13565
+ init_codex_agent_config();
13566
+ init_agent_readonly();
13567
+ }
13568
+ });
13569
+
13570
+ // ../runner/src/custom-agent-eval-evidence.ts
13571
+ var init_custom_agent_eval_evidence = __esm({
13572
+ "../runner/src/custom-agent-eval-evidence.ts"() {
13573
+ "use strict";
13574
+ }
13575
+ });
13576
+
13119
13577
  // ../runner/src/custom-agent-eval.ts
13120
13578
  var init_custom_agent_eval = __esm({
13121
13579
  "../runner/src/custom-agent-eval.ts"() {
@@ -13125,6 +13583,9 @@ var init_custom_agent_eval = __esm({
13125
13583
  init_codex_settings();
13126
13584
  init_custom_agents();
13127
13585
  init_settings2();
13586
+ init_agent_readonly();
13587
+ init_agent_definition();
13588
+ init_custom_agent_eval_evidence();
13128
13589
  }
13129
13590
  });
13130
13591
 
@@ -13646,6 +14107,7 @@ var init_src2 = __esm({
13646
14107
  init_codex_settings();
13647
14108
  init_agent_cli();
13648
14109
  init_custom_agents();
14110
+ init_agent_definition();
13649
14111
  init_agent_readonly();
13650
14112
  init_codex_agent_config();
13651
14113
  init_custom_agent_eval();
@@ -14762,14 +15224,14 @@ var init_session_sandbox = __esm({
14762
15224
  });
14763
15225
 
14764
15226
  // src/services/session-event-spool.ts
14765
- import { tmpdir } from "node:os";
14766
15227
  import { join as join9 } from "node:path";
14767
15228
  var sessionEventSpoolRoot, sessionEventDir;
14768
15229
  var init_session_event_spool = __esm({
14769
15230
  "src/services/session-event-spool.ts"() {
14770
15231
  "use strict";
14771
- sessionEventSpoolRoot = () => join9(tmpdir(), "hanoman-session-events");
14772
- sessionEventDir = (sessionId2) => join9(sessionEventSpoolRoot(), sessionId2);
15232
+ init_src2();
15233
+ sessionEventSpoolRoot = (env = process.env) => join9(resolveHome(env), "session-events");
15234
+ sessionEventDir = (sessionId2, env = process.env) => join9(sessionEventSpoolRoot(env), sessionId2);
14773
15235
  }
14774
15236
  });
14775
15237
 
@@ -14780,7 +15242,7 @@ import { createRequire } from "node:module";
14780
15242
  import { randomUUID as randomUUID2 } from "node:crypto";
14781
15243
  import { chmodSync as chmodSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync5 } from "node:fs";
14782
15244
  import { dirname as dirname7 } from "node:path";
14783
- import { tmpdir as tmpdir2 } from "node:os";
15245
+ import { tmpdir } from "node:os";
14784
15246
  function noTtyPromptEnv() {
14785
15247
  const path = askpassDenyPath();
14786
15248
  mkdirSync5(dirname7(path), { recursive: true, mode: 448 });
@@ -14862,7 +15324,8 @@ function parsePanes(out4) {
14862
15324
  activity,
14863
15325
  eventHook,
14864
15326
  created,
14865
- agentRoster
15327
+ agentRoster,
15328
+ launchClass
14866
15329
  ] = line.split(" ");
14867
15330
  if (!n2?.startsWith(PREFIX)) return [];
14868
15331
  const exited = dead === "1";
@@ -14893,7 +15356,8 @@ function parsePanes(out4) {
14893
15356
  startedAt: Number(created) || 0,
14894
15357
  // SPEC-909 · ADR-0146 · sesi yang lahir sebelum pembaruan tak punya opsi ini → false.
14895
15358
  eventHook: eventHook === "1",
14896
- agentRoster: parseAgentRoster(agentRoster)
15359
+ agentRoster: parseAgentRoster(agentRoster),
15360
+ launchClass: launchClass === "agent" || launchClass === "terminal" ? launchClass : void 0
14897
15361
  }];
14898
15362
  });
14899
15363
  }
@@ -14909,6 +15373,7 @@ function parseAgentRoster(value) {
14909
15373
  name: row.name,
14910
15374
  ...typeof row.id === "string" ? { id: row.id } : {},
14911
15375
  ...typeof row.model === "string" ? { model: row.model } : {},
15376
+ ...typeof row.definitionHash === "string" && /^[a-f0-9]{64}$/.test(row.definitionHash) ? { definitionHash: row.definitionHash } : {},
14912
15377
  ...typeof row.timeoutSeconds === "number" ? { timeoutSeconds: row.timeoutSeconds } : {}
14913
15378
  }];
14914
15379
  });
@@ -14923,7 +15388,7 @@ function sessionEventEnv(sessionId2, env = process.env) {
14923
15388
  HANOMAN_SESSION_ID: sessionId2,
14924
15389
  HANOMAN_EVENT_URL: `http://127.0.0.1:${port2}/api/session-events`,
14925
15390
  HANOMAN_EVENT_TOKEN: sessionEventToken(sessionId2),
14926
- HANOMAN_EVENT_DIR: sessionEventDir(sessionId2),
15391
+ HANOMAN_EVENT_DIR: sessionEventDir(sessionId2, env),
14927
15392
  ...host2 ? { HANOMAN_EVENT_HOST: host2 } : {}
14928
15393
  };
14929
15394
  }
@@ -14957,7 +15422,7 @@ function sessionKind(o, projectId, cwd) {
14957
15422
  if (projectId.startsWith("telegram:")) return "telegram";
14958
15423
  if (projectId.startsWith("vps")) return "vps";
14959
15424
  if (o.command) return "shell";
14960
- if (cwd.includes("/.worktrees/")) return "worktree";
15425
+ if (/[\\/]\.worktrees[\\/]/.test(cwd)) return "worktree";
14961
15426
  return "terminal";
14962
15427
  }
14963
15428
  function captureTranscript(id2) {
@@ -15166,11 +15631,25 @@ function createSession(projectId, cwd, opts = {}) {
15166
15631
  if (opts.flow) tmux("set-option", "-t", name(id2), "@hanoman_flow", opts.flow);
15167
15632
  if (opts.branch) tmux("set-option", "-t", name(id2), "@hanoman_branch", opts.branch);
15168
15633
  tmux("set-option", "-t", name(id2), "@hanoman_agent", agent);
15634
+ const kind = sessionKind({ ...opts, id: id2 }, projectId, cwd);
15635
+ tmux(
15636
+ "set-option",
15637
+ "-t",
15638
+ name(id2),
15639
+ "@hanoman_launch_class",
15640
+ opts.command || kind === "terminal" ? "terminal" : "agent"
15641
+ );
15169
15642
  if (liveAgentDefs.length > 0) {
15643
+ const inherited = {
15644
+ model: opts.model,
15645
+ effort: agent === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort
15646
+ };
15170
15647
  const roster = liveAgentDefs.map((def2) => ({
15171
15648
  ...def2.id ? { id: def2.id } : {},
15172
15649
  name: def2.name,
15173
- ...def2.model ? { model: def2.model } : {},
15650
+ ...def2.model ?? inherited.model ? { model: def2.model ?? inherited.model } : {},
15651
+ // Native files were rendered with customDefs, even if another Codex file failed to write.
15652
+ definitionHash: agentDefinitionHash(def2, customDefs, agent, inherited),
15174
15653
  ...def2.timeoutSeconds ? { timeoutSeconds: def2.timeoutSeconds } : {}
15175
15654
  }));
15176
15655
  tmux("set-option", "-t", name(id2), "@hanoman_agent_roster", JSON.stringify(roster));
@@ -15185,7 +15664,7 @@ function createSession(projectId, cwd, opts = {}) {
15185
15664
  projectId,
15186
15665
  specId: opts.specId,
15187
15666
  flow: opts.flow,
15188
- kind: sessionKind({ id: id2, specId: opts.specId, flow: opts.flow, command: opts.command }, projectId, cwd),
15667
+ kind,
15189
15668
  agent,
15190
15669
  model: opts.model,
15191
15670
  effort: opts.effort,
@@ -15482,6 +15961,7 @@ var init_pty = __esm({
15482
15961
  init_session_event_token();
15483
15962
  init_session_sandbox();
15484
15963
  init_session_event_spool();
15964
+ init_src2();
15485
15965
  init_session_event_spool();
15486
15966
  socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
15487
15967
  PREFIX = "hanoman-";
@@ -15525,18 +16005,18 @@ var init_pty = __esm({
15525
16005
  rootBypassEnv = (uid = process.getuid?.()) => uid === 0 ? { IS_SANDBOX: "1" } : {};
15526
16006
  frame = (f) => JSON.stringify(f);
15527
16007
  name = (id2) => PREFIX + id2;
15528
- promptFilePath = (id2) => `${tmpdir2()}/hanoman-prompts/${id2}`;
15529
- goalGatePath = (id2) => `${tmpdir2()}/hanoman-goal-gates/${id2}.sh`;
15530
- goalStatePath = (id2) => `${tmpdir2()}/hanoman-goal-gates/${id2}.count`;
15531
- agentTempDir = (id2) => `${tmpdir2()}/hanoman-agents/${id2}`;
16008
+ promptFilePath = (id2) => `${tmpdir()}/hanoman-prompts/${id2}`;
16009
+ goalGatePath = (id2) => `${tmpdir()}/hanoman-goal-gates/${id2}.sh`;
16010
+ goalStatePath = (id2) => `${tmpdir()}/hanoman-goal-gates/${id2}.count`;
16011
+ agentTempDir = (id2) => `${tmpdir()}/hanoman-agents/${id2}`;
15532
16012
  agentsFilePath = (id2) => `${agentTempDir(id2)}/claude.json`;
15533
- askpassDenyPath = () => `${tmpdir2()}/hanoman-askpass/deny.sh`;
16013
+ askpassDenyPath = () => `${tmpdir()}/hanoman-askpass/deny.sh`;
15534
16014
  ASKPASS_DENY = `#!/bin/sh
15535
16015
  echo "hanoman: tak ada manusia di pane ini \u2014 permintaan ketikan ditolak: $1" >&2
15536
16016
  echo "hanoman: buka kuncinya di luar sesi (mis. ssh-add ~/.ssh/id_rsa), lalu ulangi." >&2
15537
16017
  exit 1
15538
16018
  `;
15539
- NO_SERVER = /no server running|error connecting to/i;
16019
+ NO_SERVER = /^(?:no server running on .+|error connecting to .+ \((?:No such file or directory|Connection refused)\))$/i;
15540
16020
  TmuxError = class extends Error {
15541
16021
  constructor(message, noServer) {
15542
16022
  super(message);
@@ -15563,7 +16043,8 @@ exit 1
15563
16043
  "#{window_activity}",
15564
16044
  "#{@hanoman_event_hook}",
15565
16045
  "#{session_created}",
15566
- "#{@hanoman_agent_roster}"
16046
+ "#{@hanoman_agent_roster}",
16047
+ "#{@hanoman_launch_class}"
15567
16048
  ].join(" ");
15568
16049
  toSessionInfo = ({
15569
16050
  id: id2,
@@ -16434,6 +16915,59 @@ var init_settings3 = __esm({
16434
16915
  }
16435
16916
  });
16436
16917
 
16918
+ // src/services/scheduler/config.ts
16919
+ async function getScheduler() {
16920
+ return (await getSetting()).scheduler;
16921
+ }
16922
+ async function setScheduler(next) {
16923
+ const cur = await getSetting();
16924
+ const data = { ...cur, scheduler: next };
16925
+ await prisma.setting.upsert({ where: { id: 1 }, update: { data }, create: { id: 1, data } });
16926
+ return next;
16927
+ }
16928
+ var init_config3 = __esm({
16929
+ "src/services/scheduler/config.ts"() {
16930
+ "use strict";
16931
+ init_db();
16932
+ init_settings3();
16933
+ }
16934
+ });
16935
+
16936
+ // src/services/session-launch-gate.ts
16937
+ import { cpus, loadavg, platform } from "node:os";
16938
+ async function createAgentSession(projectId, cwd, opts = {}) {
16939
+ const id2 = opts.id ?? (opts.specId ? sessionIdForSpec(opts.specId) : void 0);
16940
+ return withSessionAdmission(
16941
+ { id: id2 },
16942
+ async () => createSession(projectId, cwd, opts),
16943
+ (pane) => ({ ...pane, reused: true })
16944
+ );
16945
+ }
16946
+ async function createOperatorSession(projectId, cwd, opts = {}) {
16947
+ return withSessionAdmission(
16948
+ { id: opts.id, exempt: true },
16949
+ async () => createSession(projectId, cwd, opts),
16950
+ (pane) => pane
16951
+ );
16952
+ }
16953
+ var readHostLoad, gate, withSessionAdmission, currentLaunchStatus;
16954
+ var init_session_launch_gate = __esm({
16955
+ "src/services/session-launch-gate.ts"() {
16956
+ "use strict";
16957
+ init_pty();
16958
+ init_config3();
16959
+ init_session_admission();
16960
+ readHostLoad = () => ({ platform: platform(), loadAverage: loadavg()[0] ?? NaN, cores: cpus().length });
16961
+ gate = createLaunchGate({
16962
+ listPanes: () => listPanesAsync(),
16963
+ config: () => getScheduler(),
16964
+ host: () => readHostLoad()
16965
+ });
16966
+ withSessionAdmission = gate.run;
16967
+ currentLaunchStatus = (panes, config) => launchStatus(panes, config, readHostLoad());
16968
+ }
16969
+ });
16970
+
16437
16971
  // src/services/codex-trust.ts
16438
16972
  import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
16439
16973
  import { homedir as homedir3 } from "node:os";
@@ -17348,7 +17882,7 @@ var require_src = __commonJS({
17348
17882
  });
17349
17883
 
17350
17884
  // src/services/agent-token.ts
17351
- import { randomBytes as randomBytes2, createHash, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
17885
+ import { randomBytes as randomBytes2, createHash as createHash2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
17352
17886
  function toAgentTokenView(t) {
17353
17887
  return {
17354
17888
  id: t.id,
@@ -17414,7 +17948,7 @@ var init_agent_token = __esm({
17414
17948
  "src/services/agent-token.ts"() {
17415
17949
  "use strict";
17416
17950
  init_db();
17417
- hash = (token) => createHash("sha256").update(token).digest("hex");
17951
+ hash = (token) => createHash2("sha256").update(token).digest("hex");
17418
17952
  }
17419
17953
  });
17420
17954
 
@@ -17544,7 +18078,7 @@ function telegramReloadNeeded(before2, after) {
17544
18078
  const strip2 = ({ engine: _engine, ...rest }) => rest;
17545
18079
  return JSON.stringify(strip2(before2)) !== JSON.stringify(strip2(after));
17546
18080
  }
17547
- var init_config3 = __esm({
18081
+ var init_config4 = __esm({
17548
18082
  "src/services/telegram/config.ts"() {
17549
18083
  "use strict";
17550
18084
  init_src();
@@ -17554,7 +18088,7 @@ var init_config3 = __esm({
17554
18088
  });
17555
18089
 
17556
18090
  // src/services/telegram/protocol.ts
17557
- import { createHash as createHash2 } from "node:crypto";
18091
+ import { createHash as createHash3 } from "node:crypto";
17558
18092
  function normalizeTelegramCommand(text) {
17559
18093
  const clean2 = text.trim();
17560
18094
  const match = clean2.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
@@ -17621,7 +18155,7 @@ function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
17621
18155
  };
17622
18156
  }
17623
18157
  function inboundDigest(input) {
17624
- return createHash2("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
18158
+ return createHash3("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
17625
18159
  }
17626
18160
  function sanitizeTelegramOutput(text, exactSecrets = []) {
17627
18161
  let clean2 = text.replace(ANSI, "").replace(CONTROLS, "");
@@ -17788,7 +18322,7 @@ var init_typing = __esm({
17788
18322
  });
17789
18323
 
17790
18324
  // src/services/telegram/gateway.ts
17791
- import { createHash as createHash3 } from "node:crypto";
18325
+ import { createHash as createHash4 } from "node:crypto";
17792
18326
  var digestRaw, TelegramGateway;
17793
18327
  var init_gateway = __esm({
17794
18328
  "src/services/telegram/gateway.ts"() {
@@ -17797,7 +18331,7 @@ var init_gateway = __esm({
17797
18331
  init_protocol();
17798
18332
  init_runtime();
17799
18333
  init_typing();
17800
- digestRaw = (update2) => createHash3("sha256").update(JSON.stringify(update2)).digest("hex");
18334
+ digestRaw = (update2) => createHash4("sha256").update(JSON.stringify(update2)).digest("hex");
17801
18335
  TelegramGateway = class {
17802
18336
  constructor(deps) {
17803
18337
  this.deps = deps;
@@ -18104,14 +18638,14 @@ var init_engine_command = __esm({
18104
18638
  });
18105
18639
 
18106
18640
  // src/services/telegram/session.ts
18107
- import { createHash as createHash4 } from "node:crypto";
18641
+ import { createHash as createHash5 } from "node:crypto";
18108
18642
  var chatHash, telegramOperatorSessionId, formatTelegramTurn, TelegramSessionCoordinator;
18109
18643
  var init_session = __esm({
18110
18644
  "src/services/telegram/session.ts"() {
18111
18645
  "use strict";
18112
18646
  init_src2();
18113
18647
  init_engine_command();
18114
- chatHash = (chatId) => createHash4("sha256").update(chatId).digest("hex").slice(0, 16);
18648
+ chatHash = (chatId) => createHash5("sha256").update(chatId).digest("hex").slice(0, 16);
18115
18649
  telegramOperatorSessionId = (chatId) => `telegram-${chatHash(chatId)}`;
18116
18650
  formatTelegramTurn = (input) => `[Telegram update ${input.updateId} \xB7 chat ${input.chatId} \xB7 kind ${input.kind}]
18117
18651
  ${input.text}`;
@@ -18138,7 +18672,7 @@ ${input.text}`;
18138
18672
  }
18139
18673
  if (!context) throw new Error("gagal membuat binding chat Telegram");
18140
18674
  const sessionId2 = telegramOperatorSessionId(input.chatId);
18141
- const live = this.deps.port.getSession(sessionId2);
18675
+ const live = await this.deps.port.getSession(sessionId2);
18142
18676
  if (live && !live.exited) {
18143
18677
  if (!await this.deps.port.sendToPane(sessionId2, formatTelegramTurn(input))) {
18144
18678
  throw new Error("pane operator tidak menerima steer");
@@ -18160,7 +18694,7 @@ ${input.text}`;
18160
18694
  summary: context.summary,
18161
18695
  memories: context.memories
18162
18696
  });
18163
- const born = this.deps.port.createSession(projectId, cwd, {
18697
+ const born = await this.deps.port.createSession(projectId, cwd, {
18164
18698
  id: sessionId2,
18165
18699
  prompt,
18166
18700
  agent: engine.agent,
@@ -18173,8 +18707,11 @@ ${input.text}`;
18173
18707
  }
18174
18708
  });
18175
18709
  if (born.id !== sessionId2 || born.exited) throw new Error("pane operator gagal lahir");
18710
+ if (born.reused && !await this.deps.port.sendToPane(sessionId2, formatTelegramTurn(input))) {
18711
+ throw new Error("pane operator tidak menerima steer");
18712
+ }
18176
18713
  await this.deps.store.bindSession(input.chatId, sessionId2);
18177
- return { sessionId: sessionId2, created: true };
18714
+ return { sessionId: sessionId2, created: !born.reused };
18178
18715
  }
18179
18716
  /**
18180
18717
  * SPEC-492 · empat command runtime tak pernah menyentuh pane: ia soal transport, bukan isi
@@ -18191,7 +18728,7 @@ ${input.text}`;
18191
18728
  const cmd = parseEngineCommand(input.text, ctx);
18192
18729
  if (!cmd) return null;
18193
18730
  const sessionId2 = telegramOperatorSessionId(input.chatId);
18194
- const live = this.deps.port.getSession(sessionId2);
18731
+ const live = await this.deps.port.getSession(sessionId2);
18195
18732
  const alive = Boolean(live && !live.exited);
18196
18733
  let text;
18197
18734
  if (cmd.kind === "show") {
@@ -18515,7 +19052,7 @@ function parseTelegramAllowedUserIds(raw) {
18515
19052
  function telegramSessionDeps(input) {
18516
19053
  return {
18517
19054
  store: input.store,
18518
- port: { getSession, createSession, sendToPane, killSession },
19055
+ port: { getSession: getSessionAsync, createSession: createAgentSession, sendToPane, killSession },
18519
19056
  // SPEC-492 · BUKAN `sessionAgentDefaults`: sesi operator Telegram sebagian besar membaca API
18520
19057
  // lalu merangkum, bukan menulis kode, jadi ia boleh punya runtime/model/effort sendiri.
18521
19058
  defaults: telegramAgentDefaults,
@@ -18652,8 +19189,9 @@ var init_bootstrap = __esm({
18652
19189
  init_codex_trust();
18653
19190
  init_settings3();
18654
19191
  init_pty();
19192
+ init_session_launch_gate();
18655
19193
  init_client();
18656
- init_config3();
19194
+ init_config4();
18657
19195
  init_gateway();
18658
19196
  init_session();
18659
19197
  init_store();
@@ -21149,7 +21687,7 @@ var require_websocket = __commonJS({
21149
21687
  var http = __require("http");
21150
21688
  var net = __require("net");
21151
21689
  var tls = __require("tls");
21152
- var { randomBytes: randomBytes11, createHash: createHash12 } = __require("crypto");
21690
+ var { randomBytes: randomBytes11, createHash: createHash13 } = __require("crypto");
21153
21691
  var { Duplex, Readable } = __require("stream");
21154
21692
  var { URL: URL2 } = __require("url");
21155
21693
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -21817,7 +22355,7 @@ var require_websocket = __commonJS({
21817
22355
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
21818
22356
  return;
21819
22357
  }
21820
- const digest2 = createHash12("sha1").update(key + GUID).digest("base64");
22358
+ const digest2 = createHash13("sha1").update(key + GUID).digest("base64");
21821
22359
  if (res.headers["sec-websocket-accept"] !== digest2) {
21822
22360
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
21823
22361
  return;
@@ -22186,7 +22724,7 @@ var require_websocket_server = __commonJS({
22186
22724
  var EventEmitter = __require("events");
22187
22725
  var http = __require("http");
22188
22726
  var { Duplex } = __require("stream");
22189
- var { createHash: createHash12 } = __require("crypto");
22727
+ var { createHash: createHash13 } = __require("crypto");
22190
22728
  var extension2 = require_extension();
22191
22729
  var PerMessageDeflate2 = require_permessage_deflate();
22192
22730
  var subprotocol2 = require_subprotocol();
@@ -22493,7 +23031,7 @@ var require_websocket_server = __commonJS({
22493
23031
  );
22494
23032
  }
22495
23033
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
22496
- const digest2 = createHash12("sha1").update(key + GUID).digest("base64");
23034
+ const digest2 = createHash13("sha1").update(key + GUID).digest("base64");
22497
23035
  const headers = [
22498
23036
  "HTTP/1.1 101 Switching Protocols",
22499
23037
  "Upgrade: websocket",
@@ -23047,15 +23585,15 @@ var init_sync_client = __esm({
23047
23585
  // src/services/agent-tool-catalog.ts
23048
23586
  import { readFileSync as readFileSync17 } from "node:fs";
23049
23587
  import { homedir as homedir10 } from "node:os";
23050
- import { join as join31 } from "node:path";
23588
+ import { join as join32 } from "node:path";
23051
23589
  function mcpServerNames(repoDir) {
23052
23590
  const names = [];
23053
- const claudeJson = readJson2(join31(home(), ".claude.json"));
23591
+ const claudeJson = readJson2(join32(home(), ".claude.json"));
23054
23592
  names.push(...serversOf(claudeJson));
23055
23593
  if (repoDir) {
23056
23594
  const projects = claudeJson?.projects;
23057
23595
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
23058
- names.push(...serversOf(readJson2(join31(repoDir, ".mcp.json"))));
23596
+ names.push(...serversOf(readJson2(join32(repoDir, ".mcp.json"))));
23059
23597
  }
23060
23598
  names.push(...codexServers());
23061
23599
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -23084,7 +23622,7 @@ var init_agent_tool_catalog = __esm({
23084
23622
  codexServers = () => {
23085
23623
  let text;
23086
23624
  try {
23087
- text = readFileSync17(join31(home(), ".codex", "config.toml"), "utf8");
23625
+ text = readFileSync17(join32(home(), ".codex", "config.toml"), "utf8");
23088
23626
  } catch {
23089
23627
  return [];
23090
23628
  }
@@ -23100,7 +23638,7 @@ var init_agent_tool_catalog = __esm({
23100
23638
  });
23101
23639
 
23102
23640
  // src/services/builtin-agents.ts
23103
- import { createHash as createHash9 } from "node:crypto";
23641
+ import { createHash as createHash10 } from "node:crypto";
23104
23642
  async function seedBuiltinAgents() {
23105
23643
  try {
23106
23644
  const setting = await getSetting();
@@ -23190,7 +23728,7 @@ var init_builtin_agents2 = __esm({
23190
23728
  init_settings3();
23191
23729
  init_tombstone();
23192
23730
  init_sync_notify();
23193
- digest = (parts) => createHash9("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
23731
+ digest = (parts) => createHash10("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
23194
23732
  legacyFingerprint = (name2, description, instructions, tools) => digest([name2, description, instructions, [...tools].join(",")]);
23195
23733
  fingerprint = (a) => digest([
23196
23734
  a.name,
@@ -23224,37 +23762,13 @@ __export(custom_agents_exports, {
23224
23762
  unknownMentions: () => unknownMentions,
23225
23763
  validateGraph: () => validateGraph
23226
23764
  });
23227
- function smartBuiltinSelected(row, context) {
23228
- switch (row.name) {
23229
- case "scout":
23230
- return hasPhase(context, "Plan") || hasPhase(context, "Execute") || hasPhase(context, "Audit") || context.changedFiles.length === 0;
23231
- case "blast-radius":
23232
- return hasPhase(context, "Execute") || hasPhase(context, "Audit") || context.changedFiles.length > 0;
23233
- case "security-reviewer":
23234
- return (hasPhase(context, "Execute") || hasPhase(context, "Audit")) && touchesExternalInput(context);
23235
- case "spec-auditor":
23236
- return hasPhase(context, "Plan") || hasPhase(context, "Execute");
23237
- case "dep-auditor":
23238
- return touchesDependency(context.changedFiles);
23239
- case "root-causer":
23240
- return hasPhase(context, "Audit");
23241
- case "qa-verifier":
23242
- return context.runtime === "claude" && hasPhase(context, "Execute") && workspacePolicyOf(row.workspacePolicy) === "isolated-worktree" && touchesExecutableWork(context.changedFiles);
23243
- case "edge-case-hunter":
23244
- return context.runtime === "claude" && hasPhase(context, "Execute") && workspacePolicyOf(row.workspacePolicy) === "isolated-worktree";
23245
- default:
23246
- return true;
23247
- }
23248
- }
23249
23765
  function selectAgentRows(rows, context) {
23250
23766
  return rows.filter((row) => {
23251
23767
  if (!row.enabled) return false;
23252
23768
  const runtime = runtimeOf(row.runtime);
23253
23769
  if (runtime !== null && runtime !== context.runtime) return false;
23254
23770
  if (workspacePolicyOf(row.workspacePolicy) === "isolated-worktree" && context.runtime !== "claude") return false;
23255
- if (activationOf(row.activation) === "always") return true;
23256
- const builtin = row.projectId === null && BUILTIN_AGENT_NAMES.includes(row.name);
23257
- return builtin ? smartBuiltinSelected(row, context) : true;
23771
+ return true;
23258
23772
  });
23259
23773
  }
23260
23774
  function currentCustomAgentRuntimeSupport() {
@@ -23369,7 +23883,7 @@ async function installCustomAgents() {
23369
23883
  codexSupportRefreshTimer.unref();
23370
23884
  }
23371
23885
  }
23372
- var phasesOf, hasPhase, touchesDependency, touchesExternalInput, touchesExecutableWork, cache4, codexNativeSupport, codexSupportRefreshTimer, codexSupportRefreshTail, repoDirCache, asCustomAgent, defaultCodexSupportProbe;
23886
+ var cache4, codexNativeSupport, codexSupportRefreshTimer, codexSupportRefreshTail, repoDirCache, asCustomAgent, defaultCodexSupportProbe;
23373
23887
  var init_custom_agents2 = __esm({
23374
23888
  "src/services/custom-agents.ts"() {
23375
23889
  "use strict";
@@ -23380,14 +23894,6 @@ var init_custom_agents2 = __esm({
23380
23894
  init_agent_tool_catalog();
23381
23895
  init_builtin_agents2();
23382
23896
  init_codex_version();
23383
- phasesOf = (flow) => flow ? PIPELINES[flow] : [];
23384
- hasPhase = (context, name2) => phasesOf(context.flow).includes(name2);
23385
- touchesDependency = (files) => files.some((path) => /(^|\/)(?:package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|Cargo\.(?:toml|lock)|go\.(?:mod|sum)|requirements[^/]*\.txt|pyproject\.toml)$/i.test(path));
23386
- touchesExternalInput = (context) => {
23387
- const surface = [context.prompt ?? "", ...context.changedFiles].join("\n");
23388
- return /(?:^|[\W_/.-])(route|routes|handler|auth|oauth|api|cli|config|filesystem|upload|webhook|input)(?:$|[\W_/.-])/i.test(surface);
23389
- };
23390
- touchesExecutableWork = (files) => files.some((path) => !/(^|\/)(?:docs?|internal\/docs)\//i.test(path) && /(?:\.(?:[cm]?[jt]sx?|py|go|rs|java|rb|php)|(?:^|\/)test(?:s)?\/)/i.test(path));
23391
23897
  cache4 = [];
23392
23898
  codexNativeSupport = { version: null, ok: false };
23393
23899
  codexSupportRefreshTimer = null;
@@ -26575,7 +27081,7 @@ var require_multipart2 = __commonJS({
26575
27081
  parts = this.parts(options3);
26576
27082
  }
26577
27083
  this.savedRequestFiles = [];
26578
- const tmpdir8 = options3?.tmpdir || os.tmpdir();
27084
+ const tmpdir7 = options3?.tmpdir || os.tmpdir();
26579
27085
  this.tmpUploads = [];
26580
27086
  let i = 0;
26581
27087
  for await (const part of parts) {
@@ -26583,7 +27089,7 @@ var require_multipart2 = __commonJS({
26583
27089
  if (!part.file) {
26584
27090
  continue;
26585
27091
  }
26586
- const filepath = path.join(tmpdir8, generateId() + path.extname(part.filename || "file" + i++));
27092
+ const filepath = path.join(tmpdir7, generateId() + path.extname(part.filename || "file" + i++));
26587
27093
  const target2 = createWriteStream2(filepath);
26588
27094
  try {
26589
27095
  this.tmpUploads.push(filepath);
@@ -26691,12 +27197,13 @@ var require_multipart2 = __commonJS({
26691
27197
  });
26692
27198
 
26693
27199
  // src/app.ts
27200
+ init_session_admission();
26694
27201
  import Fastify from "fastify";
26695
27202
  import fastifyStatic from "@fastify/static";
26696
27203
  import websocket from "@fastify/websocket";
26697
27204
  import cookie from "@fastify/cookie";
26698
27205
  import { fileURLToPath as fileURLToPath5 } from "node:url";
26699
- import { dirname as dirname14 } from "node:path";
27206
+ import { dirname as dirname15 } from "node:path";
26700
27207
  import { existsSync as existsSync18 } from "node:fs";
26701
27208
 
26702
27209
  // src/web-dir.ts
@@ -27255,8 +27762,8 @@ async function projects_default(app2) {
27255
27762
  if (await prisma.project.findUnique({ where: { id: id2 } }))
27256
27763
  return reply.code(409).send({ error: `project "${id2}" sudah ada` });
27257
27764
  if (b.handledBy?.length) {
27258
- const gate = await checkHandledBy(b.handledBy);
27259
- if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
27765
+ const gate2 = await checkHandledBy(b.handledBy);
27766
+ if (!gate2.ok) return reply.code(gate2.code).send({ error: gate2.error });
27260
27767
  }
27261
27768
  if (b.kind === "from-scratch" && b.repoDir) {
27262
27769
  try {
@@ -27286,12 +27793,12 @@ async function projects_default(app2) {
27286
27793
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
27287
27794
  if (!await prisma.project.findUnique({ where: { id: id2 } })) return reply.code(404).send({ error: "not found" });
27288
27795
  if ("autoMerge" in parsed.data) {
27289
- const gate = await checkAutoMerge(await resolveRepoDir(id2), parsed.data.autoMerge);
27290
- if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
27796
+ const gate2 = await checkAutoMerge(await resolveRepoDir(id2), parsed.data.autoMerge);
27797
+ if (!gate2.ok) return reply.code(gate2.code).send({ error: gate2.error });
27291
27798
  }
27292
27799
  if (parsed.data.handledBy?.length) {
27293
- const gate = await checkHandledBy(parsed.data.handledBy);
27294
- if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
27800
+ const gate2 = await checkHandledBy(parsed.data.handledBy);
27801
+ if (!gate2.ok) return reply.code(gate2.code).send({ error: gate2.error });
27295
27802
  }
27296
27803
  const data = { ...parsed.data };
27297
27804
  if ("autoMerge" in data && data.autoMerge === null) data.autoMerge = Prisma.DbNull;
@@ -27552,6 +28059,7 @@ async function pullIntoCurrent(repoDir, source, opts = {}) {
27552
28059
 
27553
28060
  // src/routes/specs.ts
27554
28061
  init_pty();
28062
+ init_session_launch_gate();
27555
28063
  init_settings3();
27556
28064
  init_codex_trust();
27557
28065
  init_db();
@@ -27560,7 +28068,7 @@ init_db();
27560
28068
  import { execFile as execFile5 } from "node:child_process";
27561
28069
  import { promisify as promisify4 } from "node:util";
27562
28070
  import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
27563
- import { tmpdir as tmpdir3 } from "node:os";
28071
+ import { tmpdir as tmpdir2 } from "node:os";
27564
28072
  import { join as join12, resolve as resolve11 } from "node:path";
27565
28073
  var exec4 = promisify4(execFile5);
27566
28074
  var GIT3 = { maxBuffer: 1 << 24 };
@@ -27568,7 +28076,7 @@ var MAX = 256 * 1024;
27568
28076
  var worktreeDir = (repoDir, specId) => join12(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
27569
28077
  async function withTempIndex(wt, fn) {
27570
28078
  const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
27571
- const dir2 = await mkdtemp(join12(tmpdir3(), "hanoman-idx-"));
28079
+ const dir2 = await mkdtemp(join12(tmpdir2(), "hanoman-idx-"));
27572
28080
  const tmp = join12(dir2, "index");
27573
28081
  await copyFile(resolve11(wt, idx), tmp);
27574
28082
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
@@ -27846,13 +28354,15 @@ var prodReaperDeps = {
27846
28354
  var pending = /* @__PURE__ */ new Map();
27847
28355
  var sessionIdOf = (entry) => entry.split(".")[0] ?? entry;
27848
28356
  function releaseWorktree(repoDir, cwd, projectId, deps = prodReaperDeps) {
27849
- let path;
27850
28357
  try {
27851
- path = realGit.trashWorktree(repoDir, cwd);
28358
+ return releaseWorktreeToTrash(repoDir, cwd, projectId, deps);
27852
28359
  } catch {
27853
28360
  realGit.removeWorktree(repoDir, cwd);
27854
28361
  return null;
27855
28362
  }
28363
+ }
28364
+ function releaseWorktreeToTrash(repoDir, cwd, projectId, deps = prodReaperDeps) {
28365
+ const path = (deps.trash ?? realGit.trashWorktree)(repoDir, cwd);
27856
28366
  if (!path) return null;
27857
28367
  const entry = path.slice(trashDirOf(repoDir).length + 1);
27858
28368
  pending.set(path, { path, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() });
@@ -35586,8 +36096,8 @@ ${item.context}` : item.context;
35586
36096
  depIds = d.ids;
35587
36097
  }
35588
36098
  if ("autoMerge" in parsed.data) {
35589
- const gate = await checkAutoMerge(await resolveRepoDir(spec.projectId), autoMerge);
35590
- if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
36099
+ const gate2 = await checkAutoMerge(await resolveRepoDir(spec.projectId), autoMerge);
36100
+ if (!gate2.ok) return reply.code(gate2.code).send({ error: gate2.error });
35591
36101
  }
35592
36102
  if (stage !== void 0) {
35593
36103
  if (STAGES.indexOf(stage) >= STAGES.indexOf(spec.stage))
@@ -35623,10 +36133,10 @@ ${item.context}` : item.context;
35623
36133
  if (!spec) return reply.code(404).send({ error: "not found" });
35624
36134
  const to = parsed.data.source;
35625
36135
  if (to === spec.source) return reply.code(400).send({ error: "source tak berubah" });
35626
- const gate = checkSourceChange(spec, to, parsed.data.payload);
35627
- if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
36136
+ const gate2 = checkSourceChange(spec, to, parsed.data.payload);
36137
+ if (!gate2.ok) return reply.code(gate2.code).send({ error: gate2.error });
35628
36138
  let plan = null;
35629
- if (gate.reset) {
36139
+ if (gate2.reset) {
35630
36140
  const live = listSessions().find((s2) => s2.specId === id2 && !s2.exited);
35631
36141
  if (live)
35632
36142
  return reply.code(409).send({ error: "session-live", session: { id: live.id, agent: live.agent } });
@@ -35646,15 +36156,15 @@ ${item.context}` : item.context;
35646
36156
  );
35647
36157
  const { priority, objective } = deriveSpecFields(
35648
36158
  to,
35649
- gate.payload,
35650
- gate.payload.priority ?? spec.priority
36159
+ gate2.payload,
36160
+ gate2.payload.priority ?? spec.priority
35651
36161
  );
35652
36162
  if (plan) await applySpecReset(spec, plan);
35653
36163
  const updated = await prisma.spec.update({
35654
36164
  where: { id: id2 },
35655
36165
  data: {
35656
36166
  source: to,
35657
- payload: gate.payload,
36167
+ payload: gate2.payload,
35658
36168
  priority,
35659
36169
  objective,
35660
36170
  sourceHistory: history,
@@ -35802,7 +36312,7 @@ ${item.context}` : item.context;
35802
36312
  CODE_STYLE_CLAUSE,
35803
36313
  `Backlog item ${spec.id} \u2014 ${spec.title}.`
35804
36314
  ].join("\n\n");
35805
- const s2 = createSession(spec.projectId, r.worktree, {
36315
+ const s2 = await createAgentSession(spec.projectId, r.worktree, {
35806
36316
  id: `merge-${spec.id.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}`,
35807
36317
  specId: spec.id,
35808
36318
  model,
@@ -35867,7 +36377,7 @@ init_src();
35867
36377
  init_db();
35868
36378
  init_settings3();
35869
36379
  init_bootstrap();
35870
- init_config3();
36380
+ init_config4();
35871
36381
  async function settings_default(app2) {
35872
36382
  app2.get("/settings", async () => getSetting());
35873
36383
  app2.put("/settings", async (req, reply) => {
@@ -36050,7 +36560,7 @@ async function docs_default(app2) {
36050
36560
 
36051
36561
  // src/routes/ide.ts
36052
36562
  init_src2();
36053
- import { basename as basename5, resolve as resolve16 } from "node:path";
36563
+ import { basename as basename6 } from "node:path";
36054
36564
  import { execFile as execFile12, spawn as spawn3 } from "node:child_process";
36055
36565
  import { promisify as promisify11 } from "node:util";
36056
36566
 
@@ -36117,6 +36627,7 @@ async function repoOf(id2) {
36117
36627
 
36118
36628
  // src/routes/ide.ts
36119
36629
  init_pty();
36630
+ init_session_launch_gate();
36120
36631
  init_settings3();
36121
36632
  init_codex_trust();
36122
36633
 
@@ -36294,7 +36805,7 @@ async function deleteBranches(repoDir, names, opts) {
36294
36805
  import { execFile as execFile11 } from "node:child_process";
36295
36806
  import { realpathSync as realpathSync4 } from "node:fs";
36296
36807
  import { stat } from "node:fs/promises";
36297
- import { basename as basename4, join as join17, resolve as resolve15, sep as sep4 } from "node:path";
36808
+ import { basename as basename4, dirname as dirname9, join as join17, resolve as resolve15, sep as sep4 } from "node:path";
36298
36809
  import { promisify as promisify10 } from "node:util";
36299
36810
  var exec10 = promisify10(execFile11);
36300
36811
  var GIT7 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
@@ -36302,7 +36813,7 @@ async function out3(cwd, args) {
36302
36813
  try {
36303
36814
  return (await exec10("git", args, { cwd, ...GIT7 })).stdout;
36304
36815
  } catch {
36305
- return "";
36816
+ return null;
36306
36817
  }
36307
36818
  }
36308
36819
  function parseWorktreePorcelain(text) {
@@ -36334,9 +36845,18 @@ var real = (p3) => {
36334
36845
  try {
36335
36846
  return realpathSync4(p3);
36336
36847
  } catch {
36337
- return resolve15(p3);
36848
+ const path = resolve15(p3);
36849
+ const parent = dirname9(path);
36850
+ return parent === path ? path : join17(real(parent), basename4(path));
36338
36851
  }
36339
36852
  };
36853
+ function hasWorktreeSession(path, sessionId2, sessions) {
36854
+ const target2 = real(path);
36855
+ return sessions.some((s2) => {
36856
+ const current = real(s2.cwd);
36857
+ return s2.id === sessionId2 || current === target2 || current.startsWith(target2 + sep4);
36858
+ });
36859
+ }
36340
36860
  async function bornAt(path) {
36341
36861
  try {
36342
36862
  const st = await stat(join17(path, ".git"));
@@ -36352,14 +36872,22 @@ async function listWorktrees(repoDir, inputs) {
36352
36872
  const baseReal = real(base2);
36353
36873
  const text = await out3(base2, ["worktree", "list", "--porcelain"]);
36354
36874
  const trash = resolve15(baseReal, ".worktrees", ".trash");
36355
- const sessions = new Map([...inputs.sessions].map(([cwd, s2]) => [real(cwd), s2]));
36875
+ const sessions = new Map(inputs.sessions.map((s2) => [real(s2.cwd), { id: s2.id, specId: s2.specId }]));
36876
+ const history = /* @__PURE__ */ new Map();
36877
+ for (const h of inputs.history ?? []) {
36878
+ const path = real(h.cwd);
36879
+ const previous = history.get(path);
36880
+ if (!previous || h.startedAt > previous.startedAt) history.set(path, h);
36881
+ }
36356
36882
  const rows = [];
36357
- for (const w of parseWorktreePorcelain(text)) {
36883
+ for (const w of parseWorktreePorcelain(text ?? "")) {
36358
36884
  const path = resolve15(w.path);
36359
36885
  if (path === trash || path.startsWith(trash + sep4)) continue;
36360
36886
  const name2 = basename4(path);
36361
36887
  const deletable = ownsWorktree(baseReal, path);
36362
36888
  const spec = inputs.specs.get(name2);
36889
+ const latest = history.get(path);
36890
+ const orphan = latest && (!latest.endedAt || latest.endedReason === "reconciled") && !hasWorktreeSession(path, latest.sessionId, inputs.sessions) ? { historyId: latest.id, sessionId: latest.sessionId } : void 0;
36363
36891
  rows.push({
36364
36892
  path,
36365
36893
  name: name2,
@@ -36371,7 +36899,8 @@ async function listWorktrees(repoDir, inputs) {
36371
36899
  blocked: deletable ? null : path === baseReal ? "checkout project" : "di luar .worktrees project ini",
36372
36900
  spec: spec ? { id: spec.id, stage: spec.stage } : null,
36373
36901
  session: sessions.get(path) ?? null,
36374
- createdAt: await bornAt(path)
36902
+ createdAt: await bornAt(path),
36903
+ ...orphan ? { orphan } : {}
36375
36904
  });
36376
36905
  }
36377
36906
  rows.sort((a, b) => Number(a.deletable) - Number(b.deletable) || a.name.localeCompare(b.name));
@@ -36402,15 +36931,39 @@ async function diskBytes(w) {
36402
36931
  async function dirtyCount(w) {
36403
36932
  if (w.prunable) return 0;
36404
36933
  const s2 = await out3(w.path, ["status", "--porcelain"]);
36405
- return s2.split("\n").filter((l) => l.trim()).length;
36934
+ return s2 === null ? null : s2.split("\n").filter((l) => l.trim()).length;
36406
36935
  }
36407
36936
  async function orphanCount(repoDir, w) {
36408
- if (!w.head) return 0;
36937
+ if (!w.head) return null;
36409
36938
  const args = ["rev-list", "--count", w.head, "--not"];
36410
36939
  if (w.branch) args.push(`--exclude=${w.branch}`, "--branches", `--exclude=*/${w.branch}`, "--remotes", "--tags");
36411
36940
  else args.push("--branches", "--remotes", "--tags");
36412
- const n2 = Number.parseInt((await out3(repoDir, args)).trim(), 10);
36413
- return Number.isFinite(n2) ? n2 : 0;
36941
+ const n2 = Number.parseInt((await out3(repoDir, args))?.trim() ?? "", 10);
36942
+ return Number.isFinite(n2) ? n2 : null;
36943
+ }
36944
+ async function collectOrphanWorktrees(repoDir, names, deps) {
36945
+ const results = [];
36946
+ for (const name2 of names) {
36947
+ const row = { name: name2, ok: false, cleanup: null };
36948
+ try {
36949
+ const report = await listWorktrees(repoDir, await deps.inputs());
36950
+ const matches = report.worktrees.filter((w2) => w2.name === name2);
36951
+ if (matches.length !== 1) throw new Error("nama worktree tidak ditemukan atau ambigu");
36952
+ const w = matches[0];
36953
+ if (!w.deletable) throw new Error(`tak bisa dipungut: ${w.blocked}`);
36954
+ if (!w.orphan) throw new Error("worktree bukan sesi yatim; muat ulang daftar");
36955
+ if (hasWorktreeSession(w.path, w.orphan.sessionId, deps.sessionsNow())) {
36956
+ throw new Error("worktree kembali dipakai sesi; muat ulang daftar");
36957
+ }
36958
+ row.cleanup = deps.release(report.repoDir, w.path);
36959
+ await deps.prune(report.repoDir);
36960
+ row.ok = true;
36961
+ } catch (e) {
36962
+ row.error = e.message;
36963
+ }
36964
+ results.push(row);
36965
+ }
36966
+ return { results };
36414
36967
  }
36415
36968
  async function deleteWorktrees(repoDir, names, opts) {
36416
36969
  const report = await listWorktrees(repoDir, opts);
@@ -36498,14 +37051,322 @@ async function closeSession(id2) {
36498
37051
  return { cleanup: null };
36499
37052
  }
36500
37053
 
36501
- // src/routes/ide.ts
37054
+ // src/services/worktree-project.ts
37055
+ init_db();
37056
+ init_pty();
37057
+
37058
+ // src/services/session-history.ts
37059
+ init_db();
37060
+ init_pty();
37061
+ import { randomUUID as randomUUID7 } from "node:crypto";
37062
+
37063
+ // src/services/transcript-store.ts
37064
+ init_config2();
37065
+ init_src2();
37066
+ import { randomUUID as randomUUID6 } from "node:crypto";
37067
+ import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir4, stat as stat2 } from "node:fs/promises";
37068
+ import { join as join18, resolve as resolve16, basename as basename5 } from "node:path";
37069
+ var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
37070
+ function transcriptDir() {
37071
+ return resolve16(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
37072
+ }
37073
+ function clamp(text) {
37074
+ const buf = Buffer.from(text, "utf8");
37075
+ if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
37076
+ const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
37077
+ let tail2 = buf.subarray(cut).toString("utf8");
37078
+ const nl = tail2.indexOf("\n");
37079
+ if (nl >= 0) tail2 = tail2.slice(nl + 1);
37080
+ return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
37081
+ ${tail2}`, truncated: true };
37082
+ }
37083
+ async function saveTranscript(text) {
37084
+ if (!text.trim()) return { key: "", bytes: 0, truncated: false };
37085
+ const { body, truncated } = clamp(text);
37086
+ const dir2 = transcriptDir();
37087
+ await mkdir5(dir2, { recursive: true, mode: 448 });
37088
+ const key = `${randomUUID6()}.log`;
37089
+ await writeFile4(join18(dir2, key), body, { encoding: "utf8", mode: 384 });
37090
+ return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
37091
+ }
37092
+ async function readTranscript(key) {
37093
+ if (!key) return null;
37094
+ try {
37095
+ return await readFile4(join18(transcriptDir(), basename5(key)), "utf8");
37096
+ } catch {
37097
+ return null;
37098
+ }
37099
+ }
37100
+ async function deleteTranscript(key) {
37101
+ if (!key) return;
37102
+ try {
37103
+ await unlink4(join18(transcriptDir(), basename5(key)));
37104
+ } catch (e) {
37105
+ if (e.code !== "ENOENT") throw e;
37106
+ }
37107
+ }
37108
+ async function listTranscripts() {
37109
+ const dir2 = transcriptDir();
37110
+ let names;
37111
+ try {
37112
+ names = await readdir4(dir2);
37113
+ } catch (e) {
37114
+ if (e.code === "ENOENT") return [];
37115
+ throw e;
37116
+ }
37117
+ const rows = [];
37118
+ for (const name2 of names) {
37119
+ if (!name2.endsWith(".log")) continue;
37120
+ try {
37121
+ rows.push({ key: name2, mtimeMs: (await stat2(join18(dir2, name2))).mtimeMs });
37122
+ } catch {
37123
+ }
37124
+ }
37125
+ return rows;
37126
+ }
37127
+
37128
+ // src/services/session-history.ts
37129
+ async function worktreeHistory(projectId) {
37130
+ return prisma.sessionHistory.findMany({
37131
+ where: { projectId },
37132
+ orderBy: { startedAt: "desc" },
37133
+ select: { id: true, sessionId: true, cwd: true, startedAt: true, endedAt: true, endedReason: true }
37134
+ });
37135
+ }
37136
+ var CLOSED = "closed";
37137
+ var RECONCILED = "reconciled";
37138
+ var view3 = (r) => ({
37139
+ id: r.id,
37140
+ sessionId: r.sessionId,
37141
+ projectId: r.projectId,
37142
+ specId: r.specId,
37143
+ title: r.title,
37144
+ kind: r.kind,
37145
+ flow: r.flow,
37146
+ agent: r.agent,
37147
+ model: r.model,
37148
+ effort: r.effort,
37149
+ branch: r.branch,
37150
+ cwd: r.cwd,
37151
+ startedAt: r.startedAt.toISOString(),
37152
+ endedAt: r.endedAt?.toISOString() ?? null,
37153
+ endedReason: r.endedReason,
37154
+ reconciledAt: r.reconciledAt?.toISOString() ?? null,
37155
+ exitCode: r.exitCode,
37156
+ transcriptBytes: r.transcriptBytes
37157
+ });
37158
+ async function titleFor(specId) {
37159
+ if (!specId) return null;
37160
+ const s2 = await prisma.spec.findUnique({ where: { id: specId }, select: { title: true } });
37161
+ return s2?.title ?? null;
37162
+ }
37163
+ async function beginSession(b) {
37164
+ await prisma.sessionHistory.create({
37165
+ data: {
37166
+ id: randomUUID7(),
37167
+ sessionId: b.sessionId,
37168
+ projectId: b.projectId,
37169
+ specId: b.specId ?? null,
37170
+ title: await titleFor(b.specId),
37171
+ kind: b.kind,
37172
+ flow: b.flow ?? null,
37173
+ agent: b.agent,
37174
+ model: b.model ?? null,
37175
+ effort: b.effort ?? null,
37176
+ branch: b.branch ?? null,
37177
+ cwd: b.cwd
37178
+ }
37179
+ });
37180
+ }
37181
+ async function finishSession(d) {
37182
+ const open5 = await prisma.sessionHistory.findFirst({
37183
+ where: { sessionId: d.sessionId, endedAt: null },
37184
+ orderBy: { startedAt: "desc" }
37185
+ });
37186
+ if (!open5) return;
37187
+ let t = { key: "", bytes: 0 };
37188
+ if (d.transcript) {
37189
+ try {
37190
+ t = await saveTranscript(d.transcript);
37191
+ } catch (e) {
37192
+ console.error("riwayat sesi (transkrip tak tersimpan):", e);
37193
+ }
37194
+ }
37195
+ await prisma.sessionHistory.update({
37196
+ where: { id: open5.id },
37197
+ data: {
37198
+ endedAt: /* @__PURE__ */ new Date(),
37199
+ endedReason: CLOSED,
37200
+ exitCode: d.exitCode,
37201
+ transcriptKey: t.key || null,
37202
+ transcriptBytes: t.key ? t.bytes : null
37203
+ }
37204
+ });
37205
+ }
37206
+ async function listHistory(q) {
37207
+ const term = q.q?.trim();
37208
+ const where = {
37209
+ ...q.projectId ? { projectId: q.projectId } : {},
37210
+ ...q.specId ? { specId: q.specId } : {},
37211
+ ...q.kind ? { kind: q.kind } : {},
37212
+ ...term ? {
37213
+ // SPEC-398 · ADR-0086 · SQLite tak punya `mode: "insensitive"`; `LIKE`-nya sudah
37214
+ // case-insensitive untuk ASCII, jadi pencarian ini tetap berperilaku sama.
37215
+ OR: [
37216
+ { sessionId: { contains: term } },
37217
+ { specId: { contains: term } },
37218
+ { title: { contains: term } },
37219
+ { branch: { contains: term } }
37220
+ ]
37221
+ } : {}
37222
+ };
37223
+ const total = await prisma.sessionHistory.count({ where });
37224
+ const pageSize = q.limit ? Math.min(Math.max(Math.floor(+q.limit) || 1, 1), 200) : total || 1;
37225
+ const page = Math.max(Math.floor(+(q.page ?? 1)) || 1, 1);
37226
+ const rows = await prisma.sessionHistory.findMany({
37227
+ where,
37228
+ orderBy: { startedAt: "desc" },
37229
+ skip: (page - 1) * pageSize,
37230
+ take: pageSize
37231
+ });
37232
+ return { items: rows.map(view3), total, page, pageSize };
37233
+ }
37234
+ async function getHistory(id2) {
37235
+ const r = await prisma.sessionHistory.findUnique({ where: { id: id2 } });
37236
+ return r ? { ...view3(r), hasTranscript: !!r.transcriptKey } : null;
37237
+ }
37238
+ async function transcriptOf(id2) {
37239
+ const r = await prisma.sessionHistory.findUnique({ where: { id: id2 }, select: { transcriptKey: true } });
37240
+ if (!r?.transcriptKey) return null;
37241
+ const text = await readTranscript(r.transcriptKey);
37242
+ return text === null ? null : { text, bytes: Buffer.byteLength(text, "utf8") };
37243
+ }
37244
+ var PURGE_BATCH = 200;
37245
+ async function purgeHistory(q) {
37246
+ const where = {
37247
+ ...q.projectId ? { projectId: q.projectId } : {},
37248
+ ...q.before ? { startedAt: { lt: q.before } } : {}
37249
+ };
37250
+ const doomed = await prisma.sessionHistory.findMany({ where, select: { id: true, transcriptKey: true } });
37251
+ const report = { purged: 0, transcriptsDeleted: 0, transcriptsFailed: 0 };
37252
+ for (let i = 0; i < doomed.length; i += PURGE_BATCH) {
37253
+ const batch = doomed.slice(i, i + PURGE_BATCH);
37254
+ const { count } = await prisma.sessionHistory.deleteMany({ where: { id: { in: batch.map((d) => d.id) } } });
37255
+ report.purged += count;
37256
+ for (const d of batch) {
37257
+ if (!d.transcriptKey) continue;
37258
+ try {
37259
+ await deleteTranscript(d.transcriptKey);
37260
+ report.transcriptsDeleted++;
37261
+ } catch {
37262
+ report.transcriptsFailed++;
37263
+ }
37264
+ }
37265
+ }
37266
+ return report;
37267
+ }
37268
+ var TRANSCRIPT_GC_GRACE_MS = 60 * 6e4;
37269
+ async function reconcileTranscripts(opts = {}) {
37270
+ const rows = await prisma.sessionHistory.findMany({
37271
+ where: { transcriptKey: { not: null } },
37272
+ select: { id: true, transcriptKey: true }
37273
+ });
37274
+ const referenced = new Set(rows.map((r) => r.transcriptKey));
37275
+ const onDisk = await listTranscripts();
37276
+ const present = new Set(onDisk.map((f) => f.key));
37277
+ const cutoff = Date.now() - (opts.graceMs ?? TRANSCRIPT_GC_GRACE_MS);
37278
+ const report = { orphans: 0, dangling: 0, failed: 0 };
37279
+ for (const f of onDisk) {
37280
+ if (referenced.has(f.key) || f.mtimeMs > cutoff) continue;
37281
+ if (opts.dryRun) {
37282
+ report.orphans++;
37283
+ continue;
37284
+ }
37285
+ try {
37286
+ await deleteTranscript(f.key);
37287
+ report.orphans++;
37288
+ } catch {
37289
+ report.failed++;
37290
+ }
37291
+ }
37292
+ for (const r of rows) {
37293
+ if (present.has(r.transcriptKey)) continue;
37294
+ if (opts.dryRun) {
37295
+ report.dangling++;
37296
+ continue;
37297
+ }
37298
+ try {
37299
+ await prisma.sessionHistory.update({
37300
+ where: { id: r.id },
37301
+ data: { transcriptKey: null, transcriptBytes: null }
37302
+ });
37303
+ report.dangling++;
37304
+ } catch {
37305
+ report.failed++;
37306
+ }
37307
+ }
37308
+ return report;
37309
+ }
37310
+ async function reconcileHistory(liveSessionIds) {
37311
+ const open5 = await prisma.sessionHistory.findMany({
37312
+ where: { endedAt: null },
37313
+ select: { id: true, sessionId: true, updatedAt: true }
37314
+ });
37315
+ const live = new Set(liveSessionIds);
37316
+ const at = /* @__PURE__ */ new Date();
37317
+ let closed = 0;
37318
+ for (const r of open5) {
37319
+ if (live.has(r.sessionId)) continue;
37320
+ await prisma.sessionHistory.update({
37321
+ where: { id: r.id },
37322
+ data: { endedAt: r.updatedAt, endedReason: RECONCILED, reconciledAt: at }
37323
+ });
37324
+ closed++;
37325
+ }
37326
+ return closed;
37327
+ }
37328
+ function installSessionHistory() {
37329
+ registerSessionHooks({
37330
+ onBirth: (b) => {
37331
+ void beginSession(b).catch((e) => console.error("riwayat sesi (lahir):", e));
37332
+ },
37333
+ onDeath: (d) => {
37334
+ void finishSession(d).catch((e) => console.error("riwayat sesi (tutup):", e));
37335
+ }
37336
+ });
37337
+ }
37338
+
37339
+ // src/services/worktree-project.ts
36502
37340
  init_session_id();
37341
+ function worktreeSessions() {
37342
+ return listSessions().map((s2) => ({ cwd: s2.cwd, id: s2.id, specId: s2.specId ?? null }));
37343
+ }
37344
+ async function projectWorktreeInputs(projectId, sessions = worktreeSessions) {
37345
+ const [specs, history] = await Promise.all([
37346
+ prisma.spec.findMany({ where: { projectId }, select: { id: true, stage: true } }),
37347
+ worktreeHistory(projectId)
37348
+ ]);
37349
+ return {
37350
+ specs: new Map(specs.map((s2) => [sessionIdForSpec(s2.id), s2])),
37351
+ history,
37352
+ sessions: sessions()
37353
+ };
37354
+ }
37355
+ async function detectOrphanWorktrees(deps = { repos: prodReaperDeps.repos, inputs: projectWorktreeInputs }) {
37356
+ const found = [];
37357
+ for (const { projectId, repoDir } of await deps.repos()) {
37358
+ const report = await listWorktrees(repoDir, await deps.inputs(projectId));
37359
+ const count = report.worktrees.filter((w) => w.orphan).length;
37360
+ if (count) found.push({ projectId, count });
37361
+ }
37362
+ return found;
37363
+ }
36503
37364
 
36504
37365
  // src/services/repo-fs.ts
36505
37366
  import { lstat as lstat2, rename as rename3, rm as rm5 } from "node:fs/promises";
36506
37367
  import { createWriteStream } from "node:fs";
36507
- import { join as join18 } from "node:path";
36508
- import { randomUUID as randomUUID6 } from "node:crypto";
37368
+ import { join as join19 } from "node:path";
37369
+ import { randomUUID as randomUUID8 } from "node:crypto";
36509
37370
  import { pipeline } from "node:stream/promises";
36510
37371
  var EntryExistsError = class extends Error {
36511
37372
  code = "ENTRY_EXISTS";
@@ -36565,7 +37426,7 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
36565
37426
  if (current && !opts.overwrite) return { status: "exists" };
36566
37427
  if (current && !current.isFile())
36567
37428
  throw new PathContainmentError("repository path ditolak: target bukan file regular");
36568
- const temp = join18(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
37429
+ const temp = join19(entry.parent, `.hanoman-${randomUUID8()}.tmp`);
36569
37430
  try {
36570
37431
  await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
36571
37432
  if (opts.isTruncated?.()) {
@@ -36593,13 +37454,6 @@ async function lockInputs(id2) {
36593
37454
  sessionBranches: new Set(sessions)
36594
37455
  };
36595
37456
  }
36596
- async function worktreeInputs(id2) {
36597
- const specs = await prisma.spec.findMany({ where: { projectId: id2 }, select: { id: true, stage: true } });
36598
- return {
36599
- specs: new Map(specs.map((s2) => [sessionIdForSpec(s2.id), { id: s2.id, stage: s2.stage }])),
36600
- sessions: new Map(listSessions().filter((s2) => s2.projectId === id2 && !s2.exited).map((s2) => [resolve16(s2.cwd), { id: s2.id, specId: s2.specId ?? null }]))
36601
- };
36602
- }
36603
37457
  var execAsync = promisify11(execFile12);
36604
37458
  function entryError(reply, e) {
36605
37459
  if (e instanceof EntryMissingError) return reply.code(404).send({ error: "not found" });
@@ -36870,7 +37724,7 @@ async function ide_default(app2) {
36870
37724
  const fmt = q.format === "tar" ? "tar" : "zip";
36871
37725
  const child = spawn3("git", ["archive", `--format=${fmt}`, "--end-of-options", ref], { cwd: repoDir });
36872
37726
  reply.header("content-type", fmt === "zip" ? "application/zip" : "application/x-tar");
36873
- reply.header("content-disposition", `attachment; filename="${basename5(repoDir)}-${ref.replace(/[^\w.-]/g, "_")}.${fmt}"`);
37727
+ reply.header("content-disposition", `attachment; filename="${basename6(repoDir)}-${ref.replace(/[^\w.-]/g, "_")}.${fmt}"`);
36874
37728
  return reply.send(child.stdout);
36875
37729
  });
36876
37730
  app2.get("/projects/:id/commit/:sha", async (req, reply) => {
@@ -37019,7 +37873,7 @@ async function ide_default(app2) {
37019
37873
  const { id: id2 } = req.params;
37020
37874
  const repoDir = await repoOf(id2);
37021
37875
  if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
37022
- return listWorktrees(repoDir, await worktreeInputs(id2));
37876
+ return listWorktrees(repoDir, await projectWorktreeInputs(id2));
37023
37877
  });
37024
37878
  app2.get("/projects/:id/worktrees/stats", async (req, reply) => {
37025
37879
  const { id: id2 } = req.params;
@@ -37027,7 +37881,7 @@ async function ide_default(app2) {
37027
37881
  if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
37028
37882
  if (!repoDir) return reply.code(400).send({ error: "project tidak punya repoDir" });
37029
37883
  const { name: name2 } = req.query;
37030
- const report = await listWorktrees(repoDir, await worktreeInputs(id2));
37884
+ const report = await listWorktrees(repoDir, await projectWorktreeInputs(id2));
37031
37885
  const w = report.worktrees.find((x) => x.name === name2);
37032
37886
  if (!w) return reply.code(404).send({ error: "not found" });
37033
37887
  return worktreeStats(report.repoDir, w);
@@ -37040,10 +37894,22 @@ async function ide_default(app2) {
37040
37894
  const b = req.body;
37041
37895
  if (!Array.isArray(b?.names) || b.names.some((n2) => typeof n2 !== "string" || !n2))
37042
37896
  return reply.code(400).send({ error: "names wajib berisi nama worktree" });
37897
+ if (b.orphanOnly !== void 0 && typeof b.orphanOnly !== "boolean")
37898
+ return reply.code(400).send({ error: "orphanOnly wajib boolean" });
37899
+ if (b.orphanOnly === true) {
37900
+ if (b.deleteBranch === true)
37901
+ return reply.code(400).send({ error: "pemungutan yatim tidak menghapus branch" });
37902
+ return collectOrphanWorktrees(repoDir, b.names, {
37903
+ inputs: () => projectWorktreeInputs(id2),
37904
+ sessionsNow: worktreeSessions,
37905
+ release: (repo, path) => releaseWorktreeToTrash(repo, path, id2),
37906
+ prune: prodReaperDeps.prune
37907
+ });
37908
+ }
37043
37909
  const locks = await lockInputs(id2);
37044
37910
  return deleteWorktrees(repoDir, b.names, {
37045
37911
  withBranch: b.deleteBranch === true,
37046
- ...await worktreeInputs(id2),
37912
+ ...await projectWorktreeInputs(id2),
37047
37913
  closeSession,
37048
37914
  release: (repo, path) => releaseWorktree(repo, path, id2),
37049
37915
  prune: async (repo) => {
@@ -37074,22 +37940,22 @@ async function finishGraphOp(reply, id2, repoDir, r, verb) {
37074
37940
  CODE_STYLE_CLAUSE,
37075
37941
  `${verb} via git graph project ${id2}.`
37076
37942
  ].join("\n\n");
37077
- const s2 = createSession(id2, r.worktree, { id: basename5(r.worktree), model, effort: effort2, agent, prompt });
37943
+ const s2 = await createAgentSession(id2, r.worktree, { id: basename6(r.worktree), model, effort: effort2, agent, prompt });
37078
37944
  return { status: "conflict", sessionId: s2.id };
37079
37945
  }
37080
37946
 
37081
37947
  // src/routes/fs.ts
37082
- import { readdir as readdir4 } from "node:fs/promises";
37948
+ import { readdir as readdir5 } from "node:fs/promises";
37083
37949
  import { homedir as homedir4 } from "node:os";
37084
- import { resolve as resolve17, dirname as dirname9, join as join19 } from "node:path";
37950
+ import { resolve as resolve18, dirname as dirname10, join as join20 } from "node:path";
37085
37951
  async function fs_default(app2) {
37086
37952
  app2.get("/fs/browse", async (req, reply) => {
37087
37953
  const q = req.query.path;
37088
- const dir2 = q && q.trim() ? resolve17(q.trim()) : homedir4();
37954
+ const dir2 = q && q.trim() ? resolve18(q.trim()) : homedir4();
37089
37955
  try {
37090
- const ents = await readdir4(dir2, { withFileTypes: true });
37091
- const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join19(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
37092
- const parent = dirname9(dir2);
37956
+ const ents = await readdir5(dir2, { withFileTypes: true });
37957
+ const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join20(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
37958
+ const parent = dirname10(dir2);
37093
37959
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries3 };
37094
37960
  } catch {
37095
37961
  return reply.code(400).send({ error: `tak bisa membaca folder "${dir2}"` });
@@ -37114,6 +37980,7 @@ init_settings3();
37114
37980
  init_codex_trust();
37115
37981
  init_pty();
37116
37982
  init_session_phases();
37983
+ init_session_launch_gate();
37117
37984
  init_pty();
37118
37985
  var LaunchError = class extends Error {
37119
37986
  // SPEC-447 · `blockers` hanya terisi untuk kind "blocked"; route memetakannya ke body 409.
@@ -37138,107 +38005,111 @@ function buildResumeCtx(repoDir, id2, flow, worktreeKept) {
37138
38005
  }
37139
38006
  async function startSpecSession(spec, opts) {
37140
38007
  const id2 = sessionIdForSpec(spec.id);
37141
- const pane = getSession(id2);
37142
- if (pane && !pane.exited) return { id: pane.id, reused: true };
37143
- try {
37144
- assertLaunchApproved(spec);
37145
- } catch (error) {
37146
- throw new LaunchError(error.message, "not-approved");
37147
- }
37148
- const repoDir = await resolveRepoDir(spec.projectId);
37149
- if (!repoDir) throw new LaunchError(`project "${spec.projectId}" belum di-bind ke checkout lokal`, "needs-bind");
37150
- if (!opts.force) {
37151
- const blockers = await blockersForSpec(spec, repoDir);
37152
- if (blockers.length)
37153
- throw new LaunchError(`${spec.id} ${blockedNote(blockers)}`, "blocked", blockers);
37154
- }
37155
- if (pane) killSession(id2);
37156
- const setting = await getSetting();
37157
- const agent = opts.agent ?? setting.agent;
37158
- const agentDefaults = agent === "codex" ? { model: setting.codex.model, effort: setting.codex.effort } : { model: setting.model, effort: setting.effort };
37159
- const model = opts.model ?? agentDefaults.model;
37160
- const effort2 = opts.effort ?? agentDefaults.effort;
37161
- const isContinue = spec.stage === "done";
37162
- const worktree = `${repoDir}/.worktrees/${id2}`;
37163
- const branchTo = `hanoman/${id2}`;
37164
- const resume = !isContinue && spec.baseSha ? resumeState(repoDir, worktree, branchTo, spec.headSha) : null;
37165
- const isGoalFlow = isGoalShapedFlow(opts.flow);
37166
- const goalArgs = {
37167
- flow: opts.flow,
37168
- specId: spec.id,
37169
- branchTo,
37170
- spec: { payload: spec.payload ?? void 0, objective: spec.objective }
37171
- };
37172
- const goal = isGoalFlow || (opts.goal ?? setting.goal.enabled) ? resolveGoalCondition(goalArgs, opts.goalCondition, isGoalFlow ? null : setting.goal.condition) : void 0;
37173
- const verifyScope = opts.verifyScope ?? setting.verifyScope;
37174
- const recordedMethod = readSpecMethod(spec.payload);
37175
- const method = resolveMethod(opts.method ?? recordedMethod ?? setting.method);
37176
- if (agent === "codex") ensureCodexTrust(repoDir);
37177
- let baseSha;
37178
- if (resume?.worktreeKept) {
37179
- baseSha = spec.baseSha;
37180
- } else {
38008
+ return withSessionAdmission({ id: id2, force: opts.force }, async () => {
38009
+ const pane = await getSessionAsync(id2);
37181
38010
  try {
37182
- const born = realGit.addWorktree(repoDir, worktree, resume?.base ?? spec.branchFrom ?? "HEAD");
37183
- baseSha = resume ? spec.baseSha : born;
37184
- } catch (e) {
37185
- throw new LaunchError(`gagal membuat worktree: ${e.message}`, "worktree");
38011
+ assertLaunchApproved(spec);
38012
+ } catch (error) {
38013
+ throw new LaunchError(error.message, "not-approved");
38014
+ }
38015
+ const repoDir = await resolveRepoDir(spec.projectId);
38016
+ if (!repoDir) throw new LaunchError(`project "${spec.projectId}" belum di-bind ke checkout lokal`, "needs-bind");
38017
+ if (!opts.force) {
38018
+ const blockers = await blockersForSpec(spec, repoDir);
38019
+ if (blockers.length)
38020
+ throw new LaunchError(`${spec.id} ${blockedNote(blockers)}`, "blocked", blockers);
38021
+ }
38022
+ if (pane) killSession(id2);
38023
+ const setting = await getSetting();
38024
+ const agent = opts.agent ?? setting.agent;
38025
+ const agentDefaults = agent === "codex" ? { model: setting.codex.model, effort: setting.codex.effort } : { model: setting.model, effort: setting.effort };
38026
+ const model = opts.model ?? agentDefaults.model;
38027
+ const effort2 = opts.effort ?? agentDefaults.effort;
38028
+ const isContinue = spec.stage === "done";
38029
+ const worktree = `${repoDir}/.worktrees/${id2}`;
38030
+ const branchTo = `hanoman/${id2}`;
38031
+ const resume = !isContinue && spec.baseSha ? resumeState(repoDir, worktree, branchTo, spec.headSha) : null;
38032
+ const isGoalFlow = isGoalShapedFlow(opts.flow);
38033
+ const goalArgs = {
38034
+ flow: opts.flow,
38035
+ specId: spec.id,
38036
+ branchTo,
38037
+ spec: { payload: spec.payload ?? void 0, objective: spec.objective }
38038
+ };
38039
+ const goal = isGoalFlow || (opts.goal ?? setting.goal.enabled) ? resolveGoalCondition(goalArgs, opts.goalCondition, isGoalFlow ? null : setting.goal.condition) : void 0;
38040
+ const verifyScope = opts.verifyScope ?? setting.verifyScope;
38041
+ const recordedMethod = readSpecMethod(spec.payload);
38042
+ const method = resolveMethod(opts.method ?? recordedMethod ?? setting.method);
38043
+ if (agent === "codex") ensureCodexTrust(repoDir);
38044
+ let baseSha;
38045
+ if (resume?.worktreeKept) {
38046
+ baseSha = spec.baseSha;
38047
+ } else {
38048
+ try {
38049
+ const born = realGit.addWorktree(repoDir, worktree, resume?.base ?? spec.branchFrom ?? "HEAD");
38050
+ baseSha = resume ? spec.baseSha : born;
38051
+ } catch (e) {
38052
+ throw new LaunchError(`gagal membuat worktree: ${e.message}`, "worktree");
38053
+ }
38054
+ if (!resume) await prisma.spec.update({
38055
+ where: { id: spec.id },
38056
+ data: { baseSha, headSha: null, startedAt: /* @__PURE__ */ new Date() }
38057
+ });
37186
38058
  }
37187
- if (!resume) await prisma.spec.update({
37188
- where: { id: spec.id },
37189
- data: { baseSha, headSha: null, startedAt: /* @__PURE__ */ new Date() }
38059
+ if (!recordedMethod) {
38060
+ const stamped = stampSpecMethod(spec.payload, method.id);
38061
+ if (stamped) await prisma.spec.update({ where: { id: spec.id }, data: { payload: stamped } });
38062
+ }
38063
+ const attachments = {
38064
+ dir: specAttachmentsDir(repoDir, id2),
38065
+ items: await syncSpecAttachmentsDir(spec.id, spec.projectId)
38066
+ };
38067
+ const brief = {
38068
+ id: spec.id,
38069
+ title: spec.title,
38070
+ source: spec.source,
38071
+ priority: spec.priority,
38072
+ objective: spec.objective,
38073
+ payload: spec.payload ?? void 0
38074
+ };
38075
+ const resumeCtx = resume ? buildResumeCtx(repoDir, id2, opts.flow, resume.worktreeKept) : void 0;
38076
+ let prompt;
38077
+ if (isGoalFlow) {
38078
+ prompt = startGoalPrompt(opts.flow, brief, branchTo, {
38079
+ autonomy: opts.autonomy,
38080
+ verifyScope,
38081
+ resume: resumeCtx,
38082
+ method: method.id,
38083
+ attachments
38084
+ });
38085
+ } else if (isContinue) {
38086
+ prompt = continuePrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments);
38087
+ } else if (resumeCtx) {
38088
+ prompt = resumePrompt(opts.flow, brief, branchTo, resumeCtx, opts.autonomy, verifyScope, method.id, attachments);
38089
+ } else {
38090
+ prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments);
38091
+ }
38092
+ const scopeEnv = { HANOMAN_BASE_SHA: baseSha, HANOMAN_VERIFY_SCOPE: verifyScope };
38093
+ const s2 = createSession(spec.projectId, worktree, {
38094
+ specId: spec.id,
38095
+ flow: opts.flow,
38096
+ model,
38097
+ effort: effort2,
38098
+ goal,
38099
+ agent,
38100
+ phaseFile: phaseFilePath(repoDir, id2),
38101
+ decisionFile: decisionFilePath(repoDir, id2),
38102
+ attachmentsDir: attachments.items.length ? attachments.dir : void 0,
38103
+ prompt,
38104
+ env: scopeEnv
37190
38105
  });
37191
- }
37192
- if (!recordedMethod) {
37193
- const stamped = stampSpecMethod(spec.payload, method.id);
37194
- if (stamped) await prisma.spec.update({ where: { id: spec.id }, data: { payload: stamped } });
37195
- }
37196
- const attachments = {
37197
- dir: specAttachmentsDir(repoDir, id2),
37198
- items: await syncSpecAttachmentsDir(spec.id, spec.projectId)
37199
- };
37200
- const brief = {
37201
- id: spec.id,
37202
- title: spec.title,
37203
- source: spec.source,
37204
- priority: spec.priority,
37205
- objective: spec.objective,
37206
- payload: spec.payload ?? void 0
37207
- };
37208
- const resumeCtx = resume ? buildResumeCtx(repoDir, id2, opts.flow, resume.worktreeKept) : void 0;
37209
- let prompt;
37210
- if (isGoalFlow) {
37211
- prompt = startGoalPrompt(opts.flow, brief, branchTo, {
37212
- autonomy: opts.autonomy,
37213
- verifyScope,
37214
- resume: resumeCtx,
37215
- method: method.id,
37216
- attachments
37217
- });
37218
- } else if (isContinue) {
37219
- prompt = continuePrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments);
37220
- } else if (resumeCtx) {
37221
- prompt = resumePrompt(opts.flow, brief, branchTo, resumeCtx, opts.autonomy, verifyScope, method.id, attachments);
37222
- } else {
37223
- prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments);
37224
- }
37225
- const scopeEnv = { HANOMAN_BASE_SHA: baseSha, HANOMAN_VERIFY_SCOPE: verifyScope };
37226
- const s2 = createSession(spec.projectId, worktree, {
37227
- specId: spec.id,
37228
- flow: opts.flow,
37229
- model,
37230
- effort: effort2,
37231
- goal,
37232
- agent,
37233
- phaseFile: phaseFilePath(repoDir, id2),
37234
- decisionFile: decisionFilePath(repoDir, id2),
37235
- attachmentsDir: attachments.items.length ? attachments.dir : void 0,
37236
- prompt,
37237
- env: scopeEnv
37238
- });
37239
- return resume ? { id: s2.id, resumed: true } : { id: s2.id };
38106
+ return resume ? { id: s2.id, resumed: true } : { id: s2.id };
38107
+ }, (pane) => ({ id: pane.id, reused: true }));
37240
38108
  }
37241
38109
 
38110
+ // src/routes/terminal.ts
38111
+ init_session_launch_gate();
38112
+
37242
38113
  // src/services/ws-admission.ts
37243
38114
  import { randomBytes as randomBytes5 } from "node:crypto";
37244
38115
 
@@ -37291,7 +38162,7 @@ function setBounded(map, key, value, maxKeys) {
37291
38162
  // src/services/auth.ts
37292
38163
  init_src2();
37293
38164
  init_db();
37294
- import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual3, createHash as createHash5 } from "node:crypto";
38165
+ import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual3, createHash as createHash6 } from "node:crypto";
37295
38166
  import { promisify as promisify12 } from "node:util";
37296
38167
  var scrypt = promisify12(scryptCb);
37297
38168
  var COOKIE_NAME = "hn_session";
@@ -37309,7 +38180,7 @@ async function verifyPassword(pw, stored) {
37309
38180
  return key.length === want.length && timingSafeEqual3(key, want);
37310
38181
  }
37311
38182
  var newSessionToken = () => randomBytes3(32).toString("base64url");
37312
- var sessionId = (token) => createHash5("sha256").update(token).digest("hex");
38183
+ var sessionId = (token) => createHash6("sha256").update(token).digest("hex");
37313
38184
  async function createSession2(userId) {
37314
38185
  const token = newSessionToken();
37315
38186
  await prisma.session.create({
@@ -37370,9 +38241,9 @@ function cookieOpts(req) {
37370
38241
 
37371
38242
  // src/services/device-token.ts
37372
38243
  init_db();
37373
- import { randomBytes as randomBytes4, createHash as createHash6 } from "node:crypto";
38244
+ import { randomBytes as randomBytes4, createHash as createHash7 } from "node:crypto";
37374
38245
  var newDeviceToken = () => randomBytes4(32).toString("base64url");
37375
- var tokenHash = (token) => createHash6("sha256").update(token).digest("hex");
38246
+ var tokenHash = (token) => createHash7("sha256").update(token).digest("hex");
37376
38247
  async function issueDeviceToken(userId, name2) {
37377
38248
  const token = newDeviceToken();
37378
38249
  const row = await prisma.deviceToken.create({ data: { userId, name: name2, tokenHash: tokenHash(token) } });
@@ -37571,13 +38442,13 @@ function installCommand(m, agent, shell = shellBin()) {
37571
38442
 
37572
38443
  // src/services/terminal-diag.ts
37573
38444
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync5 } from "node:fs";
37574
- import { join as join20 } from "node:path";
38445
+ import { join as join21 } from "node:path";
37575
38446
  var DIAG_MAX_BYTES = 2 * 1024 * 1024;
37576
38447
  var KINDS = /* @__PURE__ */ new Set(["key", "comp", "data", "ack", "pred"]);
37577
38448
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
37578
38449
  function diagFile(home3, sessionId2) {
37579
38450
  if (!ID.test(sessionId2)) throw new Error(`id sesi tak sah untuk diag: ${sessionId2}`);
37580
- return join20(home3, "diag", `${sessionId2}.jsonl`);
38451
+ return join21(home3, "diag", `${sessionId2}.jsonl`);
37581
38452
  }
37582
38453
  function usable(ev) {
37583
38454
  if (!ev || typeof ev !== "object") return false;
@@ -37588,7 +38459,7 @@ function appendDiag(home3, sessionId2, events) {
37588
38459
  const file = diagFile(home3, sessionId2);
37589
38460
  const rows = events.filter(usable);
37590
38461
  if (!rows.length) return;
37591
- mkdirSync9(join20(home3, "diag"), { recursive: true });
38462
+ mkdirSync9(join21(home3, "diag"), { recursive: true });
37592
38463
  try {
37593
38464
  if (statSync3(file).size > DIAG_MAX_BYTES) rmSync5(file);
37594
38465
  } catch {
@@ -37605,9 +38476,9 @@ init_uploads();
37605
38476
  // src/services/session-dialog.ts
37606
38477
  init_tui_dialog();
37607
38478
  init_pty();
37608
- import { createHash as createHash7 } from "node:crypto";
38479
+ import { createHash as createHash8 } from "node:crypto";
37609
38480
  var DIALOG_CHUNK_MS = 50;
37610
- var screenHashOf = (paneText2) => createHash7("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
38481
+ var screenHashOf = (paneText2) => createHash8("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
37611
38482
  var answerable = (paneText2) => {
37612
38483
  const s2 = readDialogScreen(paneText2);
37613
38484
  if (s2?.kind !== "question") return null;
@@ -38144,10 +39015,10 @@ init_config2();
38144
39015
  init_pty();
38145
39016
  init_session_sandbox();
38146
39017
  import { execFile as execFile13 } from "node:child_process";
38147
- import { randomUUID as randomUUID7 } from "node:crypto";
39018
+ import { randomUUID as randomUUID9 } from "node:crypto";
38148
39019
  import { mkdirSync as mkdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
38149
- import { tmpdir as tmpdir4 } from "node:os";
38150
- import { join as join21 } from "node:path";
39020
+ import { tmpdir as tmpdir3 } from "node:os";
39021
+ import { join as join22 } from "node:path";
38151
39022
  var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
38152
39023
  function leadArgv(o) {
38153
39024
  if (o.agent === "codex") {
@@ -38175,11 +39046,11 @@ function leadProcess(prompt, o, env = process.env) {
38175
39046
  const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
38176
39047
  if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
38177
39048
  } };
38178
- const promptDir = join21(tmpdir4(), "hanoman-prompts");
39049
+ const promptDir = join22(tmpdir3(), "hanoman-prompts");
38179
39050
  mkdirSync10(promptDir, { recursive: true, mode: 448 });
38180
- const promptFile = join21(promptDir, `oneshot-${randomUUID7()}`);
39051
+ const promptFile = join22(promptDir, `oneshot-${randomUUID9()}`);
38181
39052
  writeFileSync7(promptFile, prompt, { flag: "wx", mode: 384 });
38182
- const workspace = o.cwd ?? join21(tmpdir4(), `hanoman-oneshot-${randomUUID7()}`);
39053
+ const workspace = o.cwd ?? join22(tmpdir3(), `hanoman-oneshot-${randomUUID9()}`);
38183
39054
  if (!o.cwd) mkdirSync10(workspace, { recursive: false, mode: 448 });
38184
39055
  try {
38185
39056
  const argsWithoutPrompt = directArgs.slice(0, -1);
@@ -38994,6 +39865,9 @@ async function terminal_default(app2, opts) {
38994
39865
  app2.post("/terminal/sessions", async (req, reply) => {
38995
39866
  const parsed = zTerminalSession.safeParse(req.body);
38996
39867
  if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
39868
+ if (req.agent && "force" in parsed.data && parsed.data.force) {
39869
+ return reply.code(403).send({ error: "force hanya boleh digunakan manusia melalui dashboard" });
39870
+ }
38997
39871
  if ("spec" in parsed.data) {
38998
39872
  const spec = await prisma.spec.findUnique({ where: { id: parsed.data.spec } });
38999
39873
  if (!spec) return reply.code(404).send({ error: "spec not found" });
@@ -39037,10 +39911,10 @@ async function terminal_default(app2, opts) {
39037
39911
  if (inst) {
39038
39912
  const m = METHODS[inst.method];
39039
39913
  if (!m) return reply.code(400).send({ error: `metode "${inst.method}" tak dikenal` });
39040
- const s4 = createSession(project.id, repoDir2, { command: installCommand(m, inst.agent) });
39914
+ const s4 = await createOperatorSession(project.id, repoDir2, { command: installCommand(m, inst.agent) });
39041
39915
  return reply.code(201).send({ id: s4.id });
39042
39916
  }
39043
- const s3 = createSession(project.id, repoDir2, { command: [shellBin()] });
39917
+ const s3 = await createOperatorSession(project.id, repoDir2, { command: [shellBin()] });
39044
39918
  return reply.code(201).send({ id: s3.id });
39045
39919
  }
39046
39920
  const repoDir = await resolveRepoDir(project.id);
@@ -39049,139 +39923,143 @@ async function terminal_default(app2, opts) {
39049
39923
  }
39050
39924
  if (parsed.data.flow === "reverse") {
39051
39925
  const id2 = `reverse-${project.id.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}`;
39052
- const live = getSession(id2);
39053
- if (live && !live.exited) return reply.code(201).send({ id: live.id });
39054
- const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
39055
- if (agent2 === "codex") ensureCodexTrust(repoDir);
39056
- const wt = `${repoDir}/.worktrees/${id2}`;
39057
- let reused;
39058
- try {
39059
- reused = ensureWorktree(repoDir, wt, "HEAD");
39060
- } catch (e) {
39061
- return reply.code(422).send({ error: `gagal membuat worktree: ${e.message}` });
39062
- }
39063
- const s3 = createSession(project.id, wt, {
39064
- id: id2,
39065
- flow: "reverse",
39066
- model: model2,
39067
- effort: effort3,
39068
- agent: agent2,
39069
- phaseFile: phaseFilePath(repoDir, id2),
39070
- decisionFile: decisionFilePath(repoDir, id2),
39071
- prompt: startProjectPrompt("reverse", {
39072
- id: project.id,
39073
- name: project.name,
39074
- desc: project.desc,
39075
- stack: project.stack
39076
- }, "reverse-docs") + resumeNote(reused)
39077
- });
39078
- return reply.code(201).send({ id: s3.id });
39926
+ const result = await withSessionAdmission({ id: id2, force: parsed.data.force }, async () => {
39927
+ const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
39928
+ if (agent2 === "codex") ensureCodexTrust(repoDir);
39929
+ const wt = `${repoDir}/.worktrees/${id2}`;
39930
+ let reused;
39931
+ try {
39932
+ reused = ensureWorktree(repoDir, wt, "HEAD");
39933
+ } catch (e) {
39934
+ return { code: 422, body: { error: `gagal membuat worktree: ${e.message}` } };
39935
+ }
39936
+ const s3 = createSession(project.id, wt, {
39937
+ id: id2,
39938
+ flow: "reverse",
39939
+ model: model2,
39940
+ effort: effort3,
39941
+ agent: agent2,
39942
+ phaseFile: phaseFilePath(repoDir, id2),
39943
+ decisionFile: decisionFilePath(repoDir, id2),
39944
+ prompt: startProjectPrompt("reverse", {
39945
+ id: project.id,
39946
+ name: project.name,
39947
+ desc: project.desc,
39948
+ stack: project.stack
39949
+ }, "reverse-docs") + resumeNote(reused)
39950
+ });
39951
+ return { code: 201, body: { id: s3.id } };
39952
+ }, (pane) => ({ code: 201, body: { id: pane.id } }));
39953
+ return reply.code(result.code).send(result.body);
39079
39954
  }
39080
39955
  if (parsed.data.flow === "scaffold") {
39081
39956
  const id2 = `scaffold-${project.id.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}`;
39082
- const live = getSession(id2);
39083
- if (live && !live.exited) return reply.code(201).send({ id: live.id });
39084
- const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
39085
- if (agent2 === "codex") ensureCodexTrust(repoDir);
39086
- const wt = `${repoDir}/.worktrees/${id2}`;
39087
- let reused;
39088
- try {
39089
- realGit.initRepo(repoDir);
39090
- reused = ensureWorktree(repoDir, wt, "HEAD");
39091
- } catch (e) {
39092
- return reply.code(422).send({ error: `gagal membuat worktree: ${e.message}` });
39093
- }
39094
- const s3 = createSession(project.id, wt, {
39095
- id: id2,
39096
- flow: "scaffold",
39097
- model: model2,
39098
- effort: effort3,
39099
- agent: agent2,
39100
- phaseFile: phaseFilePath(repoDir, id2),
39101
- decisionFile: decisionFilePath(repoDir, id2),
39102
- prompt: startScaffoldPrompt(
39103
- { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
39104
- "scaffold-docs"
39105
- ) + resumeNote(reused)
39106
- });
39107
- return reply.code(201).send({ id: s3.id });
39957
+ const result = await withSessionAdmission({ id: id2, force: parsed.data.force }, async () => {
39958
+ const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
39959
+ if (agent2 === "codex") ensureCodexTrust(repoDir);
39960
+ const wt = `${repoDir}/.worktrees/${id2}`;
39961
+ let reused;
39962
+ try {
39963
+ realGit.initRepo(repoDir);
39964
+ reused = ensureWorktree(repoDir, wt, "HEAD");
39965
+ } catch (e) {
39966
+ return { code: 422, body: { error: `gagal membuat worktree: ${e.message}` } };
39967
+ }
39968
+ const s3 = createSession(project.id, wt, {
39969
+ id: id2,
39970
+ flow: "scaffold",
39971
+ model: model2,
39972
+ effort: effort3,
39973
+ agent: agent2,
39974
+ phaseFile: phaseFilePath(repoDir, id2),
39975
+ decisionFile: decisionFilePath(repoDir, id2),
39976
+ prompt: startScaffoldPrompt(
39977
+ { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
39978
+ "scaffold-docs"
39979
+ ) + resumeNote(reused)
39980
+ });
39981
+ return { code: 201, body: { id: s3.id } };
39982
+ }, (pane) => ({ code: 201, body: { id: pane.id } }));
39983
+ return reply.code(result.code).send(result.body);
39108
39984
  }
39109
39985
  if (parsed.data.flow === "prd") {
39110
39986
  const { brief, branchFrom, fromAudit } = parsed.data;
39111
39987
  const slug = brief.title.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
39112
39988
  if (!slug) return reply.code(400).send({ error: "judul PRD kosong" });
39113
39989
  const id2 = `prd-${slug}`;
39114
- const live = getSession(id2);
39115
- if (live && !live.exited) return reply.code(201).send({ id: live.id });
39116
- const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
39117
- if (agent2 === "codex") ensureCodexTrust(repoDir);
39118
- const wt = `${repoDir}/.worktrees/${id2}`;
39119
- let reused;
39120
- try {
39121
- reused = ensureWorktree(repoDir, wt, branchFrom ?? "HEAD");
39122
- } catch (e) {
39123
- return reply.code(422).send({ error: `gagal membuat worktree: ${e.message}` });
39124
- }
39125
- const auditDoc = fromAudit ? await readAuditDoc(fromAudit) : null;
39126
- const s3 = createSession(project.id, wt, {
39127
- id: id2,
39128
- flow: "prd",
39129
- branch: `prd/${slug}`,
39130
- model: model2,
39131
- effort: effort3,
39132
- agent: agent2,
39133
- phaseFile: phaseFilePath(repoDir, id2),
39134
- decisionFile: decisionFilePath(repoDir, id2),
39135
- prompt: startPrdPrompt(
39136
- { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
39137
- brief,
39138
- `prd/${slug}`,
39139
- auditDoc ? { id: fromAudit, path: auditDoc.path, content: auditDoc.content } : void 0
39140
- ) + resumeNote(reused)
39141
- });
39142
- return reply.code(201).send({ id: s3.id });
39990
+ const result = await withSessionAdmission({ id: id2, force: parsed.data.force }, async () => {
39991
+ const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
39992
+ if (agent2 === "codex") ensureCodexTrust(repoDir);
39993
+ const wt = `${repoDir}/.worktrees/${id2}`;
39994
+ let reused;
39995
+ try {
39996
+ reused = ensureWorktree(repoDir, wt, branchFrom ?? "HEAD");
39997
+ } catch (e) {
39998
+ return { code: 422, body: { error: `gagal membuat worktree: ${e.message}` } };
39999
+ }
40000
+ const auditDoc = fromAudit ? await readAuditDoc(fromAudit) : null;
40001
+ const s3 = createSession(project.id, wt, {
40002
+ id: id2,
40003
+ flow: "prd",
40004
+ branch: `prd/${slug}`,
40005
+ model: model2,
40006
+ effort: effort3,
40007
+ agent: agent2,
40008
+ phaseFile: phaseFilePath(repoDir, id2),
40009
+ decisionFile: decisionFilePath(repoDir, id2),
40010
+ prompt: startPrdPrompt(
40011
+ { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
40012
+ brief,
40013
+ `prd/${slug}`,
40014
+ auditDoc ? { id: fromAudit, path: auditDoc.path, content: auditDoc.content } : void 0
40015
+ ) + resumeNote(reused)
40016
+ });
40017
+ return { code: 201, body: { id: s3.id } };
40018
+ }, (pane) => ({ code: 201, body: { id: pane.id } }));
40019
+ return reply.code(result.code).send(result.body);
39143
40020
  }
39144
40021
  if (parsed.data.flow === "breakdown") {
39145
40022
  const { prdPath } = parsed.data;
39146
- const content = await readPrd(project.id, prdPath);
39147
- if (content === null) return reply.code(400).send({ error: "PRD tak terbaca" });
39148
40023
  const base2 = prdPath.slice(prdPath.lastIndexOf("/") + 1).replace(/\.md$/, "");
39149
40024
  const slug = base2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
39150
40025
  if (!slug) return reply.code(400).send({ error: "path PRD tak valid" });
39151
40026
  const id2 = `breakdown-${slug}`;
39152
- const live = getSession(id2);
39153
- if (live && !live.exited) return reply.code(201).send({ id: live.id });
39154
- const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
39155
- if (agent2 === "codex") ensureCodexTrust(repoDir);
39156
- const wt = `${repoDir}/.worktrees/${id2}`;
39157
- let reused;
39158
- try {
39159
- reused = ensureWorktree(repoDir, wt, "HEAD");
39160
- } catch (e) {
39161
- return reply.code(422).send({ error: `gagal membuat worktree: ${e.message}` });
39162
- }
39163
- const titleM = content.match(/^#\s+(.+)$/m);
39164
- const title = titleM ? titleM[1].trim() : slug;
39165
- const s3 = createSession(project.id, wt, {
39166
- id: id2,
39167
- flow: "breakdown",
39168
- branch: `breakdown/${slug}`,
39169
- model: model2,
39170
- effort: effort3,
39171
- agent: agent2,
39172
- phaseFile: phaseFilePath(repoDir, id2),
39173
- decisionFile: decisionFilePath(repoDir, id2),
39174
- prompt: startBreakdownPrompt(
39175
- { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
39176
- { title, path: prdPath, content },
39177
- `breakdown/${slug}`
39178
- ) + resumeNote(reused)
39179
- });
39180
- return reply.code(201).send({ id: s3.id });
40027
+ const result = await withSessionAdmission({ id: id2, force: parsed.data.force }, async () => {
40028
+ const content = await readPrd(project.id, prdPath, await listSessionsAsync());
40029
+ if (content === null) return { code: 400, body: { error: "PRD tak terbaca" } };
40030
+ const { agent: agent2, model: model2, effort: effort3 } = await sessionAgentDefaults();
40031
+ if (agent2 === "codex") ensureCodexTrust(repoDir);
40032
+ const wt = `${repoDir}/.worktrees/${id2}`;
40033
+ let reused;
40034
+ try {
40035
+ reused = ensureWorktree(repoDir, wt, "HEAD");
40036
+ } catch (e) {
40037
+ return { code: 422, body: { error: `gagal membuat worktree: ${e.message}` } };
40038
+ }
40039
+ const titleM = content.match(/^#\s+(.+)$/m);
40040
+ const title = titleM ? titleM[1].trim() : slug;
40041
+ const s3 = createSession(project.id, wt, {
40042
+ id: id2,
40043
+ flow: "breakdown",
40044
+ branch: `breakdown/${slug}`,
40045
+ model: model2,
40046
+ effort: effort3,
40047
+ agent: agent2,
40048
+ phaseFile: phaseFilePath(repoDir, id2),
40049
+ decisionFile: decisionFilePath(repoDir, id2),
40050
+ prompt: startBreakdownPrompt(
40051
+ { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
40052
+ { title, path: prdPath, content },
40053
+ `breakdown/${slug}`
40054
+ ) + resumeNote(reused)
40055
+ });
40056
+ return { code: 201, body: { id: s3.id } };
40057
+ }, (pane) => ({ code: 201, body: { id: pane.id } }));
40058
+ return reply.code(result.code).send(result.body);
39181
40059
  }
39182
40060
  const { agent, model, effort: effort2 } = await terminalAgentDefaults(parsed.data);
39183
40061
  if (agent === "codex") ensureCodexTrust(repoDir);
39184
- const s2 = createSession(project.id, repoDir, { agent, model, effort: effort2 });
40062
+ const s2 = await createOperatorSession(project.id, repoDir, { agent, model, effort: effort2 });
39185
40063
  return reply.code(201).send({ id: s2.id });
39186
40064
  });
39187
40065
  app2.get("/terminal/sessions/:id/phases", async (req, reply) => {
@@ -39286,7 +40164,7 @@ async function terminal_default(app2, opts) {
39286
40164
  CODE_STYLE_CLAUSE,
39287
40165
  `Sesi PRD ${s2.id}.`
39288
40166
  ].join("\n\n");
39289
- const cs = createSession(s2.projectId, r.worktree, {
40167
+ const cs = await createAgentSession(s2.projectId, r.worktree, {
39290
40168
  id: `merge-${id2.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}`,
39291
40169
  model,
39292
40170
  effort: effort2,
@@ -39671,8 +40549,8 @@ import { readFileSync as readFileSync10 } from "node:fs";
39671
40549
  init_config2();
39672
40550
  import { spawn as spawn4 } from "node:child_process";
39673
40551
  import { mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
39674
- import { tmpdir as tmpdir5 } from "node:os";
39675
- import { dirname as dirname10, join as join22 } from "node:path";
40552
+ import { tmpdir as tmpdir4 } from "node:os";
40553
+ import { dirname as dirname11, join as join23 } from "node:path";
39676
40554
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
39677
40555
  function consoleArgv(t) {
39678
40556
  return [
@@ -39687,8 +40565,8 @@ function consoleArgv(t) {
39687
40565
  ];
39688
40566
  }
39689
40567
  function askpassScript() {
39690
- const dir2 = mkdtempSync(join22(tmpdir5(), "hanoman-askpass-"));
39691
- const path = join22(dir2, "askpass.sh");
40568
+ const dir2 = mkdtempSync(join23(tmpdir4(), "hanoman-askpass-"));
40569
+ const path = join23(dir2, "askpass.sh");
39692
40570
  writeFileSync8(path, `#!/bin/sh
39693
40571
  printf '%s' "$HANOMAN_SSH_PASSWORD"
39694
40572
  `, { mode: 448 });
@@ -39728,7 +40606,7 @@ function sshExec(t, remoteCmd, opts = {}) {
39728
40606
  const timer9 = setTimeout(() => p3.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
39729
40607
  const done = (r) => {
39730
40608
  clearTimeout(timer9);
39731
- if (askpass) rmSync7(dirname10(askpass), { recursive: true, force: true });
40609
+ if (askpass) rmSync7(dirname11(askpass), { recursive: true, force: true });
39732
40610
  resolve22(r);
39733
40611
  };
39734
40612
  p3.stdout.on("data", (d) => {
@@ -39749,17 +40627,17 @@ function sshExec(t, remoteCmd, opts = {}) {
39749
40627
  // src/services/vps-audit.ts
39750
40628
  init_db();
39751
40629
  import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
39752
- import { join as join24 } from "node:path";
40630
+ import { join as join25 } from "node:path";
39753
40631
  import { fileURLToPath as fileURLToPath3 } from "node:url";
39754
40632
 
39755
40633
  // src/runner/deps.ts
39756
40634
  import { existsSync as existsSync13 } from "node:fs";
39757
- import { dirname as dirname11, join as join23 } from "node:path";
40635
+ import { dirname as dirname12, join as join24 } from "node:path";
39758
40636
  function repoRootFrom(startDir) {
39759
40637
  let dir2 = startDir;
39760
40638
  for (let i = 0; i < 8; i++) {
39761
- if (existsSync13(join23(dir2, "pnpm-workspace.yaml"))) return dir2;
39762
- const parent = dirname11(dir2);
40639
+ if (existsSync13(join24(dir2, "pnpm-workspace.yaml"))) return dir2;
40640
+ const parent = dirname12(dir2);
39763
40641
  if (parent === dir2) break;
39764
40642
  dir2 = parent;
39765
40643
  }
@@ -39871,7 +40749,7 @@ var packagedScript = (f) => fileURLToPath3(new URL(`../scripts/vps/${f}`, import
39871
40749
  var moduleDir = () => fileURLToPath3(new URL(".", import.meta.url));
39872
40750
  var scriptPath = (f) => {
39873
40751
  const packed = packagedScript(f);
39874
- return existsSync14(packed) ? packed : join24(repoRoot(moduleDir()), "server", "scripts", "vps", f);
40752
+ return existsSync14(packed) ? packed : join25(repoRoot(moduleDir()), "server", "scripts", "vps", f);
39875
40753
  };
39876
40754
  async function itemStatesOf(vpsId) {
39877
40755
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
@@ -40081,25 +40959,25 @@ init_config2();
40081
40959
  import { execFileSync as execFileSync3 } from "node:child_process";
40082
40960
  import { chmodSync as chmodSync7, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
40083
40961
  import { homedir as homedir5 } from "node:os";
40084
- import { join as join25 } from "node:path";
40962
+ import { join as join26 } from "node:path";
40085
40963
  var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? resolveDataDirs().sshKeys;
40086
40964
  var KEY_FILES = ["id_ed25519", "id_ed25519.pub"];
40087
40965
  function adoptLegacyKey(dir2) {
40088
40966
  if (effectiveStr("HANOMAN_SSH_KEY_DIR")) return;
40089
- const legacy = join25(homedir5(), ".hanoman");
40967
+ const legacy = join26(homedir5(), ".hanoman");
40090
40968
  if (legacy === dir2) return;
40091
- if (!KEY_FILES.every((f) => existsSync15(join25(legacy, f)))) return;
40969
+ if (!KEY_FILES.every((f) => existsSync15(join26(legacy, f)))) return;
40092
40970
  mkdirSync11(dir2, { recursive: true, mode: 448 });
40093
40971
  for (const f of KEY_FILES) {
40094
- copyFileSync(join25(legacy, f), join25(dir2, f));
40095
- chmodSync7(join25(dir2, f), 384);
40972
+ copyFileSync(join26(legacy, f), join26(dir2, f));
40973
+ chmodSync7(join26(dir2, f), 384);
40096
40974
  }
40097
- for (const f of KEY_FILES) unlinkSync2(join25(legacy, f));
40975
+ for (const f of KEY_FILES) unlinkSync2(join26(legacy, f));
40098
40976
  console.log(`vps: key SSH dipindah dari ${legacy} ke ${dir2} (SPEC-846 \u2014 satu batas backup)`);
40099
40977
  }
40100
40978
  function ensureHanomanKey() {
40101
40979
  const dir2 = keyDir();
40102
- const privPath = join25(dir2, KEY_FILES[0]);
40980
+ const privPath = join26(dir2, KEY_FILES[0]);
40103
40981
  const pubPath = `${privPath}.pub`;
40104
40982
  if (!existsSync15(privPath)) adoptLegacyKey(dir2);
40105
40983
  if (!existsSync15(privPath)) {
@@ -40126,7 +41004,7 @@ async function bootstrapKey(t, password) {
40126
41004
  }
40127
41005
 
40128
41006
  // src/routes/vps.ts
40129
- init_pty();
41007
+ init_session_launch_gate();
40130
41008
  init_settings3();
40131
41009
  init_sync_notify();
40132
41010
  function keyMissing(v) {
@@ -40383,7 +41261,7 @@ async function vps_default(app2) {
40383
41261
  const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
40384
41262
  if (!v) return reply.code(404).send({ error: "not found" });
40385
41263
  if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
40386
- const s2 = createSession(`vps-console:${v.id}`, homedir6(), { id: `vpsc-${v.id}`, command: consoleArgv(v) });
41264
+ const s2 = await createOperatorSession(`vps-console:${v.id}`, homedir6(), { id: `vpsc-${v.id}`, command: consoleArgv(v) });
40387
41265
  return reply.code(201).send({ id: s2.id });
40388
41266
  });
40389
41267
  app2.post("/vps/:id/session", async (req, reply) => {
@@ -40392,7 +41270,7 @@ async function vps_default(app2) {
40392
41270
  if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
40393
41271
  const checks = v.audit ?? [];
40394
41272
  const { model, effort: effort2 } = await sessionModel();
40395
- const s2 = createSession(`vps:${v.id}`, homedir6(), {
41273
+ const s2 = await createAgentSession(`vps:${v.id}`, homedir6(), {
40396
41274
  model,
40397
41275
  effort: effort2,
40398
41276
  prompt: [
@@ -40412,7 +41290,7 @@ init_config2();
40412
41290
  import { execFileSync as execFileSync4 } from "node:child_process";
40413
41291
  import { readFileSync as readFileSync14 } from "node:fs";
40414
41292
  import { homedir as homedir7 } from "node:os";
40415
- import { join as join26 } from "node:path";
41293
+ import { join as join27 } from "node:path";
40416
41294
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
40417
41295
  var TTL_MS2 = 3e4;
40418
41296
  var lastOk = null;
@@ -40425,7 +41303,7 @@ var LABELS = {
40425
41303
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
40426
41304
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
40427
41305
  function credsFile() {
40428
- return join26(effectiveStr("CLAUDE_CONFIG_DIR") ?? join26(homedir7(), ".claude"), ".credentials.json");
41306
+ return join27(effectiveStr("CLAUDE_CONFIG_DIR") ?? join27(homedir7(), ".claude"), ".credentials.json");
40429
41307
  }
40430
41308
  function readAccessToken() {
40431
41309
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
@@ -40497,16 +41375,16 @@ async function getLimits() {
40497
41375
  }
40498
41376
 
40499
41377
  // src/services/codex-limits.ts
40500
- import { open as open3, readdir as readdir5, stat as stat2 } from "node:fs/promises";
41378
+ import { open as open3, readdir as readdir6, stat as stat3 } from "node:fs/promises";
40501
41379
  import { homedir as homedir8 } from "node:os";
40502
- import { join as join27 } from "node:path";
41380
+ import { join as join28 } from "node:path";
40503
41381
  var TTL_MS3 = 3e4;
40504
41382
  var STALE_AFTER_MS = 12 * 36e5;
40505
41383
  var TAIL_BYTES = 512 * 1024;
40506
41384
  var MAX_FILES = 8;
40507
41385
  var cache2 = null;
40508
41386
  var freshUntil2 = 0;
40509
- var codexSessionsDir = () => join27(process.env.CODEX_HOME ?? join27(homedir8(), ".codex"), "sessions");
41387
+ var codexSessionsDir = () => join28(process.env.CODEX_HOME ?? join28(homedir8(), ".codex"), "sessions");
40510
41388
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
40511
41389
  function windowLabel(minutes) {
40512
41390
  if (minutes === 300) return "Sesi 5 jam";
@@ -40534,14 +41412,14 @@ function toWindow(slot, raw, reached) {
40534
41412
  async function recentRollouts(dir2) {
40535
41413
  let entries3;
40536
41414
  try {
40537
- entries3 = (await readdir5(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
41415
+ entries3 = (await readdir6(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
40538
41416
  } catch {
40539
41417
  return [];
40540
41418
  }
40541
41419
  const stamped = await Promise.all(entries3.map(async (rel) => {
40542
- const full = join27(dir2, rel);
41420
+ const full = join28(dir2, rel);
40543
41421
  try {
40544
- return { full, mtime: (await stat2(full)).mtimeMs };
41422
+ return { full, mtime: (await stat3(full)).mtimeMs };
40545
41423
  } catch {
40546
41424
  return null;
40547
41425
  }
@@ -40625,8 +41503,8 @@ async function codex(app2) {
40625
41503
  // src/services/model-catalog.ts
40626
41504
  init_src2();
40627
41505
  init_src();
40628
- import { readFile as readFile4, writeFile as writeFile4, rename as rename4, mkdir as mkdir5 } from "node:fs/promises";
40629
- import { dirname as dirname12, join as join29 } from "node:path";
41506
+ import { readFile as readFile5, writeFile as writeFile5, rename as rename4, mkdir as mkdir6 } from "node:fs/promises";
41507
+ import { dirname as dirname13, join as join30 } from "node:path";
40630
41508
 
40631
41509
  // src/services/model-catalog-parser.ts
40632
41510
  init_zod();
@@ -40702,8 +41580,8 @@ init_config2();
40702
41580
  init_session_sandbox();
40703
41581
  import { spawn as spawn5 } from "node:child_process";
40704
41582
  import { mkdtemp as mkdtemp2, rm as rm6 } from "node:fs/promises";
40705
- import { tmpdir as tmpdir6 } from "node:os";
40706
- import { join as join28 } from "node:path";
41583
+ import { tmpdir as tmpdir5 } from "node:os";
41584
+ import { join as join29 } from "node:path";
40707
41585
  var MAX_BYTES = 4 * 1024 * 1024;
40708
41586
  var TIMEOUT_MS = 2e4;
40709
41587
  var quote2 = (s2) => "'" + s2.replaceAll("'", "'\\''") + "'";
@@ -40799,7 +41677,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
40799
41677
  });
40800
41678
  }
40801
41679
  async function probeModelCatalog(agent) {
40802
- const cwd = await mkdtemp2(join28(tmpdir6(), "hanoman-models-"));
41680
+ const cwd = await mkdtemp2(join29(tmpdir5(), "hanoman-models-"));
40803
41681
  const env = { ...process.env };
40804
41682
  for (const key of ["HANOMAN_CLAUDE_BIN", "HANOMAN_CODEX_BIN", "HANOMAN_PODMAN_BIN"]) {
40805
41683
  const value = effectiveStr(key);
@@ -40876,19 +41754,19 @@ function createModelCatalogService(deps) {
40876
41754
  }
40877
41755
  return { snapshot: () => state, refresh };
40878
41756
  }
40879
- var cacheFile = () => join29(resolveHome(), "model-catalog.json");
41757
+ var cacheFile = () => join30(resolveHome(), "model-catalog.json");
40880
41758
  var modelCatalogService = createModelCatalogService({
40881
41759
  probe: probeModelCatalog,
40882
41760
  read: async () => {
40883
- const content = await readFile4(cacheFile(), "utf8");
41761
+ const content = await readFile5(cacheFile(), "utf8");
40884
41762
  if (Buffer.byteLength(content) > 4 * 1024 * 1024) throw new Error("oversized cache");
40885
41763
  return JSON.parse(content);
40886
41764
  },
40887
41765
  write: async (catalog) => {
40888
41766
  const file = cacheFile();
40889
- await mkdir5(dirname12(file), { recursive: true });
41767
+ await mkdir6(dirname13(file), { recursive: true });
40890
41768
  const temp = file + "." + process.pid + ".tmp";
40891
- await writeFile4(temp, JSON.stringify(catalog), { mode: 384 });
41769
+ await writeFile5(temp, JSON.stringify(catalog), { mode: 384 });
40892
41770
  await rename4(temp, file);
40893
41771
  },
40894
41772
  now: Date.now,
@@ -40920,7 +41798,7 @@ init_src();
40920
41798
  init_src();
40921
41799
  init_config2();
40922
41800
  import { readFileSync as readFileSync15 } from "node:fs";
40923
- import { resolve as resolve18, dirname as dirname13 } from "node:path";
41801
+ import { resolve as resolve19, dirname as dirname14 } from "node:path";
40924
41802
  import { fileURLToPath as fileURLToPath4 } from "node:url";
40925
41803
  var UPDATE_COMMAND = "npm i -g hanoman@latest --prefer-online";
40926
41804
  function composeUpdate(x) {
@@ -40945,8 +41823,8 @@ var lastFetchAt = 0;
40945
41823
  var lastLatest = null;
40946
41824
  var lastStatus = "unavailable";
40947
41825
  function runningVersion() {
40948
- const here = dirname13(fileURLToPath4(import.meta.url));
40949
- for (const p3 of [resolve18(here, "build-info.json"), resolve18(here, "../package.json")]) {
41826
+ const here = dirname14(fileURLToPath4(import.meta.url));
41827
+ for (const p3 of [resolve19(here, "build-info.json"), resolve19(here, "../package.json")]) {
40950
41828
  try {
40951
41829
  const v = JSON.parse(readFileSync15(p3, "utf8")).version;
40952
41830
  if (typeof v === "string" && v) return v;
@@ -41138,20 +42016,10 @@ init_src();
41138
42016
  init_src();
41139
42017
 
41140
42018
  // src/services/scheduler/state.ts
42019
+ init_session_launch_gate();
42020
+ init_src();
41141
42021
  init_pty();
41142
-
41143
- // src/services/scheduler/config.ts
41144
- init_db();
41145
- init_settings3();
41146
- async function getScheduler() {
41147
- return (await getSetting()).scheduler;
41148
- }
41149
- async function setScheduler(next) {
41150
- const cur = await getSetting();
41151
- const data = { ...cur, scheduler: next };
41152
- await prisma.setting.upsert({ where: { id: 1 }, update: { data }, create: { id: 1, data } });
41153
- return next;
41154
- }
42022
+ init_config3();
41155
42023
 
41156
42024
  // src/services/scheduler/registry.ts
41157
42025
  var sources = /* @__PURE__ */ new Map();
@@ -41278,7 +42146,7 @@ async function isQueued(id2) {
41278
42146
  // src/services/scheduler/state.ts
41279
42147
  async function buildSchedulerState() {
41280
42148
  const cfg = await getScheduler();
41281
- const live = (await listSessionsAsync()).filter((s2) => !s2.exited);
42149
+ const live = (await listPanesAsync()).filter((s2) => !s2.exited);
41282
42150
  const srcView = (id2, sc, minCount) => {
41283
42151
  const last = getLastRun(id2);
41284
42152
  return {
@@ -41296,8 +42164,16 @@ async function buildSchedulerState() {
41296
42164
  ];
41297
42165
  const counts = await queueCounts();
41298
42166
  const launchedSpecs = new Set((await listQueue("launched")).map((q) => q.specId));
41299
- const sessions = live.filter((s2) => !!s2.specId && launchedSpecs.has(s2.specId));
41300
- return { config: cfg, cap: cfg.maxConcurrent, liveCount: live.length, sources: sources2, queueCounts: counts, sessions };
42167
+ const sessions = live.filter((s2) => !!s2.specId && launchedSpecs.has(s2.specId)).map((s2) => zSchedulerSessionView.parse(s2));
42168
+ return {
42169
+ config: cfg,
42170
+ cap: cfg.maxConcurrent,
42171
+ liveCount: live.length,
42172
+ sources: sources2,
42173
+ queueCounts: counts,
42174
+ sessions,
42175
+ admission: currentLaunchStatus(live, cfg)
42176
+ };
41301
42177
  }
41302
42178
 
41303
42179
  // src/services/tickets-list.ts
@@ -41385,6 +42261,7 @@ init_pty();
41385
42261
  init_db();
41386
42262
  init_pty();
41387
42263
  init_session_phases();
42264
+ init_config3();
41388
42265
  init_notifications2();
41389
42266
 
41390
42267
  // src/services/lead/apply.ts
@@ -42248,7 +43125,7 @@ async function events_default(app2, opts) {
42248
43125
  // src/routes/device-tokens.ts
42249
43126
  init_src();
42250
43127
  init_db();
42251
- var view3 = (t) => ({
43128
+ var view4 = (t) => ({
42252
43129
  id: t.id,
42253
43130
  name: t.name,
42254
43131
  createdAt: t.createdAt.toISOString(),
@@ -42256,7 +43133,7 @@ var view3 = (t) => ({
42256
43133
  revokedAt: t.revokedAt?.toISOString() ?? null
42257
43134
  });
42258
43135
  async function device_tokens_default(app2) {
42259
- app2.get("/device-tokens", async (req) => (await prisma.deviceToken.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: "desc" } })).map(view3));
43136
+ app2.get("/device-tokens", async (req) => (await prisma.deviceToken.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: "desc" } })).map(view4));
42260
43137
  app2.post("/device-tokens", async (req, reply) => {
42261
43138
  const p3 = zIssueDeviceToken.safeParse(req.body);
42262
43139
  if (!p3.success) return reply.code(400).send({ error: p3.error.flatten() });
@@ -42492,7 +43369,7 @@ async function presence_default(app2) {
42492
43369
 
42493
43370
  // src/routes/session-results.ts
42494
43371
  init_db();
42495
- var view4 = (r) => ({
43372
+ var view5 = (r) => ({
42496
43373
  id: r.id,
42497
43374
  projectId: r.projectId,
42498
43375
  specId: r.specId,
@@ -42515,7 +43392,7 @@ async function session_results_default(app2) {
42515
43392
  orderBy: { createdAt: "desc" },
42516
43393
  take: take3
42517
43394
  });
42518
- return rows.map(view4);
43395
+ return rows.map(view5);
42519
43396
  });
42520
43397
  app2.delete("/session-results", async (req, reply) => {
42521
43398
  const { projectId, before: before2 } = req.query;
@@ -42534,280 +43411,6 @@ async function session_results_default(app2) {
42534
43411
  });
42535
43412
  }
42536
43413
 
42537
- // src/services/session-history.ts
42538
- init_db();
42539
- init_pty();
42540
- import { randomUUID as randomUUID9 } from "node:crypto";
42541
-
42542
- // src/services/transcript-store.ts
42543
- init_config2();
42544
- init_src2();
42545
- import { randomUUID as randomUUID8 } from "node:crypto";
42546
- import { mkdir as mkdir6, writeFile as writeFile5, readFile as readFile5, unlink as unlink4, readdir as readdir6, stat as stat3 } from "node:fs/promises";
42547
- import { join as join30, resolve as resolve19, basename as basename6 } from "node:path";
42548
- var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
42549
- function transcriptDir() {
42550
- return resolve19(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
42551
- }
42552
- function clamp(text) {
42553
- const buf = Buffer.from(text, "utf8");
42554
- if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
42555
- const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
42556
- let tail2 = buf.subarray(cut).toString("utf8");
42557
- const nl = tail2.indexOf("\n");
42558
- if (nl >= 0) tail2 = tail2.slice(nl + 1);
42559
- return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
42560
- ${tail2}`, truncated: true };
42561
- }
42562
- async function saveTranscript(text) {
42563
- if (!text.trim()) return { key: "", bytes: 0, truncated: false };
42564
- const { body, truncated } = clamp(text);
42565
- const dir2 = transcriptDir();
42566
- await mkdir6(dir2, { recursive: true, mode: 448 });
42567
- const key = `${randomUUID8()}.log`;
42568
- await writeFile5(join30(dir2, key), body, { encoding: "utf8", mode: 384 });
42569
- return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
42570
- }
42571
- async function readTranscript(key) {
42572
- if (!key) return null;
42573
- try {
42574
- return await readFile5(join30(transcriptDir(), basename6(key)), "utf8");
42575
- } catch {
42576
- return null;
42577
- }
42578
- }
42579
- async function deleteTranscript(key) {
42580
- if (!key) return;
42581
- try {
42582
- await unlink4(join30(transcriptDir(), basename6(key)));
42583
- } catch (e) {
42584
- if (e.code !== "ENOENT") throw e;
42585
- }
42586
- }
42587
- async function listTranscripts() {
42588
- const dir2 = transcriptDir();
42589
- let names;
42590
- try {
42591
- names = await readdir6(dir2);
42592
- } catch (e) {
42593
- if (e.code === "ENOENT") return [];
42594
- throw e;
42595
- }
42596
- const rows = [];
42597
- for (const name2 of names) {
42598
- if (!name2.endsWith(".log")) continue;
42599
- try {
42600
- rows.push({ key: name2, mtimeMs: (await stat3(join30(dir2, name2))).mtimeMs });
42601
- } catch {
42602
- }
42603
- }
42604
- return rows;
42605
- }
42606
-
42607
- // src/services/session-history.ts
42608
- var CLOSED = "closed";
42609
- var RECONCILED = "reconciled";
42610
- var view5 = (r) => ({
42611
- id: r.id,
42612
- sessionId: r.sessionId,
42613
- projectId: r.projectId,
42614
- specId: r.specId,
42615
- title: r.title,
42616
- kind: r.kind,
42617
- flow: r.flow,
42618
- agent: r.agent,
42619
- model: r.model,
42620
- effort: r.effort,
42621
- branch: r.branch,
42622
- cwd: r.cwd,
42623
- startedAt: r.startedAt.toISOString(),
42624
- endedAt: r.endedAt?.toISOString() ?? null,
42625
- endedReason: r.endedReason,
42626
- reconciledAt: r.reconciledAt?.toISOString() ?? null,
42627
- exitCode: r.exitCode,
42628
- transcriptBytes: r.transcriptBytes
42629
- });
42630
- async function titleFor(specId) {
42631
- if (!specId) return null;
42632
- const s2 = await prisma.spec.findUnique({ where: { id: specId }, select: { title: true } });
42633
- return s2?.title ?? null;
42634
- }
42635
- async function beginSession(b) {
42636
- await prisma.sessionHistory.create({
42637
- data: {
42638
- id: randomUUID9(),
42639
- sessionId: b.sessionId,
42640
- projectId: b.projectId,
42641
- specId: b.specId ?? null,
42642
- title: await titleFor(b.specId),
42643
- kind: b.kind,
42644
- flow: b.flow ?? null,
42645
- agent: b.agent,
42646
- model: b.model ?? null,
42647
- effort: b.effort ?? null,
42648
- branch: b.branch ?? null,
42649
- cwd: b.cwd
42650
- }
42651
- });
42652
- }
42653
- async function finishSession(d) {
42654
- const open5 = await prisma.sessionHistory.findFirst({
42655
- where: { sessionId: d.sessionId, endedAt: null },
42656
- orderBy: { startedAt: "desc" }
42657
- });
42658
- if (!open5) return;
42659
- let t = { key: "", bytes: 0 };
42660
- if (d.transcript) {
42661
- try {
42662
- t = await saveTranscript(d.transcript);
42663
- } catch (e) {
42664
- console.error("riwayat sesi (transkrip tak tersimpan):", e);
42665
- }
42666
- }
42667
- await prisma.sessionHistory.update({
42668
- where: { id: open5.id },
42669
- data: {
42670
- endedAt: /* @__PURE__ */ new Date(),
42671
- endedReason: CLOSED,
42672
- exitCode: d.exitCode,
42673
- transcriptKey: t.key || null,
42674
- transcriptBytes: t.key ? t.bytes : null
42675
- }
42676
- });
42677
- }
42678
- async function listHistory(q) {
42679
- const term = q.q?.trim();
42680
- const where = {
42681
- ...q.projectId ? { projectId: q.projectId } : {},
42682
- ...q.specId ? { specId: q.specId } : {},
42683
- ...q.kind ? { kind: q.kind } : {},
42684
- ...term ? {
42685
- // SPEC-398 · ADR-0086 · SQLite tak punya `mode: "insensitive"`; `LIKE`-nya sudah
42686
- // case-insensitive untuk ASCII, jadi pencarian ini tetap berperilaku sama.
42687
- OR: [
42688
- { sessionId: { contains: term } },
42689
- { specId: { contains: term } },
42690
- { title: { contains: term } },
42691
- { branch: { contains: term } }
42692
- ]
42693
- } : {}
42694
- };
42695
- const total = await prisma.sessionHistory.count({ where });
42696
- const pageSize = q.limit ? Math.min(Math.max(Math.floor(+q.limit) || 1, 1), 200) : total || 1;
42697
- const page = Math.max(Math.floor(+(q.page ?? 1)) || 1, 1);
42698
- const rows = await prisma.sessionHistory.findMany({
42699
- where,
42700
- orderBy: { startedAt: "desc" },
42701
- skip: (page - 1) * pageSize,
42702
- take: pageSize
42703
- });
42704
- return { items: rows.map(view5), total, page, pageSize };
42705
- }
42706
- async function getHistory(id2) {
42707
- const r = await prisma.sessionHistory.findUnique({ where: { id: id2 } });
42708
- return r ? { ...view5(r), hasTranscript: !!r.transcriptKey } : null;
42709
- }
42710
- async function transcriptOf(id2) {
42711
- const r = await prisma.sessionHistory.findUnique({ where: { id: id2 }, select: { transcriptKey: true } });
42712
- if (!r?.transcriptKey) return null;
42713
- const text = await readTranscript(r.transcriptKey);
42714
- return text === null ? null : { text, bytes: Buffer.byteLength(text, "utf8") };
42715
- }
42716
- var PURGE_BATCH = 200;
42717
- async function purgeHistory(q) {
42718
- const where = {
42719
- ...q.projectId ? { projectId: q.projectId } : {},
42720
- ...q.before ? { startedAt: { lt: q.before } } : {}
42721
- };
42722
- const doomed = await prisma.sessionHistory.findMany({ where, select: { id: true, transcriptKey: true } });
42723
- const report = { purged: 0, transcriptsDeleted: 0, transcriptsFailed: 0 };
42724
- for (let i = 0; i < doomed.length; i += PURGE_BATCH) {
42725
- const batch = doomed.slice(i, i + PURGE_BATCH);
42726
- const { count } = await prisma.sessionHistory.deleteMany({ where: { id: { in: batch.map((d) => d.id) } } });
42727
- report.purged += count;
42728
- for (const d of batch) {
42729
- if (!d.transcriptKey) continue;
42730
- try {
42731
- await deleteTranscript(d.transcriptKey);
42732
- report.transcriptsDeleted++;
42733
- } catch {
42734
- report.transcriptsFailed++;
42735
- }
42736
- }
42737
- }
42738
- return report;
42739
- }
42740
- var TRANSCRIPT_GC_GRACE_MS = 60 * 6e4;
42741
- async function reconcileTranscripts(opts = {}) {
42742
- const rows = await prisma.sessionHistory.findMany({
42743
- where: { transcriptKey: { not: null } },
42744
- select: { id: true, transcriptKey: true }
42745
- });
42746
- const referenced = new Set(rows.map((r) => r.transcriptKey));
42747
- const onDisk = await listTranscripts();
42748
- const present = new Set(onDisk.map((f) => f.key));
42749
- const cutoff = Date.now() - (opts.graceMs ?? TRANSCRIPT_GC_GRACE_MS);
42750
- const report = { orphans: 0, dangling: 0, failed: 0 };
42751
- for (const f of onDisk) {
42752
- if (referenced.has(f.key) || f.mtimeMs > cutoff) continue;
42753
- if (opts.dryRun) {
42754
- report.orphans++;
42755
- continue;
42756
- }
42757
- try {
42758
- await deleteTranscript(f.key);
42759
- report.orphans++;
42760
- } catch {
42761
- report.failed++;
42762
- }
42763
- }
42764
- for (const r of rows) {
42765
- if (present.has(r.transcriptKey)) continue;
42766
- if (opts.dryRun) {
42767
- report.dangling++;
42768
- continue;
42769
- }
42770
- try {
42771
- await prisma.sessionHistory.update({
42772
- where: { id: r.id },
42773
- data: { transcriptKey: null, transcriptBytes: null }
42774
- });
42775
- report.dangling++;
42776
- } catch {
42777
- report.failed++;
42778
- }
42779
- }
42780
- return report;
42781
- }
42782
- async function reconcileHistory(liveSessionIds) {
42783
- const open5 = await prisma.sessionHistory.findMany({
42784
- where: { endedAt: null },
42785
- select: { id: true, sessionId: true, updatedAt: true }
42786
- });
42787
- const live = new Set(liveSessionIds);
42788
- const at = /* @__PURE__ */ new Date();
42789
- let closed = 0;
42790
- for (const r of open5) {
42791
- if (live.has(r.sessionId)) continue;
42792
- await prisma.sessionHistory.update({
42793
- where: { id: r.id },
42794
- data: { endedAt: r.updatedAt, endedReason: RECONCILED, reconciledAt: at }
42795
- });
42796
- closed++;
42797
- }
42798
- return closed;
42799
- }
42800
- function installSessionHistory() {
42801
- registerSessionHooks({
42802
- onBirth: (b) => {
42803
- void beginSession(b).catch((e) => console.error("riwayat sesi (lahir):", e));
42804
- },
42805
- onDeath: (d) => {
42806
- void finishSession(d).catch((e) => console.error("riwayat sesi (tutup):", e));
42807
- }
42808
- });
42809
- }
42810
-
42811
43414
  // src/routes/session-history.ts
42812
43415
  async function session_history_default(app2) {
42813
43416
  app2.get("/terminal/history", async (req) => {
@@ -42842,17 +43445,154 @@ init_pty();
42842
43445
  // src/services/agent-invocations.ts
42843
43446
  init_src();
42844
43447
  init_db();
42845
- import { createHash as createHash8, randomUUID as randomUUID10 } from "node:crypto";
43448
+ import { createHash as createHash9, randomUUID as randomUUID10 } from "node:crypto";
42846
43449
  import { execFileSync as execFileSync5 } from "node:child_process";
42847
43450
  import { homedir as homedir9 } from "node:os";
42848
43451
  import { readFileSync as readFileSync16, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
42849
43452
  import { isAbsolute as isAbsolute7, relative as relative3, resolve as resolve20 } from "node:path";
43453
+
43454
+ // src/services/session-event-relay.ts
43455
+ init_session_event_token();
43456
+ init_session_event_spool();
43457
+ import { constants as constants3 } from "node:fs";
43458
+ import { mkdir as mkdir7, open as open4, readdir as readdir7, rm as rm7 } from "node:fs/promises";
43459
+ import { join as join31 } from "node:path";
43460
+ var MAX_EVENT_BYTES = 1e6;
43461
+ var MAX_FILES_PER_DRAIN = 1e3;
43462
+ var SESSION_ID_RE = /^[a-z0-9_-]+$/;
43463
+ var observations = /* @__PURE__ */ new Map();
43464
+ function sessionEventRelayStatus(root = sessionEventSpoolRoot()) {
43465
+ return { ...observations.get(root) ?? {
43466
+ state: "unobserved",
43467
+ checkedAt: null,
43468
+ lastDeliveryAt: null,
43469
+ lastIssueAt: null,
43470
+ retryPending: 0,
43471
+ retryAttempts: 0,
43472
+ droppedEvents: 0
43473
+ } };
43474
+ }
43475
+ async function drainSessionEventSpool(app2, root = sessionEventSpoolRoot()) {
43476
+ const previous = sessionEventRelayStatus(root);
43477
+ const current = { retries: 0, dropped: 0 };
43478
+ let delivered = 0;
43479
+ let failed = false;
43480
+ try {
43481
+ delivered = await drainSpool(app2, root, current);
43482
+ return delivered;
43483
+ } catch (error) {
43484
+ failed = true;
43485
+ throw error;
43486
+ } finally {
43487
+ const now = (/* @__PURE__ */ new Date()).toISOString();
43488
+ observations.set(root, {
43489
+ state: failed || current.retries > 0 ? "degraded" : "ready",
43490
+ checkedAt: now,
43491
+ lastDeliveryAt: delivered ? now : previous.lastDeliveryAt,
43492
+ lastIssueAt: failed || current.retries || current.dropped ? now : previous.lastIssueAt,
43493
+ retryPending: current.retries,
43494
+ retryAttempts: previous.retryAttempts + current.retries,
43495
+ droppedEvents: previous.droppedEvents + current.dropped
43496
+ });
43497
+ }
43498
+ }
43499
+ async function drainSpool(app2, root, observation) {
43500
+ await mkdir7(root, { recursive: true, mode: 448 });
43501
+ let delivered = 0;
43502
+ let examined = 0;
43503
+ let readBuffer;
43504
+ for (const session of await readdir7(root, { withFileTypes: true })) {
43505
+ if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
43506
+ const dir2 = join31(root, session.name);
43507
+ let entries3;
43508
+ try {
43509
+ entries3 = await readdir7(dir2, { withFileTypes: true });
43510
+ } catch {
43511
+ continue;
43512
+ }
43513
+ for (const entry of entries3) {
43514
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
43515
+ if (++examined > MAX_FILES_PER_DRAIN) return delivered;
43516
+ const path = join31(dir2, entry.name);
43517
+ let payload;
43518
+ let handle;
43519
+ try {
43520
+ handle = await open4(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
43521
+ const stat4 = await handle.stat();
43522
+ if (!stat4.isFile() || stat4.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
43523
+ readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
43524
+ const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
43525
+ if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
43526
+ const parsed = JSON.parse(readBuffer.subarray(0, bytesRead).toString("utf8"));
43527
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
43528
+ throw new Error("payload event bukan object");
43529
+ }
43530
+ payload = parsed;
43531
+ } catch {
43532
+ observation.dropped++;
43533
+ await rm7(path, { force: true }).catch(() => {
43534
+ });
43535
+ continue;
43536
+ } finally {
43537
+ await handle?.close().catch(() => {
43538
+ });
43539
+ }
43540
+ try {
43541
+ const response = await app2.inject({
43542
+ method: "POST",
43543
+ url: "/api/session-events",
43544
+ headers: {
43545
+ authorization: `Bearer ${sessionEventToken(session.name)}`,
43546
+ "x-hanoman-session": session.name
43547
+ },
43548
+ payload
43549
+ });
43550
+ if (response.statusCode === 429 || response.statusCode >= 500) {
43551
+ observation.retries++;
43552
+ continue;
43553
+ }
43554
+ await rm7(path, { force: true }).catch(() => {
43555
+ });
43556
+ if (response.statusCode >= 200 && response.statusCode < 300) delivered++;
43557
+ else observation.dropped++;
43558
+ } catch {
43559
+ observation.retries++;
43560
+ }
43561
+ }
43562
+ }
43563
+ return delivered;
43564
+ }
43565
+ function startSessionEventRelay(app2, options2 = {}) {
43566
+ const root = options2.root ?? sessionEventSpoolRoot();
43567
+ let running = false;
43568
+ const tick6 = async () => {
43569
+ if (running) return;
43570
+ running = true;
43571
+ try {
43572
+ await drainSessionEventSpool({ inject: async (request) => app2.inject(request) }, root);
43573
+ } catch (error) {
43574
+ console.error("session event relay gagal:", error);
43575
+ } finally {
43576
+ running = false;
43577
+ }
43578
+ };
43579
+ const timer9 = setInterval(() => {
43580
+ void tick6();
43581
+ }, options2.intervalMs ?? 250);
43582
+ timer9.unref();
43583
+ app2.addHook("onClose", async () => {
43584
+ clearInterval(timer9);
43585
+ });
43586
+ void tick6();
43587
+ }
43588
+
43589
+ // src/services/agent-invocations.ts
42850
43590
  var MAX_EXCERPT_BYTES = 4096;
42851
43591
  var MAX_TRANSCRIPT_BYTES2 = 10 * 1024 * 1024;
42852
43592
  var ANSI2 = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
42853
43593
  var snapshotHashes = /* @__PURE__ */ new Map();
42854
43594
  var keyOf = (x) => `${x.sessionId}\0${x.runtimeInvocationId}`;
42855
- var hash2 = (value) => createHash8("sha256").update(value).digest("hex");
43595
+ var hash2 = (value) => createHash9("sha256").update(value).digest("hex");
42856
43596
  var defaultGitStatus = (cwd) => {
42857
43597
  try {
42858
43598
  return execFileSync5("git", ["-C", cwd, "status", "--porcelain=v1", "-z"], {
@@ -42958,6 +43698,7 @@ async function startAgentInvocation(input, io = {}) {
42958
43698
  customAgentId: input.customAgentId ?? null,
42959
43699
  agentName: input.agentName,
42960
43700
  model: input.model ?? null,
43701
+ definitionHash: input.definitionHash ?? null,
42961
43702
  status: "running",
42962
43703
  startedAt
42963
43704
  }
@@ -43006,6 +43747,7 @@ async function stopAgentInvocation(input, io = {}) {
43006
43747
  customAgentId: input.customAgentId ?? null,
43007
43748
  agentName: input.agentName,
43008
43749
  model: input.model ?? null,
43750
+ definitionHash: input.definitionHash ?? null,
43009
43751
  startedAt,
43010
43752
  ...evidence
43011
43753
  }
@@ -43040,6 +43782,7 @@ var agentInvocationView = (row) => ({
43040
43782
  customAgentId: row.customAgentId,
43041
43783
  agentName: row.agentName,
43042
43784
  model: row.model,
43785
+ definitionHash: row.definitionHash,
43043
43786
  status: row.status,
43044
43787
  startedAt: row.startedAt.toISOString(),
43045
43788
  endedAt: row.endedAt?.toISOString() ?? null,
@@ -43052,7 +43795,8 @@ var agentInvocationView = (row) => ({
43052
43795
  workspaceChanged: row.workspaceChanged,
43053
43796
  disposition: AGENT_DISPOSITIONS.includes(row.disposition) ? row.disposition : "pending",
43054
43797
  dispositionNote: row.dispositionNote,
43055
- evaluatedAt: row.evaluatedAt?.toISOString() ?? null
43798
+ evaluatedAt: row.evaluatedAt?.toISOString() ?? null,
43799
+ reworkRequired: row.reworkRequired
43056
43800
  });
43057
43801
  var median = (values) => {
43058
43802
  if (values.length === 0) return null;
@@ -43074,8 +43818,17 @@ async function agentMetrics(query2) {
43074
43818
  };
43075
43819
  const rows = await prisma.agentInvocation.findMany({ where, orderBy: { startedAt: "desc" } });
43076
43820
  const groups = /* @__PURE__ */ new Map();
43077
- for (const row of rows) groups.set(row.agentName, [...groups.get(row.agentName) ?? [], row]);
43078
- const agents = [...groups.entries()].map(([agentName, invocations]) => {
43821
+ const variantGroups = /* @__PURE__ */ new Map();
43822
+ const add = (map, key, row) => {
43823
+ const group2 = map.get(key);
43824
+ if (group2) group2.push(row);
43825
+ else map.set(key, [row]);
43826
+ };
43827
+ for (const row of rows) {
43828
+ add(groups, row.agentName, row);
43829
+ add(variantGroups, JSON.stringify([row.agentName, row.runtime, row.model, row.definitionHash]), row);
43830
+ }
43831
+ const summarize = (agentName, invocations) => {
43079
43832
  const dispositions = { pending: 0, accepted: 0, partial: 0, rejected: 0, falsePositive: 0 };
43080
43833
  for (const row of invocations) {
43081
43834
  if (row.disposition === "accepted") dispositions.accepted++;
@@ -43094,12 +43847,47 @@ async function agentMetrics(query2) {
43094
43847
  cachedTokens: availableSum(invocations.map((row) => row.cachedTokens)),
43095
43848
  dispositions,
43096
43849
  operationalPrecision: evaluated ? (dispositions.accepted + dispositions.partial) / evaluated : null,
43097
- workspaceChanged: invocations.some((row) => row.workspaceChanged)
43850
+ workspaceChanged: invocations.some((row) => row.workspaceChanged),
43851
+ evaluatedCount: evaluated,
43852
+ rework: {
43853
+ required: invocations.filter((row) => row.reworkRequired === true).length,
43854
+ notRequired: invocations.filter((row) => row.reworkRequired === false).length,
43855
+ unknown: invocations.filter((row) => row.reworkRequired === null).length
43856
+ }
43098
43857
  };
43099
- }).sort((a, b) => a.agentName.localeCompare(b.agentName));
43100
- return { agents, recent: rows.slice(0, 100).map(agentInvocationView) };
43858
+ };
43859
+ const agents = [...groups.entries()].map(([name2, group2]) => summarize(name2, group2)).sort((a, b) => a.agentName.localeCompare(b.agentName));
43860
+ const variants = [...variantGroups.values()].map((group2) => ({
43861
+ ...summarize(group2[0].agentName, group2),
43862
+ runtime: group2[0].runtime === "codex" ? "codex" : "claude",
43863
+ model: group2[0].model,
43864
+ definitionHash: group2[0].definitionHash
43865
+ })).sort((a, b) => JSON.stringify([a.agentName, a.runtime, a.model, a.definitionHash]).localeCompare(JSON.stringify([b.agentName, b.runtime, b.model, b.definitionHash])));
43866
+ const lastEventAt = rows.reduce((latest, row) => Math.max(latest, row.startedAt.getTime(), row.endedAt?.getTime() ?? 0), 0);
43867
+ const sampleRows = /* @__PURE__ */ new Map();
43868
+ for (const group2 of groups.values()) {
43869
+ for (const category of [
43870
+ (row) => row.disposition === "pending",
43871
+ (row) => row.disposition !== "pending",
43872
+ (row) => row.reworkRequired === true
43873
+ ]) {
43874
+ for (const row of group2.filter(category).slice(0, 2)) sampleRows.set(row.id, row);
43875
+ }
43876
+ }
43877
+ return {
43878
+ agents,
43879
+ variants,
43880
+ recent: rows.slice(0, 100).map(agentInvocationView),
43881
+ samples: [...sampleRows.values()].map(agentInvocationView),
43882
+ telemetry: {
43883
+ state: rows.length ? "observed" : "unobserved",
43884
+ lastEventAt: rows.length ? new Date(lastEventAt).toISOString() : null,
43885
+ incompleteCount: rows.filter((row) => !row.endedAt || row.resultHash === null).length,
43886
+ relay: sessionEventRelayStatus()
43887
+ }
43888
+ };
43101
43889
  }
43102
- async function updateAgentInvocationDisposition(id2, disposition, note) {
43890
+ async function updateAgentInvocationDisposition(id2, disposition, note, reworkRequired) {
43103
43891
  const exists = await prisma.agentInvocation.findUnique({ where: { id: id2 } });
43104
43892
  if (!exists) return null;
43105
43893
  const row = await prisma.agentInvocation.update({
@@ -43107,7 +43895,8 @@ async function updateAgentInvocationDisposition(id2, disposition, note) {
43107
43895
  data: {
43108
43896
  disposition,
43109
43897
  dispositionNote: note?.trim() || null,
43110
- evaluatedAt: /* @__PURE__ */ new Date()
43898
+ evaluatedAt: /* @__PURE__ */ new Date(),
43899
+ ...reworkRequired !== void 0 ? { reworkRequired } : {}
43111
43900
  }
43112
43901
  });
43113
43902
  return agentInvocationView(row);
@@ -43144,6 +43933,7 @@ async function session_events_default(app2) {
43144
43933
  customAgentId: meta.id,
43145
43934
  agentName: meta.name,
43146
43935
  model: meta.model,
43936
+ definitionHash: meta.definitionHash,
43147
43937
  cwd: s2.cwd
43148
43938
  };
43149
43939
  const outcome = lifecycle === "SubagentStart" ? await startAgentInvocation(identity) : await stopAgentInvocation({
@@ -43250,9 +44040,9 @@ init_db();
43250
44040
  init_db();
43251
44041
  init_uploads();
43252
44042
  init_src();
43253
- import { createHash as createHash10, randomBytes as randomBytes6 } from "node:crypto";
44043
+ import { createHash as createHash11, randomBytes as randomBytes6 } from "node:crypto";
43254
44044
  function hashAccessKey(key) {
43255
- return createHash10("sha256").update(key).digest("hex");
44045
+ return createHash11("sha256").update(key).digest("hex");
43256
44046
  }
43257
44047
  function generateAccessKey() {
43258
44048
  const key = "hnm_tkt_" + randomBytes6(24).toString("hex");
@@ -43646,6 +44436,7 @@ async function tickets_default(app2, opts = {}) {
43646
44436
  init_zod();
43647
44437
  init_src();
43648
44438
  init_db();
44439
+ init_config3();
43649
44440
 
43650
44441
  // src/services/scheduler/cron.ts
43651
44442
  init_db();
@@ -44377,6 +45168,7 @@ async function changelog_default(app2) {
44377
45168
  }
44378
45169
 
44379
45170
  // src/routes/custom-agents.ts
45171
+ init_src2();
44380
45172
  init_src();
44381
45173
  init_db();
44382
45174
  init_sync_notify();
@@ -44398,10 +45190,28 @@ var availabilityOf = (r, requestedRuntime) => {
44398
45190
  availabilityReason: "isolated-worktree belum tersedia untuk subagent Codex"
44399
45191
  };
44400
45192
  }
45193
+ if (effectiveRuntime === "codex") {
45194
+ const support = currentCustomAgentRuntimeSupport();
45195
+ if (!support.ok) {
45196
+ return {
45197
+ available: false,
45198
+ availabilityReason: `native subagent perlu Codex >= ${CODEX_NATIVE_AGENTS_MIN_CLIENT}; versi ${support.version ?? "belum terdeteksi"}`
45199
+ };
45200
+ }
45201
+ }
44401
45202
  return { available: true };
44402
45203
  };
45204
+ var selectionReasonOf = (r, availability) => {
45205
+ if (!r.enabled) return "dinonaktifkan operator";
45206
+ if (!availability.available) return "tidak masuk registry runtime ini";
45207
+ if (activationOf(r.activation) === "smart") {
45208
+ return "tersedia sepanjang sesi; parent menilai kebutuhan dari pekerjaan terbaru";
45209
+ }
45210
+ return "selalu tersedia sepanjang sesi";
45211
+ };
44403
45212
  var view8 = (r, projectId, stamps = {}, requestedRuntime) => {
44404
45213
  const builtin = r.projectId === null && BUILTIN_AGENT_NAMES.includes(r.name);
45214
+ const availability = availabilityOf(r, requestedRuntime);
44405
45215
  return {
44406
45216
  id: r.id,
44407
45217
  projectId: r.projectId,
@@ -44423,7 +45233,8 @@ var view8 = (r, projectId, stamps = {}, requestedRuntime) => {
44423
45233
  // pernah menyentuhnya) dibaca sebagai "disunting" — lebih baik menandai berlebih daripada
44424
45234
  // menjanjikan "asli bawaan" untuk isi yang tak bisa kita buktikan.
44425
45235
  builtinEdited: builtin ? stamps[r.name] !== rowFingerprint(r) : false,
44426
- ...availabilityOf(r, requestedRuntime),
45236
+ ...availability,
45237
+ selectionReason: selectionReasonOf(r, availability),
44427
45238
  ...projectId ? { inherited: r.projectId === null } : {}
44428
45239
  };
44429
45240
  };
@@ -44631,7 +45442,8 @@ var zQuery = external_exports.object({
44631
45442
  });
44632
45443
  var zPatch = external_exports.object({
44633
45444
  disposition: external_exports.enum(["accepted", "partial", "rejected", "false-positive"]),
44634
- note: external_exports.string().max(500).nullable().optional()
45445
+ note: external_exports.string().max(500).nullable().optional(),
45446
+ reworkRequired: external_exports.boolean().nullable().optional()
44635
45447
  }).strict();
44636
45448
  async function custom_agent_metrics_default(app2) {
44637
45449
  app2.get("/custom-agents/metrics", async (req, reply) => {
@@ -44646,7 +45458,12 @@ async function custom_agent_metrics_default(app2) {
44646
45458
  const body = zPatch.safeParse(req.body);
44647
45459
  if (!body.success) return reply.code(400).send({ error: body.error.issues[0]?.message });
44648
45460
  const id2 = String(req.params.id ?? "");
44649
- const view13 = await updateAgentInvocationDisposition(id2, body.data.disposition, body.data.note);
45461
+ const view13 = await updateAgentInvocationDisposition(
45462
+ id2,
45463
+ body.data.disposition,
45464
+ body.data.note,
45465
+ body.data.reworkRequired
45466
+ );
44650
45467
  return view13 ?? reply.code(404).send({ error: "invocation tidak ditemukan" });
44651
45468
  });
44652
45469
  }
@@ -45357,8 +46174,8 @@ async function telegramInboundReadiness(deps = {}) {
45357
46174
  if (!setting.agentAccessEnabled) {
45358
46175
  return { ok: false, reason: "Akses agent mati \u2014 nyalakan master switch di Akses AI Agent.", missingCapabilities: empty, polling: polling2 };
45359
46176
  }
45360
- const gate = await verifyTelegramAgentToken(token, deps);
45361
- if (!gate.ok) return { ok: false, reason: gate.reason, missingCapabilities: gate.missing, polling: polling2 };
46177
+ const gate2 = await verifyTelegramAgentToken(token, deps);
46178
+ if (!gate2.ok) return { ok: false, reason: gate2.reason, missingCapabilities: gate2.missing, polling: polling2 };
45362
46179
  if (!polling2) {
45363
46180
  return { ok: false, reason: "Kredensial sudah sah tapi gateway belum polling \u2014 nyalakan \u201CGateway aktif\u201D.", missingCapabilities: empty, polling: polling2 };
45364
46181
  }
@@ -45397,9 +46214,9 @@ async function saveTelegramCredentials(patch, deps = {}) {
45397
46214
  }
45398
46215
  const agentToken = writes.find(([key]) => key === "HANOMAN_TELEGRAM_AGENT_TOKEN")?.[1];
45399
46216
  if (agentToken !== void 0) {
45400
- const gate = await verifyTelegramAgentToken(agentToken, deps);
45401
- if (!gate.ok) {
45402
- const detail = gate.missing.length < TELEGRAM_REQUIRED_CAPABILITIES.length ? `${gate.reason} Kurang: ${gate.missing.join(", ")}.` : gate.reason;
46217
+ const gate2 = await verifyTelegramAgentToken(agentToken, deps);
46218
+ if (!gate2.ok) {
46219
+ const detail = gate2.missing.length < TELEGRAM_REQUIRED_CAPABILITIES.length ? `${gate2.reason} Kurang: ${gate2.missing.join(", ")}.` : gate2.reason;
45403
46220
  return { ok: false, key: "HANOMAN_TELEGRAM_AGENT_TOKEN", error: detail };
45404
46221
  }
45405
46222
  }
@@ -46146,8 +46963,8 @@ ${baru}`;
46146
46963
  init_src();
46147
46964
  init_db();
46148
46965
  import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
46149
- import { tmpdir as tmpdir7 } from "node:os";
46150
- import { join as join32 } from "node:path";
46966
+ import { tmpdir as tmpdir6 } from "node:os";
46967
+ import { join as join33 } from "node:path";
46151
46968
  var STAGE_LABEL = {
46152
46969
  brainstorming: "Dirumuskan",
46153
46970
  objective: "Dirumuskan",
@@ -46202,7 +47019,7 @@ ${c.body}
46202
47019
  ${baris.join("\n")}`;
46203
47020
  }
46204
47021
  async function buildChatWorkspace(projectId) {
46205
- const dir2 = mkdtempSync2(join32(tmpdir7(), "hanoman-portal-chat-"));
47022
+ const dir2 = mkdtempSync2(join33(tmpdir6(), "hanoman-portal-chat-"));
46206
47023
  try {
46207
47024
  const project = await prisma.project.findUnique({
46208
47025
  where: { id: projectId },
@@ -46223,7 +47040,7 @@ async function buildChatWorkspace(projectId) {
46223
47040
  });
46224
47041
  const files = [];
46225
47042
  const tulis = (rel, isi) => {
46226
- writeFileSync9(join32(dir2, rel), isi, { mode: 384 });
47043
+ writeFileSync9(join33(dir2, rel), isi, { mode: 384 });
46227
47044
  files.push(rel);
46228
47045
  };
46229
47046
  tulis("project.md", renderProjectDoc(project));
@@ -46232,11 +47049,11 @@ async function buildChatWorkspace(projectId) {
46232
47049
  tulis("catatan-rilis.md", renderChangelogDoc(changelogs));
46233
47050
  const prds = await listPrds(projectId);
46234
47051
  if (prds.length) {
46235
- mkdirSync12(join32(dir2, "dokumen"), { mode: 448 });
47052
+ mkdirSync12(join33(dir2, "dokumen"), { mode: 448 });
46236
47053
  for (const prd of prds) {
46237
47054
  const isi = await readPrd(projectId, prd.path);
46238
47055
  if (!isi) continue;
46239
- tulis(join32("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
47056
+ tulis(join33("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
46240
47057
  }
46241
47058
  }
46242
47059
  return { dir: dir2, files, cleanup: () => rmSync8(dir2, { recursive: true, force: true }) };
@@ -46453,7 +47270,7 @@ var toMessageView = (m) => ({
46453
47270
  createdAt: m.createdAt.toISOString()
46454
47271
  });
46455
47272
  async function portal_chat_default(app2) {
46456
- async function gate(userId, projectId) {
47273
+ async function gate2(userId, projectId) {
46457
47274
  const s2 = await getSetting();
46458
47275
  if (!s2.portalChat.enabled) return null;
46459
47276
  if (!await hasProjectAccess(userId, projectId)) return null;
@@ -46465,13 +47282,13 @@ async function portal_chat_default(app2) {
46465
47282
  }
46466
47283
  app2.get("/portal/projects/:id/chat", async (req, reply) => {
46467
47284
  const { id: id2 } = req.params;
46468
- const cfg = await gate(req.user.id, id2);
47285
+ const cfg = await gate2(req.user.id, id2);
46469
47286
  if (!cfg) return reply.code(404).send(NOT_FOUND2);
46470
47287
  return quotaView(id2, cfg);
46471
47288
  });
46472
47289
  app2.get("/portal/projects/:id/chat/sessions", async (req, reply) => {
46473
47290
  const { id: id2 } = req.params;
46474
- if (!await gate(req.user.id, id2)) return reply.code(404).send(NOT_FOUND2);
47291
+ if (!await gate2(req.user.id, id2)) return reply.code(404).send(NOT_FOUND2);
46475
47292
  const { page, limit } = req.query;
46476
47293
  const rows = await prisma.portalChatSession.findMany({
46477
47294
  where: { projectId: id2, userId: req.user.id },
@@ -46481,7 +47298,7 @@ async function portal_chat_default(app2) {
46481
47298
  });
46482
47299
  app2.post("/portal/projects/:id/chat/sessions", async (req, reply) => {
46483
47300
  const { id: id2 } = req.params;
46484
- const cfg = await gate(req.user.id, id2);
47301
+ const cfg = await gate2(req.user.id, id2);
46485
47302
  if (!cfg) return reply.code(404).send(NOT_FOUND2);
46486
47303
  const parsed = zStart.safeParse(req.body);
46487
47304
  if (!parsed.success) return reply.code(400).send({ error: "tipe sesi tak dikenal" });
@@ -46497,7 +47314,7 @@ async function portal_chat_default(app2) {
46497
47314
  });
46498
47315
  app2.get("/portal/projects/:id/chat/sessions/:sid", async (req, reply) => {
46499
47316
  const { id: id2, sid } = req.params;
46500
- if (!await gate(req.user.id, id2)) return reply.code(404).send(NOT_FOUND2);
47317
+ if (!await gate2(req.user.id, id2)) return reply.code(404).send(NOT_FOUND2);
46501
47318
  const s2 = await ownSession(req.user.id, id2, sid);
46502
47319
  if (!s2) return reply.code(404).send(NOT_FOUND2);
46503
47320
  const { page, limit } = req.query;
@@ -46509,7 +47326,7 @@ async function portal_chat_default(app2) {
46509
47326
  });
46510
47327
  app2.post("/portal/projects/:id/chat/sessions/:sid/messages", async (req, reply) => {
46511
47328
  const { id: id2, sid } = req.params;
46512
- const cfg = await gate(req.user.id, id2);
47329
+ const cfg = await gate2(req.user.id, id2);
46513
47330
  if (!cfg) return reply.code(404).send(NOT_FOUND2);
46514
47331
  const s2 = await ownSession(req.user.id, id2, sid);
46515
47332
  if (!s2) return reply.code(404).send(NOT_FOUND2);
@@ -46747,15 +47564,15 @@ init_src();
46747
47564
  init_db();
46748
47565
 
46749
47566
  // src/services/bootstrap.ts
46750
- import { createHash as createHash11, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
46751
- import { chmod, lstat as lstat3, mkdir as mkdir7, readFile as readFile6, unlink as unlink5, writeFile as writeFile6 } from "node:fs/promises";
46752
- import { join as join33 } from "node:path";
47567
+ import { createHash as createHash12, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
47568
+ import { chmod, lstat as lstat3, mkdir as mkdir8, readFile as readFile6, unlink as unlink5, writeFile as writeFile6 } from "node:fs/promises";
47569
+ import { join as join34 } from "node:path";
46753
47570
  var TTL_MS5 = 15 * 6e4;
46754
47571
  var SETUP_TOKEN_FILE = "setup.token";
46755
47572
  var BootstrapError = class extends Error {
46756
47573
  code = "BOOTSTRAP_PROOF";
46757
47574
  };
46758
- var tokenPath = (home3) => join33(home3, SETUP_TOKEN_FILE);
47575
+ var tokenPath = (home3) => join34(home3, SETUP_TOKEN_FILE);
46759
47576
  async function readStored(home3) {
46760
47577
  try {
46761
47578
  const info = await lstat3(tokenPath(home3));
@@ -46770,7 +47587,7 @@ async function readStored(home3) {
46770
47587
  }
46771
47588
  }
46772
47589
  async function ensureSetupToken(home3, now = Date.now()) {
46773
- await mkdir7(home3, { recursive: true, mode: 448 });
47590
+ await mkdir8(home3, { recursive: true, mode: 448 });
46774
47591
  const homeInfo = await lstat3(home3);
46775
47592
  if (homeInfo.isSymbolicLink() || !homeInfo.isDirectory()) throw new BootstrapError("invalid setup home");
46776
47593
  await chmod(home3, 448);
@@ -46800,8 +47617,8 @@ ${new Date(expiresAt).toISOString()}
46800
47617
  }
46801
47618
  async function verifySetupToken(candidate, home3, now = Date.now()) {
46802
47619
  const stored = await readStored(home3);
46803
- const got = createHash11("sha256").update(candidate).digest();
46804
- const want = createHash11("sha256").update(stored.token).digest();
47620
+ const got = createHash12("sha256").update(candidate).digest();
47621
+ const want = createHash12("sha256").update(stored.token).digest();
46805
47622
  if (stored.expiresAt <= now || !timingSafeEqual4(got, want)) throw new BootstrapError("invalid setup proof");
46806
47623
  }
46807
47624
  async function consumeSetupToken(home3) {
@@ -47144,7 +47961,7 @@ function isDestructiveTelegramRequest(method, path, body) {
47144
47961
  if (verb === "POST") {
47145
47962
  if (/\/update\/apply$/.test(path)) return true;
47146
47963
  if (/\/(?:specs|terminal\/sessions)\/[^/]+\/integrate$/.test(path)) return true;
47147
- if (/\/projects\/[^/]+\/(?:git\/(?:merge|rebase|drop|reset|clean)|branches\/delete)$/.test(path)) return true;
47964
+ if (/\/projects\/[^/]+\/(?:git\/(?:merge|rebase|drop|reset|clean)|(?:branches|worktrees)\/delete)$/.test(path)) return true;
47148
47965
  if (/\/projects\/[^/]+\/git$/.test(path)) {
47149
47966
  const data = typeof body === "object" && body !== null ? body : {};
47150
47967
  return (/* @__PURE__ */ new Set([
@@ -47267,9 +48084,14 @@ var SETUP_PUBLIC = /* @__PURE__ */ new Set([
47267
48084
  "POST /api/setup"
47268
48085
  ]);
47269
48086
  function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {}) {
47270
- const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname14(fileURLToPath5(import.meta.url)), env, existsSync18);
48087
+ const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname15(fileURLToPath5(import.meta.url)), env, existsSync18);
47271
48088
  const ingress = loadIngressPolicy(env);
47272
48089
  const app2 = Fastify({ logger: false, trustProxy: trustProxyFromEnv(env) });
48090
+ app2.setErrorHandler((error, _req, reply) => {
48091
+ if (error instanceof LaunchAdmissionError)
48092
+ return reply.code(409).send({ error: error.message, kind: error.kind, admission: error.admission });
48093
+ return reply.send(error);
48094
+ });
47273
48095
  app2.addHook("onRequest", async (req, reply) => {
47274
48096
  const role = classifyIngress({ host: req.headers.host ?? "", method: req.method, url: req.url }, ingress);
47275
48097
  if (role === "denied") return reply.code(404).send({ error: "not found" });
@@ -47384,7 +48206,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
47384
48206
  await api.register(tasks_default);
47385
48207
  }, { prefix: "/api" });
47386
48208
  if (shouldServeWeb(env)) {
47387
- const dist = pickWebDir(dirname14(fileURLToPath5(import.meta.url)), env, existsSync18);
48209
+ const dist = pickWebDir(dirname15(fileURLToPath5(import.meta.url)), env, existsSync18);
47388
48210
  if (dist) {
47389
48211
  app2.register(fastifyStatic, { root: dist });
47390
48212
  app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : req.url.startsWith("/assets/") ? reply.code(404).type("text/plain").send("not found") : reply.sendFile("index.html"));
@@ -47432,8 +48254,10 @@ function startVpsMonitor() {
47432
48254
  // src/services/scheduler/engine.ts
47433
48255
  init_db();
47434
48256
  init_src();
48257
+ init_config3();
47435
48258
 
47436
48259
  // src/services/scheduler/governor.ts
48260
+ init_session_admission();
47437
48261
  init_db();
47438
48262
  init_notifications2();
47439
48263
  var ALREADY_DONE_NOTE = "spec sudah selesai \u2014 tak diluncurkan";
@@ -47462,7 +48286,7 @@ async function drainCronRuns(slots, deps) {
47462
48286
  await close("skipped", CRON_OPTOUT_NOTE);
47463
48287
  continue;
47464
48288
  }
47465
- const live = deps.liveCron(cron.id);
48289
+ const live = await deps.liveCron(cron.id);
47466
48290
  if (live) {
47467
48291
  await close("skipped", cronLiveNote(live));
47468
48292
  continue;
@@ -47486,7 +48310,8 @@ async function drainCronRuns(slots, deps) {
47486
48310
  await prisma.schedulerCron.update({ where: { id: cron.id }, data: { lastRunAt: /* @__PURE__ */ new Date() } });
47487
48311
  slots--;
47488
48312
  } catch (e) {
47489
- await close("failed", e.message);
48313
+ if (e instanceof LaunchAdmissionError) await noteCronRun(run4.id, e.message);
48314
+ else await close("failed", e.message);
47490
48315
  }
47491
48316
  }
47492
48317
  return slots;
@@ -47496,7 +48321,7 @@ async function drain(cfg, deps) {
47496
48321
  if (draining) return;
47497
48322
  draining = true;
47498
48323
  try {
47499
- let slots = cfg.maxConcurrent - deps.liveCount();
48324
+ let slots = cfg.maxConcurrent - await deps.liveCount();
47500
48325
  slots = await deps.drainCrons(slots);
47501
48326
  if (slots <= 0) return;
47502
48327
  for (const item of await queued2()) {
@@ -47511,7 +48336,7 @@ async function drain(cfg, deps) {
47511
48336
  await noteRow(item.id, blockedNote(blocked));
47512
48337
  continue;
47513
48338
  }
47514
- const liveId = deps.isLive(item.specId);
48339
+ const liveId = await deps.isLive(item.specId);
47515
48340
  if (liveId) {
47516
48341
  await markLaunched(item.id, liveId);
47517
48342
  continue;
@@ -47521,6 +48346,10 @@ async function drain(cfg, deps) {
47521
48346
  if (!await markLaunched(item.id, sessionId2)) await noteRow(item.id, canceledRaceNote(sessionId2));
47522
48347
  slots--;
47523
48348
  } catch (e) {
48349
+ if (e instanceof LaunchAdmissionError) {
48350
+ await noteRow(item.id, e.message);
48351
+ break;
48352
+ }
47524
48353
  await markFailed(item.id, e.message);
47525
48354
  }
47526
48355
  }
@@ -47535,36 +48364,39 @@ init_src2();
47535
48364
  init_settings3();
47536
48365
  init_codex_trust();
47537
48366
  init_pty();
48367
+ init_session_launch_gate();
47538
48368
  var cronSessionId = (cronId) => `cron-${cronId.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}`;
47539
- function liveCronSession(cronId) {
47540
- const s2 = getSession(cronSessionId(cronId));
48369
+ async function liveCronSession(cronId) {
48370
+ const s2 = await getSessionAsync(cronSessionId(cronId));
47541
48371
  return s2 && !s2.exited ? s2.id : null;
47542
48372
  }
47543
48373
  async function startCronSession(cron) {
47544
- const repoDir = await resolveRepoDir(cron.projectId);
47545
- if (!repoDir) throw new Error(`project "${cron.projectId}" belum di-bind ke checkout lokal`);
47546
- const project = await prisma.project.findUnique({ where: { id: cron.projectId } });
47547
- if (!project) throw new Error(`project "${cron.projectId}" tak ada`);
47548
- const { agent, model, effort: effort2 } = await terminalAgentDefaults({
47549
- agent: cron.agent ?? void 0,
47550
- model: cron.model ?? void 0,
47551
- effort: cron.effort ?? void 0
47552
- });
47553
- if (agent === "codex") ensureCodexTrust(repoDir);
47554
48374
  const id2 = cronSessionId(cron.id);
47555
- const wt = `${repoDir}/.worktrees/${id2}`;
47556
- if (!realGit.worktreeAlive(wt)) realGit.addWorktree(repoDir, wt, "HEAD");
47557
- const s2 = createSession(cron.projectId, wt, {
47558
- id: id2,
47559
- agent,
47560
- model,
47561
- effort: effort2,
47562
- prompt: cronPrompt(
47563
- { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
47564
- { name: cron.name, prompt: cron.prompt }
47565
- )
47566
- });
47567
- return { id: s2.id };
48375
+ return withSessionAdmission({ id: id2 }, async () => {
48376
+ const repoDir = await resolveRepoDir(cron.projectId);
48377
+ if (!repoDir) throw new Error(`project "${cron.projectId}" belum di-bind ke checkout lokal`);
48378
+ const project = await prisma.project.findUnique({ where: { id: cron.projectId } });
48379
+ if (!project) throw new Error(`project "${cron.projectId}" tak ada`);
48380
+ const { agent, model, effort: effort2 } = await terminalAgentDefaults({
48381
+ agent: cron.agent ?? void 0,
48382
+ model: cron.model ?? void 0,
48383
+ effort: cron.effort ?? void 0
48384
+ });
48385
+ if (agent === "codex") ensureCodexTrust(repoDir);
48386
+ const wt = `${repoDir}/.worktrees/${id2}`;
48387
+ if (!realGit.worktreeAlive(wt)) realGit.addWorktree(repoDir, wt, "HEAD");
48388
+ const s2 = createSession(cron.projectId, wt, {
48389
+ id: id2,
48390
+ agent,
48391
+ model,
48392
+ effort: effort2,
48393
+ prompt: cronPrompt(
48394
+ { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
48395
+ { name: cron.name, prompt: cron.prompt }
48396
+ )
48397
+ });
48398
+ return { id: s2.id };
48399
+ }, (pane) => ({ id: pane.id }));
47568
48400
  }
47569
48401
 
47570
48402
  // src/services/scheduler/reconcile.ts
@@ -47675,9 +48507,9 @@ async function tick3(now, deps, end2 = prodEnd) {
47675
48507
  }
47676
48508
  }
47677
48509
  var prodDeps = {
47678
- liveCount: () => listSessions().filter((s2) => !s2.exited).length,
47679
- isLive: (specId) => {
47680
- const s2 = getSession(sessionIdForSpec(specId));
48510
+ liveCount: async () => (await listSessionsAsync()).filter((s2) => !s2.exited).length,
48511
+ isLive: async (specId) => {
48512
+ const s2 = await getSessionAsync(sessionIdForSpec(specId));
47681
48513
  return s2 && !s2.exited ? s2.id : null;
47682
48514
  },
47683
48515
  // SPEC-431 · dibaca ULANG dari DB tepat sebelum launch, bukan dari baris antrean: antrean tak
@@ -48127,7 +48959,7 @@ init_session_sandbox();
48127
48959
  init_src2();
48128
48960
 
48129
48961
  // src/services/secure-home.ts
48130
- import { chmod as chmod2, lstat as lstat4, mkdir as mkdir8 } from "node:fs/promises";
48962
+ import { chmod as chmod2, lstat as lstat4, mkdir as mkdir9 } from "node:fs/promises";
48131
48963
  import { isAbsolute as isAbsolute8, resolve as resolve21 } from "node:path";
48132
48964
  var HomePermissionError = class extends Error {
48133
48965
  code = "HOME_SYMLINK";
@@ -48144,12 +48976,12 @@ async function assertNoSymlink(path, allowMissing) {
48144
48976
  async function secureHanomanHome(opts) {
48145
48977
  if (!isAbsolute8(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
48146
48978
  await assertNoSymlink(opts.home, true);
48147
- await mkdir8(opts.home, { recursive: true, mode: 448 });
48979
+ await mkdir9(opts.home, { recursive: true, mode: 448 });
48148
48980
  await assertNoSymlink(opts.home, false);
48149
48981
  await chmod2(opts.home, 448);
48150
48982
  for (const directory of opts.directories ?? []) {
48151
48983
  await assertNoSymlink(directory, true);
48152
- await mkdir8(directory, { recursive: true, mode: 448 });
48984
+ await mkdir9(directory, { recursive: true, mode: 448 });
48153
48985
  await assertNoSymlink(directory, false);
48154
48986
  await chmod2(directory, 448);
48155
48987
  }
@@ -48332,101 +49164,6 @@ function startRetentionSweep() {
48332
49164
 
48333
49165
  // src/server.ts
48334
49166
  init_uploads();
48335
-
48336
- // src/services/session-event-relay.ts
48337
- init_session_event_token();
48338
- init_session_event_spool();
48339
- import { constants as constants3 } from "node:fs";
48340
- import { mkdir as mkdir9, open as open4, readdir as readdir7, rm as rm7 } from "node:fs/promises";
48341
- import { join as join34 } from "node:path";
48342
- var MAX_EVENT_BYTES = 1e6;
48343
- var MAX_FILES_PER_DRAIN = 1e3;
48344
- var SESSION_ID_RE = /^[a-z0-9_-]+$/;
48345
- async function drainSessionEventSpool(app2, root = sessionEventSpoolRoot()) {
48346
- await mkdir9(root, { recursive: true, mode: 448 });
48347
- let delivered = 0;
48348
- let examined = 0;
48349
- let readBuffer;
48350
- for (const session of await readdir7(root, { withFileTypes: true })) {
48351
- if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
48352
- const dir2 = join34(root, session.name);
48353
- let entries3;
48354
- try {
48355
- entries3 = await readdir7(dir2, { withFileTypes: true });
48356
- } catch {
48357
- continue;
48358
- }
48359
- for (const entry of entries3) {
48360
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
48361
- if (++examined > MAX_FILES_PER_DRAIN) return delivered;
48362
- const path = join34(dir2, entry.name);
48363
- let payload;
48364
- let handle;
48365
- try {
48366
- handle = await open4(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
48367
- const stat4 = await handle.stat();
48368
- if (!stat4.isFile() || stat4.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
48369
- readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
48370
- const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
48371
- if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
48372
- const parsed = JSON.parse(readBuffer.subarray(0, bytesRead).toString("utf8"));
48373
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
48374
- throw new Error("payload event bukan object");
48375
- }
48376
- payload = parsed;
48377
- } catch {
48378
- await rm7(path, { force: true }).catch(() => {
48379
- });
48380
- continue;
48381
- } finally {
48382
- await handle?.close().catch(() => {
48383
- });
48384
- }
48385
- try {
48386
- const response = await app2.inject({
48387
- method: "POST",
48388
- url: "/api/session-events",
48389
- headers: {
48390
- authorization: `Bearer ${sessionEventToken(session.name)}`,
48391
- "x-hanoman-session": session.name
48392
- },
48393
- payload
48394
- });
48395
- if (response.statusCode === 429 || response.statusCode >= 500) continue;
48396
- await rm7(path, { force: true }).catch(() => {
48397
- });
48398
- if (response.statusCode >= 200 && response.statusCode < 300) delivered++;
48399
- } catch {
48400
- }
48401
- }
48402
- }
48403
- return delivered;
48404
- }
48405
- function startSessionEventRelay(app2, options2 = {}) {
48406
- const root = options2.root ?? sessionEventSpoolRoot();
48407
- let running = false;
48408
- const tick6 = async () => {
48409
- if (running) return;
48410
- running = true;
48411
- try {
48412
- await drainSessionEventSpool({ inject: async (request) => app2.inject(request) }, root);
48413
- } catch (error) {
48414
- console.error("session event relay gagal:", error);
48415
- } finally {
48416
- running = false;
48417
- }
48418
- };
48419
- const timer9 = setInterval(() => {
48420
- void tick6();
48421
- }, options2.intervalMs ?? 250);
48422
- timer9.unref();
48423
- app2.addHook("onClose", async () => {
48424
- clearInterval(timer9);
48425
- });
48426
- void tick6();
48427
- }
48428
-
48429
- // src/server.ts
48430
49167
  var app = buildApp();
48431
49168
  var port = Number(process.env.PORT ?? 8787);
48432
49169
  var host = process.env.HOST ?? "127.0.0.1";
@@ -48481,8 +49218,11 @@ bootstrapReady.then(async () => {
48481
49218
  installSessionHistory();
48482
49219
  try {
48483
49220
  const liveIds = listSessions().map((s2) => s2.id);
48484
- void reconcileHistory(liveIds).then((n2) => {
49221
+ void reconcileHistory(liveIds).then(async (n2) => {
48485
49222
  if (n2) console.log(`riwayat sesi: ${n2} baris berjalan direkonsiliasi`);
49223
+ for (const row of await detectOrphanWorktrees()) {
49224
+ console.log(`worktree yatim: ${row.projectId} \u2014 ${row.count} menunggu konfirmasi di tab Worktrees`);
49225
+ }
48486
49226
  }).catch((e) => console.error("rekonsiliasi riwayat sesi:", e));
48487
49227
  void reconcileAgentInvocations(liveIds).then((n2) => {
48488
49228
  if (n2) console.log(`custom agent: ${n2} invocation ditandai abandoned`);