hanoman 0.9.29 → 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 (26) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +35 -2
  3. package/dist/server.js +589 -303
  4. package/package.json +1 -1
  5. package/web/assets/{DalangHanomanScreen-BMrikKnt.js → DalangHanomanScreen-DdsEmtBl.js} +1 -1
  6. package/web/assets/{DocPreviewModal-5j31WUwI.js → DocPreviewModal-BbOqdfaH.js} +1 -1
  7. package/web/assets/{DocsWorkspace-GkTrniF3.js → DocsWorkspace-Cr7AXz9z.js} +1 -1
  8. package/web/assets/{IdeScreen-CAQfv1_e.js → IdeScreen-hdbQW-ui.js} +1 -1
  9. package/web/assets/{LeadScreen-Da5-1FLe.js → LeadScreen-BWE4mfJI.js} +1 -1
  10. package/web/assets/{ReviewScreen-B8KHzQpd.js → ReviewScreen-BIkpEspG.js} +1 -1
  11. package/web/assets/SchedulerScreen-3KVE-d_d.js +1 -0
  12. package/web/assets/SettingsScreen-B0-W2TeR.js +62 -0
  13. package/web/assets/{TeamScreen-P6vz88Pp.js → TeamScreen-DNdt2P1g.js} +1 -1
  14. package/web/assets/{TerminalScreen-B_cagBZd.js → TerminalScreen-BjQF48XI.js} +1 -1
  15. package/web/assets/{TriageScreen-IYkxyhdm.js → TriageScreen-CzM99CrG.js} +1 -1
  16. package/web/assets/{VpsScreen-DFGT5S8Y.js → VpsScreen-SGB_BpbS.js} +1 -1
  17. package/web/assets/fields-of8G961J.js +1 -0
  18. package/web/assets/{index-0T3s-I82.css → index-C3KwOJxI.css} +1 -1
  19. package/web/assets/{index-C-iSWHn3.js → index-C5P4ykKO.js} +207 -207
  20. package/web/assets/{live-lSo-u7M6.js → live-DQen7wLm.js} +1 -1
  21. package/web/assets/{sched-lead-shared-BTz6aILx.js → sched-lead-shared-C34TvLVv.js} +1 -1
  22. package/web/assets/{settings-rebase-CPawFVpp.js → settings-rebase-DHIWk4mV.js} +1 -1
  23. package/web/index.html +2 -2
  24. package/web/assets/SchedulerScreen-CBd4O5kw.js +0 -1
  25. package/web/assets/SettingsScreen-BxQ3CH2D.js +0 -62
  26. package/web/assets/fields-CmVwp6QK.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),
@@ -5575,7 +5577,7 @@ function resolvePhasePlan(input) {
5575
5577
  if (!input.nativeAgents || cfg?.enabled === false) return null;
5576
5578
  const cells = cfg?.[input.runtime] ?? {};
5577
5579
  const coerce3 = input.runtime === "codex" ? coerceCodexEffort : coerceClaudeEffort;
5578
- const resolve25 = (key, fallback2) => {
5580
+ const resolve25 = (key, fallback3) => {
5579
5581
  const cell4 = cells[key];
5580
5582
  const override = input.phaseOverrides?.[key];
5581
5583
  if (input.runtime === "antigravity") {
@@ -5587,9 +5589,9 @@ function resolvePhasePlan(input) {
5587
5589
  }
5588
5590
  return { model: model2, effort: null };
5589
5591
  }
5590
- const picked = override?.model ?? cell4?.model ?? fallback2.model;
5592
+ const picked = override?.model ?? cell4?.model ?? fallback3.model;
5591
5593
  const model = input.runtime === "claude" && picked === CLAUDE_DEFAULT_ALIAS ? CLAUDE_SUBAGENT_INHERIT : picked;
5592
- return { model, effort: coerce3(model, override?.effort ?? cell4?.effort ?? fallback2.effort ?? "medium") };
5594
+ return { model, effort: coerce3(model, override?.effort ?? cell4?.effort ?? fallback3.effort ?? "medium") };
5593
5595
  };
5594
5596
  const reviewer = REVIEWER_FLOWS.has(input.flow) ? {
5595
5597
  agentName: phaseAgentName(REVIEWER_CELL),
@@ -9122,6 +9124,17 @@ var init_config_registry = __esm({
9122
9124
  // runtime
9123
9125
  { key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", example: "1000", default: "1000", min: 100 },
9124
9126
  { key: "HANOMAN_UPDATE_FETCH", group: "runtime", label: "Deteksi update saat boot", kind: "bool", apply: "live", category: "knob", default: "1" },
9127
+ // ADR-0224 · limit Antigravity membaca token agy di Keychain dan memanggil Cloud Code API Google.
9128
+ {
9129
+ key: "HANOMAN_ANTIGRAVITY_LIMITS_FETCH",
9130
+ group: "runtime",
9131
+ label: "Baca limit Antigravity",
9132
+ kind: "bool",
9133
+ apply: "live",
9134
+ category: "knob",
9135
+ default: "1",
9136
+ help: "Membaca token agy di Keychain macOS lalu memanggil API kuota Google (endpoint internal, tak terdokumentasi). Matikan bila tak ingin hanoman menyentuh kredensial agy."
9137
+ },
9125
9138
  // SPEC-398 · ADR-0087 · deteksi update kini membandingkan semver dengan registry npm, bukan
9126
9139
  // menghitung commit di repo git (HANOMAN_REPO_ROOT dicabut bersama jalur git itu).
9127
9140
  {
@@ -14235,6 +14248,18 @@ var init_system = __esm({
14235
14248
  build: () => ({ method: "GET", path: "/limits/codex" }),
14236
14249
  shape: (raw) => raw
14237
14250
  },
14251
+ {
14252
+ name: "hanoman_limits_antigravity",
14253
+ title: "Sisa kuota Antigravity",
14254
+ description: "Sisa kuota Antigravity CLI (agy) per grup model (Gemini; Claude/GPT) untuk jendela mingguan dan 5 jam, berikut kapan resetnya. Dibaca live dari Cloud Code API Google memakai token agy yang sedang hidup dan di-cache 60 detik; token yang kedaluwarsa (agy tak berjalan) memberi `stale` atau `unavailable`, bukan angka karangan. Endpoint terpisah dari hanoman_limits karena sumbernya beda. Tak menuntut capability apa pun.",
14255
+ inputSchema: obj({ properties: {} }),
14256
+ mode: "read",
14257
+ capability: null,
14258
+ samplePath: "/limits/antigravity",
14259
+ sampleMethod: "GET",
14260
+ build: () => ({ method: "GET", path: "/limits/antigravity" }),
14261
+ shape: (raw) => raw
14262
+ },
14238
14263
  {
14239
14264
  name: "hanoman_overview_get",
14240
14265
  title: "Ringkasan kinerja hari ini (Overview)",
@@ -17073,6 +17098,11 @@ var init_verify_scope = __esm({
17073
17098
  ' test-nya memang berjalan, jangan menerima "no test files" sebagai bukti.',
17074
17099
  "- Typecheck: hanya paket yang tersentuh (mis. `pnpm --filter ./server typecheck`).",
17075
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.",
17076
17106
  "- Lint: hanya berkas yang berubah, bukan seluruh repo.",
17077
17107
  "- Build penuh: hanya bila yang kamu ubah memang soal build/bundling.",
17078
17108
  "- Boot server + curl / smoke end-to-end: hanya bila task ini menyentuh endpoint atau perilaku",
@@ -17418,7 +17448,7 @@ Stack: ${project.stack || "\u2014"}`,
17418
17448
  ${cron.prompt}`
17419
17449
  ].join("\n\n");
17420
17450
  }
17421
- 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;
17422
17452
  var init_prompt = __esm({
17423
17453
  "../runner/src/prompt.ts"() {
17424
17454
  "use strict";
@@ -17435,12 +17465,14 @@ var init_prompt = __esm({
17435
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.`;
17436
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.`;
17437
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.';
17438
17469
  phaseInstruction = (phases2, method) => {
17439
17470
  const base3 = `Kerjakan fase berurutan: ${phases2.join(" \u2192 ")}.
17440
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.`;
17441
17472
  if (!phases2.includes("Plan") || !phases2.includes("Execute")) return base3;
17442
17473
  return base3 + `
17443
- 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}`;
17444
17476
  };
17445
17477
  skillInstruction = (flow2, phases2, method) => {
17446
17478
  const lines2 = phases2.map((p3) => {
@@ -18597,7 +18629,7 @@ function backlogGuide(flow2, phase, ctx) {
18597
18629
  const m = ctx.method;
18598
18630
  switch (phase) {
18599
18631
  case "Brainstorm":
18600
- 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.` : "");
18601
18633
  case "Objective":
18602
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.";
18603
18635
  case "Spec":
@@ -18606,11 +18638,11 @@ function backlogGuide(flow2, phase, ctx) {
18606
18638
  // default skill) lolos hampa. Nama dikunci seperti Brainstorm, dan path persisnya diteruskan
18607
18639
  // orchestrator ke Execute & reviewer — bukan pola glob yang bisa mencocoki plan backlog lain.
18608
18640
  case "Plan":
18609
- 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}`;
18610
18642
  case "Execute":
18611
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.`;
18612
18644
  case "Audit":
18613
- 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.";
18614
18646
  case "Laporan":
18615
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;
18616
18648
  case "Goal":
@@ -18752,7 +18784,7 @@ function withPhaseDelegation(phaseDefs, roster, runtime) {
18752
18784
  ${clause}` } : def2;
18753
18785
  });
18754
18786
  }
18755
- 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;
18756
18788
  var init_phase_agents = __esm({
18757
18789
  "../runner/src/phase-agents.ts"() {
18758
18790
  "use strict";
@@ -18762,13 +18794,15 @@ var init_phase_agents = __esm({
18762
18794
  REVIEWER_AGENT_NAME = phaseAgentName(REVIEWER_CELL);
18763
18795
  PROJECT_FLOWS = /* @__PURE__ */ new Set(["reverse", "scaffold", "prd", "breakdown"]);
18764
18796
  DOC_WRITING_PHASES = /* @__PURE__ */ new Set(["Docs teknis", "Konvensi & index", "Doc index"]);
18765
- 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.";
18766
18799
  PHASE_AGENT_RULES = [
18767
18800
  "Batas peran fase:",
18768
18801
  "- JANGAN menulis `$HANOMAN_PHASE_FILE` \u2014 orchestrator satu-satunya penulis marker fase.",
18769
18802
  "- JANGAN `git push` \u2014 push milik orchestrator. Commit artefak fasemu sendiri sebelum melapor.",
18770
18803
  `- JANGAN memanggil agen berawalan \`${PHASE_AGENT_PREFIX}\`. Custom agent lain boleh dipanggil bila relevan.`,
18771
18804
  "- Kerjakan HANYA fase ini; fase lain dikerjakan agen fasenya sendiri.",
18805
+ FOREGROUND_RULE,
18772
18806
  // T1 · ADR-0164 amandemen 2026-09-23 · 22 laporan tanpa `Status:` di audit: agen fase mengakhiri
18773
18807
  // gilirannya sambil menunggu proses latarnya sendiri, dan teks "menunggu" itu dibaca orchestrator
18774
18808
  // sebagai laporan final. Giliranmu yang berakhir = laporanmu, jadi menunggu harus terjadi DI DALAM giliran.
@@ -18784,6 +18818,7 @@ var init_phase_agents = __esm({
18784
18818
  "Klaim tanpa bukti bukan bukti. Maksimal 1200 kata."
18785
18819
  ].join("\n");
18786
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.`;
18787
18822
  TERMINAL_ASK = /([Aa])jukan SATU pertanyaan per giliran ke manusia di terminal ini, tunggu jawabannya(?=[,.])/;
18788
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";
18789
18824
  TERMINAL_HANDOFF = "tulis ringkasan hasil + daftar pertanyaan yang belum terjawab ke terminal.";
@@ -18794,6 +18829,7 @@ var init_phase_agents = __esm({
18794
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.",
18795
18830
  "- JANGAN menulis `$HANOMAN_PHASE_FILE` dan JANGAN memperbaiki temuanmu sendiri \u2014 perbaikan milik agen Execute, marker milik orchestrator.",
18796
18831
  `- JANGAN memanggil agen berawalan \`${PHASE_AGENT_PREFIX}\`.`,
18832
+ FOREGROUND_RULE,
18797
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:`."
18798
18834
  ].join("\n");
18799
18835
  roleOf = (phase) => phase === "Execute" || phase === "Goal" ? "execute" : phase === "Kerjakan" || phase === "Verifikasi" ? "none" : "research";
@@ -20691,6 +20727,7 @@ __export(src_exports, {
20691
20727
  GOAL_TUI_PASTE_LIMIT: () => GOAL_TUI_PASTE_LIMIT,
20692
20728
  PHASE_CONTEXT_HEADER: () => PHASE_CONTEXT_HEADER,
20693
20729
  PIPELINES: () => PIPELINES,
20730
+ PLAN_SHAPE: () => PLAN_SHAPE,
20694
20731
  PROJECT_METHOD: () => PROJECT_METHOD,
20695
20732
  READ_ONLY_DENIED_COMMANDS: () => READ_ONLY_DENIED_COMMANDS,
20696
20733
  READ_ONLY_DENIED_OPERATORS: () => READ_ONLY_DENIED_OPERATORS,
@@ -50111,6 +50148,8 @@ var init_settings3 = __esm({
50111
50148
  notifySound: "short",
50112
50149
  notifyDecision: true,
50113
50150
  notifyDecisionSound: "alert",
50151
+ notifyDecisionTelegram: true,
50152
+ // ADR-0226 · tak berefek tanpa bot token + allowlist
50114
50153
  agentAccessEnabled: false,
50115
50154
  // SPEC-257 · akses AI agent off sampai dibuka manusia
50116
50155
  scheduler: SCHEDULER_DEFAULTS,
@@ -55925,6 +55964,300 @@ var init_live_phases = __esm({
55925
55964
  }
55926
55965
  });
55927
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
+
55928
56261
  // src/services/notifications.ts
55929
56262
  async function recordDrift(vpsId, vpsName, drift, snapshotId) {
55930
56263
  if (drift.length === 0) return;
@@ -56045,6 +56378,7 @@ async function scanDecisions(read2 = liveDecisionsAsync) {
56045
56378
  await prisma.notification.create({
56046
56379
  data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
56047
56380
  });
56381
+ void pushDecisionToTelegram({ title, specId: s2.specId ?? null, sessionId: s2.id });
56048
56382
  }
56049
56383
  }
56050
56384
  async function notificationsFeed(p3 = {}) {
@@ -56067,6 +56401,7 @@ var init_notifications2 = __esm({
56067
56401
  init_db();
56068
56402
  init_pty();
56069
56403
  init_live_phases();
56404
+ init_notify();
56070
56405
  awaiting = /* @__PURE__ */ new Set();
56071
56406
  DEFAULT_FEED_TAKE = 50;
56072
56407
  }
@@ -56721,7 +57056,7 @@ var init_runtime_defaults2 = __esm({
56721
57056
  });
56722
57057
 
56723
57058
  // src/services/agent-token.ts
56724
- 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";
56725
57060
  function toAgentTokenView(t) {
56726
57061
  return {
56727
57062
  id: t.id,
@@ -56808,112 +57143,11 @@ var init_agent_token = __esm({
56808
57143
  "src/services/agent-token.ts"() {
56809
57144
  "use strict";
56810
57145
  init_db();
56811
- hash2 = (token) => createHash11("sha256").update(token).digest("hex");
57146
+ hash2 = (token) => createHash12("sha256").update(token).digest("hex");
56812
57147
  AGENT_CREATOR_PREFIX = "agent:";
56813
57148
  }
56814
57149
  });
56815
57150
 
56816
- // src/services/telegram/client.ts
56817
- var TelegramApiError, TelegramApiClient;
56818
- var init_client = __esm({
56819
- "src/services/telegram/client.ts"() {
56820
- "use strict";
56821
- TelegramApiError = class extends Error {
56822
- constructor(method, code, message, retryAfter) {
56823
- super(`Telegram ${method} gagal${code ? ` (${code})` : ""}: ${message}`);
56824
- this.method = method;
56825
- this.code = code;
56826
- this.retryAfter = retryAfter;
56827
- this.name = "TelegramApiError";
56828
- }
56829
- };
56830
- TelegramApiClient = class {
56831
- constructor(token, transport = (url2, init) => fetch(url2, init), baseUrl = "https://api.telegram.org") {
56832
- this.token = token;
56833
- this.transport = transport;
56834
- this.baseUrl = baseUrl;
56835
- }
56836
- safe(message) {
56837
- return message.split(this.token).join("[REDACTED]").replace(/https:\/\/api\.telegram\.org\/bot[^/\s]+/g, "https://api.telegram.org/bot[REDACTED]");
56838
- }
56839
- async call(method, body, signal) {
56840
- let response;
56841
- try {
56842
- response = await this.transport(`${this.baseUrl}/bot${this.token}/${method}`, {
56843
- method: "POST",
56844
- headers: { "content-type": "application/json" },
56845
- body: JSON.stringify(body),
56846
- signal
56847
- });
56848
- } catch (error) {
56849
- throw new TelegramApiError(method, void 0, this.safe(error.message || "network error"));
56850
- }
56851
- if (!response.ok) {
56852
- const failed = await response.json().catch(() => null);
56853
- throw new TelegramApiError(method, response.status, `HTTP ${response.status}`, failed?.parameters?.retry_after);
56854
- }
56855
- let envelope;
56856
- try {
56857
- envelope = await response.json();
56858
- } catch {
56859
- throw new TelegramApiError(method, response.status, "respons bukan JSON");
56860
- }
56861
- if (!envelope.ok || envelope.result === void 0) {
56862
- throw new TelegramApiError(
56863
- method,
56864
- envelope.error_code,
56865
- this.safe(envelope.description ?? "respons tidak valid"),
56866
- envelope.parameters?.retry_after
56867
- );
56868
- }
56869
- return envelope.result;
56870
- }
56871
- getMe() {
56872
- return this.call("getMe", {});
56873
- }
56874
- async getUpdates(input) {
56875
- if (!Number.isInteger(input.offset) || input.offset < 0) throw new Error("offset harus integer non-negatif");
56876
- if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new Error("limit harus 1..100");
56877
- if (!Number.isInteger(input.timeout) || input.timeout < 0 || input.timeout > 50) throw new Error("timeout harus 0..50");
56878
- return this.call("getUpdates", {
56879
- offset: input.offset,
56880
- limit: input.limit,
56881
- timeout: input.timeout,
56882
- allowed_updates: ["message", "callback_query"]
56883
- }, input.signal);
56884
- }
56885
- sendMessage(input) {
56886
- return this.call("sendMessage", {
56887
- chat_id: input.chatId,
56888
- text: input.text,
56889
- ...input.replyMarkup ? { reply_markup: input.replyMarkup } : {}
56890
- });
56891
- }
56892
- /**
56893
- * SPEC-493 · indikator "typing…". Argumennya posisional (beda dari tetangganya) sesuai kontrak
56894
- * yang diminta. Telegram TAK punya API stop-typing: statusnya padam sendiri ~5 detik sesudah
56895
- * panggilan terakhir, dan pesan masuk apa pun langsung menghapusnya.
56896
- */
56897
- sendChatAction(chatId, action = "typing") {
56898
- return this.call("sendChatAction", { chat_id: chatId, action });
56899
- }
56900
- editMessageText(input) {
56901
- return this.call("editMessageText", {
56902
- chat_id: input.chatId,
56903
- message_id: input.messageId,
56904
- text: input.text
56905
- });
56906
- }
56907
- answerCallbackQuery(input) {
56908
- return this.call("answerCallbackQuery", {
56909
- callback_query_id: input.callbackQueryId,
56910
- ...input.text ? { text: input.text } : {}
56911
- });
56912
- }
56913
- };
56914
- }
56915
- });
56916
-
56917
57151
  // src/services/telegram/config.ts
56918
57152
  async function setTelegramEngine(next) {
56919
57153
  const cur = await getSetting();
@@ -56949,129 +57183,6 @@ var init_config4 = __esm({
56949
57183
  }
56950
57184
  });
56951
57185
 
56952
- // src/services/telegram/protocol.ts
56953
- import { createHash as createHash12 } from "node:crypto";
56954
- function normalizeTelegramCommand(text2) {
56955
- const clean3 = text2.trim();
56956
- const match = clean3.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
56957
- if (!match) return clean3;
56958
- const args = match[2]?.trim().replace(/\s+/g, " ");
56959
- return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
56960
- }
56961
- function parseConfirmationCallback(data) {
56962
- if (data.length > 64) return null;
56963
- const match = data.match(/^tgcf:([A-Za-z0-9_-]{1,40}):(approve|deny)$/);
56964
- return match ? { token: match[1], action: match[2] } : null;
56965
- }
56966
- function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
56967
- const root = record(update2);
56968
- const updateId = root && Number.isInteger(root.update_id) && Number(root.update_id) >= 0 ? Number(root.update_id) : null;
56969
- if (!root || updateId === null) return { ok: false, updateId: null, reason: "malformed" };
56970
- const callback = record(root.callback_query);
56971
- const message = callback ? record(callback.message) : record(root.message);
56972
- const from = callback ? record(callback.from) : record(message?.from);
56973
- const chat = record(message?.chat);
56974
- const chatId = typeof chat?.id === "number" && Number.isSafeInteger(chat.id) ? String(chat.id) : void 0;
56975
- const userId = typeof from?.id === "number" && Number.isSafeInteger(from.id) ? String(from.id) : void 0;
56976
- if (!message || !from || !chat || !chatId || !userId) return { ok: false, updateId, reason: "malformed", chatId, userId };
56977
- if (chat.type !== "private") return { ok: false, updateId, reason: "group", chatId, userId };
56978
- if (from.is_bot === true) return { ok: false, updateId, reason: "sender-bot", chatId, userId };
56979
- if (!allowlist.has(userId)) return { ok: false, updateId, reason: "not-allowed", chatId, userId };
56980
- const messageId = typeof message.message_id === "number" && Number.isInteger(message.message_id) ? message.message_id : null;
56981
- if (messageId === null) return { ok: false, updateId, reason: "malformed", chatId, userId };
56982
- if (callback) {
56983
- const callbackQueryId = typeof callback.id === "string" ? callback.id : "";
56984
- const data = typeof callback.data === "string" ? callback.data : "";
56985
- const parsed = parseConfirmationCallback(data);
56986
- if (!callbackQueryId || !parsed) return { ok: false, updateId, reason: "invalid-callback", chatId, userId };
56987
- return {
56988
- ok: true,
56989
- input: {
56990
- updateId,
56991
- chatId,
56992
- userId,
56993
- messageId,
56994
- kind: "callback",
56995
- text: data,
56996
- callbackQueryId,
56997
- callbackToken: parsed.token,
56998
- callbackAction: parsed.action
56999
- }
57000
- };
57001
- }
57002
- if (typeof message.text !== "string" || !message.text.trim()) {
57003
- return { ok: false, updateId, reason: "non-text", chatId, userId };
57004
- }
57005
- const text2 = normalizeTelegramCommand(message.text);
57006
- if (text2.length > maxText) return { ok: false, updateId, reason: "too-long", chatId, userId };
57007
- return {
57008
- ok: true,
57009
- input: {
57010
- updateId,
57011
- chatId,
57012
- userId,
57013
- messageId,
57014
- kind: text2.startsWith("/") ? "command" : "text",
57015
- text: text2
57016
- }
57017
- };
57018
- }
57019
- function inboundDigest(input) {
57020
- return createHash12("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
57021
- }
57022
- function sanitizeTelegramOutput(text2, exactSecrets = []) {
57023
- let clean3 = text2.replace(ANSI, "").replace(CONTROLS, "");
57024
- for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
57025
- clean3 = clean3.split(secret).join("[REDACTED]");
57026
- }
57027
- for (const pattern of CREDENTIAL_PATTERNS) clean3 = clean3.replace(pattern, "[REDACTED]");
57028
- return clean3.trim();
57029
- }
57030
- function splitTelegramText(text2, max = 4096, exactSecrets = []) {
57031
- if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
57032
- const clean3 = sanitizeTelegramOutput(text2, exactSecrets);
57033
- if (!clean3) return [];
57034
- const chunks = [];
57035
- let rest = clean3;
57036
- while (rest.length > max) {
57037
- let cut = max;
57038
- const before2 = rest.charCodeAt(cut - 1);
57039
- const after = rest.charCodeAt(cut);
57040
- if (before2 >= 55296 && before2 <= 56319 && after >= 56320 && after <= 57343) cut--;
57041
- const window2 = rest.slice(0, cut);
57042
- const newline2 = window2.lastIndexOf("\n");
57043
- const space = window2.lastIndexOf(" ");
57044
- const boundary = Math.max(newline2, space);
57045
- if (boundary >= Math.floor(max / 2)) cut = boundary + 1;
57046
- chunks.push(rest.slice(0, cut));
57047
- rest = rest.slice(cut);
57048
- }
57049
- if (rest) chunks.push(rest);
57050
- return chunks;
57051
- }
57052
- var record, ANSI, CONTROLS, CREDENTIAL_PATTERNS, TELEGRAM_GATEWAY_FAILURE_KIND, TELEGRAM_FINAL_REPLY_KINDS;
57053
- var init_protocol = __esm({
57054
- "src/services/telegram/protocol.ts"() {
57055
- "use strict";
57056
- record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
57057
- ANSI = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
57058
- CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
57059
- CREDENTIAL_PATTERNS = [
57060
- /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g,
57061
- /\bhnm_(?:agent_)?[A-Za-z0-9_-]{16,}\b/g,
57062
- /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g
57063
- ];
57064
- TELEGRAM_GATEWAY_FAILURE_KIND = "gateway-failure";
57065
- TELEGRAM_FINAL_REPLY_KINDS = /* @__PURE__ */ new Set([
57066
- "final",
57067
- "decision",
57068
- "failure",
57069
- "confirmation",
57070
- TELEGRAM_GATEWAY_FAILURE_KIND
57071
- ]);
57072
- }
57073
- });
57074
-
57075
57186
  // src/services/telegram/runtime.ts
57076
57187
  function setTelegramRuntime(input) {
57077
57188
  registration = {
@@ -63404,7 +63515,7 @@ function parseCodexVersion(out4) {
63404
63515
  return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
63405
63516
  }
63406
63517
  async function getCodexVersion(now = Date.now()) {
63407
- if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
63518
+ if (cache3 && now - cache3.at < TTL_MS5) return cache3.version;
63408
63519
  const version = await probeCodexVersion();
63409
63520
  cache3 = { at: now, version };
63410
63521
  return version;
@@ -63420,7 +63531,7 @@ async function codexVersionInfo() {
63420
63531
  function _resetCodexVersionCache() {
63421
63532
  cache3 = null;
63422
63533
  }
63423
- var run3, CODEX_MIN_CLIENT, TTL_MS4, cache3, codexBin2, runVersion;
63534
+ var run3, CODEX_MIN_CLIENT, TTL_MS5, cache3, codexBin2, runVersion;
63424
63535
  var init_codex_version = __esm({
63425
63536
  "src/services/codex-version.ts"() {
63426
63537
  "use strict";
@@ -63429,7 +63540,7 @@ var init_codex_version = __esm({
63429
63540
  init_config2();
63430
63541
  run3 = promisify16(execFile17);
63431
63542
  CODEX_MIN_CLIENT = "0.144.0";
63432
- TTL_MS4 = 5 * 6e4;
63543
+ TTL_MS5 = 5 * 6e4;
63433
63544
  cache3 = null;
63434
63545
  codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
63435
63546
  runVersion = async (bin, args) => {
@@ -63440,17 +63551,17 @@ var init_codex_version = __esm({
63440
63551
  });
63441
63552
 
63442
63553
  // src/services/agent-tool-catalog.ts
63443
- import { readFileSync as readFileSync25 } from "node:fs";
63444
- import { homedir as homedir12 } from "node:os";
63445
- import { join as join47 } from "node:path";
63554
+ import { readFileSync as readFileSync26 } from "node:fs";
63555
+ import { homedir as homedir13 } from "node:os";
63556
+ import { join as join48 } from "node:path";
63446
63557
  function mcpServerNames(repoDir) {
63447
63558
  const names = [];
63448
- const claudeJson = readJson2(join47(home(), ".claude.json"));
63559
+ const claudeJson = readJson2(join48(home(), ".claude.json"));
63449
63560
  names.push(...serversOf(claudeJson));
63450
63561
  if (repoDir) {
63451
63562
  const projects = claudeJson?.projects;
63452
63563
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
63453
- names.push(...serversOf(readJson2(join47(repoDir, ".mcp.json"))));
63564
+ names.push(...serversOf(readJson2(join48(repoDir, ".mcp.json"))));
63454
63565
  }
63455
63566
  names.push(...codexServers());
63456
63567
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -63463,10 +63574,10 @@ var init_agent_tool_catalog = __esm({
63463
63574
  "src/services/agent-tool-catalog.ts"() {
63464
63575
  "use strict";
63465
63576
  init_src();
63466
- home = () => process.env.HOME || homedir12();
63577
+ home = () => process.env.HOME || homedir13();
63467
63578
  readJson2 = (path) => {
63468
63579
  try {
63469
- return JSON.parse(readFileSync25(path, "utf8"));
63580
+ return JSON.parse(readFileSync26(path, "utf8"));
63470
63581
  } catch {
63471
63582
  return null;
63472
63583
  }
@@ -63479,7 +63590,7 @@ var init_agent_tool_catalog = __esm({
63479
63590
  codexServers = () => {
63480
63591
  let text2;
63481
63592
  try {
63482
- text2 = readFileSync25(join47(home(), ".codex", "config.toml"), "utf8");
63593
+ text2 = readFileSync26(join48(home(), ".codex", "config.toml"), "utf8");
63483
63594
  } catch {
63484
63595
  return [];
63485
63596
  }
@@ -73124,7 +73235,7 @@ init_session_phases();
73124
73235
  init_stage_machine();
73125
73236
  init_notifications2();
73126
73237
  init_sync_notify();
73127
- import { createHash as createHash10 } from "node:crypto";
73238
+ import { createHash as createHash11 } from "node:crypto";
73128
73239
  var specNum = (id2) => Number.parseInt(id2.match(/\d+/)?.[0] ?? "0", 10);
73129
73240
  var selectOf = (shape) => Object.fromEntries(
73130
73241
  Object.keys(shape).filter((k) => k !== "blockedBy").map((k) => [k, true])
@@ -73216,7 +73327,7 @@ async function liveSignature() {
73216
73327
  async function specsDigest() {
73217
73328
  const agg = await prisma.spec.aggregate({ _count: { _all: true }, _max: { updatedAt: true }, _sum: { version: true } });
73218
73329
  const db = `${agg._count._all}:${agg._max.updatedAt?.getTime() ?? 0}:${agg._sum.version ?? 0}`;
73219
- return createHash10("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
73330
+ return createHash11("sha1").update(`${db}|${await liveSignature()}`).digest("hex");
73220
73331
  }
73221
73332
  async function liveSpecs(filter = {}) {
73222
73333
  await liveOverlayTick();
@@ -76879,6 +76990,10 @@ async function fail(req, deps, reason, flowId = null, step = null) {
76879
76990
  // src/services/lead/detect.ts
76880
76991
  var answers = /* @__PURE__ */ new Map();
76881
76992
  var capped = /* @__PURE__ */ new Set();
76993
+ function askSignature(a) {
76994
+ const raw = a.questions.length ? a.questions.map((q2) => q2.question).join("\n") : a.message;
76995
+ return raw.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 2e3);
76996
+ }
76882
76997
  var failures = /* @__PURE__ */ new Map();
76883
76998
  var failCapped = /* @__PURE__ */ new Set();
76884
76999
  var failedAt = /* @__PURE__ */ new Map();
@@ -76950,7 +77065,9 @@ async function admitAsk(s2, deps = prodDetectDeps) {
76950
77065
  if (!(await deps.optIn()).includes(s2.projectId)) return no("project tak opt-in lead");
76951
77066
  if (!leadActive(cfg, s2.projectId)) return no("lead dijeda untuk project ini");
76952
77067
  if (deps.exited(s2.id)) return no("pane mati");
76953
- if ((answers.get(s2.id) ?? 0) >= cfg.maxAutoAnswers) {
77068
+ const run6 = answers.get(s2.id);
77069
+ const looping = !!run6 && (s2.sig === void 0 || s2.sig === run6.sig);
77070
+ if (run6 && looping && run6.n >= cfg.maxAutoAnswers) {
76954
77071
  if (!capped.has(s2.id)) {
76955
77072
  capped.add(s2.id);
76956
77073
  const row = await recordDecision({
@@ -76959,9 +77076,9 @@ async function admitAsk(s2, deps = prodDetectDeps) {
76959
77076
  sessionId: s2.id,
76960
77077
  gate: "detected",
76961
77078
  kind: "quality",
76962
- question: `Sesi ${s2.id} sudah dijawab otomatis ${cfg.maxAutoAnswers}\xD7 berturut-turut.`,
76963
- answer: "Berhenti menjawab sesi ini; serahkan ke operator.",
76964
- reason: "Batas jawaban otomatis per sesi tercapai \u2014 pengulangan menandakan lead tak benar-benar membuka jalan buntunya (AC-11).",
77079
+ question: `Sesi ${s2.id} sudah dijawab otomatis ${cfg.maxAutoAnswers}\xD7 berturut-turut untuk pertanyaan yang sama.`,
77080
+ answer: "Berhenti menjawab pertanyaan ini; serahkan ke operator.",
77081
+ reason: "Pertanyaan yang sama terus kembali sesudah dijawab \u2014 pengulangan menandakan lead tak benar-benar membuka jalan buntunya (AC-11, ADR-0223).",
76965
77082
  refs: [],
76966
77083
  confidence: "tinggi",
76967
77084
  action: "none",
@@ -77017,7 +77134,13 @@ async function answerAsk(ask, s2, deps = prodDetectDeps) {
77017
77134
  const out4 = { at: chain.at, flowId: chain.flowId, step: chain.step };
77018
77135
  if (chain.acted && chain.done) {
77019
77136
  if (s2.decisionFile) deps.clearMarker(s2.decisionFile);
77020
- answers.set(ask.sessionId, (answers.get(ask.sessionId) ?? 0) + 1);
77137
+ const sig = askSignature(ask);
77138
+ const prev = answers.get(ask.sessionId);
77139
+ if (prev?.sig === sig) prev.n++;
77140
+ else {
77141
+ answers.set(ask.sessionId, { sig, n: 1 });
77142
+ capped.delete(ask.sessionId);
77143
+ }
77021
77144
  failures.delete(ask.sessionId);
77022
77145
  return { answered: true, reason: "", ...out4 };
77023
77146
  }
@@ -77025,7 +77148,7 @@ async function answerAsk(ask, s2, deps = prodDetectDeps) {
77025
77148
  failures.set(ask.sessionId, (failures.get(ask.sessionId) ?? 0) + 1);
77026
77149
  failedAt.set(ask.sessionId, deps.now());
77027
77150
  }
77028
- return { answered: false, reason: chain.reason, ...out4 };
77151
+ return { answered: false, reason: chain.reason, ...out4, ...chain.mismatch ? { mismatch: true } : {} };
77029
77152
  }
77030
77153
  async function runChain(ask, s2, deps) {
77031
77154
  const sid = ask.sessionId;
@@ -77043,6 +77166,7 @@ async function runChain(ask, s2, deps) {
77043
77166
  return { ...r, flowId, step };
77044
77167
  };
77045
77168
  const stop = (reason, failed = false) => ({ acted, done: false, failed, reason, at, flowId, step });
77169
+ const asked = ask.source === "ask-tool";
77046
77170
  if (ask.source === "turn-end") {
77047
77171
  const read2 = readCodexTurn(ask.message);
77048
77172
  if (!read2.asking) return stop(read2.reason);
@@ -77079,6 +77203,13 @@ async function runChain(ask, s2, deps) {
77079
77203
  // `tunggal` di tiap langkah — `chainSteps` kosong tepat di tempat yang paling membutuhkannya.
77080
77204
  chain: true
77081
77205
  };
77206
+ if (asked && q2) {
77207
+ const seen2 = await waitDialogFor(sid, q2, ask.questions, deps);
77208
+ if (seen2 === "none") return stop("dialog tak muncul di pane \u2014 tak ada yang diketik", true);
77209
+ if (seen2 === "other") {
77210
+ return i === 0 ? { ...stop("dialog di pane adalah pertanyaan lain \u2014 tak ada yang diketik"), mismatch: true } : stop("dialog di pane bukan pertanyaan berikutnya rantai ini \u2014 tak ada yang diketik");
77211
+ }
77212
+ }
77082
77213
  let row;
77083
77214
  try {
77084
77215
  row = await deps.decide({ ...req, flowId }, deps.decideDeps);
@@ -77102,9 +77233,11 @@ async function runChain(ask, s2, deps) {
77102
77233
  if (!row) return stop("lead tak menghasilkan keputusan yang berlaku");
77103
77234
  if (row.status !== "berlaku") return stop("lead tak menghasilkan keputusan yang berlaku", true);
77104
77235
  let before2 = "";
77105
- if (ask.source === "ask-tool") {
77106
- const text2 = await waitDialog(sid, deps);
77107
- if (text2 === null) return stop("dialog tak muncul di pane \u2014 tak ada yang diketik", true);
77236
+ if (asked && q2) {
77237
+ const text2 = deps.pane(sid);
77238
+ const screen = readDialogScreen(text2);
77239
+ if (!screen || !dialogFits(screen, q2, ask.questions))
77240
+ return stop("layar dialog berubah selagi lead memutuskan \u2014 tak ada yang diketik");
77108
77241
  before2 = dialogKey(text2);
77109
77242
  }
77110
77243
  if (isTakenOver(sid)) return stop("diambil alih operator");
@@ -77136,13 +77269,29 @@ async function afterLastAnswer(id2, deps) {
77136
77269
  }
77137
77270
  return "pending";
77138
77271
  }
77139
- async function waitDialog(id2, deps) {
77272
+ async function waitDialogFor(id2, q2, all, deps) {
77273
+ let sawDialog = false;
77140
77274
  for (let i = 0; i < DIALOG_WAIT_TRIES; i++) {
77141
- const text2 = deps.pane(id2);
77142
- if (readDialogScreen(text2)) return text2;
77275
+ const screen = readDialogScreen(deps.pane(id2));
77276
+ if (screen?.kind === "question") {
77277
+ if (dialogFits(screen, q2, all)) return "fit";
77278
+ sawDialog = true;
77279
+ }
77143
77280
  await deps.sleep(CHAIN_POLL_MS);
77144
77281
  }
77145
- return null;
77282
+ return sawDialog ? "other" : "none";
77283
+ }
77284
+ var bare = (s2) => s2.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
77285
+ function dialogFits(screen, q2, all) {
77286
+ if (screen.kind !== "question") return false;
77287
+ if (screen.tabs.length && screen.tabs.length !== all.length) return false;
77288
+ const title = bare(screen.title);
77289
+ const text2 = bare(q2.question);
77290
+ if (title.length >= 6 && text2 && !text2.startsWith(title) && !title.startsWith(text2)) return false;
77291
+ const labels = q2.options.map((o) => bare(o.label)).filter(Boolean);
77292
+ const shown = screen.options.map(bare).filter(Boolean);
77293
+ if (labels.length && shown.length && !shown.some((s2) => labels.some((l) => l.startsWith(s2) || s2.startsWith(l)))) return false;
77294
+ return true;
77146
77295
  }
77147
77296
  async function waitScreenChange(id2, before2, deps) {
77148
77297
  for (let i = 0; i < CHAIN_POLL_TRIES; i++) {
@@ -77157,6 +77306,8 @@ init_pty();
77157
77306
  var ASK_BUCKET = { capacity: 5, refillMs: 1e4 };
77158
77307
  var GLOBAL_BUCKET = { capacity: 20, refillMs: 500 };
77159
77308
  var SEEN_MAX = 512;
77309
+ var ASK_QUEUE_MAX = 8;
77310
+ var ASK_MISMATCH_MAX = 2;
77160
77311
  var entries = /* @__PURE__ */ new Map();
77161
77312
  var seen = [];
77162
77313
  var seenSet = /* @__PURE__ */ new Set();
@@ -77236,7 +77387,8 @@ async function intakeAsk(input, deps = prodAskDeps) {
77236
77387
  const verdict = await deps.admit({
77237
77388
  id: input.sessionId,
77238
77389
  specId: input.specId,
77239
- projectId: input.projectId
77390
+ projectId: input.projectId,
77391
+ sig: askSignature(input.event)
77240
77392
  });
77241
77393
  if (!verdict.ok) return { status: "rejected", reason: verdict.reason };
77242
77394
  seenSet.add(key);
@@ -77252,12 +77404,16 @@ async function intakeAsk(input, deps = prodAskDeps) {
77252
77404
  };
77253
77405
  const ask = toAsk({ sessionId: input.sessionId, agent: input.agent, event: input.event, now });
77254
77406
  const cur = entries.get(input.sessionId);
77255
- if (cur?.running) {
77256
- cur.pending = { ask, ctx };
77407
+ if (cur?.busy) {
77408
+ cur.queue.push({ ask, ctx, skips: 0 });
77409
+ if (cur.queue.length > ASK_QUEUE_MAX) {
77410
+ const dropped = cur.queue.shift();
77411
+ console.log(`lead ask ${input.sessionId}: antrean penuh \u2014 tanya ${dropped.ask.askId} dibuang`);
77412
+ }
77257
77413
  return { status: "accepted" };
77258
77414
  }
77259
77415
  clearTakeover(input.sessionId);
77260
- entries.set(input.sessionId, { ask, ctx, pending: null, running: false });
77416
+ entries.set(input.sessionId, { ask, ctx, skips: 0, queue: [], busy: true, running: false });
77261
77417
  void prune(deps);
77262
77418
  void run2(input.sessionId, deps);
77263
77419
  return { status: "accepted" };
@@ -77311,16 +77467,19 @@ async function run2(sessionId2, deps) {
77311
77467
  step: r.step,
77312
77468
  state: r.answered ? "answered" : "failed"
77313
77469
  };
77314
- if (!e.pending) break;
77315
- const next = e.pending;
77316
- e.pending = null;
77470
+ if (r.mismatch && e.queue.length && e.skips < ASK_MISMATCH_MAX)
77471
+ e.queue.push({ ask: { ...e.ask, state: "queued" }, ctx: e.ctx, skips: e.skips + 1 });
77472
+ const next = e.queue.shift();
77473
+ if (!next) break;
77317
77474
  e.ask = next.ask;
77318
77475
  e.ctx = next.ctx;
77476
+ e.skips = next.skips;
77319
77477
  }
77320
77478
  } finally {
77321
77479
  clearQueued(sessionId2);
77322
77480
  clearDeciding(sessionId2);
77323
77481
  e.running = false;
77482
+ e.busy = false;
77324
77483
  inFlight3--;
77325
77484
  const nextId = waiting.shift();
77326
77485
  if (nextId) void run2(nextId, deps);
@@ -79247,10 +79406,131 @@ async function getCodexLimits(dir2) {
79247
79406
  return dto;
79248
79407
  }
79249
79408
 
79409
+ // src/services/antigravity-limits.ts
79410
+ init_config2();
79411
+ import { execFileSync as execFileSync9 } from "node:child_process";
79412
+ import { readFileSync as readFileSync25 } from "node:fs";
79413
+ import { homedir as homedir12 } from "node:os";
79414
+ import { join as join44 } from "node:path";
79415
+ var QUOTA_URL = "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary";
79416
+ var USER_AGENT = "antigravity (hanoman)";
79417
+ var TTL_MS4 = 6e4;
79418
+ var EXPIRY_SKEW_MS = 3e4;
79419
+ var lastOk2 = null;
79420
+ var freshUntil3 = 0;
79421
+ var UNAVAILABLE2 = { status: "unavailable", windows: [], fetchedAt: null };
79422
+ var securityRun2 = (args) => execFileSync9("security", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
79423
+ function readAgyToken(run6 = securityRun2) {
79424
+ try {
79425
+ let raw = run6(["find-generic-password", "-s", "gemini", "-a", "antigravity", "-w"]).trim();
79426
+ const prefix = "go-keyring-base64:";
79427
+ if (raw.startsWith(prefix)) raw = Buffer.from(raw.slice(prefix.length), "base64").toString("utf8");
79428
+ const tok = JSON.parse(raw)?.token;
79429
+ if (typeof tok?.access_token !== "string" || !tok.access_token) return null;
79430
+ const expiry = typeof tok.expiry === "string" ? Date.parse(tok.expiry) : NaN;
79431
+ return { accessToken: tok.access_token, expiry: Number.isFinite(expiry) ? expiry : null };
79432
+ } catch {
79433
+ return null;
79434
+ }
79435
+ }
79436
+ var agyHome = () => join44(homedir12(), ".gemini", "antigravity-cli");
79437
+ function defaultProject() {
79438
+ try {
79439
+ return readFileSync25(join44(agyHome(), "cache", "default_project_id.txt"), "utf8").trim() || null;
79440
+ } catch {
79441
+ return null;
79442
+ }
79443
+ }
79444
+ var severityOf2 = (pct3) => pct3 >= 90 ? "critical" : pct3 >= 70 ? "warning" : "normal";
79445
+ function windowLabel2(window2) {
79446
+ if (window2 === "5h") return "Sesi 5 jam";
79447
+ if (window2 === "weekly") return "Mingguan";
79448
+ return window2;
79449
+ }
79450
+ function groupLabel(name2) {
79451
+ if (/^gemini/i.test(name2)) return "Gemini";
79452
+ if (/claude/i.test(name2) && /gpt/i.test(name2)) return "Claude/GPT";
79453
+ return name2;
79454
+ }
79455
+ function mapQuota(json) {
79456
+ const groups = json?.groups;
79457
+ if (!Array.isArray(groups)) return [];
79458
+ return groups.flatMap((g) => {
79459
+ const group2 = groupLabel(g.displayName ?? "");
79460
+ return (Array.isArray(g.buckets) ? g.buckets : []).flatMap((b) => {
79461
+ if (typeof b.remainingFraction !== "number" || !b.bucketId) return [];
79462
+ const remaining = Math.max(0, Math.min(1, b.remainingFraction));
79463
+ const usedPct = Math.round((1 - remaining) * 100);
79464
+ const label2 = windowLabel2(b.window ?? "");
79465
+ return [{
79466
+ key: `antigravity:${b.bucketId}`,
79467
+ label: group2 ? `${group2} \xB7 ${label2}` : label2,
79468
+ usedPct,
79469
+ resetsAt: b.resetTime ?? null,
79470
+ severity: severityOf2(usedPct),
79471
+ // API tak punya padanan `is_active` claude. Yang paling dekat: bucket yang HABIS.
79472
+ isActive: remaining === 0
79473
+ }];
79474
+ });
79475
+ });
79476
+ }
79477
+ function fallback2() {
79478
+ return lastOk2 ? { ...lastOk2, status: "stale" } : UNAVAILABLE2;
79479
+ }
79480
+ var prodDeps = {
79481
+ platform: process.platform,
79482
+ // `HANOMAN_ANTIGRAVITY_LIMITS_FETCH=0` mematikan sumbernya sama sekali (cermin
79483
+ // `HANOMAN_UPDATE_FETCH`): suite test server berjalan di macOS operator, dan route ini tak boleh
79484
+ // membaca Keychain asli lalu memanggil Google dari dalam test.
79485
+ token: () => effectiveStr("HANOMAN_ANTIGRAVITY_LIMITS_FETCH") === "0" ? null : readAgyToken(),
79486
+ project: defaultProject,
79487
+ fetch: (...a) => fetch(...a),
79488
+ now: Date.now
79489
+ };
79490
+ async function fetchQuota(deps) {
79491
+ if (deps.platform !== "darwin") return fallback2();
79492
+ const tok = deps.token();
79493
+ if (!tok) return fallback2();
79494
+ if (tok.expiry !== null && tok.expiry - EXPIRY_SKEW_MS <= deps.now()) return fallback2();
79495
+ const project = deps.project();
79496
+ try {
79497
+ const res = await deps.fetch(QUOTA_URL, {
79498
+ method: "POST",
79499
+ headers: {
79500
+ authorization: `Bearer ${tok.accessToken}`,
79501
+ "content-type": "application/json",
79502
+ "user-agent": USER_AGENT
79503
+ },
79504
+ body: JSON.stringify(project ? { project } : {}),
79505
+ // Endpoint yang menggantung tak boleh menumpuk koneksi di bawah siar 30 dtk (SPEC-197).
79506
+ signal: AbortSignal.timeout(5e3)
79507
+ });
79508
+ if (!res.ok) return fallback2();
79509
+ const windows = mapQuota(await res.json());
79510
+ if (!windows.length) return fallback2();
79511
+ const dto = {
79512
+ status: "ok",
79513
+ windows,
79514
+ fetchedAt: new Date(deps.now()).toISOString()
79515
+ };
79516
+ lastOk2 = dto;
79517
+ return dto;
79518
+ } catch {
79519
+ return fallback2();
79520
+ }
79521
+ }
79522
+ async function getAntigravityLimits(deps = prodDeps) {
79523
+ if (lastOk2 && deps.now() < freshUntil3) return lastOk2;
79524
+ const dto = await fetchQuota(deps);
79525
+ if (dto.status === "ok") freshUntil3 = deps.now() + TTL_MS4;
79526
+ return dto;
79527
+ }
79528
+
79250
79529
  // src/routes/limits.ts
79251
79530
  async function limits(app3) {
79252
79531
  app3.get("/limits", async () => getLimits());
79253
79532
  app3.get("/limits/codex", async () => getCodexLimits());
79533
+ app3.get("/limits/antigravity", async () => getAntigravityLimits());
79254
79534
  }
79255
79535
 
79256
79536
  // src/routes/codex.ts
@@ -79266,7 +79546,7 @@ init_session_sandbox();
79266
79546
  import { execFile as execFile18 } from "node:child_process";
79267
79547
  import { promisify as promisify17 } from "node:util";
79268
79548
  var run4 = promisify17(execFile18);
79269
- var TTL_MS5 = 5 * 6e4;
79549
+ var TTL_MS6 = 5 * 6e4;
79270
79550
  var unavailable = (error) => ({ version: null, error });
79271
79551
  var CHANGED2 = "Konfigurasi runtime berubah; periksa versi kembali.";
79272
79552
  function parseAntigravityVersion(output) {
@@ -79318,7 +79598,7 @@ function createAntigravityVersionService(deps = {}) {
79318
79598
  cache6 = void 0;
79319
79599
  pending2 = void 0;
79320
79600
  }
79321
- if (cache6 && now() - cache6.at < TTL_MS5) return Promise.resolve(cache6.info);
79601
+ if (cache6 && now() - cache6.at < TTL_MS6) return Promise.resolve(cache6.info);
79322
79602
  if (pending2) return pending2;
79323
79603
  const startedGeneration = generation2;
79324
79604
  const request = probe().catch(() => unavailable("Versi Antigravity tidak dapat diperiksa.")).then((info) => {
@@ -79344,7 +79624,7 @@ init_src2();
79344
79624
  init_src();
79345
79625
  import { randomUUID as randomUUID14 } from "node:crypto";
79346
79626
  import { readFile as readFile8, writeFile as writeFile6, rename as rename5, mkdir as mkdir9, rm as rm9 } from "node:fs/promises";
79347
- import { dirname as dirname18, join as join45 } from "node:path";
79627
+ import { dirname as dirname18, join as join46 } from "node:path";
79348
79628
 
79349
79629
  // src/services/model-catalog-parser.ts
79350
79630
  init_zod();
@@ -79451,7 +79731,7 @@ init_session_sandbox();
79451
79731
  import { spawn as spawn5 } from "node:child_process";
79452
79732
  import { mkdtemp as mkdtemp2, rm as rm8 } from "node:fs/promises";
79453
79733
  import { tmpdir as tmpdir9 } from "node:os";
79454
- import { join as join44 } from "node:path";
79734
+ import { join as join45 } from "node:path";
79455
79735
  var MAX_BYTES = 4 * 1024 * 1024;
79456
79736
  var TIMEOUT_MS = 2e4;
79457
79737
  var quote3 = (s2) => "'" + s2.replaceAll("'", "'\\''") + "'";
@@ -79550,7 +79830,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
79550
79830
  }
79551
79831
  async function probeModelCatalog(agent) {
79552
79832
  assertAntigravityLaunch(agent);
79553
- const cwd = await mkdtemp2(join44(tmpdir9(), "hanoman-models-"));
79833
+ const cwd = await mkdtemp2(join45(tmpdir9(), "hanoman-models-"));
79554
79834
  const env = agentEnvironment(agent, process.env);
79555
79835
  for (const key of ["HANOMAN_CLAUDE_BIN", "HANOMAN_CODEX_BIN", "HANOMAN_PODMAN_BIN", "HANOMAN_ANTIGRAVITY_BIN"]) {
79556
79836
  const value = effectiveStr(key);
@@ -79661,7 +79941,7 @@ async function writeModelCatalogCache(file, catalog, current = () => true) {
79661
79941
  await rm9(temp, { force: true });
79662
79942
  }
79663
79943
  }
79664
- var cacheFile = () => join45(resolveHome(), "model-catalog.json");
79944
+ var cacheFile = () => join46(resolveHome(), "model-catalog.json");
79665
79945
  var modelCatalogService = createModelCatalogService({
79666
79946
  probe: probeModelCatalog,
79667
79947
  key: catalogProbeKey,
@@ -81158,6 +81438,9 @@ var GROUPS = [
81158
81438
  // ekor + cache 30s, jadi biayanya jauh di bawah panggilan jaringan claude. Frame lahir hanya saat
81159
81439
  // isinya berubah (dedup signature), dan snapshot codex baru bergerak saat ada sesi codex jalan.
81160
81440
  { everyTicks: 30, last: "", build: async () => ({ t: "codexLimits", limits: await getCodexLimits() }) },
81441
+ // ADR-0224 · limit Antigravity — grup TERPISAH. Panggilan jaringan ber-cache 60 dtk di service, dan
81442
+ // nol panggilan saat token agy kedaluwarsa; dedup signature membuat frame lahir saat isinya berubah.
81443
+ { everyTicks: 30, last: "", build: async () => ({ t: "antigravityLimits", limits: await getAntigravityLimits() }) },
81161
81444
  // SPEC-214 · deteksi update jarang berubah; recompute tiap 300 dtk, dedup signature → siar hanya
81162
81445
  // saat status berubah. getUpdateStatus cache 15s + fetch ter-gate (server.ts) → attach tak menahan.
81163
81446
  { everyTicks: 300, last: "", build: async () => ({ t: "update", update: await getUpdateStatus() }) },
@@ -81585,7 +81868,7 @@ init_src();
81585
81868
  init_db();
81586
81869
  import { gunzipSync, gzipSync } from "node:zlib";
81587
81870
  import { mkdir as mkdir10, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
81588
- import { join as join46 } from "node:path";
81871
+ import { join as join47 } from "node:path";
81589
81872
 
81590
81873
  // src/services/device-auth.ts
81591
81874
  function bearerToken2(req) {
@@ -81841,9 +82124,9 @@ async function sync_default(app3) {
81841
82124
  const deviceId = req.device.id;
81842
82125
  const withTranscriptKey = await Promise.all(parsed.data.entries.map(async (e) => {
81843
82126
  if (!e.transcript) return e;
81844
- const dir2 = join46(resolveDataDirs().home, "remote-transcripts", deviceId);
82127
+ const dir2 = join47(resolveDataDirs().home, "remote-transcripts", deviceId);
81845
82128
  await mkdir10(dir2, { recursive: true, mode: 448 });
81846
- const final = join46(dir2, `${e.seq}.txt`);
82129
+ const final = join47(dir2, `${e.seq}.txt`);
81847
82130
  const tmp = `${final}.tmp`;
81848
82131
  await writeFile7(tmp, e.transcript, { encoding: "utf8", mode: 384 });
81849
82132
  await rename6(tmp, final);
@@ -82238,7 +82521,10 @@ async function session_events_default(app3) {
82238
82521
  event
82239
82522
  });
82240
82523
  if (r.status === "duplicate") return reply.code(202).send({ duplicate: true });
82241
- if (r.status === "rate-limited") return reply.code(429).send({ error: "terlalu banyak event" });
82524
+ if (r.status === "rate-limited") {
82525
+ console.log(`lead ask ${sessionId2}: tanya ${event.askId} ditolak batas laju \u2014 menunggu manusia`);
82526
+ return reply.code(429).send({ error: "terlalu banyak event" });
82527
+ }
82242
82528
  if (r.status === "rejected") return reply.code(202).send({ rejected: true, reason: r.reason });
82243
82529
  return reply.code(202).send({ accepted: true });
82244
82530
  });
@@ -85126,8 +85412,8 @@ init_codex_trust();
85126
85412
  init_qa_attachment_transfer();
85127
85413
  init_uploads();
85128
85414
  import { createHash as createHash21 } from "node:crypto";
85129
- import { execFileSync as execFileSync9 } from "node:child_process";
85130
- import { join as join48 } from "node:path";
85415
+ import { execFileSync as execFileSync10 } from "node:child_process";
85416
+ import { join as join49 } from "node:path";
85131
85417
  var QaSessionError = class extends Error {
85132
85418
  constructor(code, message, needsBind = false) {
85133
85419
  super(message);
@@ -85154,14 +85440,14 @@ async function startQaFindingSession(d, fid) {
85154
85440
  const saved = await saveSessionUpload(id2, bytes, a.mimeType);
85155
85441
  files2.push(`${JSON.stringify(a.filename)} (${a.mimeType}): ${saved.path}`);
85156
85442
  }
85157
- const wt = join48(repoDir, ".worktrees", id2);
85443
+ const wt = join49(repoDir, ".worktrees", id2);
85158
85444
  const branch = `qa/${id2.slice(3)}`;
85159
85445
  const reused = realGit.worktreeAlive(wt);
85160
85446
  try {
85161
85447
  if (!reused) {
85162
85448
  const base3 = realGit.revParse(repoDir, branch) ? branch : "HEAD";
85163
85449
  realGit.addWorktree(repoDir, wt, base3);
85164
- execFileSync9("git", ["checkout", "-B", branch], { cwd: wt, encoding: "utf8" });
85450
+ execFileSync10("git", ["checkout", "-B", branch], { cwd: wt, encoding: "utf8" });
85165
85451
  }
85166
85452
  } catch (e) {
85167
85453
  throw new QaSessionError(422, `Gagal membuat worktree QA: ${e.message}`);
@@ -86695,7 +86981,7 @@ init_src();
86695
86981
  init_db();
86696
86982
  import { mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync5, rmSync as rmSync14, writeFileSync as writeFileSync19 } from "node:fs";
86697
86983
  import { tmpdir as tmpdir10 } from "node:os";
86698
- import { join as join49 } from "node:path";
86984
+ import { join as join50 } from "node:path";
86699
86985
  var STAGE_LABEL = {
86700
86986
  brainstorming: "Dirumuskan",
86701
86987
  objective: "Dirumuskan",
@@ -86738,7 +87024,7 @@ function renderTicketDoc(tickets2) {
86738
87024
  ${baris.join("\n")}`;
86739
87025
  }
86740
87026
  async function buildChatWorkspace(projectId) {
86741
- const dir2 = mkdtempSync5(join49(tmpdir10(), "hanoman-portal-chat-"));
87027
+ const dir2 = mkdtempSync5(join50(tmpdir10(), "hanoman-portal-chat-"));
86742
87028
  try {
86743
87029
  const project = await prisma.project.findUnique({
86744
87030
  where: { id: projectId },
@@ -86753,7 +87039,7 @@ async function buildChatWorkspace(projectId) {
86753
87039
  });
86754
87040
  const files2 = [];
86755
87041
  const tulis = (rel, isi) => {
86756
- writeFileSync19(join49(dir2, rel), isi, { mode: 384 });
87042
+ writeFileSync19(join50(dir2, rel), isi, { mode: 384 });
86757
87043
  files2.push(rel);
86758
87044
  };
86759
87045
  tulis("project.md", renderProjectDoc(project));
@@ -86761,11 +87047,11 @@ async function buildChatWorkspace(projectId) {
86761
87047
  tulis("laporan.md", renderTicketDoc(tickets2.map((t) => toPortalTicket(t, null))));
86762
87048
  const prds = await listPrds(projectId);
86763
87049
  if (prds.length) {
86764
- mkdirSync20(join49(dir2, "dokumen"), { mode: 448 });
87050
+ mkdirSync20(join50(dir2, "dokumen"), { mode: 448 });
86765
87051
  for (const prd of prds) {
86766
87052
  const isi = await readPrd(projectId, prd.path);
86767
87053
  if (!isi) continue;
86768
- tulis(join49("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
87054
+ tulis(join50("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
86769
87055
  }
86770
87056
  }
86771
87057
  return { dir: dir2, files: files2, cleanup: () => rmSync14(dir2, { recursive: true, force: true }) };
@@ -87292,13 +87578,13 @@ init_db();
87292
87578
  // src/services/bootstrap.ts
87293
87579
  import { createHash as createHash22, randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
87294
87580
  import { chmod as chmod2, lstat as lstat3, mkdir as mkdir11, readFile as readFile9, unlink as unlink5, writeFile as writeFile8 } from "node:fs/promises";
87295
- import { join as join50 } from "node:path";
87296
- var TTL_MS6 = 15 * 6e4;
87581
+ import { join as join51 } from "node:path";
87582
+ var TTL_MS7 = 15 * 6e4;
87297
87583
  var SETUP_TOKEN_FILE = "setup.token";
87298
87584
  var BootstrapError = class extends Error {
87299
87585
  code = "BOOTSTRAP_PROOF";
87300
87586
  };
87301
- var tokenPath = (home3) => join50(home3, SETUP_TOKEN_FILE);
87587
+ var tokenPath = (home3) => join51(home3, SETUP_TOKEN_FILE);
87302
87588
  async function readStored(home3) {
87303
87589
  try {
87304
87590
  const info = await lstat3(tokenPath(home3));
@@ -87330,7 +87616,7 @@ async function ensureSetupToken(home3, now = Date.now()) {
87330
87616
  });
87331
87617
  }
87332
87618
  const token = randomBytes11(32).toString("base64url");
87333
- const expiresAt = now + TTL_MS6;
87619
+ const expiresAt = now + TTL_MS7;
87334
87620
  try {
87335
87621
  await writeFile8(tokenPath(home3), `${token}
87336
87622
  ${new Date(expiresAt).toISOString()}
@@ -88136,7 +88422,7 @@ init_event_log();
88136
88422
  init_transcript_store();
88137
88423
  init_src2();
88138
88424
  import { readFile as readFile10 } from "node:fs/promises";
88139
- import { join as join51 } from "node:path";
88425
+ import { join as join52 } from "node:path";
88140
88426
  function decodeCursor(c) {
88141
88427
  if (!c) return null;
88142
88428
  try {
@@ -88174,7 +88460,7 @@ async function readRemoteTranscript(id2) {
88174
88460
  const row = await prisma.logEntry.findUnique({ where: { id: id2 } });
88175
88461
  if (!row || row.lane !== "transcript" || !row.transcriptKey) return null;
88176
88462
  if (row.deviceId === "local") return readTranscript(row.transcriptKey);
88177
- const path = join51(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
88463
+ const path = join52(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
88178
88464
  try {
88179
88465
  return await readFile10(path, "utf8");
88180
88466
  } catch {
@@ -88841,7 +89127,7 @@ async function tick4(now, deps, end2 = prodEnd) {
88841
89127
  console.error("scheduler drain:", e);
88842
89128
  }
88843
89129
  }
88844
- var prodDeps = {
89130
+ var prodDeps2 = {
88845
89131
  liveCount: async () => (await listSessionsAsync()).filter((s2) => !s2.exited).length,
88846
89132
  isLive: async (specId) => {
88847
89133
  const s2 = await getSessionAsync(sessionIdForSpec(specId));
@@ -88875,7 +89161,7 @@ var prodDeps = {
88875
89161
  };
88876
89162
  var TICK_MS3 = 1e4;
88877
89163
  var timer7;
88878
- function startScheduler(deps = prodDeps) {
89164
+ function startScheduler(deps = prodDeps2) {
88879
89165
  if (timer7) return;
88880
89166
  timer7 = setInterval(() => void tick4(Date.now(), deps), TICK_MS3);
88881
89167
  timer7.unref();
@@ -88910,7 +89196,7 @@ init_src();
88910
89196
  init_db();
88911
89197
  init_session_admission2();
88912
89198
  init_notifications2();
88913
- var prodDeps2 = {
89199
+ var prodDeps3 = {
88914
89200
  // ADR-0170 · mengamandemen ADR-0169 keputusan #4: `bypassCapacity` DICABUT. Auto-resume kini
88915
89201
  // tunduk cap/beban host seperti "Lanjutkan" manual — mesin 8 GB operator sudah dua kali kernel
88916
89202
  // panic akibat batch sesi paralel tepat SAAT boot (memori mac-mini-8gb-panic-agen-paralel),
@@ -88919,7 +89205,7 @@ var prodDeps2 = {
88919
89205
  recordFail: recordFailure,
88920
89206
  recordDeferred: recordFailure
88921
89207
  };
88922
- async function resumeReconciledSessions(cutoff, deps = prodDeps2) {
89208
+ async function resumeReconciledSessions(cutoff, deps = prodDeps3) {
88923
89209
  const report = { resumed: [], failed: [], deferred: [] };
88924
89210
  const specIds = await reconciledSpecIdsSince(cutoff);
88925
89211
  if (!specIds.length) return report;
@@ -89441,9 +89727,9 @@ async function pruneLogs(now, retention, opts2 = {}) {
89441
89727
  }
89442
89728
  async function reconcileRemoteTranscripts() {
89443
89729
  const { readdir: readdir10, unlink: unlink6, stat: stat8 } = await import("node:fs/promises");
89444
- const { join: join52 } = await import("node:path");
89730
+ const { join: join53 } = await import("node:path");
89445
89731
  const { resolveDataDirs: resolveDataDirs2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
89446
- const root = join52(resolveDataDirs2().home, "remote-transcripts");
89732
+ const root = join53(resolveDataDirs2().home, "remote-transcripts");
89447
89733
  let deviceDirs;
89448
89734
  try {
89449
89735
  deviceDirs = await readdir10(root);
@@ -89457,16 +89743,16 @@ async function reconcileRemoteTranscripts() {
89457
89743
  for (const dev of deviceDirs) {
89458
89744
  let files2;
89459
89745
  try {
89460
- files2 = await readdir10(join52(root, dev));
89746
+ files2 = await readdir10(join53(root, dev));
89461
89747
  } catch {
89462
89748
  continue;
89463
89749
  }
89464
89750
  for (const f of files2) {
89465
89751
  const key = `${dev}/${f}`;
89466
89752
  if (referenced.has(key)) continue;
89467
- const st = await stat8(join52(root, dev, f)).catch(() => null);
89753
+ const st = await stat8(join53(root, dev, f)).catch(() => null);
89468
89754
  if (!st || Date.now() - st.mtimeMs < gracePeriodMs) continue;
89469
- await unlink6(join52(root, dev, f)).catch(() => {
89755
+ await unlink6(join53(root, dev, f)).catch(() => {
89470
89756
  });
89471
89757
  orphans++;
89472
89758
  }