hanoman 0.9.30 → 0.9.31

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 +12 -2
  3. package/dist/server.js +323 -235
  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),
@@ -17096,6 +17098,11 @@ var init_verify_scope = __esm({
17096
17098
  ' test-nya memang berjalan, jangan menerima "no test files" sebagai bukti.',
17097
17099
  "- Typecheck: hanya paket yang tersentuh (mis. `pnpm --filter ./server typecheck`).",
17098
17100
  " JANGAN `pnpm -r typecheck` \u2014 itu menyalakan satu proses tsc per paket sekaligus.",
17101
+ "- Ritme: per task jalankan berkas test yang task itu sentuh saja.",
17102
+ " JANGAN menjalankan ulang berkas test yang sudah hijau selama kode yang diujinya",
17103
+ " tak berubah sejak itu. Typecheck paket tersentuh sesudah task yang mengubah",
17104
+ " tipe/kontrak ekspor, bukan tiap task. Gabungan semua test tersentuh + typecheck",
17105
+ " dijalankan SEKALI di akhir, sebelum melapor.",
17099
17106
  "- Lint: hanya berkas yang berubah, bukan seluruh repo.",
17100
17107
  "- Build penuh: hanya bila yang kamu ubah memang soal build/bundling.",
17101
17108
  "- Boot server + curl / smoke end-to-end: hanya bila task ini menyentuh endpoint atau perilaku",
@@ -17441,7 +17448,7 @@ Stack: ${project.stack || "\u2014"}`,
17441
17448
  ${cron.prompt}`
17442
17449
  ].join("\n\n");
17443
17450
  }
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;
17451
+ 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
17452
  var init_prompt = __esm({
17446
17453
  "../runner/src/prompt.ts"() {
17447
17454
  "use strict";
@@ -17458,12 +17465,14 @@ var init_prompt = __esm({
17458
17465
  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
17466
  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
17467
  autonomyClause = (mode) => mode === "full-control" ? AUTONOMY_CLAUSE_FULL : AUTONOMY_CLAUSE;
17468
+ 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
17469
  phaseInstruction = (phases2, method) => {
17462
17470
  const base3 = `Kerjakan fase berurutan: ${phases2.join(" \u2192 ")}.
17463
17471
  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
17472
  if (!phases2.includes("Plan") || !phases2.includes("Execute")) return base3;
17465
17473
  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 \`- [ ]\`.`;
17474
+ 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 \`- [ ]\`.
17475
+ ${PLAN_SHAPE}`;
17467
17476
  };
17468
17477
  skillInstruction = (flow2, phases2, method) => {
17469
17478
  const lines2 = phases2.map((p3) => {
@@ -18620,7 +18629,7 @@ function backlogGuide(flow2, phase, ctx) {
18620
18629
  const m = ctx.method;
18621
18630
  switch (phase) {
18622
18631
  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.` : "");
18632
+ 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
18633
  case "Objective":
18625
18634
  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
18635
  case "Spec":
@@ -18629,11 +18638,11 @@ function backlogGuide(flow2, phase, ctx) {
18629
18638
  // default skill) lolos hampa. Nama dikunci seperti Brainstorm, dan path persisnya diteruskan
18630
18639
  // orchestrator ke Execute & reviewer — bukan pola glob yang bisa mencocoki plan backlog lain.
18631
18640
  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.`;
18641
+ 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
18642
  case "Execute":
18634
18643
  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
18644
  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`.";
18645
+ 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
18646
  case "Laporan":
18638
18647
  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
18648
  case "Goal":
@@ -18775,7 +18784,7 @@ function withPhaseDelegation(phaseDefs, roster, runtime) {
18775
18784
  ${clause}` } : def2;
18776
18785
  });
18777
18786
  }
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;
18787
+ 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
18788
  var init_phase_agents = __esm({
18780
18789
  "../runner/src/phase-agents.ts"() {
18781
18790
  "use strict";
@@ -18785,13 +18794,15 @@ var init_phase_agents = __esm({
18785
18794
  REVIEWER_AGENT_NAME = phaseAgentName(REVIEWER_CELL);
18786
18795
  PROJECT_FLOWS = /* @__PURE__ */ new Set(["reverse", "scaffold", "prd", "breakdown"]);
18787
18796
  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}.`;
18797
+ 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.`;
18798
+ 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
18799
  PHASE_AGENT_RULES = [
18790
18800
  "Batas peran fase:",
18791
18801
  "- JANGAN menulis `$HANOMAN_PHASE_FILE` \u2014 orchestrator satu-satunya penulis marker fase.",
18792
18802
  "- JANGAN `git push` \u2014 push milik orchestrator. Commit artefak fasemu sendiri sebelum melapor.",
18793
18803
  `- JANGAN memanggil agen berawalan \`${PHASE_AGENT_PREFIX}\`. Custom agent lain boleh dipanggil bila relevan.`,
18794
18804
  "- Kerjakan HANYA fase ini; fase lain dikerjakan agen fasenya sendiri.",
18805
+ FOREGROUND_RULE,
18795
18806
  // T1 · ADR-0164 amandemen 2026-09-23 · 22 laporan tanpa `Status:` di audit: agen fase mengakhiri
18796
18807
  // gilirannya sambil menunggu proses latarnya sendiri, dan teks "menunggu" itu dibaca orchestrator
18797
18808
  // sebagai laporan final. Giliranmu yang berakhir = laporanmu, jadi menunggu harus terjadi DI DALAM giliran.
@@ -18807,6 +18818,7 @@ var init_phase_agents = __esm({
18807
18818
  "Klaim tanpa bukti bukan bukti. Maksimal 1200 kata."
18808
18819
  ].join("\n");
18809
18820
  ATTACHMENT_NOTE = "Bila serah-terima menyebut manifest lampiran (`INDEX.md`), baca manifest itu dan lampiran yang relevan di awal fase.";
18821
+ 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
18822
  TERMINAL_ASK = /([Aa])jukan SATU pertanyaan per giliran ke manusia di terminal ini, tunggu jawabannya(?=[,.])/;
18811
18823
  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
18824
  TERMINAL_HANDOFF = "tulis ringkasan hasil + daftar pertanyaan yang belum terjawab ke terminal.";
@@ -18817,6 +18829,7 @@ var init_phase_agents = __esm({
18817
18829
  "- 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
18830
  "- JANGAN menulis `$HANOMAN_PHASE_FILE` dan JANGAN memperbaiki temuanmu sendiri \u2014 perbaikan milik agen Execute, marker milik orchestrator.",
18819
18831
  `- JANGAN memanggil agen berawalan \`${PHASE_AGENT_PREFIX}\`.`,
18832
+ FOREGROUND_RULE,
18820
18833
  "- 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
18834
  ].join("\n");
18822
18835
  roleOf = (phase) => phase === "Execute" || phase === "Goal" ? "execute" : phase === "Kerjakan" || phase === "Verifikasi" ? "none" : "research";
@@ -20714,6 +20727,7 @@ __export(src_exports, {
20714
20727
  GOAL_TUI_PASTE_LIMIT: () => GOAL_TUI_PASTE_LIMIT,
20715
20728
  PHASE_CONTEXT_HEADER: () => PHASE_CONTEXT_HEADER,
20716
20729
  PIPELINES: () => PIPELINES,
20730
+ PLAN_SHAPE: () => PLAN_SHAPE,
20717
20731
  PROJECT_METHOD: () => PROJECT_METHOD,
20718
20732
  READ_ONLY_DENIED_COMMANDS: () => READ_ONLY_DENIED_COMMANDS,
20719
20733
  READ_ONLY_DENIED_OPERATORS: () => READ_ONLY_DENIED_OPERATORS,
@@ -50134,6 +50148,8 @@ var init_settings3 = __esm({
50134
50148
  notifySound: "short",
50135
50149
  notifyDecision: true,
50136
50150
  notifyDecisionSound: "alert",
50151
+ notifyDecisionTelegram: true,
50152
+ // ADR-0226 · tak berefek tanpa bot token + allowlist
50137
50153
  agentAccessEnabled: false,
50138
50154
  // SPEC-257 · akses AI agent off sampai dibuka manusia
50139
50155
  scheduler: SCHEDULER_DEFAULTS,
@@ -55948,6 +55964,300 @@ var init_live_phases = __esm({
55948
55964
  }
55949
55965
  });
55950
55966
 
55967
+ // src/services/telegram/client.ts
55968
+ var TelegramApiError, TelegramApiClient;
55969
+ var init_client = __esm({
55970
+ "src/services/telegram/client.ts"() {
55971
+ "use strict";
55972
+ TelegramApiError = class extends Error {
55973
+ constructor(method, code, message, retryAfter) {
55974
+ super(`Telegram ${method} gagal${code ? ` (${code})` : ""}: ${message}`);
55975
+ this.method = method;
55976
+ this.code = code;
55977
+ this.retryAfter = retryAfter;
55978
+ this.name = "TelegramApiError";
55979
+ }
55980
+ };
55981
+ TelegramApiClient = class {
55982
+ constructor(token, transport = (url2, init) => fetch(url2, init), baseUrl = "https://api.telegram.org") {
55983
+ this.token = token;
55984
+ this.transport = transport;
55985
+ this.baseUrl = baseUrl;
55986
+ }
55987
+ safe(message) {
55988
+ return message.split(this.token).join("[REDACTED]").replace(/https:\/\/api\.telegram\.org\/bot[^/\s]+/g, "https://api.telegram.org/bot[REDACTED]");
55989
+ }
55990
+ async call(method, body, signal) {
55991
+ let response;
55992
+ try {
55993
+ response = await this.transport(`${this.baseUrl}/bot${this.token}/${method}`, {
55994
+ method: "POST",
55995
+ headers: { "content-type": "application/json" },
55996
+ body: JSON.stringify(body),
55997
+ signal
55998
+ });
55999
+ } catch (error) {
56000
+ throw new TelegramApiError(method, void 0, this.safe(error.message || "network error"));
56001
+ }
56002
+ if (!response.ok) {
56003
+ const failed = await response.json().catch(() => null);
56004
+ throw new TelegramApiError(method, response.status, `HTTP ${response.status}`, failed?.parameters?.retry_after);
56005
+ }
56006
+ let envelope;
56007
+ try {
56008
+ envelope = await response.json();
56009
+ } catch {
56010
+ throw new TelegramApiError(method, response.status, "respons bukan JSON");
56011
+ }
56012
+ if (!envelope.ok || envelope.result === void 0) {
56013
+ throw new TelegramApiError(
56014
+ method,
56015
+ envelope.error_code,
56016
+ this.safe(envelope.description ?? "respons tidak valid"),
56017
+ envelope.parameters?.retry_after
56018
+ );
56019
+ }
56020
+ return envelope.result;
56021
+ }
56022
+ getMe() {
56023
+ return this.call("getMe", {});
56024
+ }
56025
+ async getUpdates(input) {
56026
+ if (!Number.isInteger(input.offset) || input.offset < 0) throw new Error("offset harus integer non-negatif");
56027
+ if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new Error("limit harus 1..100");
56028
+ if (!Number.isInteger(input.timeout) || input.timeout < 0 || input.timeout > 50) throw new Error("timeout harus 0..50");
56029
+ return this.call("getUpdates", {
56030
+ offset: input.offset,
56031
+ limit: input.limit,
56032
+ timeout: input.timeout,
56033
+ allowed_updates: ["message", "callback_query"]
56034
+ }, input.signal);
56035
+ }
56036
+ sendMessage(input) {
56037
+ return this.call("sendMessage", {
56038
+ chat_id: input.chatId,
56039
+ text: input.text,
56040
+ ...input.replyMarkup ? { reply_markup: input.replyMarkup } : {}
56041
+ });
56042
+ }
56043
+ /**
56044
+ * SPEC-493 · indikator "typing…". Argumennya posisional (beda dari tetangganya) sesuai kontrak
56045
+ * yang diminta. Telegram TAK punya API stop-typing: statusnya padam sendiri ~5 detik sesudah
56046
+ * panggilan terakhir, dan pesan masuk apa pun langsung menghapusnya.
56047
+ */
56048
+ sendChatAction(chatId, action = "typing") {
56049
+ return this.call("sendChatAction", { chat_id: chatId, action });
56050
+ }
56051
+ editMessageText(input) {
56052
+ return this.call("editMessageText", {
56053
+ chat_id: input.chatId,
56054
+ message_id: input.messageId,
56055
+ text: input.text
56056
+ });
56057
+ }
56058
+ answerCallbackQuery(input) {
56059
+ return this.call("answerCallbackQuery", {
56060
+ callback_query_id: input.callbackQueryId,
56061
+ ...input.text ? { text: input.text } : {}
56062
+ });
56063
+ }
56064
+ };
56065
+ }
56066
+ });
56067
+
56068
+ // src/services/telegram/protocol.ts
56069
+ import { createHash as createHash10 } from "node:crypto";
56070
+ function normalizeTelegramCommand(text2) {
56071
+ const clean3 = text2.trim();
56072
+ const match = clean3.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
56073
+ if (!match) return clean3;
56074
+ const args = match[2]?.trim().replace(/\s+/g, " ");
56075
+ return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
56076
+ }
56077
+ function parseConfirmationCallback(data) {
56078
+ if (data.length > 64) return null;
56079
+ const match = data.match(/^tgcf:([A-Za-z0-9_-]{1,40}):(approve|deny)$/);
56080
+ return match ? { token: match[1], action: match[2] } : null;
56081
+ }
56082
+ function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
56083
+ const root = record(update2);
56084
+ const updateId = root && Number.isInteger(root.update_id) && Number(root.update_id) >= 0 ? Number(root.update_id) : null;
56085
+ if (!root || updateId === null) return { ok: false, updateId: null, reason: "malformed" };
56086
+ const callback = record(root.callback_query);
56087
+ const message = callback ? record(callback.message) : record(root.message);
56088
+ const from = callback ? record(callback.from) : record(message?.from);
56089
+ const chat = record(message?.chat);
56090
+ const chatId = typeof chat?.id === "number" && Number.isSafeInteger(chat.id) ? String(chat.id) : void 0;
56091
+ const userId = typeof from?.id === "number" && Number.isSafeInteger(from.id) ? String(from.id) : void 0;
56092
+ if (!message || !from || !chat || !chatId || !userId) return { ok: false, updateId, reason: "malformed", chatId, userId };
56093
+ if (chat.type !== "private") return { ok: false, updateId, reason: "group", chatId, userId };
56094
+ if (from.is_bot === true) return { ok: false, updateId, reason: "sender-bot", chatId, userId };
56095
+ if (!allowlist.has(userId)) return { ok: false, updateId, reason: "not-allowed", chatId, userId };
56096
+ const messageId = typeof message.message_id === "number" && Number.isInteger(message.message_id) ? message.message_id : null;
56097
+ if (messageId === null) return { ok: false, updateId, reason: "malformed", chatId, userId };
56098
+ if (callback) {
56099
+ const callbackQueryId = typeof callback.id === "string" ? callback.id : "";
56100
+ const data = typeof callback.data === "string" ? callback.data : "";
56101
+ const parsed = parseConfirmationCallback(data);
56102
+ if (!callbackQueryId || !parsed) return { ok: false, updateId, reason: "invalid-callback", chatId, userId };
56103
+ return {
56104
+ ok: true,
56105
+ input: {
56106
+ updateId,
56107
+ chatId,
56108
+ userId,
56109
+ messageId,
56110
+ kind: "callback",
56111
+ text: data,
56112
+ callbackQueryId,
56113
+ callbackToken: parsed.token,
56114
+ callbackAction: parsed.action
56115
+ }
56116
+ };
56117
+ }
56118
+ if (typeof message.text !== "string" || !message.text.trim()) {
56119
+ return { ok: false, updateId, reason: "non-text", chatId, userId };
56120
+ }
56121
+ const text2 = normalizeTelegramCommand(message.text);
56122
+ if (text2.length > maxText) return { ok: false, updateId, reason: "too-long", chatId, userId };
56123
+ return {
56124
+ ok: true,
56125
+ input: {
56126
+ updateId,
56127
+ chatId,
56128
+ userId,
56129
+ messageId,
56130
+ kind: text2.startsWith("/") ? "command" : "text",
56131
+ text: text2
56132
+ }
56133
+ };
56134
+ }
56135
+ function inboundDigest(input) {
56136
+ return createHash10("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
56137
+ }
56138
+ function sanitizeTelegramOutput(text2, exactSecrets = []) {
56139
+ let clean3 = text2.replace(ANSI, "").replace(CONTROLS, "");
56140
+ for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
56141
+ clean3 = clean3.split(secret).join("[REDACTED]");
56142
+ }
56143
+ for (const pattern of CREDENTIAL_PATTERNS) clean3 = clean3.replace(pattern, "[REDACTED]");
56144
+ return clean3.trim();
56145
+ }
56146
+ function splitTelegramText(text2, max = 4096, exactSecrets = []) {
56147
+ if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
56148
+ const clean3 = sanitizeTelegramOutput(text2, exactSecrets);
56149
+ if (!clean3) return [];
56150
+ const chunks = [];
56151
+ let rest = clean3;
56152
+ while (rest.length > max) {
56153
+ let cut = max;
56154
+ const before2 = rest.charCodeAt(cut - 1);
56155
+ const after = rest.charCodeAt(cut);
56156
+ if (before2 >= 55296 && before2 <= 56319 && after >= 56320 && after <= 57343) cut--;
56157
+ const window2 = rest.slice(0, cut);
56158
+ const newline2 = window2.lastIndexOf("\n");
56159
+ const space = window2.lastIndexOf(" ");
56160
+ const boundary = Math.max(newline2, space);
56161
+ if (boundary >= Math.floor(max / 2)) cut = boundary + 1;
56162
+ chunks.push(rest.slice(0, cut));
56163
+ rest = rest.slice(cut);
56164
+ }
56165
+ if (rest) chunks.push(rest);
56166
+ return chunks;
56167
+ }
56168
+ var record, ANSI, CONTROLS, CREDENTIAL_PATTERNS, TELEGRAM_GATEWAY_FAILURE_KIND, TELEGRAM_FINAL_REPLY_KINDS;
56169
+ var init_protocol = __esm({
56170
+ "src/services/telegram/protocol.ts"() {
56171
+ "use strict";
56172
+ record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
56173
+ ANSI = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
56174
+ CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
56175
+ CREDENTIAL_PATTERNS = [
56176
+ /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g,
56177
+ /\bhnm_(?:agent_)?[A-Za-z0-9_-]{16,}\b/g,
56178
+ /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g
56179
+ ];
56180
+ TELEGRAM_GATEWAY_FAILURE_KIND = "gateway-failure";
56181
+ TELEGRAM_FINAL_REPLY_KINDS = /* @__PURE__ */ new Set([
56182
+ "final",
56183
+ "decision",
56184
+ "failure",
56185
+ "confirmation",
56186
+ TELEGRAM_GATEWAY_FAILURE_KIND
56187
+ ]);
56188
+ }
56189
+ });
56190
+
56191
+ // src/services/telegram/notify.ts
56192
+ function recipients(raw) {
56193
+ return (raw ?? "").split(/[\s,]+/).filter((id2) => /^\d+$/.test(id2));
56194
+ }
56195
+ function dashboardOrigin(env = process.env) {
56196
+ const first = env.HANOMAN_CONTROL_ORIGINS?.split(",").map((o) => o.trim()).find(Boolean);
56197
+ if (!first) return null;
56198
+ try {
56199
+ return new URL(first).origin;
56200
+ } catch {
56201
+ return null;
56202
+ }
56203
+ }
56204
+ function decisionMessage(push, origin) {
56205
+ const title = push.title.length > TITLE_MAX ? `${push.title.slice(0, TITLE_MAX - 1)}\u2026` : push.title;
56206
+ return [
56207
+ "Sesi menunggu keputusanmu",
56208
+ title,
56209
+ push.specId ? `${push.specId} \xB7 sesi ${push.sessionId}` : `sesi ${push.sessionId}`,
56210
+ ...origin ? [`${origin}/terminal`] : []
56211
+ ].join("\n");
56212
+ }
56213
+ async function pushDecisionToTelegram(push, deps = {}) {
56214
+ try {
56215
+ const read2 = deps.read ?? effectiveStr;
56216
+ const setting = await (deps.getSetting ?? getSetting)();
56217
+ if (!setting.notifyDecisionTelegram) return 0;
56218
+ const botToken = read2("HANOMAN_TELEGRAM_BOT_TOKEN")?.trim();
56219
+ const ids = recipients(read2("HANOMAN_TELEGRAM_ALLOWED_USER_IDS"));
56220
+ if (!botToken || ids.length === 0) return 0;
56221
+ const controller = new AbortController();
56222
+ const timer11 = setTimeout(() => controller.abort(), deps.timeoutMs ?? SEND_TIMEOUT_MS);
56223
+ const send = deps.transport ?? ((url2, init) => fetch(url2, init));
56224
+ const client = new TelegramApiClient(botToken, (url2, init) => send(url2, { ...init, signal: controller.signal }));
56225
+ const text2 = decisionMessage(push, deps.origin === void 0 ? dashboardOrigin() : deps.origin);
56226
+ try {
56227
+ const results = await Promise.allSettled(ids.map((chatId) => client.sendMessage({ chatId, text: text2 })));
56228
+ let sent = 0;
56229
+ for (const [i, r] of results.entries()) {
56230
+ if (r.status === "fulfilled") {
56231
+ sent++;
56232
+ continue;
56233
+ }
56234
+ console.warn(
56235
+ `telegram: push keputusan ke ${ids[i]} gagal:`,
56236
+ sanitizeTelegramOutput(r.reason.message, [botToken])
56237
+ );
56238
+ }
56239
+ return sent;
56240
+ } finally {
56241
+ clearTimeout(timer11);
56242
+ }
56243
+ } catch (error) {
56244
+ console.warn("telegram: push keputusan gagal:", error.message);
56245
+ return 0;
56246
+ }
56247
+ }
56248
+ var SEND_TIMEOUT_MS, TITLE_MAX;
56249
+ var init_notify = __esm({
56250
+ "src/services/telegram/notify.ts"() {
56251
+ "use strict";
56252
+ init_config2();
56253
+ init_settings3();
56254
+ init_client();
56255
+ init_protocol();
56256
+ SEND_TIMEOUT_MS = 1e4;
56257
+ TITLE_MAX = 200;
56258
+ }
56259
+ });
56260
+
55951
56261
  // src/services/notifications.ts
55952
56262
  async function recordDrift(vpsId, vpsName, drift, snapshotId) {
55953
56263
  if (drift.length === 0) return;
@@ -56068,6 +56378,7 @@ async function scanDecisions(read2 = liveDecisionsAsync) {
56068
56378
  await prisma.notification.create({
56069
56379
  data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
56070
56380
  });
56381
+ void pushDecisionToTelegram({ title, specId: s2.specId ?? null, sessionId: s2.id });
56071
56382
  }
56072
56383
  }
56073
56384
  async function notificationsFeed(p3 = {}) {
@@ -56090,6 +56401,7 @@ var init_notifications2 = __esm({
56090
56401
  init_db();
56091
56402
  init_pty();
56092
56403
  init_live_phases();
56404
+ init_notify();
56093
56405
  awaiting = /* @__PURE__ */ new Set();
56094
56406
  DEFAULT_FEED_TAKE = 50;
56095
56407
  }
@@ -56744,7 +57056,7 @@ var init_runtime_defaults2 = __esm({
56744
57056
  });
56745
57057
 
56746
57058
  // src/services/agent-token.ts
56747
- import { randomBytes as randomBytes2, createHash as createHash11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
57059
+ import { randomBytes as randomBytes2, createHash as createHash12, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
56748
57060
  function toAgentTokenView(t) {
56749
57061
  return {
56750
57062
  id: t.id,
@@ -56831,112 +57143,11 @@ var init_agent_token = __esm({
56831
57143
  "src/services/agent-token.ts"() {
56832
57144
  "use strict";
56833
57145
  init_db();
56834
- hash2 = (token) => createHash11("sha256").update(token).digest("hex");
57146
+ hash2 = (token) => createHash12("sha256").update(token).digest("hex");
56835
57147
  AGENT_CREATOR_PREFIX = "agent:";
56836
57148
  }
56837
57149
  });
56838
57150
 
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
57151
  // src/services/telegram/config.ts
56941
57152
  async function setTelegramEngine(next) {
56942
57153
  const cur = await getSetting();
@@ -56972,129 +57183,6 @@ var init_config4 = __esm({
56972
57183
  }
56973
57184
  });
56974
57185
 
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
57186
  // src/services/telegram/runtime.ts
57099
57187
  function setTelegramRuntime(input) {
57100
57188
  registration = {
@@ -73147,7 +73235,7 @@ init_session_phases();
73147
73235
  init_stage_machine();
73148
73236
  init_notifications2();
73149
73237
  init_sync_notify();
73150
- import { createHash as createHash10 } from "node:crypto";
73238
+ import { createHash as createHash11 } from "node:crypto";
73151
73239
  var specNum = (id2) => Number.parseInt(id2.match(/\d+/)?.[0] ?? "0", 10);
73152
73240
  var selectOf = (shape) => Object.fromEntries(
73153
73241
  Object.keys(shape).filter((k) => k !== "blockedBy").map((k) => [k, true])
@@ -73239,7 +73327,7 @@ async function liveSignature() {
73239
73327
  async function specsDigest() {
73240
73328
  const agg = await prisma.spec.aggregate({ _count: { _all: true }, _max: { updatedAt: true }, _sum: { version: true } });
73241
73329
  const db = `${agg._count._all}:${agg._max.updatedAt?.getTime() ?? 0}:${agg._sum.version ?? 0}`;
73242
- return createHash10("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
73330
+ return createHash11("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
73243
73331
  }
73244
73332
  async function liveSpecs(filter = {}) {
73245
73333
  await liveOverlayTick();