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.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +35 -2
- package/dist/server.js +589 -303
- package/package.json +1 -1
- package/web/assets/{DalangHanomanScreen-BMrikKnt.js → DalangHanomanScreen-DdsEmtBl.js} +1 -1
- package/web/assets/{DocPreviewModal-5j31WUwI.js → DocPreviewModal-BbOqdfaH.js} +1 -1
- package/web/assets/{DocsWorkspace-GkTrniF3.js → DocsWorkspace-Cr7AXz9z.js} +1 -1
- package/web/assets/{IdeScreen-CAQfv1_e.js → IdeScreen-hdbQW-ui.js} +1 -1
- package/web/assets/{LeadScreen-Da5-1FLe.js → LeadScreen-BWE4mfJI.js} +1 -1
- package/web/assets/{ReviewScreen-B8KHzQpd.js → ReviewScreen-BIkpEspG.js} +1 -1
- package/web/assets/SchedulerScreen-3KVE-d_d.js +1 -0
- package/web/assets/SettingsScreen-B0-W2TeR.js +62 -0
- package/web/assets/{TeamScreen-P6vz88Pp.js → TeamScreen-DNdt2P1g.js} +1 -1
- package/web/assets/{TerminalScreen-B_cagBZd.js → TerminalScreen-BjQF48XI.js} +1 -1
- package/web/assets/{TriageScreen-IYkxyhdm.js → TriageScreen-CzM99CrG.js} +1 -1
- package/web/assets/{VpsScreen-DFGT5S8Y.js → VpsScreen-SGB_BpbS.js} +1 -1
- package/web/assets/fields-of8G961J.js +1 -0
- package/web/assets/{index-0T3s-I82.css → index-C3KwOJxI.css} +1 -1
- package/web/assets/{index-C-iSWHn3.js → index-C5P4ykKO.js} +207 -207
- package/web/assets/{live-lSo-u7M6.js → live-DQen7wLm.js} +1 -1
- package/web/assets/{sched-lead-shared-BTz6aILx.js → sched-lead-shared-C34TvLVv.js} +1 -1
- package/web/assets/{settings-rebase-CPawFVpp.js → settings-rebase-DHIWk4mV.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/SchedulerScreen-CBd4O5kw.js +0 -1
- package/web/assets/SettingsScreen-BxQ3CH2D.js +0 -62
- 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,
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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
|
|
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) =>
|
|
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 <
|
|
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,
|
|
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
|
-
|
|
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
|
|
63444
|
-
import { homedir as
|
|
63445
|
-
import { join as
|
|
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(
|
|
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(
|
|
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 ||
|
|
63577
|
+
home = () => process.env.HOME || homedir13();
|
|
63467
63578
|
readJson2 = (path) => {
|
|
63468
63579
|
try {
|
|
63469
|
-
return JSON.parse(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
76964
|
-
reason: "
|
|
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
|
-
|
|
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 (
|
|
77106
|
-
const text2 =
|
|
77107
|
-
|
|
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
|
|
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
|
|
77142
|
-
if (
|
|
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
|
|
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?.
|
|
77256
|
-
cur.
|
|
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,
|
|
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 (
|
|
77315
|
-
|
|
77316
|
-
|
|
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
|
|
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 <
|
|
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
|
|
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
|
|
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(
|
|
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 = () =>
|
|
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
|
|
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 =
|
|
82127
|
+
const dir2 = join47(resolveDataDirs().home, "remote-transcripts", deviceId);
|
|
81845
82128
|
await mkdir10(dir2, { recursive: true, mode: 448 });
|
|
81846
|
-
const final =
|
|
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")
|
|
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
|
|
85130
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
87296
|
-
var
|
|
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) =>
|
|
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 +
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
89730
|
+
const { join: join53 } = await import("node:path");
|
|
89445
89731
|
const { resolveDataDirs: resolveDataDirs2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
|
|
89446
|
-
const root =
|
|
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(
|
|
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(
|
|
89753
|
+
const st = await stat8(join53(root, dev, f)).catch(() => null);
|
|
89468
89754
|
if (!st || Date.now() - st.mtimeMs < gracePeriodMs) continue;
|
|
89469
|
-
await unlink6(
|
|
89755
|
+
await unlink6(join53(root, dev, f)).catch(() => {
|
|
89470
89756
|
});
|
|
89471
89757
|
orphans++;
|
|
89472
89758
|
}
|