hanoman 0.9.30 → 0.9.32

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 (24) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +62 -26
  3. package/dist/server.js +471 -336
  4. package/package.json +1 -1
  5. package/web/assets/{DalangHanomanScreen-Fd7_DcSP.js → DalangHanomanScreen-DdsEmtBl.js} +1 -1
  6. package/web/assets/{DocPreviewModal-SMRN2K98.js → DocPreviewModal-BbOqdfaH.js} +1 -1
  7. package/web/assets/{DocsWorkspace-pxj0N6QW.js → DocsWorkspace-Cr7AXz9z.js} +1 -1
  8. package/web/assets/{IdeScreen-Cr9Vqdrc.js → IdeScreen-hdbQW-ui.js} +1 -1
  9. package/web/assets/{LeadScreen-CQXpJy0y.js → LeadScreen-BWE4mfJI.js} +1 -1
  10. package/web/assets/{ReviewScreen-B7NNEjxk.js → ReviewScreen-BIkpEspG.js} +1 -1
  11. package/web/assets/{SchedulerScreen-D3rPdS-F.js → SchedulerScreen-3KVE-d_d.js} +1 -1
  12. package/web/assets/SettingsScreen-B0-W2TeR.js +62 -0
  13. package/web/assets/{TeamScreen-BBAsYLpT.js → TeamScreen-DNdt2P1g.js} +1 -1
  14. package/web/assets/{TerminalScreen-DpVp4h1n.js → TerminalScreen-BjQF48XI.js} +1 -1
  15. package/web/assets/{TriageScreen-wAPnttRe.js → TriageScreen-CzM99CrG.js} +1 -1
  16. package/web/assets/{VpsScreen-DeQVx9Jy.js → VpsScreen-SGB_BpbS.js} +1 -1
  17. package/web/assets/fields-of8G961J.js +1 -0
  18. package/web/assets/{index-BQw6xQI4.js → index-C5P4ykKO.js} +3 -3
  19. package/web/assets/{live-CONO0GTu.js → live-DQen7wLm.js} +1 -1
  20. package/web/assets/{sched-lead-shared-CDR0pOqs.js → sched-lead-shared-C34TvLVv.js} +1 -1
  21. package/web/assets/{settings-rebase-CpaFw0Tm.js → settings-rebase-DHIWk4mV.js} +1 -1
  22. package/web/index.html +1 -1
  23. package/web/assets/SettingsScreen-Bx13PWKQ.js +0 -62
  24. package/web/assets/fields-qnBoIMR_.js +0 -1
package/dist/server.js CHANGED
@@ -5474,6 +5474,8 @@ var init_entities = __esm({
5474
5474
  // SPEC-184
5475
5475
  notifyDecisionSound: external_exports.enum(NOTIFY_SOUNDS).default("alert"),
5476
5476
  // SPEC-184
5477
+ notifyDecisionTelegram: external_exports.boolean().default(true),
5478
+ // ADR-0226 · dorong "menunggu keputusan" ke Telegram
5477
5479
  agentAccessEnabled: external_exports.boolean().default(false),
5478
5480
  // SPEC-257 · master switch akses AI agent
5479
5481
  scheduler: zScheduler.default(SCHEDULER_DEFAULTS),
@@ -7613,12 +7615,16 @@ var init_session_admission = __esm({
7613
7615
  function flowForSource(source) {
7614
7616
  return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : source === "no_effort" ? "no_effort" : "feature";
7615
7617
  }
7618
+ function formatExitIntent(pid, code) {
7619
+ return `${pid} ${code}
7620
+ `;
7621
+ }
7616
7622
  function subKey(topic, params) {
7617
7623
  const canon = {};
7618
7624
  for (const k of Object.keys(params).sort()) if (params[k] !== void 0) canon[k] = params[k];
7619
7625
  return `${topic}|${JSON.stringify(canon)}`;
7620
7626
  }
7621
- var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zMarkSpecReviewed, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zSetupApply, zChangePassword, zAdminPasswordReset, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, PROVISION_PROFILES, COMPONENT_IDS, zProvision, UPDATE_RESTART_EXIT, CONFIG_RESTART_EXIT, MANUAL_RESTART_EXIT, zUpdateApplyBody, MAX_SUBS, zSubPage, zSubLimit, zTopicParams, zEventsClientMsg, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
7627
+ var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zMarkSpecReviewed, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zSetupApply, zChangePassword, zAdminPasswordReset, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, PROVISION_PROFILES, COMPONENT_IDS, zProvision, UPDATE_RESTART_EXIT, CONFIG_RESTART_EXIT, MANUAL_RESTART_EXIT, EXIT_INTENT_FILE, zUpdateApplyBody, MAX_SUBS, zSubPage, zSubLimit, zTopicParams, zEventsClientMsg, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
7622
7628
  var init_dto = __esm({
7623
7629
  "../shared/src/dto.ts"() {
7624
7630
  "use strict";
@@ -8153,6 +8159,7 @@ var init_dto = __esm({
8153
8159
  UPDATE_RESTART_EXIT = 75;
8154
8160
  CONFIG_RESTART_EXIT = 76;
8155
8161
  MANUAL_RESTART_EXIT = 77;
8162
+ EXIT_INTENT_FILE = "exit-intent";
8156
8163
  zUpdateApplyBody = external_exports.object({ confirm: external_exports.boolean().optional() });
8157
8164
  MAX_SUBS = 16;
8158
8165
  zSubPage = external_exports.number().int().min(1).max(1e4);
@@ -17096,6 +17103,11 @@ var init_verify_scope = __esm({
17096
17103
  ' test-nya memang berjalan, jangan menerima "no test files" sebagai bukti.',
17097
17104
  "- Typecheck: hanya paket yang tersentuh (mis. `pnpm --filter ./server typecheck`).",
17098
17105
  " JANGAN `pnpm -r typecheck` \u2014 itu menyalakan satu proses tsc per paket sekaligus.",
17106
+ "- Ritme: per task jalankan berkas test yang task itu sentuh saja.",
17107
+ " JANGAN menjalankan ulang berkas test yang sudah hijau selama kode yang diujinya",
17108
+ " tak berubah sejak itu. Typecheck paket tersentuh sesudah task yang mengubah",
17109
+ " tipe/kontrak ekspor, bukan tiap task. Gabungan semua test tersentuh + typecheck",
17110
+ " dijalankan SEKALI di akhir, sebelum melapor.",
17099
17111
  "- Lint: hanya berkas yang berubah, bukan seluruh repo.",
17100
17112
  "- Build penuh: hanya bila yang kamu ubah memang soal build/bundling.",
17101
17113
  "- Boot server + curl / smoke end-to-end: hanya bila task ini menyentuh endpoint atau perilaku",
@@ -17441,7 +17453,7 @@ Stack: ${project.stack || "\u2014"}`,
17441
17453
  ${cron.prompt}`
17442
17454
  ].join("\n\n");
17443
17455
  }
17444
- var PIPELINES, WORK_PHASES, DECIDER, ASK_ROUTE, AMBIGUOUS, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, auditContinuationForOrchestrator, writesCode, scopeClause, codeStyleClause, methodClause, attachmentClause, orchestratorAttachmentClause, PROJECT_METHOD, specContext, goalDetail, goalBlock, goalContext, projectContext, scaffoldContext, prdBriefBlock, prdAuditBlock, prdContext, breakdownContext, prdPhaseLines, breakdownPhaseLines, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
17456
+ var PIPELINES, WORK_PHASES, DECIDER, ASK_ROUTE, AMBIGUOUS, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, PLAN_SHAPE, phaseInstruction, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, auditContinuationForOrchestrator, writesCode, scopeClause, codeStyleClause, methodClause, attachmentClause, orchestratorAttachmentClause, PROJECT_METHOD, specContext, goalDetail, goalBlock, goalContext, projectContext, scaffoldContext, prdBriefBlock, prdAuditBlock, prdContext, breakdownContext, prdPhaseLines, breakdownPhaseLines, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
17445
17457
  var init_prompt = __esm({
17446
17458
  "../runner/src/prompt.ts"() {
17447
17459
  "use strict";
@@ -17458,12 +17470,14 @@ var init_prompt = __esm({
17458
17470
  AUTONOMY_CLAUSE = `Jalankan seluruh pipeline sampai tuntas tanpa berhenti di batas antar-fase. Checkpoint "review"/"approval"/"need review" milik skill superpowers BUKAN titik berhenti di sini \u2014 lanjut saja ke fase berikutnya. Berhenti HANYA untuk ${AMBIGUOUS}: JANGAN memutuskannya sendiri, ${ASK_ROUTE} Selain itu, terus lanjut.`;
17459
17471
  AUTONOMY_CLAUSE_FULL = `Kamu diluncurkan scheduler \u2014 mungkin tak ada manusia yang sedang menonton terminal ini. Tembus seluruh pipeline sampai stage \`done\`, lalu commit & push: checkpoint "review"/"approval" milik skill BUKAN titik berhenti dan jangan menunggu persetujuan siapa pun. Tetapi JANGAN memutuskan sendiri ${AMBIGUOUS}: ${ASK_ROUTE} Catat keputusan penting di pesan commit. Merge ke branch utama tetap dilakukan manusia, bukan kamu.`;
17460
17472
  autonomyClause = (mode) => mode === "full-control" ? AUTONOMY_CLAUSE_FULL : AUTONOMY_CLAUSE;
17473
+ PLAN_SHAPE = 'Bentuk plan: rencana, bukan kode. Tiap task `- [ ]` adalah satu irisan perilaku yang bisa diverifikasi: berkas yang disentuh, perilaku yang dibangun, AC yang dipenuhinya, dan test yang membuktikannya (nama test + kasus intinya dalam satu kalimat). JANGAN menulis isi implementasi maupun isi test lengkap \u2014 Execute yang menulisnya; kode di plan ditulis dua kali. Potongan kode hanya untuk kontrak yang tak boleh ditafsirkan (signature/tipe ekspor, skema, bentuk payload API), masing-masing pendek. Jangan memecah tulis-test, jalankan, implementasi, dan commit menjadi kotak terpisah. Sasaran \xB1300 baris; lebih panjang biasanya tanda kode ikut tertulis. Aturan ini mengalahkan anjuran "kode lengkap di setiap langkah" dan "langkah 2\u20135 menit" milik skill plan mana pun.';
17461
17474
  phaseInstruction = (phases2, method) => {
17462
17475
  const base3 = `Kerjakan fase berurutan: ${phases2.join(" \u2192 ")}.
17463
17476
  Setiap kali sebuah fase selesai (atau kamu putuskan dilewati), append satu baris ke berkas di $HANOMAN_PHASE_FILE \u2014 persis: \`echo "<Nama Fase> done" >> "$HANOMAN_PHASE_FILE"\`, atau \`skipped\` sebagai ganti \`done\`. Nama fase ditulis apa adanya seperti di atas.`;
17464
17477
  if (!phases2.includes("Plan") || !phases2.includes("Execute")) return base3;
17465
17478
  return base3 + `
17466
- Execute BELUM selesai selama plan (\`${method.planDir}/**\`) masih punya task \`- [ ]\`: kerjakan SEMUA PR/task sampai tiap kotak jadi \`- [x]\` sebelum menulis \`Execute done\`. hanoman menahan backlog di \`executing\`, bukan \`done\`, selama masih ada \`- [ ]\`.`;
17479
+ Execute BELUM selesai selama plan (\`${method.planDir}/**\`) masih punya task \`- [ ]\`: kerjakan SEMUA PR/task sampai tiap kotak jadi \`- [x]\` sebelum menulis \`Execute done\`. hanoman menahan backlog di \`executing\`, bukan \`done\`, selama masih ada \`- [ ]\`.
17480
+ ${PLAN_SHAPE}`;
17467
17481
  };
17468
17482
  skillInstruction = (flow2, phases2, method) => {
17469
17483
  const lines2 = phases2.map((p3) => {
@@ -18620,7 +18634,7 @@ function backlogGuide(flow2, phase, ctx) {
18620
18634
  const m = ctx.method;
18621
18635
  switch (phase) {
18622
18636
  case "Brainstorm":
18623
- return `- Brainstorm: gali konteks, alternatif, dan keputusan untuk backlog ini dari Source of Truth dan kode. Tulis hasilnya sebagai bagian \`## Konteks & keputusan\` di dokumen spec baru di \`${m.specDir}/\` (nama berkas \`<YYYY-MM-DD>-<spec-id>-<slug>-design.md\`) dan laporkan path-nya.` + (ctx.fromAudit ? ` Backlog ini LANJUTAN audit ${ctx.fromAudit}: baca \`internal/docs/research/audit-${ctx.fromAudit.toLowerCase()}-*.md\` lebih dulu dan pakai temuannya sebagai bahan \u2014 jangan menginvestigasi ulang dari nol.` : "");
18637
+ return `- Brainstorm: gali konteks, alternatif, dan keputusan untuk backlog ini dari Source of Truth dan kode. Tulis hasilnya sebagai bagian \`## Konteks & keputusan\` di dokumen spec baru di \`${m.specDir}/\` (nama berkas \`<YYYY-MM-DD>-<spec-id>-<slug>-design.md\`) dan laporkan path-nya. ${FRONT_LOADED_DECISIONS("sampai Execute")} Tulis jawabannya ke \`## Konteks & keputusan\`.` + (ctx.fromAudit ? ` Backlog ini LANJUTAN audit ${ctx.fromAudit}: baca \`internal/docs/research/audit-${ctx.fromAudit.toLowerCase()}-*.md\` lebih dulu dan pakai temuannya sebagai bahan \u2014 jangan menginvestigasi ulang dari nol.` : "");
18624
18638
  case "Objective":
18625
18639
  return "- Objective: baca dokumen spec dari fase Brainstorm (path-nya di serah-terima), lalu tambahkan bagian `## Objective` berisi SATU objective terukur beserta kriteria suksesnya.";
18626
18640
  case "Spec":
@@ -18629,11 +18643,11 @@ function backlogGuide(flow2, phase, ctx) {
18629
18643
  // default skill) lolos hampa. Nama dikunci seperti Brainstorm, dan path persisnya diteruskan
18630
18644
  // orchestrator ke Execute & reviewer — bukan pola glob yang bisa mencocoki plan backlog lain.
18631
18645
  case "Plan":
18632
- return `- Plan: tulis plan implementasi berkotak \`- [ ]\` untuk backlog ini dari dokumen spec-nya (path di serah-terima) ke SATU berkas \`${m.planDir}/<YYYY-MM-DD>-<spec-id>-<slug>.md\` (spec-id backlog ini huruf kecil, mis. \`2026-09-25-spec-123-ekspor-csv.md\` \u2014 nama tanpa spec-id membuat hanoman tak menemukan plan-nya). Laporkan path PERSIS berkas itu di \`Artefak:\`; orchestrator meneruskannya ke Execute.`;
18646
+ return `- Plan: tulis plan implementasi berkotak \`- [ ]\` untuk backlog ini dari dokumen spec-nya (path di serah-terima) ke SATU berkas \`${m.planDir}/<YYYY-MM-DD>-<spec-id>-<slug>.md\` (spec-id backlog ini huruf kecil, mis. \`2026-09-25-spec-123-ekspor-csv.md\` \u2014 nama tanpa spec-id membuat hanoman tak menemukan plan-nya). Laporkan path PERSIS berkas itu di \`Artefak:\`; orchestrator meneruskannya ke Execute. ${PLAN_SHAPE}`;
18633
18647
  case "Execute":
18634
18648
  return `- Execute: kerjakan plan backlog ini \u2014 path persisnya di baris \`Artefak fase sebelumnya:\` serah-terima (bila tak ada: berkas di \`${m.planDir}/\` yang namanya memuat spec-id backlog ini). Execute BELUM selesai selama plan masih punya task \`- [ ]\`: kerjakan SEMUA task sampai tiap kotak jadi \`- [x]\` sebelum melapor \`Status: selesai\`. hanoman menahan backlog di \`executing\`, bukan \`done\`, selama masih ada \`- [ ]\`. Laporan \`Status: selesai\` wajib memuat tabel \`AC \u2192 bukti\`: setiap acceptance criteria dokumen spec (qa jalur-cepat: dokumen audit) \u2192 perintah yang kamu jalankan beserta hasil yang kamu baca, atau path:baris yang membuktikannya. AC tanpa bukti = belum selesai; laporanmu diperiksa ulang sebelum fase ditutup.`;
18635
18649
  case "Audit":
18636
- return flow2 === "audit" ? "- Audit: ini audit-only \u2014 investigasi SAJA, JANGAN menulis perbaikan kode apa pun. Telusuri akar masalah / log / jawaban dan nilai apakah issue terdefinisi dengan baik. Laporkan temuan beserta buktinya; dokumennya ditulis fase Laporan." : "- Audit: telusuri akar masalah dengan bukti (reproduksi, log, kode) dan tulis dokumen audit ke `internal/docs/research/audit-<spec-id>-<slug>.md`. Lalu putuskan jalurnya dari hasil Audit, bukan default: bila temuan berconfidence tinggi dan perbaikannya bisa dikerjakan langsung (diff kecil, akar masalah jelas), tulis `Rekomendasi fase: jalur-cepat` beserta alasannya \u2014 orchestrator akan melewati Spec & Plan dan dokumen audit menjadi doc-of-record perbaikan itu. Bila temuan luas, berisiko, atau ambigu, tulis `Rekomendasi fase: penuh`.";
18650
+ return flow2 === "audit" ? "- Audit: ini audit-only \u2014 investigasi SAJA, JANGAN menulis perbaikan kode apa pun. Telusuri akar masalah / log / jawaban dan nilai apakah issue terdefinisi dengan baik. Laporkan temuan beserta buktinya; dokumennya ditulis fase Laporan." : "- Audit: telusuri akar masalah dengan bukti (reproduksi, log, kode) dan tulis dokumen audit ke `internal/docs/research/audit-<spec-id>-<slug>.md`. Lalu putuskan jalurnya dari hasil Audit, bukan default: bila temuan berconfidence tinggi dan perbaikannya bisa dikerjakan langsung (diff kecil, akar masalah jelas), tulis `Rekomendasi fase: jalur-cepat` beserta alasannya \u2014 orchestrator akan melewati Spec & Plan dan dokumen audit menjadi doc-of-record perbaikan itu. Bila temuan luas, berisiko, atau ambigu, tulis `Rekomendasi fase: penuh`. " + FRONT_LOADED_DECISIONS("Spec, Plan, dan Execute") + " Catat jawabannya di dokumen audit.";
18637
18651
  case "Laporan":
18638
18652
  return "- Laporan: tulis DOKUMEN AUDIT ke Source of Truth `internal/docs/research/audit-<spec-id>-<slug>.md` (ikuti konvensi audit yang ada), tautkan di `internal/docs/README.md`, memuat: keluhan/pertanyaan, temuan (dengan bukti/log), apakah issue terdefinisi baik, dan rekomendasi tindak lanjut. Commit dokumen itu. Tak ada kode fitur.\n\n" + ESCALATION_CONTRACT;
18639
18653
  case "Goal":
@@ -18775,7 +18789,7 @@ function withPhaseDelegation(phaseDefs, roster, runtime) {
18775
18789
  ${clause}` } : def2;
18776
18790
  });
18777
18791
  }
18778
- var REVIEWER_AGENT_NAME, PROJECT_FLOWS, DOC_WRITING_PHASES, PHASE_AGENT_AUTONOMY, PHASE_AGENT_RULES, PHASE_AGENT_REPORT, ATTACHMENT_NOTE, TERMINAL_ASK, PHASE_AGENT_ASK, TERMINAL_HANDOFF, PHASE_AGENT_HANDOFF, forPhaseAgent, REVIEWER_RULES, roleOf, liveCustoms, NATIVE_PHASE_PROTOCOL;
18792
+ var REVIEWER_AGENT_NAME, PROJECT_FLOWS, DOC_WRITING_PHASES, PHASE_AGENT_AUTONOMY, FOREGROUND_RULE, PHASE_AGENT_RULES, PHASE_AGENT_REPORT, ATTACHMENT_NOTE, FRONT_LOADED_DECISIONS, TERMINAL_ASK, PHASE_AGENT_ASK, TERMINAL_HANDOFF, PHASE_AGENT_HANDOFF, forPhaseAgent, REVIEWER_RULES, roleOf, liveCustoms, NATIVE_PHASE_PROTOCOL;
18779
18793
  var init_phase_agents = __esm({
18780
18794
  "../runner/src/phase-agents.ts"() {
18781
18795
  "use strict";
@@ -18785,13 +18799,15 @@ var init_phase_agents = __esm({
18785
18799
  REVIEWER_AGENT_NAME = phaseAgentName(REVIEWER_CELL);
18786
18800
  PROJECT_FLOWS = /* @__PURE__ */ new Set(["reverse", "scaffold", "prd", "breakdown"]);
18787
18801
  DOC_WRITING_PHASES = /* @__PURE__ */ new Set(["Docs teknis", "Konvensi & index", "Doc index"]);
18788
- PHASE_AGENT_AUTONOMY = `Kamu dipanggil orchestrator sesi hanoman, bukan manusia. Checkpoint "review"/"approval" milik skill BUKAN titik berhenti. Tetapi kamu TIDAK BOLEH memutuskan sendiri hal yang masih ambigu dan akan mempengaruhi hasil \u2014 percabangan data model, kontrak API, scope, perilaku yang terlihat pengguna, atau asumsi yang terpaksa kamu ambil agar bisa lanjut \u2014 dan kamu tak bisa bertanya langsung. Daftarkan SEMUANYA di bagian \`Keputusan terbuka:\` laporanmu (tanpa batas jumlah; tiap butir berupa pertanyaan bernomor diakhiri \`?\` beserta opsi dan rekomendasimu), begitu juga bila panduan fase menyuruhmu bertanya ke manusia, lalu tulis \`Status: menunggu-keputusan\` dan berhenti. Jawabannya datang sebagai pesan susulan ke agen yang sama \u2014 lanjutkan dari sana; bila jawaban itu memunculkan ambiguitas baru, laporkan lagi dengan cara yang sama. Yang menjawab ditentukan hanoman \u2014 ${DECIDER}.`;
18802
+ PHASE_AGENT_AUTONOMY = `Kamu dipanggil orchestrator sesi hanoman, bukan manusia. Checkpoint "review"/"approval" milik skill BUKAN titik berhenti. Tetapi kamu TIDAK BOLEH memutuskan sendiri hal yang masih ambigu dan akan mempengaruhi hasil \u2014 percabangan data model, kontrak API, scope, perilaku yang terlihat pengguna, atau asumsi yang terpaksa kamu ambil agar bisa lanjut \u2014 dan kamu tak bisa bertanya langsung. Daftarkan SEMUANYA di bagian \`Keputusan terbuka:\` laporanmu (tanpa batas jumlah; tiap butir berupa pertanyaan bernomor diakhiri \`?\` beserta opsi dan rekomendasimu), begitu juga bila panduan fase menyuruhmu bertanya ke manusia, lalu tulis \`Status: menunggu-keputusan\` dan berhenti. Jawabannya datang sebagai pesan susulan ke agen yang sama \u2014 lanjutkan dari sana; bila jawaban itu memunculkan ambiguitas baru, laporkan lagi dengan cara yang sama. Yang menjawab ditentukan hanoman \u2014 ${DECIDER}. Keputusan yang sudah dijawab (baris \`Keputusan manusia sejauh ini:\` serah-terima, bagian keputusan dokumen spec atau dokumen audit) sudah final \u2014 JANGAN menanyakannya ulang; kumpulkan ambiguitas baru fasemu dan laporkan sekaligus.`;
18803
+ FOREGROUND_RULE = "- Jalankan test/typecheck/build di FOREGROUND dengan batas waktu dan baca hasilnya di giliran yang sama. JANGAN melepasnya ke latar lalu menunggu dengan jeda tidur berdurasi tetap (`sleep N` berulang) \u2014 itu menit yang terbuang tanpa hasil; set yang terlalu lama untuk satu perintah dipecah. Proses yang memang harus hidup di latar (server untuk smoke) ditunggu dengan pemeriksaan kesiapan berbatas waktu yang berhenti sendiri begitu siap.";
18789
18804
  PHASE_AGENT_RULES = [
18790
18805
  "Batas peran fase:",
18791
18806
  "- JANGAN menulis `$HANOMAN_PHASE_FILE` \u2014 orchestrator satu-satunya penulis marker fase.",
18792
18807
  "- JANGAN `git push` \u2014 push milik orchestrator. Commit artefak fasemu sendiri sebelum melapor.",
18793
18808
  `- JANGAN memanggil agen berawalan \`${PHASE_AGENT_PREFIX}\`. Custom agent lain boleh dipanggil bila relevan.`,
18794
18809
  "- Kerjakan HANYA fase ini; fase lain dikerjakan agen fasenya sendiri.",
18810
+ FOREGROUND_RULE,
18795
18811
  // T1 · ADR-0164 amandemen 2026-09-23 · 22 laporan tanpa `Status:` di audit: agen fase mengakhiri
18796
18812
  // gilirannya sambil menunggu proses latarnya sendiri, dan teks "menunggu" itu dibaca orchestrator
18797
18813
  // sebagai laporan final. Giliranmu yang berakhir = laporanmu, jadi menunggu harus terjadi DI DALAM giliran.
@@ -18807,6 +18823,7 @@ var init_phase_agents = __esm({
18807
18823
  "Klaim tanpa bukti bukan bukti. Maksimal 1200 kata."
18808
18824
  ].join("\n");
18809
18825
  ATTACHMENT_NOTE = "Bila serah-terima menyebut manifest lampiran (`INDEX.md`), baca manifest itu dan lampiran yang relevan di awal fase.";
18826
+ FRONT_LOADED_DECISIONS = (until) => `Kumpulkan di fase ini SEMUA keputusan yang akan dibutuhkan ${until} \u2014 bukan hanya yang muncul sekarang: data model & migrasi/backfill, kontrak API, scope & yang di luar scope, perilaku terlihat pengguna, sumber data, kasus tepi \u2014 dan ajukan sekaligus sebagai \`Keputusan terbuka:\` dalam satu putaran; satu putaran di depan jauh lebih murah daripada beberapa putaran yang tersebar.`;
18810
18827
  TERMINAL_ASK = /([Aa])jukan SATU pertanyaan per giliran ke manusia di terminal ini, tunggu jawabannya(?=[,.])/;
18811
18828
  PHASE_AGENT_ASK = "jukan pertanyaannya sebagai butir `Keputusan terbuka:` di laporanmu \u2014 satu topik per putaran, beserta konteks & opsinya, karena kamu tak berhadapan langsung dengan manusia \u2014 lalu tulis `Status: menunggu-keputusan` dan berhenti; jawabannya datang sebagai pesan susulan ke agen yang sama";
18812
18829
  TERMINAL_HANDOFF = "tulis ringkasan hasil + daftar pertanyaan yang belum terjawab ke terminal.";
@@ -18817,6 +18834,7 @@ var init_phase_agents = __esm({
18817
18834
  "- JANGAN mengubah, membuat, atau menghapus berkas apa pun di worktree, JANGAN commit, JANGAN `git push`/`git stash`/`git checkout`/`git reset`. Kamu membaca dan menjalankan perintah verifikasi saja.",
18818
18835
  "- JANGAN menulis `$HANOMAN_PHASE_FILE` dan JANGAN memperbaiki temuanmu sendiri \u2014 perbaikan milik agen Execute, marker milik orchestrator.",
18819
18836
  `- JANGAN memanggil agen berawalan \`${PHASE_AGENT_PREFIX}\`.`,
18837
+ FOREGROUND_RULE,
18820
18838
  "- JANGAN mengakhiri giliran atau melapor selama masih menunggu proses/tugas latar milikmu sendiri (test yang kamu jalankan di latar): tunggu sampai selesai dan baca hasilnya, lalu tulis laporan lengkap berawalan `Status:`."
18821
18839
  ].join("\n");
18822
18840
  roleOf = (phase) => phase === "Execute" || phase === "Goal" ? "execute" : phase === "Kerjakan" || phase === "Verifikasi" ? "none" : "research";
@@ -20714,6 +20732,7 @@ __export(src_exports, {
20714
20732
  GOAL_TUI_PASTE_LIMIT: () => GOAL_TUI_PASTE_LIMIT,
20715
20733
  PHASE_CONTEXT_HEADER: () => PHASE_CONTEXT_HEADER,
20716
20734
  PIPELINES: () => PIPELINES,
20735
+ PLAN_SHAPE: () => PLAN_SHAPE,
20717
20736
  PROJECT_METHOD: () => PROJECT_METHOD,
20718
20737
  READ_ONLY_DENIED_COMMANDS: () => READ_ONLY_DENIED_COMMANDS,
20719
20738
  READ_ONLY_DENIED_OPERATORS: () => READ_ONLY_DENIED_OPERATORS,
@@ -50134,6 +50153,8 @@ var init_settings3 = __esm({
50134
50153
  notifySound: "short",
50135
50154
  notifyDecision: true,
50136
50155
  notifyDecisionSound: "alert",
50156
+ notifyDecisionTelegram: true,
50157
+ // ADR-0226 · tak berefek tanpa bot token + allowlist
50137
50158
  agentAccessEnabled: false,
50138
50159
  // SPEC-257 · akses AI agent off sampai dibuka manusia
50139
50160
  scheduler: SCHEDULER_DEFAULTS,
@@ -55948,6 +55969,300 @@ var init_live_phases = __esm({
55948
55969
  }
55949
55970
  });
55950
55971
 
55972
+ // src/services/telegram/client.ts
55973
+ var TelegramApiError, TelegramApiClient;
55974
+ var init_client = __esm({
55975
+ "src/services/telegram/client.ts"() {
55976
+ "use strict";
55977
+ TelegramApiError = class extends Error {
55978
+ constructor(method, code, message, retryAfter) {
55979
+ super(`Telegram ${method} gagal${code ? ` (${code})` : ""}: ${message}`);
55980
+ this.method = method;
55981
+ this.code = code;
55982
+ this.retryAfter = retryAfter;
55983
+ this.name = "TelegramApiError";
55984
+ }
55985
+ };
55986
+ TelegramApiClient = class {
55987
+ constructor(token, transport = (url2, init) => fetch(url2, init), baseUrl = "https://api.telegram.org") {
55988
+ this.token = token;
55989
+ this.transport = transport;
55990
+ this.baseUrl = baseUrl;
55991
+ }
55992
+ safe(message) {
55993
+ return message.split(this.token).join("[REDACTED]").replace(/https:\/\/api\.telegram\.org\/bot[^/\s]+/g, "https://api.telegram.org/bot[REDACTED]");
55994
+ }
55995
+ async call(method, body, signal) {
55996
+ let response;
55997
+ try {
55998
+ response = await this.transport(`${this.baseUrl}/bot${this.token}/${method}`, {
55999
+ method: "POST",
56000
+ headers: { "content-type": "application/json" },
56001
+ body: JSON.stringify(body),
56002
+ signal
56003
+ });
56004
+ } catch (error) {
56005
+ throw new TelegramApiError(method, void 0, this.safe(error.message || "network error"));
56006
+ }
56007
+ if (!response.ok) {
56008
+ const failed = await response.json().catch(() => null);
56009
+ throw new TelegramApiError(method, response.status, `HTTP ${response.status}`, failed?.parameters?.retry_after);
56010
+ }
56011
+ let envelope;
56012
+ try {
56013
+ envelope = await response.json();
56014
+ } catch {
56015
+ throw new TelegramApiError(method, response.status, "respons bukan JSON");
56016
+ }
56017
+ if (!envelope.ok || envelope.result === void 0) {
56018
+ throw new TelegramApiError(
56019
+ method,
56020
+ envelope.error_code,
56021
+ this.safe(envelope.description ?? "respons tidak valid"),
56022
+ envelope.parameters?.retry_after
56023
+ );
56024
+ }
56025
+ return envelope.result;
56026
+ }
56027
+ getMe() {
56028
+ return this.call("getMe", {});
56029
+ }
56030
+ async getUpdates(input) {
56031
+ if (!Number.isInteger(input.offset) || input.offset < 0) throw new Error("offset harus integer non-negatif");
56032
+ if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new Error("limit harus 1..100");
56033
+ if (!Number.isInteger(input.timeout) || input.timeout < 0 || input.timeout > 50) throw new Error("timeout harus 0..50");
56034
+ return this.call("getUpdates", {
56035
+ offset: input.offset,
56036
+ limit: input.limit,
56037
+ timeout: input.timeout,
56038
+ allowed_updates: ["message", "callback_query"]
56039
+ }, input.signal);
56040
+ }
56041
+ sendMessage(input) {
56042
+ return this.call("sendMessage", {
56043
+ chat_id: input.chatId,
56044
+ text: input.text,
56045
+ ...input.replyMarkup ? { reply_markup: input.replyMarkup } : {}
56046
+ });
56047
+ }
56048
+ /**
56049
+ * SPEC-493 · indikator "typing…". Argumennya posisional (beda dari tetangganya) sesuai kontrak
56050
+ * yang diminta. Telegram TAK punya API stop-typing: statusnya padam sendiri ~5 detik sesudah
56051
+ * panggilan terakhir, dan pesan masuk apa pun langsung menghapusnya.
56052
+ */
56053
+ sendChatAction(chatId, action = "typing") {
56054
+ return this.call("sendChatAction", { chat_id: chatId, action });
56055
+ }
56056
+ editMessageText(input) {
56057
+ return this.call("editMessageText", {
56058
+ chat_id: input.chatId,
56059
+ message_id: input.messageId,
56060
+ text: input.text
56061
+ });
56062
+ }
56063
+ answerCallbackQuery(input) {
56064
+ return this.call("answerCallbackQuery", {
56065
+ callback_query_id: input.callbackQueryId,
56066
+ ...input.text ? { text: input.text } : {}
56067
+ });
56068
+ }
56069
+ };
56070
+ }
56071
+ });
56072
+
56073
+ // src/services/telegram/protocol.ts
56074
+ import { createHash as createHash10 } from "node:crypto";
56075
+ function normalizeTelegramCommand(text2) {
56076
+ const clean3 = text2.trim();
56077
+ const match = clean3.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
56078
+ if (!match) return clean3;
56079
+ const args = match[2]?.trim().replace(/\s+/g, " ");
56080
+ return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
56081
+ }
56082
+ function parseConfirmationCallback(data) {
56083
+ if (data.length > 64) return null;
56084
+ const match = data.match(/^tgcf:([A-Za-z0-9_-]{1,40}):(approve|deny)$/);
56085
+ return match ? { token: match[1], action: match[2] } : null;
56086
+ }
56087
+ function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
56088
+ const root = record(update2);
56089
+ const updateId = root && Number.isInteger(root.update_id) && Number(root.update_id) >= 0 ? Number(root.update_id) : null;
56090
+ if (!root || updateId === null) return { ok: false, updateId: null, reason: "malformed" };
56091
+ const callback = record(root.callback_query);
56092
+ const message = callback ? record(callback.message) : record(root.message);
56093
+ const from = callback ? record(callback.from) : record(message?.from);
56094
+ const chat = record(message?.chat);
56095
+ const chatId = typeof chat?.id === "number" && Number.isSafeInteger(chat.id) ? String(chat.id) : void 0;
56096
+ const userId = typeof from?.id === "number" && Number.isSafeInteger(from.id) ? String(from.id) : void 0;
56097
+ if (!message || !from || !chat || !chatId || !userId) return { ok: false, updateId, reason: "malformed", chatId, userId };
56098
+ if (chat.type !== "private") return { ok: false, updateId, reason: "group", chatId, userId };
56099
+ if (from.is_bot === true) return { ok: false, updateId, reason: "sender-bot", chatId, userId };
56100
+ if (!allowlist.has(userId)) return { ok: false, updateId, reason: "not-allowed", chatId, userId };
56101
+ const messageId = typeof message.message_id === "number" && Number.isInteger(message.message_id) ? message.message_id : null;
56102
+ if (messageId === null) return { ok: false, updateId, reason: "malformed", chatId, userId };
56103
+ if (callback) {
56104
+ const callbackQueryId = typeof callback.id === "string" ? callback.id : "";
56105
+ const data = typeof callback.data === "string" ? callback.data : "";
56106
+ const parsed = parseConfirmationCallback(data);
56107
+ if (!callbackQueryId || !parsed) return { ok: false, updateId, reason: "invalid-callback", chatId, userId };
56108
+ return {
56109
+ ok: true,
56110
+ input: {
56111
+ updateId,
56112
+ chatId,
56113
+ userId,
56114
+ messageId,
56115
+ kind: "callback",
56116
+ text: data,
56117
+ callbackQueryId,
56118
+ callbackToken: parsed.token,
56119
+ callbackAction: parsed.action
56120
+ }
56121
+ };
56122
+ }
56123
+ if (typeof message.text !== "string" || !message.text.trim()) {
56124
+ return { ok: false, updateId, reason: "non-text", chatId, userId };
56125
+ }
56126
+ const text2 = normalizeTelegramCommand(message.text);
56127
+ if (text2.length > maxText) return { ok: false, updateId, reason: "too-long", chatId, userId };
56128
+ return {
56129
+ ok: true,
56130
+ input: {
56131
+ updateId,
56132
+ chatId,
56133
+ userId,
56134
+ messageId,
56135
+ kind: text2.startsWith("/") ? "command" : "text",
56136
+ text: text2
56137
+ }
56138
+ };
56139
+ }
56140
+ function inboundDigest(input) {
56141
+ return createHash10("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
56142
+ }
56143
+ function sanitizeTelegramOutput(text2, exactSecrets = []) {
56144
+ let clean3 = text2.replace(ANSI, "").replace(CONTROLS, "");
56145
+ for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
56146
+ clean3 = clean3.split(secret).join("[REDACTED]");
56147
+ }
56148
+ for (const pattern of CREDENTIAL_PATTERNS) clean3 = clean3.replace(pattern, "[REDACTED]");
56149
+ return clean3.trim();
56150
+ }
56151
+ function splitTelegramText(text2, max = 4096, exactSecrets = []) {
56152
+ if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
56153
+ const clean3 = sanitizeTelegramOutput(text2, exactSecrets);
56154
+ if (!clean3) return [];
56155
+ const chunks = [];
56156
+ let rest = clean3;
56157
+ while (rest.length > max) {
56158
+ let cut = max;
56159
+ const before2 = rest.charCodeAt(cut - 1);
56160
+ const after = rest.charCodeAt(cut);
56161
+ if (before2 >= 55296 && before2 <= 56319 && after >= 56320 && after <= 57343) cut--;
56162
+ const window2 = rest.slice(0, cut);
56163
+ const newline2 = window2.lastIndexOf("\n");
56164
+ const space = window2.lastIndexOf(" ");
56165
+ const boundary = Math.max(newline2, space);
56166
+ if (boundary >= Math.floor(max / 2)) cut = boundary + 1;
56167
+ chunks.push(rest.slice(0, cut));
56168
+ rest = rest.slice(cut);
56169
+ }
56170
+ if (rest) chunks.push(rest);
56171
+ return chunks;
56172
+ }
56173
+ var record, ANSI, CONTROLS, CREDENTIAL_PATTERNS, TELEGRAM_GATEWAY_FAILURE_KIND, TELEGRAM_FINAL_REPLY_KINDS;
56174
+ var init_protocol = __esm({
56175
+ "src/services/telegram/protocol.ts"() {
56176
+ "use strict";
56177
+ record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
56178
+ ANSI = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
56179
+ CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
56180
+ CREDENTIAL_PATTERNS = [
56181
+ /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g,
56182
+ /\bhnm_(?:agent_)?[A-Za-z0-9_-]{16,}\b/g,
56183
+ /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g
56184
+ ];
56185
+ TELEGRAM_GATEWAY_FAILURE_KIND = "gateway-failure";
56186
+ TELEGRAM_FINAL_REPLY_KINDS = /* @__PURE__ */ new Set([
56187
+ "final",
56188
+ "decision",
56189
+ "failure",
56190
+ "confirmation",
56191
+ TELEGRAM_GATEWAY_FAILURE_KIND
56192
+ ]);
56193
+ }
56194
+ });
56195
+
56196
+ // src/services/telegram/notify.ts
56197
+ function recipients(raw) {
56198
+ return (raw ?? "").split(/[\s,]+/).filter((id2) => /^\d+$/.test(id2));
56199
+ }
56200
+ function dashboardOrigin(env = process.env) {
56201
+ const first = env.HANOMAN_CONTROL_ORIGINS?.split(",").map((o) => o.trim()).find(Boolean);
56202
+ if (!first) return null;
56203
+ try {
56204
+ return new URL(first).origin;
56205
+ } catch {
56206
+ return null;
56207
+ }
56208
+ }
56209
+ function decisionMessage(push, origin) {
56210
+ const title = push.title.length > TITLE_MAX ? `${push.title.slice(0, TITLE_MAX - 1)}\u2026` : push.title;
56211
+ return [
56212
+ "Sesi menunggu keputusanmu",
56213
+ title,
56214
+ push.specId ? `${push.specId} \xB7 sesi ${push.sessionId}` : `sesi ${push.sessionId}`,
56215
+ ...origin ? [`${origin}/terminal`] : []
56216
+ ].join("\n");
56217
+ }
56218
+ async function pushDecisionToTelegram(push, deps = {}) {
56219
+ try {
56220
+ const read2 = deps.read ?? effectiveStr;
56221
+ const setting = await (deps.getSetting ?? getSetting)();
56222
+ if (!setting.notifyDecisionTelegram) return 0;
56223
+ const botToken = read2("HANOMAN_TELEGRAM_BOT_TOKEN")?.trim();
56224
+ const ids = recipients(read2("HANOMAN_TELEGRAM_ALLOWED_USER_IDS"));
56225
+ if (!botToken || ids.length === 0) return 0;
56226
+ const controller = new AbortController();
56227
+ const timer11 = setTimeout(() => controller.abort(), deps.timeoutMs ?? SEND_TIMEOUT_MS);
56228
+ const send = deps.transport ?? ((url2, init) => fetch(url2, init));
56229
+ const client = new TelegramApiClient(botToken, (url2, init) => send(url2, { ...init, signal: controller.signal }));
56230
+ const text2 = decisionMessage(push, deps.origin === void 0 ? dashboardOrigin() : deps.origin);
56231
+ try {
56232
+ const results = await Promise.allSettled(ids.map((chatId) => client.sendMessage({ chatId, text: text2 })));
56233
+ let sent = 0;
56234
+ for (const [i, r] of results.entries()) {
56235
+ if (r.status === "fulfilled") {
56236
+ sent++;
56237
+ continue;
56238
+ }
56239
+ console.warn(
56240
+ `telegram: push keputusan ke ${ids[i]} gagal:`,
56241
+ sanitizeTelegramOutput(r.reason.message, [botToken])
56242
+ );
56243
+ }
56244
+ return sent;
56245
+ } finally {
56246
+ clearTimeout(timer11);
56247
+ }
56248
+ } catch (error) {
56249
+ console.warn("telegram: push keputusan gagal:", error.message);
56250
+ return 0;
56251
+ }
56252
+ }
56253
+ var SEND_TIMEOUT_MS, TITLE_MAX;
56254
+ var init_notify = __esm({
56255
+ "src/services/telegram/notify.ts"() {
56256
+ "use strict";
56257
+ init_config2();
56258
+ init_settings3();
56259
+ init_client();
56260
+ init_protocol();
56261
+ SEND_TIMEOUT_MS = 1e4;
56262
+ TITLE_MAX = 200;
56263
+ }
56264
+ });
56265
+
55951
56266
  // src/services/notifications.ts
55952
56267
  async function recordDrift(vpsId, vpsName, drift, snapshotId) {
55953
56268
  if (drift.length === 0) return;
@@ -56068,6 +56383,7 @@ async function scanDecisions(read2 = liveDecisionsAsync) {
56068
56383
  await prisma.notification.create({
56069
56384
  data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
56070
56385
  });
56386
+ void pushDecisionToTelegram({ title, specId: s2.specId ?? null, sessionId: s2.id });
56071
56387
  }
56072
56388
  }
56073
56389
  async function notificationsFeed(p3 = {}) {
@@ -56090,6 +56406,7 @@ var init_notifications2 = __esm({
56090
56406
  init_db();
56091
56407
  init_pty();
56092
56408
  init_live_phases();
56409
+ init_notify();
56093
56410
  awaiting = /* @__PURE__ */ new Set();
56094
56411
  DEFAULT_FEED_TAKE = 50;
56095
56412
  }
@@ -56167,6 +56484,45 @@ var init_device_sockets = __esm({
56167
56484
  }
56168
56485
  });
56169
56486
 
56487
+ // src/services/exit-guard.ts
56488
+ import { spawn as spawn3 } from "node:child_process";
56489
+ import { writeFileSync as writeFileSync15 } from "node:fs";
56490
+ import { join as join29 } from "node:path";
56491
+ function watchdogScript(pid, graceMs) {
56492
+ const secs = Math.max(1, Math.ceil(graceMs / 1e3));
56493
+ return `s=$(ps -o lstart= -p ${pid}); sleep ${secs}; [ -n "$s" ] && [ "$(ps -o lstart= -p ${pid})" = "$s" ] && kill -9 ${pid}`;
56494
+ }
56495
+ function launchWatchdog(script2, env = process.env) {
56496
+ if (env.VITEST) return false;
56497
+ const child = spawn3("/bin/sh", ["-c", script2], { detached: true, stdio: "ignore" });
56498
+ child.on("error", () => {
56499
+ });
56500
+ child.unref();
56501
+ return true;
56502
+ }
56503
+ function exitGuarded(code, d = {}) {
56504
+ const pid = d.pid ?? process.pid;
56505
+ try {
56506
+ const write2 = d.writeIntent ?? ((p3, t) => writeFileSync15(p3, t, { mode: 384 }));
56507
+ write2(join29(d.home ?? resolveHome(), EXIT_INTENT_FILE), formatExitIntent(pid, code));
56508
+ } catch {
56509
+ }
56510
+ try {
56511
+ (d.launch ?? launchWatchdog)(watchdogScript(pid, d.graceMs ?? EXIT_WATCHDOG_MS));
56512
+ } catch {
56513
+ }
56514
+ (d.exit ?? ((c) => process.exit(c)))(code);
56515
+ }
56516
+ var EXIT_WATCHDOG_MS;
56517
+ var init_exit_guard = __esm({
56518
+ "src/services/exit-guard.ts"() {
56519
+ "use strict";
56520
+ init_src();
56521
+ init_src2();
56522
+ EXIT_WATCHDOG_MS = 5e3;
56523
+ }
56524
+ });
56525
+
56170
56526
  // src/services/update.ts
56171
56527
  import { readFileSync as readFileSync16 } from "node:fs";
56172
56528
  import { resolve as resolve20, dirname as dirname13 } from "node:path";
@@ -56202,7 +56558,7 @@ function supervised() {
56202
56558
  }
56203
56559
  function requestRestartForUpdate() {
56204
56560
  const delay2 = effectiveInt("HANOMAN_UPDATE_RESTART_DELAY_MS") ?? 250;
56205
- setTimeout(() => (exiter ?? ((c) => process.exit(c)))(UPDATE_RESTART_EXIT), delay2);
56561
+ setTimeout(() => (exiter ?? exitGuarded)(UPDATE_RESTART_EXIT), delay2);
56206
56562
  }
56207
56563
  async function maybeFetch() {
56208
56564
  if (!effectiveBool("HANOMAN_UPDATE_FETCH")) return;
@@ -56248,6 +56604,7 @@ var init_update = __esm({
56248
56604
  "use strict";
56249
56605
  init_src();
56250
56606
  init_config2();
56607
+ init_exit_guard();
56251
56608
  UPDATE_COMMAND = "npm i -g hanoman@latest --prefer-online";
56252
56609
  RESULT_TTL_MS = 15e3;
56253
56610
  FETCH_TTL_MS = 5 * 6e4;
@@ -56744,7 +57101,7 @@ var init_runtime_defaults2 = __esm({
56744
57101
  });
56745
57102
 
56746
57103
  // src/services/agent-token.ts
56747
- import { randomBytes as randomBytes2, createHash as createHash11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
57104
+ import { randomBytes as randomBytes2, createHash as createHash12, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
56748
57105
  function toAgentTokenView(t) {
56749
57106
  return {
56750
57107
  id: t.id,
@@ -56831,112 +57188,11 @@ var init_agent_token = __esm({
56831
57188
  "src/services/agent-token.ts"() {
56832
57189
  "use strict";
56833
57190
  init_db();
56834
- hash2 = (token) => createHash11("sha256").update(token).digest("hex");
57191
+ hash2 = (token) => createHash12("sha256").update(token).digest("hex");
56835
57192
  AGENT_CREATOR_PREFIX = "agent:";
56836
57193
  }
56837
57194
  });
56838
57195
 
56839
- // src/services/telegram/client.ts
56840
- var TelegramApiError, TelegramApiClient;
56841
- var init_client = __esm({
56842
- "src/services/telegram/client.ts"() {
56843
- "use strict";
56844
- TelegramApiError = class extends Error {
56845
- constructor(method, code, message, retryAfter) {
56846
- super(`Telegram ${method} gagal${code ? ` (${code})` : ""}: ${message}`);
56847
- this.method = method;
56848
- this.code = code;
56849
- this.retryAfter = retryAfter;
56850
- this.name = "TelegramApiError";
56851
- }
56852
- };
56853
- TelegramApiClient = class {
56854
- constructor(token, transport = (url2, init) => fetch(url2, init), baseUrl = "https://api.telegram.org") {
56855
- this.token = token;
56856
- this.transport = transport;
56857
- this.baseUrl = baseUrl;
56858
- }
56859
- safe(message) {
56860
- return message.split(this.token).join("[REDACTED]").replace(/https:\/\/api\.telegram\.org\/bot[^/\s]+/g, "https://api.telegram.org/bot[REDACTED]");
56861
- }
56862
- async call(method, body, signal) {
56863
- let response;
56864
- try {
56865
- response = await this.transport(`${this.baseUrl}/bot${this.token}/${method}`, {
56866
- method: "POST",
56867
- headers: { "content-type": "application/json" },
56868
- body: JSON.stringify(body),
56869
- signal
56870
- });
56871
- } catch (error) {
56872
- throw new TelegramApiError(method, void 0, this.safe(error.message || "network error"));
56873
- }
56874
- if (!response.ok) {
56875
- const failed = await response.json().catch(() => null);
56876
- throw new TelegramApiError(method, response.status, `HTTP ${response.status}`, failed?.parameters?.retry_after);
56877
- }
56878
- let envelope;
56879
- try {
56880
- envelope = await response.json();
56881
- } catch {
56882
- throw new TelegramApiError(method, response.status, "respons bukan JSON");
56883
- }
56884
- if (!envelope.ok || envelope.result === void 0) {
56885
- throw new TelegramApiError(
56886
- method,
56887
- envelope.error_code,
56888
- this.safe(envelope.description ?? "respons tidak valid"),
56889
- envelope.parameters?.retry_after
56890
- );
56891
- }
56892
- return envelope.result;
56893
- }
56894
- getMe() {
56895
- return this.call("getMe", {});
56896
- }
56897
- async getUpdates(input) {
56898
- if (!Number.isInteger(input.offset) || input.offset < 0) throw new Error("offset harus integer non-negatif");
56899
- if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new Error("limit harus 1..100");
56900
- if (!Number.isInteger(input.timeout) || input.timeout < 0 || input.timeout > 50) throw new Error("timeout harus 0..50");
56901
- return this.call("getUpdates", {
56902
- offset: input.offset,
56903
- limit: input.limit,
56904
- timeout: input.timeout,
56905
- allowed_updates: ["message", "callback_query"]
56906
- }, input.signal);
56907
- }
56908
- sendMessage(input) {
56909
- return this.call("sendMessage", {
56910
- chat_id: input.chatId,
56911
- text: input.text,
56912
- ...input.replyMarkup ? { reply_markup: input.replyMarkup } : {}
56913
- });
56914
- }
56915
- /**
56916
- * SPEC-493 · indikator "typing…". Argumennya posisional (beda dari tetangganya) sesuai kontrak
56917
- * yang diminta. Telegram TAK punya API stop-typing: statusnya padam sendiri ~5 detik sesudah
56918
- * panggilan terakhir, dan pesan masuk apa pun langsung menghapusnya.
56919
- */
56920
- sendChatAction(chatId, action = "typing") {
56921
- return this.call("sendChatAction", { chat_id: chatId, action });
56922
- }
56923
- editMessageText(input) {
56924
- return this.call("editMessageText", {
56925
- chat_id: input.chatId,
56926
- message_id: input.messageId,
56927
- text: input.text
56928
- });
56929
- }
56930
- answerCallbackQuery(input) {
56931
- return this.call("answerCallbackQuery", {
56932
- callback_query_id: input.callbackQueryId,
56933
- ...input.text ? { text: input.text } : {}
56934
- });
56935
- }
56936
- };
56937
- }
56938
- });
56939
-
56940
57196
  // src/services/telegram/config.ts
56941
57197
  async function setTelegramEngine(next) {
56942
57198
  const cur = await getSetting();
@@ -56972,129 +57228,6 @@ var init_config4 = __esm({
56972
57228
  }
56973
57229
  });
56974
57230
 
56975
- // src/services/telegram/protocol.ts
56976
- import { createHash as createHash12 } from "node:crypto";
56977
- function normalizeTelegramCommand(text2) {
56978
- const clean3 = text2.trim();
56979
- const match = clean3.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
56980
- if (!match) return clean3;
56981
- const args = match[2]?.trim().replace(/\s+/g, " ");
56982
- return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
56983
- }
56984
- function parseConfirmationCallback(data) {
56985
- if (data.length > 64) return null;
56986
- const match = data.match(/^tgcf:([A-Za-z0-9_-]{1,40}):(approve|deny)$/);
56987
- return match ? { token: match[1], action: match[2] } : null;
56988
- }
56989
- function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
56990
- const root = record(update2);
56991
- const updateId = root && Number.isInteger(root.update_id) && Number(root.update_id) >= 0 ? Number(root.update_id) : null;
56992
- if (!root || updateId === null) return { ok: false, updateId: null, reason: "malformed" };
56993
- const callback = record(root.callback_query);
56994
- const message = callback ? record(callback.message) : record(root.message);
56995
- const from = callback ? record(callback.from) : record(message?.from);
56996
- const chat = record(message?.chat);
56997
- const chatId = typeof chat?.id === "number" && Number.isSafeInteger(chat.id) ? String(chat.id) : void 0;
56998
- const userId = typeof from?.id === "number" && Number.isSafeInteger(from.id) ? String(from.id) : void 0;
56999
- if (!message || !from || !chat || !chatId || !userId) return { ok: false, updateId, reason: "malformed", chatId, userId };
57000
- if (chat.type !== "private") return { ok: false, updateId, reason: "group", chatId, userId };
57001
- if (from.is_bot === true) return { ok: false, updateId, reason: "sender-bot", chatId, userId };
57002
- if (!allowlist.has(userId)) return { ok: false, updateId, reason: "not-allowed", chatId, userId };
57003
- const messageId = typeof message.message_id === "number" && Number.isInteger(message.message_id) ? message.message_id : null;
57004
- if (messageId === null) return { ok: false, updateId, reason: "malformed", chatId, userId };
57005
- if (callback) {
57006
- const callbackQueryId = typeof callback.id === "string" ? callback.id : "";
57007
- const data = typeof callback.data === "string" ? callback.data : "";
57008
- const parsed = parseConfirmationCallback(data);
57009
- if (!callbackQueryId || !parsed) return { ok: false, updateId, reason: "invalid-callback", chatId, userId };
57010
- return {
57011
- ok: true,
57012
- input: {
57013
- updateId,
57014
- chatId,
57015
- userId,
57016
- messageId,
57017
- kind: "callback",
57018
- text: data,
57019
- callbackQueryId,
57020
- callbackToken: parsed.token,
57021
- callbackAction: parsed.action
57022
- }
57023
- };
57024
- }
57025
- if (typeof message.text !== "string" || !message.text.trim()) {
57026
- return { ok: false, updateId, reason: "non-text", chatId, userId };
57027
- }
57028
- const text2 = normalizeTelegramCommand(message.text);
57029
- if (text2.length > maxText) return { ok: false, updateId, reason: "too-long", chatId, userId };
57030
- return {
57031
- ok: true,
57032
- input: {
57033
- updateId,
57034
- chatId,
57035
- userId,
57036
- messageId,
57037
- kind: text2.startsWith("/") ? "command" : "text",
57038
- text: text2
57039
- }
57040
- };
57041
- }
57042
- function inboundDigest(input) {
57043
- return createHash12("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
57044
- }
57045
- function sanitizeTelegramOutput(text2, exactSecrets = []) {
57046
- let clean3 = text2.replace(ANSI, "").replace(CONTROLS, "");
57047
- for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
57048
- clean3 = clean3.split(secret).join("[REDACTED]");
57049
- }
57050
- for (const pattern of CREDENTIAL_PATTERNS) clean3 = clean3.replace(pattern, "[REDACTED]");
57051
- return clean3.trim();
57052
- }
57053
- function splitTelegramText(text2, max = 4096, exactSecrets = []) {
57054
- if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
57055
- const clean3 = sanitizeTelegramOutput(text2, exactSecrets);
57056
- if (!clean3) return [];
57057
- const chunks = [];
57058
- let rest = clean3;
57059
- while (rest.length > max) {
57060
- let cut = max;
57061
- const before2 = rest.charCodeAt(cut - 1);
57062
- const after = rest.charCodeAt(cut);
57063
- if (before2 >= 55296 && before2 <= 56319 && after >= 56320 && after <= 57343) cut--;
57064
- const window2 = rest.slice(0, cut);
57065
- const newline2 = window2.lastIndexOf("\n");
57066
- const space = window2.lastIndexOf(" ");
57067
- const boundary = Math.max(newline2, space);
57068
- if (boundary >= Math.floor(max / 2)) cut = boundary + 1;
57069
- chunks.push(rest.slice(0, cut));
57070
- rest = rest.slice(cut);
57071
- }
57072
- if (rest) chunks.push(rest);
57073
- return chunks;
57074
- }
57075
- var record, ANSI, CONTROLS, CREDENTIAL_PATTERNS, TELEGRAM_GATEWAY_FAILURE_KIND, TELEGRAM_FINAL_REPLY_KINDS;
57076
- var init_protocol = __esm({
57077
- "src/services/telegram/protocol.ts"() {
57078
- "use strict";
57079
- record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
57080
- ANSI = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
57081
- CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
57082
- CREDENTIAL_PATTERNS = [
57083
- /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g,
57084
- /\bhnm_(?:agent_)?[A-Za-z0-9_-]{16,}\b/g,
57085
- /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g
57086
- ];
57087
- TELEGRAM_GATEWAY_FAILURE_KIND = "gateway-failure";
57088
- TELEGRAM_FINAL_REPLY_KINDS = /* @__PURE__ */ new Set([
57089
- "final",
57090
- "decision",
57091
- "failure",
57092
- "confirmation",
57093
- TELEGRAM_GATEWAY_FAILURE_KIND
57094
- ]);
57095
- }
57096
- });
57097
-
57098
57231
  // src/services/telegram/runtime.ts
57099
57232
  function setTelegramRuntime(input) {
57100
57233
  registration = {
@@ -62748,15 +62881,15 @@ var init_client2 = __esm({
62748
62881
 
62749
62882
  // src/services/logs/spool.ts
62750
62883
  import { appendFile, mkdir as mkdir7, readFile as readFile7, readdir as readdir7, rm as rm6, stat as stat6 } from "node:fs/promises";
62751
- import { join as join32 } from "node:path";
62884
+ import { join as join33 } from "node:path";
62752
62885
  function spoolDir(lane) {
62753
- return join32(resolveDataDirs().home, "log-spool", lane);
62886
+ return join33(resolveDataDirs().home, "log-spool", lane);
62754
62887
  }
62755
62888
  async function currentSegment(lane) {
62756
62889
  const dir2 = spoolDir(lane);
62757
62890
  await mkdir7(dir2, { recursive: true, mode: 448 });
62758
62891
  if (currentFile && currentBytes < LOG_SPOOL_SEGMENT_BYTES) return currentFile;
62759
- currentFile = join32(dir2, `${Date.now()}-${++segCounter}.ndjson`);
62892
+ currentFile = join33(dir2, `${Date.now()}-${++segCounter}.ndjson`);
62760
62893
  currentBytes = 0;
62761
62894
  return currentFile;
62762
62895
  }
@@ -62778,7 +62911,7 @@ async function readSpoolSegments(lane) {
62778
62911
  const files2 = names.filter((n2) => n2.endsWith(".ndjson")).sort();
62779
62912
  const out4 = [];
62780
62913
  for (const name2 of files2) {
62781
- const file = join32(dir2, name2);
62914
+ const file = join33(dir2, name2);
62782
62915
  const raw = await readFile7(file, "utf8").catch(() => "");
62783
62916
  const entries4 = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
62784
62917
  out4.push({ file, entries: entries4 });
@@ -63465,15 +63598,15 @@ var init_codex_version = __esm({
63465
63598
  // src/services/agent-tool-catalog.ts
63466
63599
  import { readFileSync as readFileSync26 } from "node:fs";
63467
63600
  import { homedir as homedir13 } from "node:os";
63468
- import { join as join48 } from "node:path";
63601
+ import { join as join49 } from "node:path";
63469
63602
  function mcpServerNames(repoDir) {
63470
63603
  const names = [];
63471
- const claudeJson = readJson2(join48(home(), ".claude.json"));
63604
+ const claudeJson = readJson2(join49(home(), ".claude.json"));
63472
63605
  names.push(...serversOf(claudeJson));
63473
63606
  if (repoDir) {
63474
63607
  const projects = claudeJson?.projects;
63475
63608
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
63476
- names.push(...serversOf(readJson2(join48(repoDir, ".mcp.json"))));
63609
+ names.push(...serversOf(readJson2(join49(repoDir, ".mcp.json"))));
63477
63610
  }
63478
63611
  names.push(...codexServers());
63479
63612
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -63502,7 +63635,7 @@ var init_agent_tool_catalog = __esm({
63502
63635
  codexServers = () => {
63503
63636
  let text2;
63504
63637
  try {
63505
- text2 = readFileSync26(join48(home(), ".codex", "config.toml"), "utf8");
63638
+ text2 = readFileSync26(join49(home(), ".codex", "config.toml"), "utf8");
63506
63639
  } catch {
63507
63640
  return [];
63508
63641
  }
@@ -70445,8 +70578,8 @@ init_local_binding();
70445
70578
  init_session_id();
70446
70579
  init_uploads();
70447
70580
  import { mkdir as mkdir6, readdir as readdir5, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "node:fs/promises";
70448
- import { join as join29 } from "node:path";
70449
- var specAttachmentsDir = (repoDir, sessionId2) => join29(repoDir, ".worktrees", ".attachments", sessionId2);
70581
+ import { join as join30 } from "node:path";
70582
+ var specAttachmentsDir = (repoDir, sessionId2) => join30(repoDir, ".worktrees", ".attachments", sessionId2);
70450
70583
  var INDEX = "INDEX.md";
70451
70584
  var humanSize = (n2) => n2 >= 1024 * 1024 ? `${(n2 / 1024 / 1024).toFixed(1)} MB` : n2 >= 1024 ? `${Math.round(n2 / 1024)} KB` : `${n2} B`;
70452
70585
  function uniqueName(taken, filename) {
@@ -70496,19 +70629,19 @@ async function syncSpecAttachmentsDir(specId, projectId) {
70496
70629
  for (const a of rows) {
70497
70630
  let bytes;
70498
70631
  try {
70499
- bytes = await readFile6(join29(uploadDir(), a.storageKey));
70632
+ bytes = await readFile6(join30(uploadDir(), a.storageKey));
70500
70633
  } catch {
70501
70634
  continue;
70502
70635
  }
70503
70636
  const filename = uniqueName(taken, a.filename);
70504
- const path = join29(dir2, filename);
70637
+ const path = join30(dir2, filename);
70505
70638
  await writeFile5(path, bytes, { mode: 384 });
70506
70639
  items.push({ filename, mimeType: a.mimeType, size: a.size, path });
70507
70640
  }
70508
- await writeFile5(join29(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
70641
+ await writeFile5(join30(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
70509
70642
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
70510
70643
  for (const name2 of await readdir5(dir2)) {
70511
- if (!keep.has(name2)) await rm4(join29(dir2, name2), { recursive: true, force: true }).catch(() => {
70644
+ if (!keep.has(name2)) await rm4(join30(dir2, name2), { recursive: true, force: true }).catch(() => {
70512
70645
  });
70513
70646
  }
70514
70647
  return items;
@@ -73147,7 +73280,7 @@ init_session_phases();
73147
73280
  init_stage_machine();
73148
73281
  init_notifications2();
73149
73282
  init_sync_notify();
73150
- import { createHash as createHash10 } from "node:crypto";
73283
+ import { createHash as createHash11 } from "node:crypto";
73151
73284
  var specNum = (id2) => Number.parseInt(id2.match(/\d+/)?.[0] ?? "0", 10);
73152
73285
  var selectOf = (shape) => Object.fromEntries(
73153
73286
  Object.keys(shape).filter((k) => k !== "blockedBy").map((k) => [k, true])
@@ -73239,7 +73372,7 @@ async function liveSignature() {
73239
73372
  async function specsDigest() {
73240
73373
  const agg = await prisma.spec.aggregate({ _count: { _all: true }, _max: { updatedAt: true }, _sum: { version: true } });
73241
73374
  const db = `${agg._count._all}:${agg._max.updatedAt?.getTime() ?? 0}:${agg._sum.version ?? 0}`;
73242
- return createHash10("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
73375
+ return createHash11("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
73243
73376
  }
73244
73377
  async function liveSpecs(filter = {}) {
73245
73378
  await liveOverlayTick();
@@ -73954,7 +74087,7 @@ async function docs_default(app3) {
73954
74087
  // src/routes/ide.ts
73955
74088
  init_src2();
73956
74089
  import { basename as basename10 } from "node:path";
73957
- import { spawn as spawn3 } from "node:child_process";
74090
+ import { spawn as spawn4 } from "node:child_process";
73958
74091
 
73959
74092
  // src/services/git-remotes.ts
73960
74093
  import { execFile as execFile14 } from "node:child_process";
@@ -74024,7 +74157,7 @@ init_codex_trust();
74024
74157
  // src/services/repo-fs.ts
74025
74158
  import { lstat as lstat2, rename as rename4, rm as rm5 } from "node:fs/promises";
74026
74159
  import { createWriteStream } from "node:fs";
74027
- import { join as join30 } from "node:path";
74160
+ import { join as join31 } from "node:path";
74028
74161
  import { randomUUID as randomUUID11 } from "node:crypto";
74029
74162
  import { pipeline } from "node:stream/promises";
74030
74163
  var EntryExistsError = class extends Error {
@@ -74085,7 +74218,7 @@ async function saveUpload2(repoDir, rel, source, opts2 = {}) {
74085
74218
  if (current && !opts2.overwrite) return { status: "exists" };
74086
74219
  if (current && !current.isFile())
74087
74220
  throw new PathContainmentError("repository path ditolak: target bukan file regular");
74088
- const temp = join30(entry2.parent, `.hanoman-${randomUUID11()}.tmp`);
74221
+ const temp = join31(entry2.parent, `.hanoman-${randomUUID11()}.tmp`);
74089
74222
  try {
74090
74223
  await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
74091
74224
  if (opts2.isTruncated?.()) {
@@ -74369,7 +74502,7 @@ async function ide_default(app3) {
74369
74502
  const ref = q2.ref || "HEAD";
74370
74503
  if (!/^[\w./@{}-]+$/.test(ref)) return reply.code(400).send({ error: "ref tidak valid" });
74371
74504
  const fmt = q2.format === "tar" ? "tar" : "zip";
74372
- const child = spawn3("git", ["archive", `--format=${fmt}`, "--end-of-options", ref], { cwd: repoDir });
74505
+ const child = spawn4("git", ["archive", `--format=${fmt}`, "--end-of-options", ref], { cwd: repoDir });
74373
74506
  reply.header("content-type", fmt === "zip" ? "application/zip" : "application/x-tar");
74374
74507
  reply.header("content-disposition", `attachment; filename="${basename10(repoDir)}-${ref.replace(/[^\w.-]/g, "_")}.${fmt}"`);
74375
74508
  return reply.send(child.stdout);
@@ -74576,14 +74709,14 @@ async function finishGraphOp(reply, id2, repoDir, r, verb) {
74576
74709
  // src/routes/fs.ts
74577
74710
  import { readdir as readdir6 } from "node:fs/promises";
74578
74711
  import { homedir as homedir6 } from "node:os";
74579
- import { resolve as resolve22, dirname as dirname14, join as join31 } from "node:path";
74712
+ import { resolve as resolve22, dirname as dirname14, join as join32 } from "node:path";
74580
74713
  async function fs_default(app3) {
74581
74714
  app3.get("/fs/browse", async (req, reply) => {
74582
74715
  const q2 = req.query.path;
74583
74716
  const dir2 = q2 && q2.trim() ? resolve22(q2.trim()) : homedir6();
74584
74717
  try {
74585
74718
  const ents = await readdir6(dir2, { withFileTypes: true });
74586
- const entries4 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join31(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
74719
+ const entries4 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join32(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
74587
74720
  const parent = dirname14(dir2);
74588
74721
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries4 };
74589
74722
  } catch {
@@ -74612,11 +74745,11 @@ init_src2();
74612
74745
  init_db();
74613
74746
  import { execFile as execFile15, spawnSync as spawnSync4 } from "node:child_process";
74614
74747
  import { existsSync as existsSync16, readdirSync as readdirSync8, rmSync as rmSync9 } from "node:fs";
74615
- import { join as join33 } from "node:path";
74748
+ import { join as join34 } from "node:path";
74616
74749
  import { promisify as promisify14 } from "node:util";
74617
74750
 
74618
74751
  // src/services/git-auth.ts
74619
- import { chmodSync as chmodSync8, mkdirSync as mkdirSync15, writeFileSync as writeFileSync15 } from "node:fs";
74752
+ import { chmodSync as chmodSync8, mkdirSync as mkdirSync15, writeFileSync as writeFileSync16 } from "node:fs";
74620
74753
  import { tmpdir as tmpdir6 } from "node:os";
74621
74754
  import { dirname as dirname15 } from "node:path";
74622
74755
  var gitAskpassPath = () => `${tmpdir6()}/hanoman-askpass/token.sh`;
@@ -74637,7 +74770,7 @@ var isGithubHttps = (remote) => /^https:\/\/github\.com\//.test(remote);
74637
74770
  function gitAuthEnv(token) {
74638
74771
  const path = gitAskpassPath();
74639
74772
  mkdirSync15(dirname15(path), { recursive: true, mode: 448 });
74640
- writeFileSync15(path, ASKPASS_TOKEN, { mode: 448 });
74773
+ writeFileSync16(path, ASKPASS_TOKEN, { mode: 448 });
74641
74774
  chmodSync8(path, 448);
74642
74775
  return { GIT_ASKPASS: path, GIT_TERMINAL_PROMPT: "0", HANOMAN_GIT_TOKEN: token };
74643
74776
  }
@@ -74679,7 +74812,7 @@ var CheckoutError = class extends Error {
74679
74812
  var CLONE_TIMEOUT_MS = 10 * 6e4;
74680
74813
  var FETCH_TIMEOUT_MS = 2 * 6e4;
74681
74814
  var AUTH_RE = /Authentication failed|could not read (Username|Password)|Invalid username or password|HTTP 40[13]|returned error: 40[13]|Repository not found|Permission denied \(publickey\)/i;
74682
- var checkoutRoot = () => join33(resolveHome(), "projects");
74815
+ var checkoutRoot = () => join34(resolveHome(), "projects");
74683
74816
  var slug = (id2) => id2.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
74684
74817
  var sameRemote = (a, b) => {
74685
74818
  const norm5 = (u) => httpsRemote(u).replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
@@ -74713,7 +74846,7 @@ async function doClone(projectId, opts2) {
74713
74846
  if (!project) throw new CheckoutError(`project "${projectId}" tidak ada`, "not-found");
74714
74847
  if (!project.gitRemote) throw new CheckoutError(`project "${projectId}" tidak punya gitRemote untuk di-clone`, "no-remote");
74715
74848
  const remote = project.gitRemote;
74716
- const dest = opts2.dir ?? join33(checkoutRoot(), slug(project.id));
74849
+ const dest = opts2.dir ?? join34(checkoutRoot(), slug(project.id));
74717
74850
  if (existsSync16(dest) && !isEmptyDir(dest)) {
74718
74851
  const origin = originOf2(dest);
74719
74852
  if (origin && sameRemote(origin, remote)) {
@@ -75297,13 +75430,13 @@ init_local_binding();
75297
75430
 
75298
75431
  // src/services/terminal-diag.ts
75299
75432
  import { appendFileSync as appendFileSync4, mkdirSync as mkdirSync16, statSync as statSync4, rmSync as rmSync10 } from "node:fs";
75300
- import { join as join34 } from "node:path";
75433
+ import { join as join35 } from "node:path";
75301
75434
  var DIAG_MAX_BYTES = 2 * 1024 * 1024;
75302
75435
  var KINDS = /* @__PURE__ */ new Set(["key", "comp", "data", "ack", "pred"]);
75303
75436
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
75304
75437
  function diagFile(home3, sessionId2) {
75305
75438
  if (!ID.test(sessionId2)) throw new Error(`id sesi tak sah untuk diag: ${sessionId2}`);
75306
- return join34(home3, "diag", `${sessionId2}.jsonl`);
75439
+ return join35(home3, "diag", `${sessionId2}.jsonl`);
75307
75440
  }
75308
75441
  function usable(ev) {
75309
75442
  if (!ev || typeof ev !== "object") return false;
@@ -75314,7 +75447,7 @@ function appendDiag(home3, sessionId2, events) {
75314
75447
  const file = diagFile(home3, sessionId2);
75315
75448
  const rows = events.filter(usable);
75316
75449
  if (!rows.length) return;
75317
- mkdirSync16(join34(home3, "diag"), { recursive: true });
75450
+ mkdirSync16(join35(home3, "diag"), { recursive: true });
75318
75451
  try {
75319
75452
  if (statSync4(file).size > DIAG_MAX_BYTES) rmSync10(file);
75320
75453
  } catch {
@@ -75408,7 +75541,7 @@ init_session_event_spool();
75408
75541
  init_ingress_policy();
75409
75542
  import { constants as constants4 } from "node:fs";
75410
75543
  import { mkdir as mkdir8, open as open3, readdir as readdir8, rm as rm7 } from "node:fs/promises";
75411
- import { join as join35 } from "node:path";
75544
+ import { join as join36 } from "node:path";
75412
75545
  var MAX_EVENT_BYTES = 1e6;
75413
75546
  var MAX_FILES_PER_DRAIN = 1e3;
75414
75547
  var SESSION_ID_RE = /^[a-z0-9_-]+$/;
@@ -75459,7 +75592,7 @@ async function drainSpool(app3, root, observation, host2) {
75459
75592
  let readBuffer;
75460
75593
  for (const session of await readdir8(root, { withFileTypes: true })) {
75461
75594
  if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
75462
- const dir2 = join35(root, session.name);
75595
+ const dir2 = join36(root, session.name);
75463
75596
  let entries4;
75464
75597
  try {
75465
75598
  entries4 = await readdir8(dir2, { withFileTypes: true });
@@ -75471,7 +75604,7 @@ async function drainSpool(app3, root, observation, host2) {
75471
75604
  if (!entry2.isFile() || !entry2.name.endsWith(".json")) continue;
75472
75605
  const eventAt = /^(\d{1,15})-/.exec(entry2.name)?.[1];
75473
75606
  if (++examined > MAX_FILES_PER_DRAIN) return delivered;
75474
- const path = join35(dir2, entry2.name);
75607
+ const path = join36(dir2, entry2.name);
75475
75608
  let payload;
75476
75609
  let handle;
75477
75610
  try {
@@ -76498,9 +76631,9 @@ init_session_sandbox();
76498
76631
  init_session_sandbox();
76499
76632
  import { execFile as execFile16 } from "node:child_process";
76500
76633
  import { randomUUID as randomUUID13 } from "node:crypto";
76501
- import { mkdirSync as mkdirSync17, rmSync as rmSync11, writeFileSync as writeFileSync16 } from "node:fs";
76634
+ import { mkdirSync as mkdirSync17, rmSync as rmSync11, writeFileSync as writeFileSync17 } from "node:fs";
76502
76635
  import { constants as constants5, tmpdir as tmpdir7 } from "node:os";
76503
- import { join as join36 } from "node:path";
76636
+ import { join as join37 } from "node:path";
76504
76637
  var binFor = (agent) => agent === "antigravity" ? antigravityBin() : agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
76505
76638
  function leadArgv(o) {
76506
76639
  if (o.agent === "antigravity") return [
@@ -76538,11 +76671,11 @@ function leadProcess(prompt, o, env = process.env) {
76538
76671
  const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
76539
76672
  if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
76540
76673
  } };
76541
- const promptDir = join36(tmpdir7(), "hanoman-prompts");
76674
+ const promptDir = join37(tmpdir7(), "hanoman-prompts");
76542
76675
  mkdirSync17(promptDir, { recursive: true, mode: 448 });
76543
- const promptFile = join36(promptDir, `oneshot-${randomUUID13()}`);
76544
- writeFileSync16(promptFile, prompt, { flag: "wx", mode: 384 });
76545
- const workspace = o.cwd ?? join36(tmpdir7(), `hanoman-oneshot-${randomUUID13()}`);
76676
+ const promptFile = join37(promptDir, `oneshot-${randomUUID13()}`);
76677
+ writeFileSync17(promptFile, prompt, { flag: "wx", mode: 384 });
76678
+ const workspace = o.cwd ?? join37(tmpdir7(), `hanoman-oneshot-${randomUUID13()}`);
76546
76679
  if (!o.cwd) mkdirSync17(workspace, { recursive: false, mode: 448 });
76547
76680
  try {
76548
76681
  const argsWithoutPrompt = directArgs.slice(0, -1);
@@ -77401,13 +77534,13 @@ async function run2(sessionId2, deps) {
77401
77534
  // src/services/decision-guard.ts
77402
77535
  init_src2();
77403
77536
  init_db();
77404
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync6, writeFileSync as writeFileSync17 } from "node:fs";
77405
- import { join as join37 } from "node:path";
77537
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync6, writeFileSync as writeFileSync18 } from "node:fs";
77538
+ import { join as join38 } from "node:path";
77406
77539
  var SESSION_HEADER = "x-hanoman-session";
77407
77540
  var SAFE_ID = /^[A-Za-z0-9._-]{1,200}$/;
77408
77541
  var launchers = /* @__PURE__ */ new Map();
77409
77542
  var STALE_MS = 30 * 24 * 60 * 6e4;
77410
- var launcherDir = (env = process.env) => join37(resolveHome(env), "session-launchers");
77543
+ var launcherDir = (env = process.env) => join38(resolveHome(env), "session-launchers");
77411
77544
  function recordLauncher(sessionId2, agentId, env = process.env) {
77412
77545
  if (!SAFE_ID.test(sessionId2)) return;
77413
77546
  const known2 = launchersFor(sessionId2, env);
@@ -77415,11 +77548,11 @@ function recordLauncher(sessionId2, agentId, env = process.env) {
77415
77548
  try {
77416
77549
  const dir2 = launcherDir(env);
77417
77550
  mkdirSync18(dir2, { recursive: true, mode: 448 });
77418
- writeFileSync17(join37(dir2, sessionId2), [...known2].join("\n"), { mode: 384 });
77551
+ writeFileSync18(join38(dir2, sessionId2), [...known2].join("\n"), { mode: 384 });
77419
77552
  const now = Date.now();
77420
77553
  for (const f of readdirSync9(dir2)) {
77421
77554
  try {
77422
- if (now - statSync6(join37(dir2, f)).mtimeMs > STALE_MS) rmSync12(join37(dir2, f), { force: true });
77555
+ if (now - statSync6(join38(dir2, f)).mtimeMs > STALE_MS) rmSync12(join38(dir2, f), { force: true });
77423
77556
  } catch {
77424
77557
  }
77425
77558
  }
@@ -77431,7 +77564,7 @@ function launchersFor(sessionId2, env) {
77431
77564
  if (hot) return hot;
77432
77565
  const ids = /* @__PURE__ */ new Set();
77433
77566
  try {
77434
- for (const l of readFileSync18(join37(launcherDir(env), sessionId2), "utf8").split("\n")) if (l.trim()) ids.add(l.trim());
77567
+ for (const l of readFileSync18(join38(launcherDir(env), sessionId2), "utf8").split("\n")) if (l.trim()) ids.add(l.trim());
77435
77568
  } catch {
77436
77569
  }
77437
77570
  launchers.set(sessionId2, ids);
@@ -78329,10 +78462,10 @@ import { readFileSync as readFileSync20 } from "node:fs";
78329
78462
 
78330
78463
  // src/services/vps-ssh.ts
78331
78464
  init_config2();
78332
- import { spawn as spawn4 } from "node:child_process";
78333
- import { mkdtempSync as mkdtempSync4, rmSync as rmSync13, writeFileSync as writeFileSync18 } from "node:fs";
78465
+ import { spawn as spawn5 } from "node:child_process";
78466
+ import { mkdtempSync as mkdtempSync4, rmSync as rmSync13, writeFileSync as writeFileSync19 } from "node:fs";
78334
78467
  import { tmpdir as tmpdir8 } from "node:os";
78335
- import { dirname as dirname16, join as join38 } from "node:path";
78468
+ import { dirname as dirname16, join as join39 } from "node:path";
78336
78469
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
78337
78470
  function consoleArgv(t) {
78338
78471
  return [
@@ -78347,9 +78480,9 @@ function consoleArgv(t) {
78347
78480
  ];
78348
78481
  }
78349
78482
  function askpassScript() {
78350
- const dir2 = mkdtempSync4(join38(tmpdir8(), "hanoman-askpass-"));
78351
- const path = join38(dir2, "askpass.sh");
78352
- writeFileSync18(path, `#!/bin/sh
78483
+ const dir2 = mkdtempSync4(join39(tmpdir8(), "hanoman-askpass-"));
78484
+ const path = join39(dir2, "askpass.sh");
78485
+ writeFileSync19(path, `#!/bin/sh
78353
78486
  printf '%s' "$HANOMAN_SSH_PASSWORD"
78354
78487
  `, { mode: 448 });
78355
78488
  return path;
@@ -78383,7 +78516,7 @@ function sshExec(t, remoteCmd, opts2 = {}) {
78383
78516
  HANOMAN_SSH_PASSWORD: opts2.password
78384
78517
  } : process.env;
78385
78518
  return new Promise((resolve25) => {
78386
- const p3 = spawn4(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
78519
+ const p3 = spawn5(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
78387
78520
  let out4 = "";
78388
78521
  const timer11 = setTimeout(() => p3.kill("SIGKILL"), opts2.timeoutMs ?? 6e4);
78389
78522
  const done = (r) => {
@@ -78409,16 +78542,16 @@ function sshExec(t, remoteCmd, opts2 = {}) {
78409
78542
  // src/services/vps-audit.ts
78410
78543
  init_db();
78411
78544
  import { existsSync as existsSync20, readFileSync as readFileSync19 } from "node:fs";
78412
- import { join as join40 } from "node:path";
78545
+ import { join as join41 } from "node:path";
78413
78546
  import { fileURLToPath as fileURLToPath4 } from "node:url";
78414
78547
 
78415
78548
  // src/runner/deps.ts
78416
78549
  import { existsSync as existsSync19 } from "node:fs";
78417
- import { dirname as dirname17, join as join39 } from "node:path";
78550
+ import { dirname as dirname17, join as join40 } from "node:path";
78418
78551
  function repoRootFrom(startDir) {
78419
78552
  let dir2 = startDir;
78420
78553
  for (let i = 0; i < 8; i++) {
78421
- if (existsSync19(join39(dir2, "pnpm-workspace.yaml"))) return dir2;
78554
+ if (existsSync19(join40(dir2, "pnpm-workspace.yaml"))) return dir2;
78422
78555
  const parent = dirname17(dir2);
78423
78556
  if (parent === dir2) break;
78424
78557
  dir2 = parent;
@@ -78531,7 +78664,7 @@ var packagedScript = (f) => fileURLToPath4(new URL(`../scripts/vps/${f}`, import
78531
78664
  var moduleDir = () => fileURLToPath4(new URL(".", import.meta.url));
78532
78665
  var scriptPath = (f) => {
78533
78666
  const packed = packagedScript(f);
78534
- return existsSync20(packed) ? packed : join40(repoRoot(moduleDir()), "server", "scripts", "vps", f);
78667
+ return existsSync20(packed) ? packed : join41(repoRoot(moduleDir()), "server", "scripts", "vps", f);
78535
78668
  };
78536
78669
  async function itemStatesOf(vpsId) {
78537
78670
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
@@ -78778,25 +78911,25 @@ init_config2();
78778
78911
  import { execFileSync as execFileSync7 } from "node:child_process";
78779
78912
  import { chmodSync as chmodSync9, copyFileSync, existsSync as existsSync21, mkdirSync as mkdirSync19, readFileSync as readFileSync22, unlinkSync as unlinkSync5 } from "node:fs";
78780
78913
  import { homedir as homedir8 } from "node:os";
78781
- import { join as join41 } from "node:path";
78914
+ import { join as join42 } from "node:path";
78782
78915
  var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? resolveDataDirs().sshKeys;
78783
78916
  var KEY_FILES = ["id_ed25519", "id_ed25519.pub"];
78784
78917
  function adoptLegacyKey(dir2) {
78785
78918
  if (effectiveStr("HANOMAN_SSH_KEY_DIR")) return;
78786
- const legacy = join41(homedir8(), ".hanoman");
78919
+ const legacy = join42(homedir8(), ".hanoman");
78787
78920
  if (legacy === dir2) return;
78788
- if (!KEY_FILES.every((f) => existsSync21(join41(legacy, f)))) return;
78921
+ if (!KEY_FILES.every((f) => existsSync21(join42(legacy, f)))) return;
78789
78922
  mkdirSync19(dir2, { recursive: true, mode: 448 });
78790
78923
  for (const f of KEY_FILES) {
78791
- copyFileSync(join41(legacy, f), join41(dir2, f));
78792
- chmodSync9(join41(dir2, f), 384);
78924
+ copyFileSync(join42(legacy, f), join42(dir2, f));
78925
+ chmodSync9(join42(dir2, f), 384);
78793
78926
  }
78794
- for (const f of KEY_FILES) unlinkSync5(join41(legacy, f));
78927
+ for (const f of KEY_FILES) unlinkSync5(join42(legacy, f));
78795
78928
  console.log(`vps: key SSH dipindah dari ${legacy} ke ${dir2} (SPEC-846 \u2014 satu batas backup)`);
78796
78929
  }
78797
78930
  function ensureHanomanKey() {
78798
78931
  const dir2 = keyDir();
78799
- const privPath = join41(dir2, KEY_FILES[0]);
78932
+ const privPath = join42(dir2, KEY_FILES[0]);
78800
78933
  const pubPath = `${privPath}.pub`;
78801
78934
  if (!existsSync21(privPath)) adoptLegacyKey(dir2);
78802
78935
  if (!existsSync21(privPath)) {
@@ -79109,7 +79242,7 @@ init_config2();
79109
79242
  import { execFileSync as execFileSync8 } from "node:child_process";
79110
79243
  import { readFileSync as readFileSync24 } from "node:fs";
79111
79244
  import { homedir as homedir10, userInfo } from "node:os";
79112
- import { join as join42 } from "node:path";
79245
+ import { join as join43 } from "node:path";
79113
79246
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
79114
79247
  var TTL_MS2 = 3e4;
79115
79248
  var lastOk = null;
@@ -79122,7 +79255,7 @@ var LABELS = {
79122
79255
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
79123
79256
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
79124
79257
  function credsFile() {
79125
- return join42(effectiveStr("CLAUDE_CONFIG_DIR") ?? join42(homedir10(), ".claude"), ".credentials.json");
79258
+ return join43(effectiveStr("CLAUDE_CONFIG_DIR") ?? join43(homedir10(), ".claude"), ".credentials.json");
79126
79259
  }
79127
79260
  var securityRun = (args) => execFileSync8("security", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
79128
79261
  var osAccount = () => {
@@ -79207,14 +79340,14 @@ async function getLimits() {
79207
79340
  // src/services/codex-limits.ts
79208
79341
  import { open as open4, readdir as readdir9, stat as stat7 } from "node:fs/promises";
79209
79342
  import { homedir as homedir11 } from "node:os";
79210
- import { join as join43 } from "node:path";
79343
+ import { join as join44 } from "node:path";
79211
79344
  var TTL_MS3 = 3e4;
79212
79345
  var STALE_AFTER_MS = 12 * 36e5;
79213
79346
  var TAIL_BYTES = 512 * 1024;
79214
79347
  var MAX_FILES = 8;
79215
79348
  var cache2 = null;
79216
79349
  var freshUntil2 = 0;
79217
- var codexSessionsDir = () => join43(process.env.CODEX_HOME ?? join43(homedir11(), ".codex"), "sessions");
79350
+ var codexSessionsDir = () => join44(process.env.CODEX_HOME ?? join44(homedir11(), ".codex"), "sessions");
79218
79351
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
79219
79352
  function windowLabel(minutes) {
79220
79353
  if (minutes === 300) return "Sesi 5 jam";
@@ -79247,7 +79380,7 @@ async function recentRollouts(dir2) {
79247
79380
  return [];
79248
79381
  }
79249
79382
  const stamped = await Promise.all(entries4.map(async (rel) => {
79250
- const full = join43(dir2, rel);
79383
+ const full = join44(dir2, rel);
79251
79384
  try {
79252
79385
  return { full, mtime: (await stat7(full)).mtimeMs };
79253
79386
  } catch {
@@ -79323,7 +79456,7 @@ init_config2();
79323
79456
  import { execFileSync as execFileSync9 } from "node:child_process";
79324
79457
  import { readFileSync as readFileSync25 } from "node:fs";
79325
79458
  import { homedir as homedir12 } from "node:os";
79326
- import { join as join44 } from "node:path";
79459
+ import { join as join45 } from "node:path";
79327
79460
  var QUOTA_URL = "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary";
79328
79461
  var USER_AGENT = "antigravity (hanoman)";
79329
79462
  var TTL_MS4 = 6e4;
@@ -79345,10 +79478,10 @@ function readAgyToken(run6 = securityRun2) {
79345
79478
  return null;
79346
79479
  }
79347
79480
  }
79348
- var agyHome = () => join44(homedir12(), ".gemini", "antigravity-cli");
79481
+ var agyHome = () => join45(homedir12(), ".gemini", "antigravity-cli");
79349
79482
  function defaultProject() {
79350
79483
  try {
79351
- return readFileSync25(join44(agyHome(), "cache", "default_project_id.txt"), "utf8").trim() || null;
79484
+ return readFileSync25(join45(agyHome(), "cache", "default_project_id.txt"), "utf8").trim() || null;
79352
79485
  } catch {
79353
79486
  return null;
79354
79487
  }
@@ -79536,7 +79669,7 @@ init_src2();
79536
79669
  init_src();
79537
79670
  import { randomUUID as randomUUID14 } from "node:crypto";
79538
79671
  import { readFile as readFile8, writeFile as writeFile6, rename as rename5, mkdir as mkdir9, rm as rm9 } from "node:fs/promises";
79539
- import { dirname as dirname18, join as join46 } from "node:path";
79672
+ import { dirname as dirname18, join as join47 } from "node:path";
79540
79673
 
79541
79674
  // src/services/model-catalog-parser.ts
79542
79675
  init_zod();
@@ -79640,10 +79773,10 @@ init_src2();
79640
79773
  init_antigravity_runtime();
79641
79774
  init_config2();
79642
79775
  init_session_sandbox();
79643
- import { spawn as spawn5 } from "node:child_process";
79776
+ import { spawn as spawn6 } from "node:child_process";
79644
79777
  import { mkdtemp as mkdtemp2, rm as rm8 } from "node:fs/promises";
79645
79778
  import { tmpdir as tmpdir9 } from "node:os";
79646
- import { join as join45 } from "node:path";
79779
+ import { join as join46 } from "node:path";
79647
79780
  var MAX_BYTES = 4 * 1024 * 1024;
79648
79781
  var TIMEOUT_MS = 2e4;
79649
79782
  var quote3 = (s2) => "'" + s2.replaceAll("'", "'\\''") + "'";
@@ -79680,7 +79813,7 @@ function catalogCommand(agent, cwd, env) {
79680
79813
  function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
79681
79814
  return new Promise((resolve25, reject2) => {
79682
79815
  assertAntigravityLaunch(agent, void 0, env);
79683
- const child = spawn5(command.bin, command.args, { cwd, env: agentEnvironment(agent, env), stdio: ["pipe", "pipe", "pipe"] });
79816
+ const child = spawn6(command.bin, command.args, { cwd, env: agentEnvironment(agent, env), stdio: ["pipe", "pipe", "pipe"] });
79684
79817
  let buffer = "", bytes = 0, settled = false;
79685
79818
  let killTimer;
79686
79819
  const finish = (error, result) => {
@@ -79742,7 +79875,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
79742
79875
  }
79743
79876
  async function probeModelCatalog(agent) {
79744
79877
  assertAntigravityLaunch(agent);
79745
- const cwd = await mkdtemp2(join45(tmpdir9(), "hanoman-models-"));
79878
+ const cwd = await mkdtemp2(join46(tmpdir9(), "hanoman-models-"));
79746
79879
  const env = agentEnvironment(agent, process.env);
79747
79880
  for (const key of ["HANOMAN_CLAUDE_BIN", "HANOMAN_CODEX_BIN", "HANOMAN_PODMAN_BIN", "HANOMAN_ANTIGRAVITY_BIN"]) {
79748
79881
  const value = effectiveStr(key);
@@ -79853,7 +79986,7 @@ async function writeModelCatalogCache(file, catalog, current = () => true) {
79853
79986
  await rm9(temp, { force: true });
79854
79987
  }
79855
79988
  }
79856
- var cacheFile = () => join46(resolveHome(), "model-catalog.json");
79989
+ var cacheFile = () => join47(resolveHome(), "model-catalog.json");
79857
79990
  var modelCatalogService = createModelCatalogService({
79858
79991
  probe: probeModelCatalog,
79859
79992
  key: catalogProbeKey,
@@ -79892,8 +80025,9 @@ init_pty();
79892
80025
 
79893
80026
  // src/services/restart.ts
79894
80027
  init_src();
80028
+ init_exit_guard();
79895
80029
  function requestConfigRestart(delayMs = 50) {
79896
- setTimeout(() => process.exit(CONFIG_RESTART_EXIT), delayMs);
80030
+ setTimeout(() => exitGuarded(CONFIG_RESTART_EXIT), delayMs);
79897
80031
  }
79898
80032
  function afterReplyFlushed(raw, fn, maxWaitMs = 5e3) {
79899
80033
  let done = false;
@@ -79911,7 +80045,7 @@ function afterReplyFlushed(raw, fn, maxWaitMs = 5e3) {
79911
80045
  timer11 = setTimeout(run6, maxWaitMs);
79912
80046
  }
79913
80047
  function requestManualRestart(delayMs = 250) {
79914
- setTimeout(() => process.exit(MANUAL_RESTART_EXIT), delayMs);
80048
+ setTimeout(() => exitGuarded(MANUAL_RESTART_EXIT), delayMs);
79915
80049
  }
79916
80050
 
79917
80051
  // src/routes/update.ts
@@ -81780,7 +81914,7 @@ init_src();
81780
81914
  init_db();
81781
81915
  import { gunzipSync, gzipSync } from "node:zlib";
81782
81916
  import { mkdir as mkdir10, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
81783
- import { join as join47 } from "node:path";
81917
+ import { join as join48 } from "node:path";
81784
81918
 
81785
81919
  // src/services/device-auth.ts
81786
81920
  function bearerToken2(req) {
@@ -82036,9 +82170,9 @@ async function sync_default(app3) {
82036
82170
  const deviceId = req.device.id;
82037
82171
  const withTranscriptKey = await Promise.all(parsed.data.entries.map(async (e) => {
82038
82172
  if (!e.transcript) return e;
82039
- const dir2 = join47(resolveDataDirs().home, "remote-transcripts", deviceId);
82173
+ const dir2 = join48(resolveDataDirs().home, "remote-transcripts", deviceId);
82040
82174
  await mkdir10(dir2, { recursive: true, mode: 448 });
82041
- const final = join47(dir2, `${e.seq}.txt`);
82175
+ const final = join48(dir2, `${e.seq}.txt`);
82042
82176
  const tmp = `${final}.tmp`;
82043
82177
  await writeFile7(tmp, e.transcript, { encoding: "utf8", mode: 384 });
82044
82178
  await rename6(tmp, final);
@@ -85325,7 +85459,7 @@ init_qa_attachment_transfer();
85325
85459
  init_uploads();
85326
85460
  import { createHash as createHash21 } from "node:crypto";
85327
85461
  import { execFileSync as execFileSync10 } from "node:child_process";
85328
- import { join as join49 } from "node:path";
85462
+ import { join as join50 } from "node:path";
85329
85463
  var QaSessionError = class extends Error {
85330
85464
  constructor(code, message, needsBind = false) {
85331
85465
  super(message);
@@ -85352,7 +85486,7 @@ async function startQaFindingSession(d, fid) {
85352
85486
  const saved = await saveSessionUpload(id2, bytes, a.mimeType);
85353
85487
  files2.push(`${JSON.stringify(a.filename)} (${a.mimeType}): ${saved.path}`);
85354
85488
  }
85355
- const wt = join49(repoDir, ".worktrees", id2);
85489
+ const wt = join50(repoDir, ".worktrees", id2);
85356
85490
  const branch = `qa/${id2.slice(3)}`;
85357
85491
  const reused = realGit.worktreeAlive(wt);
85358
85492
  try {
@@ -86891,9 +87025,9 @@ ${baru}`;
86891
87025
  // src/services/portal-chat/workspace.ts
86892
87026
  init_src();
86893
87027
  init_db();
86894
- import { mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync5, rmSync as rmSync14, writeFileSync as writeFileSync19 } from "node:fs";
87028
+ import { mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync5, rmSync as rmSync14, writeFileSync as writeFileSync20 } from "node:fs";
86895
87029
  import { tmpdir as tmpdir10 } from "node:os";
86896
- import { join as join50 } from "node:path";
87030
+ import { join as join51 } from "node:path";
86897
87031
  var STAGE_LABEL = {
86898
87032
  brainstorming: "Dirumuskan",
86899
87033
  objective: "Dirumuskan",
@@ -86936,7 +87070,7 @@ function renderTicketDoc(tickets2) {
86936
87070
  ${baris.join("\n")}`;
86937
87071
  }
86938
87072
  async function buildChatWorkspace(projectId) {
86939
- const dir2 = mkdtempSync5(join50(tmpdir10(), "hanoman-portal-chat-"));
87073
+ const dir2 = mkdtempSync5(join51(tmpdir10(), "hanoman-portal-chat-"));
86940
87074
  try {
86941
87075
  const project = await prisma.project.findUnique({
86942
87076
  where: { id: projectId },
@@ -86951,7 +87085,7 @@ async function buildChatWorkspace(projectId) {
86951
87085
  });
86952
87086
  const files2 = [];
86953
87087
  const tulis = (rel, isi) => {
86954
- writeFileSync19(join50(dir2, rel), isi, { mode: 384 });
87088
+ writeFileSync20(join51(dir2, rel), isi, { mode: 384 });
86955
87089
  files2.push(rel);
86956
87090
  };
86957
87091
  tulis("project.md", renderProjectDoc(project));
@@ -86959,11 +87093,11 @@ async function buildChatWorkspace(projectId) {
86959
87093
  tulis("laporan.md", renderTicketDoc(tickets2.map((t) => toPortalTicket(t, null))));
86960
87094
  const prds = await listPrds(projectId);
86961
87095
  if (prds.length) {
86962
- mkdirSync20(join50(dir2, "dokumen"), { mode: 448 });
87096
+ mkdirSync20(join51(dir2, "dokumen"), { mode: 448 });
86963
87097
  for (const prd of prds) {
86964
87098
  const isi = await readPrd(projectId, prd.path);
86965
87099
  if (!isi) continue;
86966
- tulis(join50("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
87100
+ tulis(join51("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
86967
87101
  }
86968
87102
  }
86969
87103
  return { dir: dir2, files: files2, cleanup: () => rmSync14(dir2, { recursive: true, force: true }) };
@@ -87490,13 +87624,13 @@ init_db();
87490
87624
  // src/services/bootstrap.ts
87491
87625
  import { createHash as createHash22, randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
87492
87626
  import { chmod as chmod2, lstat as lstat3, mkdir as mkdir11, readFile as readFile9, unlink as unlink5, writeFile as writeFile8 } from "node:fs/promises";
87493
- import { join as join51 } from "node:path";
87627
+ import { join as join52 } from "node:path";
87494
87628
  var TTL_MS7 = 15 * 6e4;
87495
87629
  var SETUP_TOKEN_FILE = "setup.token";
87496
87630
  var BootstrapError = class extends Error {
87497
87631
  code = "BOOTSTRAP_PROOF";
87498
87632
  };
87499
- var tokenPath = (home3) => join51(home3, SETUP_TOKEN_FILE);
87633
+ var tokenPath = (home3) => join52(home3, SETUP_TOKEN_FILE);
87500
87634
  async function readStored(home3) {
87501
87635
  try {
87502
87636
  const info = await lstat3(tokenPath(home3));
@@ -88334,7 +88468,7 @@ init_event_log();
88334
88468
  init_transcript_store();
88335
88469
  init_src2();
88336
88470
  import { readFile as readFile10 } from "node:fs/promises";
88337
- import { join as join52 } from "node:path";
88471
+ import { join as join53 } from "node:path";
88338
88472
  function decodeCursor(c) {
88339
88473
  if (!c) return null;
88340
88474
  try {
@@ -88372,7 +88506,7 @@ async function readRemoteTranscript(id2) {
88372
88506
  const row = await prisma.logEntry.findUnique({ where: { id: id2 } });
88373
88507
  if (!row || row.lane !== "transcript" || !row.transcriptKey) return null;
88374
88508
  if (row.deviceId === "local") return readTranscript(row.transcriptKey);
88375
- const path = join52(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
88509
+ const path = join53(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
88376
88510
  try {
88377
88511
  return await readFile10(path, "utf8");
88378
88512
  } catch {
@@ -89639,9 +89773,9 @@ async function pruneLogs(now, retention, opts2 = {}) {
89639
89773
  }
89640
89774
  async function reconcileRemoteTranscripts() {
89641
89775
  const { readdir: readdir10, unlink: unlink6, stat: stat8 } = await import("node:fs/promises");
89642
- const { join: join53 } = await import("node:path");
89776
+ const { join: join54 } = await import("node:path");
89643
89777
  const { resolveDataDirs: resolveDataDirs2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
89644
- const root = join53(resolveDataDirs2().home, "remote-transcripts");
89778
+ const root = join54(resolveDataDirs2().home, "remote-transcripts");
89645
89779
  let deviceDirs;
89646
89780
  try {
89647
89781
  deviceDirs = await readdir10(root);
@@ -89655,16 +89789,16 @@ async function reconcileRemoteTranscripts() {
89655
89789
  for (const dev of deviceDirs) {
89656
89790
  let files2;
89657
89791
  try {
89658
- files2 = await readdir10(join53(root, dev));
89792
+ files2 = await readdir10(join54(root, dev));
89659
89793
  } catch {
89660
89794
  continue;
89661
89795
  }
89662
89796
  for (const f of files2) {
89663
89797
  const key = `${dev}/${f}`;
89664
89798
  if (referenced.has(key)) continue;
89665
- const st = await stat8(join53(root, dev, f)).catch(() => null);
89799
+ const st = await stat8(join54(root, dev, f)).catch(() => null);
89666
89800
  if (!st || Date.now() - st.mtimeMs < gracePeriodMs) continue;
89667
- await unlink6(join53(root, dev, f)).catch(() => {
89801
+ await unlink6(join54(root, dev, f)).catch(() => {
89668
89802
  });
89669
89803
  orphans++;
89670
89804
  }
@@ -89851,6 +89985,7 @@ init_transcript_store();
89851
89985
  init_client2();
89852
89986
  init_dispatcher();
89853
89987
  init_safe_outbound_request();
89988
+ init_exit_guard();
89854
89989
  installSafeGlobalFetch();
89855
89990
  var app2 = buildApp();
89856
89991
  var port = Number(process.env.PORT ?? 8787);
@@ -89869,7 +90004,7 @@ var shutdown = createShutdown({
89869
90004
  await app2.close();
89870
90005
  await prisma.$disconnect();
89871
90006
  },
89872
- exit: (code) => process.exit(code)
90007
+ exit: (code) => exitGuarded(code)
89873
90008
  });
89874
90009
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
89875
90010
  process.on("SIGINT", () => void shutdown("SIGINT"));
@@ -89945,7 +90080,7 @@ bootstrapReady.then(async () => {
89945
90080
  startOverviewRecorder();
89946
90081
  }).catch((err) => {
89947
90082
  console.error("listen gagal:", err);
89948
- process.exit(1);
90083
+ exitGuarded(1);
89949
90084
  });
89950
90085
  /*! Bundled license information:
89951
90086