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