hanoman 0.2.2 → 0.2.4

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/server.js CHANGED
@@ -4834,7 +4834,10 @@ var init_entities = __esm({
4834
4834
  //
4835
4835
  // LOKAL per mesin: `setting` TIDAK ada di FIELDS sync (server/src/services/sync.ts), jadi dua
4836
4836
  // mesin dengan versi hanoman berbeda tak bisa saling menimpa definisi bolak-balik.
4837
- builtinAgents: external_exports.record(external_exports.string(), external_exports.string()).default({})
4837
+ builtinAgents: external_exports.record(external_exports.string(), external_exports.string()).default({}),
4838
+ // SPEC-950 · marker safety policy lokal. Terpisah dari fingerprint konten agar policy sekali
4839
+ // jalan tidak membuat seed memperlakukan konfigurasi operator sebagai bawaan.
4840
+ builtinAgentPolicies: external_exports.record(external_exports.string(), external_exports.string()).default({})
4838
4841
  });
4839
4842
  zNotification = external_exports.object({
4840
4843
  id: external_exports.string(),
@@ -4996,6 +4999,15 @@ var init_agent = __esm({
4996
4999
  });
4997
5000
 
4998
5001
  // ../shared/src/custom-agent.ts
5002
+ function validateCreateWorkspacePolicy(value, ctx) {
5003
+ if (value.workspacePolicy === "isolated-worktree" && value.runtime !== "claude") {
5004
+ ctx.addIssue({
5005
+ code: external_exports.ZodIssueCode.custom,
5006
+ path: ["workspacePolicy"],
5007
+ message: "isolated-worktree hanya tersedia untuk Claude Code"
5008
+ });
5009
+ }
5010
+ }
4999
5011
  function mentionsOf(v) {
5000
5012
  if (!Array.isArray(v)) return [];
5001
5013
  const out4 = [];
@@ -5005,6 +5017,21 @@ function mentionsOf(v) {
5005
5017
  function runtimeOf(v) {
5006
5018
  return typeof v === "string" && AGENT_RUNTIMES.includes(v) ? v : null;
5007
5019
  }
5020
+ function activationOf(v) {
5021
+ return typeof v === "string" && AGENT_ACTIVATIONS.includes(v) ? v : "always";
5022
+ }
5023
+ function workspacePolicyOf(v) {
5024
+ return typeof v === "string" && AGENT_WORKSPACE_POLICIES.includes(v) ? v : "inherit";
5025
+ }
5026
+ function effortOf(v) {
5027
+ return typeof v === "string" && AGENT_EFFORTS.includes(v) ? v : null;
5028
+ }
5029
+ function maxTurnsOf(v) {
5030
+ return typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 200 ? v : null;
5031
+ }
5032
+ function timeoutSecondsOf(v) {
5033
+ return typeof v === "number" && Number.isInteger(v) && v >= 30 && v <= 3600 ? v : null;
5034
+ }
5008
5035
  function toolsOf(v) {
5009
5036
  if (!Array.isArray(v)) return null;
5010
5037
  const out4 = [];
@@ -5048,7 +5075,7 @@ function effectiveAgents(globals, project) {
5048
5075
  for (const a of project) byName.set(a.name, a);
5049
5076
  return [...byName.values()].filter((a) => a.enabled).sort((x, y) => x.name.localeCompare(y.name));
5050
5077
  }
5051
- var AGENT_RUNTIMES, zAgentRuntime, AGENT_RUNTIME_LABELS, AGENT_NAME_RE, DEFAULT_AGENT_TOOLS, MENTION_TOOL, MENTION_MAX_HOPS, GLOBAL_SCOPE, zCustomAgent, zCreateCustomAgent, zUpdateCustomAgent, customAgentId;
5078
+ var AGENT_RUNTIMES, zAgentRuntime, AGENT_RUNTIME_LABELS, AGENT_ACTIVATIONS, zAgentActivation, AGENT_EFFORTS, zAgentEffort, AGENT_WORKSPACE_POLICIES, zAgentWorkspacePolicy, AGENT_NAME_RE, DEFAULT_AGENT_TOOLS, MENTION_TOOL, MENTION_MAX_HOPS, GLOBAL_SCOPE, zCustomAgent, zCreateCustomAgentFields, zCreateCustomAgent, zUpdateCustomAgent, customAgentId, AGENT_DISPOSITIONS;
5052
5079
  var init_custom_agent = __esm({
5053
5080
  "../shared/src/custom-agent.ts"() {
5054
5081
  "use strict";
@@ -5059,6 +5086,12 @@ var init_custom_agent = __esm({
5059
5086
  claude: "Claude Code",
5060
5087
  codex: "Codex CLI"
5061
5088
  };
5089
+ AGENT_ACTIVATIONS = ["always", "smart"];
5090
+ zAgentActivation = external_exports.enum(AGENT_ACTIVATIONS);
5091
+ AGENT_EFFORTS = ["ultra", "max", "xhigh", "high", "medium", "low", "ultracode"];
5092
+ zAgentEffort = external_exports.enum(AGENT_EFFORTS);
5093
+ AGENT_WORKSPACE_POLICIES = ["inherit", "read-only", "isolated-worktree"];
5094
+ zAgentWorkspacePolicy = external_exports.enum(AGENT_WORKSPACE_POLICIES);
5062
5095
  AGENT_NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
5063
5096
  DEFAULT_AGENT_TOOLS = [
5064
5097
  "Read",
@@ -5083,11 +5116,16 @@ var init_custom_agent = __esm({
5083
5116
  model: external_exports.string().nullable(),
5084
5117
  mentions: external_exports.array(external_exports.string()).nullable(),
5085
5118
  runtime: external_exports.enum(AGENT_RUNTIMES).nullable(),
5119
+ activation: zAgentActivation.default("always"),
5120
+ effort: zAgentEffort.nullable().default(null),
5121
+ workspacePolicy: zAgentWorkspacePolicy.default("inherit"),
5122
+ maxTurns: external_exports.number().int().min(1).max(200).nullable().default(null),
5123
+ timeoutSeconds: external_exports.number().int().min(30).max(3600).nullable().default(null),
5086
5124
  enabled: external_exports.boolean(),
5087
5125
  createdAt: external_exports.string(),
5088
5126
  updatedAt: external_exports.string()
5089
5127
  });
5090
- zCreateCustomAgent = external_exports.object({
5128
+ zCreateCustomAgentFields = external_exports.object({
5091
5129
  projectId: external_exports.string().nullable().optional(),
5092
5130
  name: external_exports.string().regex(AGENT_NAME_RE),
5093
5131
  description: external_exports.string().trim().min(1).max(500),
@@ -5097,10 +5135,31 @@ var init_custom_agent = __esm({
5097
5135
  mentions: external_exports.array(external_exports.string()).nullable().optional(),
5098
5136
  // SPEC-484 · ADR-0101 · PENYARING mesin sesi, bukan pemilih proses. null/absen = ikut sesi induk.
5099
5137
  runtime: external_exports.enum(AGENT_RUNTIMES).nullable().optional(),
5138
+ activation: zAgentActivation.optional(),
5139
+ effort: zAgentEffort.nullable().optional(),
5140
+ workspacePolicy: zAgentWorkspacePolicy.optional(),
5141
+ maxTurns: external_exports.number().int().min(1).max(200).nullable().optional(),
5142
+ timeoutSeconds: external_exports.number().int().min(30).max(3600).nullable().optional(),
5100
5143
  enabled: external_exports.boolean().optional()
5101
5144
  });
5102
- zUpdateCustomAgent = zCreateCustomAgent.omit({ name: true, projectId: true }).partial();
5145
+ zCreateCustomAgent = zCreateCustomAgentFields.superRefine(validateCreateWorkspacePolicy);
5146
+ zUpdateCustomAgent = zCreateCustomAgentFields.omit({ name: true, projectId: true }).partial().superRefine((value, ctx) => {
5147
+ if (value.workspacePolicy === "isolated-worktree" && value.runtime !== void 0 && value.runtime !== "claude") {
5148
+ ctx.addIssue({
5149
+ code: external_exports.ZodIssueCode.custom,
5150
+ path: ["workspacePolicy"],
5151
+ message: "isolated-worktree hanya tersedia untuk Claude Code"
5152
+ });
5153
+ }
5154
+ });
5103
5155
  customAgentId = (projectId, name2) => `${projectId ?? GLOBAL_SCOPE}:${name2}`;
5156
+ AGENT_DISPOSITIONS = [
5157
+ "pending",
5158
+ "accepted",
5159
+ "partial",
5160
+ "rejected",
5161
+ "false-positive"
5162
+ ];
5104
5163
  }
5105
5164
  });
5106
5165
 
@@ -5115,6 +5174,12 @@ var init_builtin_agents = __esm({
5115
5174
  description: "Gunakan saat perlu tahu DI MANA sesuatu dikerjakan di basis kode, atau bagaimana sebuah alur data mengalir, sebelum menyentuh kode. Ia menyapu banyak berkas dan mengembalikan peta ringkas berisi jangkar path:baris \u2014 bukan isi berkas. Panggil dia alih-alih membaca belasan berkas sendiri.",
5116
5175
  tools: ["Read", "Glob", "Grep"],
5117
5176
  enabledByDefault: true,
5177
+ activation: "smart",
5178
+ effort: "low",
5179
+ workspacePolicy: "read-only",
5180
+ maxTurns: null,
5181
+ timeoutSeconds: null,
5182
+ models: { claude: "haiku", codex: "gpt-5.6-terra" },
5118
5183
  instructions: [
5119
5184
  "Kamu navigator basis kode. Tugasmu MENJAWAB, bukan menyalin.",
5120
5185
  "",
@@ -5141,6 +5206,12 @@ var init_builtin_agents = __esm({
5141
5206
  description: "Gunakan saat ada bug, test merah, atau perilaku tak terduga yang belum jelas sebabnya. Ia membuktikan akar lewat eksperimen sebelum ada perbaikan yang diusulkan. Jangan panggil dia untuk memperbaiki \u2014 dia mendiagnosis.",
5142
5207
  tools: ["Read", "Glob", "Grep", "Bash"],
5143
5208
  enabledByDefault: false,
5209
+ activation: "smart",
5210
+ effort: "high",
5211
+ workspacePolicy: "read-only",
5212
+ maxTurns: null,
5213
+ timeoutSeconds: null,
5214
+ models: { claude: "sonnet", codex: "gpt-5.6" },
5144
5215
  instructions: [
5145
5216
  "Kamu diagnostikus. Kamu TIDAK memperbaiki kode \u2014 kamu membuktikan sebabnya.",
5146
5217
  "",
@@ -5170,7 +5241,13 @@ var init_builtin_agents = __esm({
5170
5241
  name: "qa-verifier",
5171
5242
  description: "Gunakan SEBELUM menyatakan pekerjaan selesai atau test hijau. Ia menjalankan test yang tersentuh perubahan, memisahkan gagal palsu dari regresi, dan membuktikan bahwa test yang lulus itu benar-benar menguji perubahannya.",
5172
5243
  tools: ["Read", "Glob", "Grep", "Bash"],
5173
- enabledByDefault: true,
5244
+ enabledByDefault: false,
5245
+ activation: "smart",
5246
+ effort: "medium",
5247
+ workspacePolicy: "isolated-worktree",
5248
+ maxTurns: 40,
5249
+ timeoutSeconds: 900,
5250
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5174
5251
  instructions: [
5175
5252
  "Kamu gerbang terakhir sebelum sesuatu diumumkan hijau. Tugasmu MERAGUKAN kehijauan itu.",
5176
5253
  "",
@@ -5182,18 +5259,18 @@ var init_builtin_agents = __esm({
5182
5259
  " paralelisme antar-berkas test, sisa proses/soket/port dari run sebelumnya, variabel",
5183
5260
  " lingkungan yang bocor dari shell, dan test yang memang sudah merah SEBELUM perubahan.",
5184
5261
  " Cara memutuskannya: jalankan ulang test itu SENDIRIAN, dengan state yang bersih.",
5185
- "4. UJI RELEVANSI \u2014 langkah yang hampir tak pernah dilakukan siapa pun, dan tanpa ini 'hijau'",
5186
- " tak berarti apa-apa: siapkan pohon kerja terpisah di commit SEBELUM perubahan",
5187
- " (`git worktree add --detach <dir> <base-sha>`), pasang test barunya di sana, jalankan,",
5188
- " dan tuntut test itu MERAH. Test yang tetap hijau tanpa perubahan tidak membuktikan apa",
5189
- " pun tentang perubahan itu.",
5190
- "5. Bersihkan pohon kerja sementara (`git worktree remove`).",
5262
+ "4. UJI RELEVANSI hanya di worktree sementara dari `baseSha`, tidak pernah di worktree",
5263
+ " parent: buat worktree terpisah, pasang patch test di sana, jalankan, dan tuntut test",
5264
+ " itu MERAH. Bila `baseSha` atau patch test tidak tersedia, laporkan `belum terbukti`;",
5265
+ " jangan mencoba eksperimen kontrol di source parent.",
5266
+ "5. Bersihkan worktree sementara milikmu. Laporkan secara eksplisit bila cleanup gagal.",
5191
5267
  "",
5192
5268
  "Larangan keras:",
5193
5269
  "- JANGAN `git stash` untuk apa pun. Tumpukan stash milik REPO, bukan pohon kerja \u2014 sesi lain",
5194
5270
  " bisa mem-pop stash milikmu, dan kamu bisa mem-pop milik mereka. Isolasi memakai",
5195
5271
  " `git worktree add`, titik.",
5196
5272
  "- JANGAN mengubah test agar lulus. Bila test-nya yang salah, itu temuan, bukan pekerjaan.",
5273
+ "- JANGAN mengubah satu byte pun di worktree parent, termasuk berkas probe sementara.",
5197
5274
  "",
5198
5275
  "Gerbang bukti: setiap klaim membawa perintah DAN potongan keluarannya. Tanpa keluaran, tanpa",
5199
5276
  "klaim. 'Semua test lulus' tanpa keluaran adalah kegagalanmu, bukan laporan.",
@@ -5208,6 +5285,12 @@ var init_builtin_agents = __esm({
5208
5285
  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 tiap test baru merah dulu sebelum menyimpannya.",
5209
5286
  tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
5210
5287
  enabledByDefault: false,
5288
+ activation: "smart",
5289
+ effort: "high",
5290
+ workspacePolicy: "isolated-worktree",
5291
+ maxTurns: null,
5292
+ timeoutSeconds: null,
5293
+ models: { claude: "sonnet", codex: "gpt-5.6" },
5211
5294
  instructions: [
5212
5295
  "Kamu penambal jalur bahagia. Cakupan yang terlihat baik bukan urusanmu \u2014 kontrak yang tak",
5213
5296
  "pernah diuji itu urusanmu.",
@@ -5240,6 +5323,12 @@ var init_builtin_agents = __esm({
5240
5323
  description: "Gunakan sesudah perubahan selesai untuk menemukan tempat LAIN yang seharusnya ikut berubah tapi tidak: daftar kolom, cermin tipe antar-paket, enum kembar, dokumen kontrak, tabel konstanta. Ia mencari kegagalan senyap \u2014 yang tak memunculkan satu pun error.",
5241
5324
  tools: ["Read", "Glob", "Grep", "Bash"],
5242
5325
  enabledByDefault: true,
5326
+ activation: "smart",
5327
+ effort: "medium",
5328
+ workspacePolicy: "read-only",
5329
+ maxTurns: null,
5330
+ timeoutSeconds: null,
5331
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5243
5332
  instructions: [
5244
5333
  "Kamu pencari cermin yang hanyut. Kelas bug yang kamu buru punya satu ciri: TIDAK ADA yang",
5245
5334
  "error. Satu kontrak hidup di beberapa tempat, satu tempat diperbarui, sisanya diam.",
@@ -5272,6 +5361,12 @@ var init_builtin_agents = __esm({
5272
5361
  description: "Gunakan sebelum menutup pekerjaan untuk mengadu apa yang DIMINTA dengan apa yang benar-benar ada di diff. Ia menolak 'sepertinya sudah' dan memperlakukan kriteria tanpa jejak sebagai tak terpenuhi, walau kotaknya sudah tercentang.",
5273
5362
  tools: ["Read", "Glob", "Grep", "Bash"],
5274
5363
  enabledByDefault: false,
5364
+ activation: "smart",
5365
+ effort: "high",
5366
+ workspacePolicy: "read-only",
5367
+ maxTurns: null,
5368
+ timeoutSeconds: null,
5369
+ models: { claude: "sonnet", codex: "gpt-5.6-terra" },
5275
5370
  instructions: [
5276
5371
  "Kamu pengadu janji. Kamu tak menilai bagus atau tidaknya kode \u2014 kamu menilai apakah yang",
5277
5372
  "diminta benar-benar ada.",
@@ -5300,6 +5395,12 @@ var init_builtin_agents = __esm({
5300
5395
  description: "Gunakan sebelum menggabungkan perubahan yang menyentuh route, handler, job, CLI, atau apa pun yang menerima input dari luar. Ia menelusuri jalur konkret dari input tak terpercaya sampai ke tempat ia melukai, dan menolak melaporkan kekhawatiran yang tak bisa ia buktikan jalurnya.",
5301
5396
  tools: ["Read", "Glob", "Grep", "Bash"],
5302
5397
  enabledByDefault: true,
5398
+ activation: "smart",
5399
+ effort: "high",
5400
+ workspacePolicy: "read-only",
5401
+ maxTurns: null,
5402
+ timeoutSeconds: null,
5403
+ models: { claude: "sonnet", codex: "gpt-5.6" },
5303
5404
  instructions: [
5304
5405
  "Kamu penelusur sumber-ke-sink. Daftar kekhawatiran umum tak mengubah apa pun; yang mengubah",
5305
5406
  "adalah satu jalur konkret dari input yang tak dipercaya sampai ke tempat ia melukai.",
@@ -5334,6 +5435,12 @@ var init_builtin_agents = __esm({
5334
5435
  description: "Gunakan saat diff menambah atau menaikkan versi dependensi. Ia memeriksa advisory, lisensi, tanda pemeliharaan, dan \u2014 yang paling sering terlewat \u2014 apakah fungsinya sudah tersedia tanpa dependensi baru itu.",
5335
5436
  tools: ["Read", "Glob", "Grep", "Bash", "WebSearch", "WebFetch"],
5336
5437
  enabledByDefault: false,
5438
+ activation: "smart",
5439
+ effort: "medium",
5440
+ workspacePolicy: "read-only",
5441
+ maxTurns: null,
5442
+ timeoutSeconds: null,
5443
+ models: { claude: "haiku", codex: "gpt-5.6-terra" },
5337
5444
  instructions: [
5338
5445
  "Kamu gerbang rantai pasok. Satu dependensi masuk lewat satu baris diff dan tak pernah",
5339
5446
  "diperiksa lagi seumur hidup proyek \u2014 pemeriksaan itu terjadi sekarang atau tidak sama sekali.",
@@ -5372,12 +5479,23 @@ function modelsForRuntime(rt) {
5372
5479
  if (rt === "codex") return codex2;
5373
5480
  return [...claude, ...codex2];
5374
5481
  }
5482
+ function effortsForRuntimeModel(runtime, model) {
5483
+ if (runtime === "claude" || !runtime && model && MODELS.some((entry) => entry.id === model)) {
5484
+ return [...EFFORTS];
5485
+ }
5486
+ const codexModel2 = model ? CODEX_MODELS.find((entry) => entry.id === model) : void 0;
5487
+ if (runtime === "codex" || codexModel2) {
5488
+ return [...codexModel2?.efforts ?? codexCommonEfforts()];
5489
+ }
5490
+ const codex2 = new Set(codexCommonEfforts());
5491
+ return EFFORTS.filter((effort) => codex2.has(effort));
5492
+ }
5375
5493
  function expandTools(tools, catalogIds) {
5376
5494
  if (tools === null) return null;
5377
5495
  if (!tools.includes(ALL_TOOLS)) return tools;
5378
5496
  return catalogIds.filter((id) => id !== ALL_TOOLS);
5379
5497
  }
5380
- var ALL_TOOLS, ALL_TOOLS_ENTRY, BUILTIN_AGENT_TOOLS, mcpToolEntry;
5498
+ var ALL_TOOLS, ALL_TOOLS_ENTRY, BUILTIN_AGENT_TOOLS, mcpToolEntry, codexCommonEfforts;
5381
5499
  var init_agent_catalog = __esm({
5382
5500
  "../shared/src/agent-catalog.ts"() {
5383
5501
  "use strict";
@@ -5399,6 +5517,10 @@ var init_agent_catalog = __esm({
5399
5517
  label: `${server} \u2014 semua tool`,
5400
5518
  group: "mcp"
5401
5519
  });
5520
+ codexCommonEfforts = () => {
5521
+ const first = CODEX_MODELS[0]?.efforts ?? [];
5522
+ return first.filter((effort) => CODEX_MODELS.every((model) => model.efforts.includes(effort)));
5523
+ };
5402
5524
  }
5403
5525
  });
5404
5526
 
@@ -6679,6 +6801,8 @@ var init_api = __esm({
6679
6801
  leadFlowCancel: (id) => `${API}/lead/flows/${encodeURIComponent(id)}/cancel`,
6680
6802
  // SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
6681
6803
  customAgents: `${API}/custom-agents`,
6804
+ customAgentMetrics: `${API}/custom-agents/metrics`,
6805
+ customAgentInvocation: (id) => `${API}/custom-agents/invocations/${encodeURIComponent(id)}`,
6682
6806
  // SPEC-484 · ADR-0101 · sumber daftar tools/model/runtime untuk form (mention dari `customAgents`).
6683
6807
  customAgentCatalog: `${API}/custom-agents/catalog`,
6684
6808
  // SPEC-481 · ADR-0100 · webhook keluar (cookie-only)
@@ -12321,8 +12445,8 @@ var init_git = __esm({
12321
12445
  // di dalam repo induk, yang menjawab "true" untuk pertanyaan pertama. cwd yang tak ada membuat
12322
12446
  // spawnSync gagal (`status` null), dan itu sudah tertangkap `!== 0`.
12323
12447
  worktreeAlive: (path) => {
12324
- const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: path, encoding: "utf8" });
12325
- if (inside.status !== 0 || inside.stdout.trim() !== "true") return false;
12448
+ const inside2 = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: path, encoding: "utf8" });
12449
+ if (inside2.status !== 0 || inside2.stdout.trim() !== "true") return false;
12326
12450
  const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: path, encoding: "utf8" });
12327
12451
  return top.status === 0 && samePath(top.stdout.trim(), path);
12328
12452
  },
@@ -12353,17 +12477,27 @@ var init_git = __esm({
12353
12477
  });
12354
12478
 
12355
12479
  // ../runner/src/settings.ts
12356
- var EVENT_HOOK_COMMAND, guardSettings;
12480
+ var EVENT_SPOOL_SCRIPT, EVENT_HOOK_COMMAND, guardSettings;
12357
12481
  var init_settings2 = __esm({
12358
12482
  "../runner/src/settings.ts"() {
12359
12483
  "use strict";
12484
+ EVENT_SPOOL_SCRIPT = [
12485
+ 'const fs=require("node:fs"),p=require("node:path"),c=require("node:crypto")',
12486
+ 'let d=""',
12487
+ 'process.stdin.setEncoding("utf8")',
12488
+ 'process.stdin.on("data",x=>d+=x)',
12489
+ 'process.stdin.on("end",()=>{let t="";try{const n=Date.now()+"-"+process.pid+"-"+c.randomUUID();t=p.join(process.env.HANOMAN_EVENT_DIR,n+".tmp");const f=p.join(process.env.HANOMAN_EVENT_DIR,n+".json");fs.writeFileSync(t,d,{flag:"wx",mode:0o600});fs.renameSync(t,f)}catch{if(t)try{fs.rmSync(t,{force:true})}catch{}}})'
12490
+ ].join(";");
12360
12491
  EVENT_HOOK_COMMAND = [
12492
+ 'if [ -n "${HANOMAN_EVENT_DIR:-}" ]; then',
12493
+ `node -e '${EVENT_SPOOL_SCRIPT}' >/dev/null 2>&1;`,
12494
+ "else",
12361
12495
  'curl -sS -m 2 -X POST "$HANOMAN_EVENT_URL"',
12362
12496
  "-H 'content-type: application/json'",
12363
12497
  '-H "authorization: Bearer $HANOMAN_EVENT_TOKEN"',
12364
12498
  '-H "x-hanoman-session: $HANOMAN_SESSION_ID"',
12365
12499
  '${HANOMAN_EVENT_HOST:+-H "host: $HANOMAN_EVENT_HOST"}',
12366
- "--data-binary @- >/dev/null 2>&1; exit 0"
12500
+ "--data-binary @- >/dev/null 2>&1; fi; exit 0"
12367
12501
  ].join(" ");
12368
12502
  guardSettings = (decisionFile, goal, eventHook) => {
12369
12503
  const hooks2 = {};
@@ -12380,6 +12514,8 @@ var init_settings2 = __esm({
12380
12514
  matcher: "AskUserQuestion",
12381
12515
  hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }]
12382
12516
  }];
12517
+ hooks2.SubagentStart = [{ hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }] }];
12518
+ hooks2.SubagentStop = [{ hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }] }];
12383
12519
  }
12384
12520
  if (goal) hooks2.Stop = [{ hooks: [{ type: "prompt", prompt: goal }] }];
12385
12521
  return { hooks: hooks2 };
@@ -12458,6 +12594,10 @@ function codexHookArgs(o) {
12458
12594
  const args = [];
12459
12595
  if (stop.length) args.push("-c", `hooks.Stop=${group(stop)}`);
12460
12596
  if (submit.length) args.push("-c", `hooks.UserPromptSubmit=${group(submit)}`);
12597
+ if (o.eventHook) {
12598
+ args.push("-c", `hooks.SubagentStart=${group([EVENT_HOOK_COMMAND])}`);
12599
+ args.push("-c", `hooks.SubagentStop=${group([EVENT_HOOK_COMMAND])}`);
12600
+ }
12461
12601
  return args;
12462
12602
  }
12463
12603
  function codexGoalScript(o) {
@@ -12554,9 +12694,12 @@ var init_agent_cli = __esm({
12554
12694
  // ../runner/src/custom-agents.ts
12555
12695
  function agentPromptOf(def2, roster) {
12556
12696
  const can = liveMentions(def2, roster);
12697
+ const instructions = def2.timeoutSeconds ? `${def2.instructions}
12698
+
12699
+ Batas waktu Hanoman untuk pekerjaan ini ${def2.timeoutSeconds} detik. Prioritaskan putusan dan bukti sebelum batas itu.` : def2.instructions;
12557
12700
  if (can.length === 0) {
12558
12701
  return [
12559
- def2.instructions,
12702
+ instructions,
12560
12703
  "",
12561
12704
  "---",
12562
12705
  "Kamu TIDAK boleh mendelegasikan ke agen lain. Selesaikan sendiri lalu laporkan hasilnya.",
@@ -12566,7 +12709,7 @@ function agentPromptOf(def2, roster) {
12566
12709
  }
12567
12710
  const list2 = can.map((m) => `@${m}`).join(", ");
12568
12711
  return [
12569
- def2.instructions,
12712
+ instructions,
12570
12713
  "",
12571
12714
  "---",
12572
12715
  `Kamu boleh mendelegasikan HANYA ke: ${list2}. Panggil lewat ${MENTION_TOOL} dengan nama agennya.`,
@@ -12576,44 +12719,37 @@ function agentPromptOf(def2, roster) {
12576
12719
  CODE_STYLE_CLAUSE
12577
12720
  ].join("\n");
12578
12721
  }
12579
- function renderAgentsJson(defs) {
12722
+ function renderAgentsJson(defs, options2 = {}) {
12580
12723
  if (defs.length === 0) return "";
12581
12724
  const out4 = {};
12582
12725
  for (const d of defs) {
12726
+ const resolvedTools = resolveTools({ tools: d.tools, mentions: d.mentions });
12727
+ const readOnly = d.workspacePolicy === "read-only";
12583
12728
  out4[d.name] = {
12584
12729
  description: d.description,
12585
12730
  prompt: agentPromptOf(d, defs),
12586
- tools: resolveTools({ tools: d.tools, mentions: d.mentions }),
12587
- ...d.model ? { model: d.model } : {}
12731
+ tools: readOnly ? resolvedTools.filter((tool) => READ_ONLY_TOOLS.has(tool)) : resolvedTools,
12732
+ ...d.model ? { model: d.model } : {},
12733
+ ...d.effort ? { effort: d.effort } : {},
12734
+ ...typeof d.maxTurns === "number" ? { maxTurns: d.maxTurns } : {},
12735
+ ...d.workspacePolicy === "isolated-worktree" ? { isolation: "worktree" } : {},
12736
+ ...readOnly ? { permissionMode: "plan" } : {},
12737
+ ...readOnly && options2.readOnlyHookCommand ? {
12738
+ hooks: {
12739
+ PreToolUse: [{
12740
+ hooks: [{
12741
+ type: "command",
12742
+ command: options2.readOnlyHookCommand,
12743
+ timeout: 5
12744
+ }]
12745
+ }]
12746
+ }
12747
+ } : {}
12588
12748
  };
12589
12749
  }
12590
12750
  return JSON.stringify(out4);
12591
12751
  }
12592
- function agentRosterBlock(defs) {
12593
- if (defs.length === 0) return "";
12594
- const lines2 = [
12595
- "",
12596
- "## Custom agent hanoman",
12597
- "",
12598
- "Peran berikut tersedia untuk sesi ini. Saat sebuah tugas cocok dengan salah satunya, ADOPSI",
12599
- "perannya (baca instruksinya, kerjakan dengan sudut pandang itu) lalu kembali ke peranmu sendiri.",
12600
- "Jangan melahirkan proses agen baru.",
12601
- ""
12602
- ];
12603
- for (const d of defs) {
12604
- const can = liveMentions(d, defs);
12605
- lines2.push(`### @${d.name} \u2014 ${d.description}`);
12606
- lines2.push("");
12607
- lines2.push(d.instructions);
12608
- lines2.push("");
12609
- lines2.push(
12610
- can.length ? `Boleh berkonsultasi ke: ${can.map((m) => `@${m}`).join(", ")} (maks ${MENTION_MAX_HOPS} hop berantai).` : "Tidak boleh berkonsultasi ke peran lain."
12611
- );
12612
- lines2.push("");
12613
- }
12614
- return lines2.join("\n");
12615
- }
12616
- function agentDelegationClause(defs) {
12752
+ function agentDelegationClause(defs, runtime = "claude") {
12617
12753
  if (defs.length === 0) return "";
12618
12754
  return [
12619
12755
  "",
@@ -12625,35 +12761,343 @@ function agentDelegationClause(defs) {
12625
12761
  "",
12626
12762
  ...defs.map((d) => `- **${d.name}** \u2014 ${d.description}`),
12627
12763
  "",
12628
- `Panggil lewat tool ${MENTION_TOOL} dengan nama agennya. Mereka tak bisa mendelegasikan lagi,`,
12764
+ runtime === "codex" ? "Panggil target bernama persis lewat `spawn_agent`." : `Panggil lewat tool ${MENTION_TOOL} dengan nama agennya.`,
12765
+ "Mereka tak bisa mendelegasikan lagi,",
12629
12766
  "jadi tak ada rantai panggilan yang perlu kamu jaga. Laporan mereka adalah MASUKAN \u2014 kamu yang",
12630
12767
  "memutuskan, dan kamu yang bertanggung jawab atas hasilnya.",
12631
12768
  ""
12632
12769
  ].join("\n");
12633
12770
  }
12634
- var liveMentions;
12771
+ var liveMentions, READ_ONLY_TOOLS;
12635
12772
  var init_custom_agents = __esm({
12636
12773
  "../runner/src/custom-agents.ts"() {
12637
12774
  "use strict";
12638
12775
  init_src();
12639
12776
  init_code_style();
12640
12777
  liveMentions = (def2, roster) => {
12778
+ if (def2.workspacePolicy === "read-only") return [];
12641
12779
  const names = new Set(roster.map((r) => r.name));
12642
12780
  return def2.mentions.filter((m) => names.has(m) && m !== def2.name);
12643
12781
  };
12782
+ READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "Bash", "WebFetch", "WebSearch"]);
12783
+ }
12784
+ });
12785
+
12786
+ // ../runner/src/agent-readonly.ts
12787
+ import { chmodSync, writeFileSync } from "node:fs";
12788
+ import { join } from "node:path";
12789
+ function denyReadOnly(detail) {
12790
+ return { allowed: false, reason: `Hanoman read-only policy: ${detail}` };
12791
+ }
12792
+ function tokenizeReadOnlyCommand(command) {
12793
+ const tokens = [];
12794
+ let token = "";
12795
+ let quote2 = "";
12796
+ let escaped = false;
12797
+ let active = false;
12798
+ for (let index = 0; index < command.length; index++) {
12799
+ const char = command[index];
12800
+ if (escaped) {
12801
+ token += char;
12802
+ escaped = false;
12803
+ active = true;
12804
+ continue;
12805
+ }
12806
+ if (quote2) {
12807
+ if (char === quote2) quote2 = "";
12808
+ else if (char === "\\" && quote2 === '"') escaped = true;
12809
+ else token += char;
12810
+ active = true;
12811
+ continue;
12812
+ }
12813
+ if (char === "'" || char === '"') {
12814
+ quote2 = char;
12815
+ active = true;
12816
+ } else if (char === "\\") {
12817
+ escaped = true;
12818
+ active = true;
12819
+ } else if (/\s/.test(char)) {
12820
+ if (active) {
12821
+ tokens.push(token);
12822
+ token = "";
12823
+ active = false;
12824
+ }
12825
+ } else {
12826
+ token += char;
12827
+ active = true;
12828
+ }
12829
+ }
12830
+ if (quote2 || escaped) return null;
12831
+ if (active) tokens.push(token);
12832
+ return tokens;
12833
+ }
12834
+ function evaluateReadOnlyPayload(payload, policy, environment) {
12835
+ if (!payload || typeof payload !== "object") return denyReadOnly("payload hook tidak sah");
12836
+ const event = payload;
12837
+ const tool = typeof event.tool_name === "string" ? event.tool_name : typeof event.toolName === "string" ? event.toolName : "";
12838
+ if (!tool) return denyReadOnly("nama tool tidak tersedia");
12839
+ if (policy.directTools.includes(tool)) return { allowed: true };
12840
+ if (policy.deniedTools.includes(tool) || tool.startsWith("mcp__")) {
12841
+ return denyReadOnly(`tool ${tool} dapat mengubah state`);
12842
+ }
12843
+ if (!policy.shellTools.includes(tool)) return denyReadOnly(`tool ${tool} tidak terbukti read-only`);
12844
+ const rawInput = event.tool_input;
12845
+ const input = rawInput && typeof rawInput === "object" ? rawInput : {};
12846
+ const command = typeof input.command === "string" ? input.command : typeof input.cmd === "string" ? input.cmd : "";
12847
+ const trimmed = command.trim();
12848
+ if (!trimmed) return denyReadOnly("perintah shell kosong atau tidak dikenal");
12849
+ if (/\r|\n|;|&&|\|\||\||[<>]|\$|`/.test(trimmed)) {
12850
+ return denyReadOnly("operator shell yang dapat merangkai atau menulis dilarang");
12851
+ }
12852
+ const tokens = tokenizeReadOnlyCommand(trimmed);
12853
+ if (!tokens?.length) return denyReadOnly("perintah shell tidak dapat diparse dengan aman");
12854
+ const first = tokens[0] ?? "";
12855
+ const commandName = first.split("/").pop() ?? "";
12856
+ if (first !== commandName) {
12857
+ return denyReadOnly("executable ber-path tidak diizinkan; gunakan command allowlist dari PATH");
12858
+ }
12859
+ if (commandName === "git") {
12860
+ const subcommand = tokens[1] ?? "";
12861
+ if (!policy.gitCommands.includes(subcommand)) {
12862
+ return denyReadOnly(`git ${subcommand || "<kosong>"} bukan operasi baca yang diizinkan`);
12863
+ }
12864
+ const args = tokens.slice(2);
12865
+ if (args.some((arg) => arg === "--output" || arg.startsWith("--output=") || arg === "--ext-diff" || arg === "--textconv")) {
12866
+ return denyReadOnly("opsi git dapat menulis atau menjalankan helper eksternal");
12867
+ }
12868
+ if (subcommand !== "status" && (!args.includes("--no-ext-diff") || !args.includes("--no-textconv"))) {
12869
+ return denyReadOnly("git diff/show/log wajib menonaktifkan helper eksternal dan textconv");
12870
+ }
12871
+ return { allowed: true };
12872
+ }
12873
+ if (!policy.shellCommands.includes(commandName)) {
12874
+ return denyReadOnly(`perintah ${commandName || "<kosong>"} tidak terbukti read-only`);
12875
+ }
12876
+ if (commandName === "rg" && environment.RIPGREP_CONFIG_PATH?.trim()) {
12877
+ return denyReadOnly("RIPGREP_CONFIG_PATH dapat menyuntikkan preprocessor eksternal");
12878
+ }
12879
+ 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="))) {
12880
+ return denyReadOnly("opsi rg dapat menjalankan helper eksternal");
12881
+ }
12882
+ if (commandName === "sed") {
12883
+ const quiet = tokens[1] === "-n" || tokens[1] === "--quiet" || tokens[1] === "--silent";
12884
+ const printOnly = /^\d+(?:,\d+)?p$/.test(tokens[2] ?? "");
12885
+ const files = tokens.slice(3);
12886
+ if (!quiet || !printOnly || files.length === 0 || files.some((arg) => arg.startsWith("-"))) {
12887
+ return denyReadOnly("hanya sed -n '<baris>[,<baris>]p' <berkas> yang diizinkan");
12888
+ }
12889
+ }
12890
+ return { allowed: true };
12891
+ }
12892
+ function readOnlyHookSource() {
12893
+ return [
12894
+ '"use strict";',
12895
+ `const denyReadOnly = ${denyReadOnly.toString()};`,
12896
+ `const tokenizeReadOnlyCommand = ${tokenizeReadOnlyCommand.toString()};`,
12897
+ `const evaluate = ${evaluateReadOnlyPayload.toString()};`,
12898
+ `const policy = ${JSON.stringify(POLICY)};`,
12899
+ "let input = '';",
12900
+ "process.stdin.setEncoding('utf8');",
12901
+ "process.stdin.on('data', chunk => { input += chunk; });",
12902
+ "process.stdin.on('end', () => {",
12903
+ " let payload;",
12904
+ " try { payload = JSON.parse(input); } catch { payload = null; }",
12905
+ " const decision = evaluate(payload, policy, process.env);",
12906
+ " if (!decision.allowed) { process.stderr.write(decision.reason + '\\n'); process.exitCode = 2; }",
12907
+ "});",
12908
+ "process.stdin.resume();",
12909
+ ""
12910
+ ].join("\n");
12911
+ }
12912
+ function writeReadOnlyHook(dir2) {
12913
+ const path = join(dir2, "custom-agent-readonly.cjs");
12914
+ writeFileSync(path, readOnlyHookSource(), { mode: 384 });
12915
+ chmodSync(path, 384);
12916
+ return { path, command: `node ${shellQuote(path)}` };
12917
+ }
12918
+ var POLICY, shellQuote;
12919
+ var init_agent_readonly = __esm({
12920
+ "../runner/src/agent-readonly.ts"() {
12921
+ "use strict";
12922
+ POLICY = {
12923
+ directTools: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
12924
+ shellTools: ["Bash", "local_shell", "exec_command"],
12925
+ deniedTools: ["Write", "Edit", "Task", "apply_patch", "spawn_agent"],
12926
+ shellCommands: ["rg", "sed", "head", "tail", "wc", "ls"],
12927
+ gitCommands: ["diff", "show", "status", "log"]
12928
+ };
12929
+ shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
12930
+ }
12931
+ });
12932
+
12933
+ // ../runner/src/runtime-profile.ts
12934
+ function resolveHardening(env) {
12935
+ if (env.HANOMAN_HARDENING === "1") return true;
12936
+ return env.HANOMAN_SESSION_SANDBOX === "podman" || filled(env.HANOMAN_PUBLIC_ORIGINS) || filled(env.HANOMAN_TRUST_PROXY);
12937
+ }
12938
+ function resolveDeployment(env) {
12939
+ if (resolveHardening(env)) return "public";
12940
+ return env.HANOMAN_DEPLOYMENT === "public" ? "public" : "local";
12941
+ }
12942
+ var filled;
12943
+ var init_runtime_profile = __esm({
12944
+ "../runner/src/runtime-profile.ts"() {
12945
+ "use strict";
12946
+ filled = (v) => !!v && v.trim() !== "";
12947
+ }
12948
+ });
12949
+
12950
+ // ../runner/src/codex-agent-config.ts
12951
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync2 } from "node:fs";
12952
+ import { join as join2 } from "node:path";
12953
+ function codexNativeAgentsSupported(version) {
12954
+ const parsed = version ? /(\d+)\.(\d+)\.(\d+)/.exec(version)?.[0] : null;
12955
+ return parsed ? cmpVersion(parsed, CODEX_NATIVE_AGENTS_MIN_CLIENT) >= 0 : false;
12956
+ }
12957
+ function codexNativeVersionProbe(env, codexBin3 = env.HANOMAN_CODEX_BIN ?? "codex") {
12958
+ const sandbox = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
12959
+ if (sandbox !== "podman") return { bin: codexBin3, args: ["--version"] };
12960
+ return {
12961
+ bin: env.HANOMAN_PODMAN_BIN ?? "podman",
12962
+ args: [
12963
+ "run",
12964
+ "--rm",
12965
+ "--read-only",
12966
+ "--cap-drop=ALL",
12967
+ "--userns=keep-id",
12968
+ "--network",
12969
+ "none",
12970
+ env.HANOMAN_SESSION_IMAGE ?? "hanoman-agent:latest",
12971
+ "/bin/sh",
12972
+ "-lc",
12973
+ `${shellQuote2(codexBin3)} --version`
12974
+ ]
12975
+ };
12976
+ }
12977
+ function renderCodexAgentToml(def2, roster, options2 = {}) {
12978
+ const lines2 = [
12979
+ `name = ${tomlString(def2.name)}`,
12980
+ `description = ${tomlString(def2.description)}`,
12981
+ `developer_instructions = ${tomlString(agentPromptOf(def2, roster))}`,
12982
+ ...def2.model ? [`model = ${tomlString(def2.model)}`] : [],
12983
+ ...def2.effort ? [`model_reasoning_effort = ${tomlString(def2.effort)}`] : [],
12984
+ ...def2.workspacePolicy === "read-only" ? ['sandbox_mode = "read-only"'] : []
12985
+ ];
12986
+ if (def2.workspacePolicy === "read-only" && options2.readOnlyHookCommand) {
12987
+ lines2.push(
12988
+ "",
12989
+ "[[hooks.PreToolUse]]",
12990
+ "",
12991
+ "[[hooks.PreToolUse.hooks]]",
12992
+ 'type = "command"',
12993
+ `command = ${tomlString(options2.readOnlyHookCommand)}`,
12994
+ "timeout = 5"
12995
+ );
12996
+ }
12997
+ return `${lines2.join("\n")}
12998
+ `;
12999
+ }
13000
+ function materializeCodexAgents(defs, tempDir, options2 = {}) {
13001
+ if (defs.length === 0) {
13002
+ return { args: [], delegationClause: "", configPaths: [], warnings: [], liveDefs: [] };
13003
+ }
13004
+ if ("clientVersion" in options2 && !codexNativeAgentsSupported(options2.clientVersion ?? null)) {
13005
+ const seen2 = options2.clientVersion ?? "tak terdeteksi";
13006
+ return {
13007
+ args: [],
13008
+ delegationClause: "",
13009
+ configPaths: [],
13010
+ liveDefs: [],
13011
+ warnings: defs.map((def2) => ({
13012
+ agentName: def2.name,
13013
+ reason: `Codex ${seen2} tidak mendukung custom agent native; butuh >= ${CODEX_NATIVE_AGENTS_MIN_CLIENT}`
13014
+ }))
13015
+ };
13016
+ }
13017
+ const write2 = options2.writeFile ?? ((path, content) => writeFileSync2(path, content, { mode: 384 }));
13018
+ const chmod3 = options2.chmod ?? chmodSync2;
13019
+ const successful = [];
13020
+ const warnings = [];
13021
+ for (const [index, def2] of defs.entries()) {
13022
+ if (def2.workspacePolicy === "isolated-worktree") {
13023
+ warnings.push({
13024
+ agentName: def2.name,
13025
+ reason: "isolated-worktree belum tersedia untuk subagent Codex"
13026
+ });
13027
+ continue;
13028
+ }
13029
+ const path = join2(tempDir, `${String(index).padStart(2, "0")}-${safeFilename(def2.name)}.toml`);
13030
+ try {
13031
+ write2(path, renderCodexAgentToml(def2, defs, options2));
13032
+ chmod3(path, 384);
13033
+ successful.push({ def: def2, path });
13034
+ } catch (error) {
13035
+ warnings.push({
13036
+ agentName: def2.name,
13037
+ reason: error instanceof Error ? error.message : String(error)
13038
+ });
13039
+ }
13040
+ }
13041
+ if (successful.length === 0) {
13042
+ return { args: [], delegationClause: "", configPaths: [], warnings, liveDefs: [] };
13043
+ }
13044
+ const args = [
13045
+ "-c",
13046
+ "agents.enabled=true",
13047
+ "-c",
13048
+ "agents.max_concurrent_threads_per_session=3"
13049
+ ];
13050
+ for (const { def: def2, path } of successful) {
13051
+ const key = `agents.${tomlKey(def2.name)}`;
13052
+ args.push("-c", `${key}.description=${tomlString(def2.description)}`);
13053
+ args.push("-c", `${key}.config_file=${tomlString(path)}`);
13054
+ }
13055
+ const liveDefs = successful.map((entry) => entry.def);
13056
+ return {
13057
+ args,
13058
+ delegationClause: agentDelegationClause(liveDefs, "codex"),
13059
+ configPaths: successful.map((entry) => entry.path),
13060
+ warnings,
13061
+ liveDefs
13062
+ };
13063
+ }
13064
+ var CODEX_NATIVE_AGENTS_MIN_CLIENT, shellQuote2, tomlString, tomlKey, safeFilename;
13065
+ var init_codex_agent_config = __esm({
13066
+ "../runner/src/codex-agent-config.ts"() {
13067
+ "use strict";
13068
+ init_src();
13069
+ init_custom_agents();
13070
+ init_runtime_profile();
13071
+ CODEX_NATIVE_AGENTS_MIN_CLIENT = "0.151.0";
13072
+ shellQuote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`;
13073
+ tomlString = (value) => JSON.stringify(value);
13074
+ tomlKey = (value) => JSON.stringify(value);
13075
+ safeFilename = (name2) => name2.replace(/[^a-z0-9-]/gi, "-");
13076
+ }
13077
+ });
13078
+
13079
+ // ../runner/src/custom-agent-eval.ts
13080
+ var init_custom_agent_eval = __esm({
13081
+ "../runner/src/custom-agent-eval.ts"() {
13082
+ "use strict";
13083
+ init_src();
13084
+ init_codex_agent_config();
13085
+ init_codex_settings();
13086
+ init_custom_agents();
13087
+ init_settings2();
12644
13088
  }
12645
13089
  });
12646
13090
 
12647
13091
  // ../runner/src/paths.ts
12648
13092
  import { homedir } from "node:os";
12649
- import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve5 } from "node:path";
13093
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve as resolve5 } from "node:path";
12650
13094
  function resolveHome(env = process.env, home3 = homedir()) {
12651
13095
  const v = env.HANOMAN_HOME?.trim();
12652
- return v ? v : join(home3, ".hanoman");
13096
+ return v ? v : join3(home3, ".hanoman");
12653
13097
  }
12654
13098
  function resolveDataDirs(env = process.env, home3 = homedir()) {
12655
13099
  const root = resolveHome(env, home3);
12656
- const dir2 = (key, name2) => env[key]?.trim() || join(root, name2);
13100
+ const dir2 = (key, name2) => env[key]?.trim() || join3(root, name2);
12657
13101
  return {
12658
13102
  home: root,
12659
13103
  transcripts: dir2("HANOMAN_TRANSCRIPT_DIR", "transcripts"),
@@ -12675,7 +13119,7 @@ function resolveDbUrl(env, schemaDir2) {
12675
13119
  return absoluteFileUrl(own, schemaDir2);
12676
13120
  }
12677
13121
  const raw = env.DATABASE_URL?.trim();
12678
- if (!raw || !raw.startsWith("file:")) return `file:${join(resolveHome(env), "hanoman.db")}`;
13122
+ if (!raw || !raw.startsWith("file:")) return `file:${join3(resolveHome(env), "hanoman.db")}`;
12679
13123
  return absoluteFileUrl(raw, schemaDir2);
12680
13124
  }
12681
13125
  function absoluteFileUrl(raw, schemaDir2) {
@@ -12703,7 +13147,7 @@ var init_paths = __esm({
12703
13147
 
12704
13148
  // ../runner/src/skills.ts
12705
13149
  import { homedir as homedir2 } from "node:os";
12706
- import { basename as basename2, dirname as dirname3, join as join2, resolve as resolve6 } from "node:path";
13150
+ import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve6 } from "node:path";
12707
13151
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "node:fs";
12708
13152
  function agentSkillHome(agent, env = process.env, osHome = homedir2()) {
12709
13153
  const own = (agent === "codex" ? env.HANOMAN_CODEX_HOME : env.HANOMAN_CLAUDE_HOME)?.trim();
@@ -12712,10 +13156,10 @@ function agentSkillHome(agent, env = process.env, osHome = homedir2()) {
12712
13156
  const cx = env.CODEX_HOME?.trim();
12713
13157
  if (cx) return cx;
12714
13158
  }
12715
- return join2(osHome, agent === "codex" ? ".codex" : ".claude");
13159
+ return join4(osHome, agent === "codex" ? ".codex" : ".claude");
12716
13160
  }
12717
13161
  function agentsSkillHome(env = process.env, osHome = homedir2()) {
12718
- return env.HANOMAN_AGENTS_HOME?.trim() || join2(osHome, ".agents");
13162
+ return env.HANOMAN_AGENTS_HOME?.trim() || join4(osHome, ".agents");
12719
13163
  }
12720
13164
  function dirsIn(dir2) {
12721
13165
  try {
@@ -12732,7 +13176,7 @@ function readJson(file) {
12732
13176
  }
12733
13177
  }
12734
13178
  function isSkillDir(dir2) {
12735
- return existsSync3(join2(dir2, "SKILL.md"));
13179
+ return existsSync3(join4(dir2, "SKILL.md"));
12736
13180
  }
12737
13181
  function asSkill(dir2, pkg, name2 = basename2(dir2)) {
12738
13182
  return { id: pkg ? `${pkg}:${name2}` : name2, name: name2, pkg, dir: dir2 };
@@ -12740,7 +13184,7 @@ function asSkill(dir2, pkg, name2 = basename2(dir2)) {
12740
13184
  function skillsUnder(dir2, pkg, depth = 2) {
12741
13185
  const out4 = [];
12742
13186
  for (const name2 of dirsIn(dir2)) {
12743
- const sub = join2(dir2, name2);
13187
+ const sub = join4(dir2, name2);
12744
13188
  if (isSkillDir(sub)) out4.push(asSkill(sub, pkg, name2));
12745
13189
  else if (depth > 1) out4.push(...skillsUnder(sub, pkg, depth - 1));
12746
13190
  }
@@ -12748,7 +13192,7 @@ function skillsUnder(dir2, pkg, depth = 2) {
12748
13192
  }
12749
13193
  function manifestSkills(installPath, pkg) {
12750
13194
  for (const marker of [".claude-plugin", ".codex-plugin"]) {
12751
- const j = readJson(join2(installPath, marker, "plugin.json"));
13195
+ const j = readJson(join4(installPath, marker, "plugin.json"));
12752
13196
  const declared = j?.skills;
12753
13197
  if (!Array.isArray(declared)) continue;
12754
13198
  const out4 = [];
@@ -12764,11 +13208,11 @@ function manifestSkills(installPath, pkg) {
12764
13208
  }
12765
13209
  function pluginSkills(installPath, pkg) {
12766
13210
  const declared = manifestSkills(installPath, pkg);
12767
- return declared.length ? declared : skillsUnder(join2(installPath, "skills"), pkg);
13211
+ return declared.length ? declared : skillsUnder(join4(installPath, "skills"), pkg);
12768
13212
  }
12769
13213
  function lockPluginNames(agentsHome) {
12770
13214
  const out4 = /* @__PURE__ */ new Map();
12771
- const j = readJson(join2(agentsHome, ".skill-lock.json"));
13215
+ const j = readJson(join4(agentsHome, ".skill-lock.json"));
12772
13216
  const skills = j?.skills;
12773
13217
  if (!skills || typeof skills !== "object") return out4;
12774
13218
  for (const [name2, entry] of Object.entries(skills)) {
@@ -12782,7 +13226,7 @@ function splitPluginKey(key) {
12782
13226
  return at > 0 ? { pkg: key.slice(0, at), marketplace: key.slice(at + 1) } : { pkg: key, marketplace: "" };
12783
13227
  }
12784
13228
  function manifestRoots(home3) {
12785
- const j = readJson(join2(home3, "plugins", "installed_plugins.json"));
13229
+ const j = readJson(join4(home3, "plugins", "installed_plugins.json"));
12786
13230
  const plugins = j?.plugins;
12787
13231
  if (!plugins || typeof plugins !== "object") return [];
12788
13232
  const out4 = [];
@@ -12796,18 +13240,18 @@ function manifestRoots(home3) {
12796
13240
  return out4;
12797
13241
  }
12798
13242
  function cacheRoots(home3) {
12799
- const cache6 = join2(home3, "plugins", "cache");
13243
+ const cache6 = join4(home3, "plugins", "cache");
12800
13244
  const out4 = [];
12801
13245
  for (const marketplace of dirsIn(cache6))
12802
- for (const pkg of dirsIn(join2(cache6, marketplace)))
12803
- for (const version of dirsIn(join2(cache6, marketplace, pkg)))
12804
- out4.push({ pkg, marketplace, dir: join2(cache6, marketplace, pkg, version) });
13246
+ for (const pkg of dirsIn(join4(cache6, marketplace)))
13247
+ for (const version of dirsIn(join4(cache6, marketplace, pkg)))
13248
+ out4.push({ pkg, marketplace, dir: join4(cache6, marketplace, pkg, version) });
12805
13249
  return out4;
12806
13250
  }
12807
13251
  function disabledPlugins(agent, home3) {
12808
13252
  const out4 = /* @__PURE__ */ new Set();
12809
13253
  if (agent === "claude") {
12810
- const en = readJson(join2(home3, "settings.json"))?.enabledPlugins;
13254
+ const en = readJson(join4(home3, "settings.json"))?.enabledPlugins;
12811
13255
  if (en && typeof en === "object") {
12812
13256
  for (const [k, v] of Object.entries(en)) if (v === false) out4.add(k);
12813
13257
  }
@@ -12815,7 +13259,7 @@ function disabledPlugins(agent, home3) {
12815
13259
  }
12816
13260
  let toml;
12817
13261
  try {
12818
- toml = readFileSync2(join2(home3, "config.toml"), "utf8");
13262
+ toml = readFileSync2(join4(home3, "config.toml"), "utf8");
12819
13263
  } catch {
12820
13264
  return out4;
12821
13265
  }
@@ -12851,18 +13295,18 @@ function scanAgentSkills(agent, env = process.env, osHome = homedir2()) {
12851
13295
  skills.push(s2);
12852
13296
  }
12853
13297
  };
12854
- const userDir = join2(home3, "skills");
13298
+ const userDir = join4(home3, "skills");
12855
13299
  add(userDir, skillsUnder(userDir, null));
12856
13300
  for (const r of [...manifestRoots(home3), ...cacheRoots(home3)]) {
12857
13301
  if (disabled.has(`${r.pkg}@${r.marketplace}`)) continue;
12858
13302
  const found = pluginSkills(r.dir, r.pkg);
12859
13303
  if (!found.length) continue;
12860
13304
  packages.add(r.pkg);
12861
- add(join2(r.dir, "skills"), found);
13305
+ add(join4(r.dir, "skills"), found);
12862
13306
  }
12863
13307
  if (agent === "codex") {
12864
13308
  const agentsHome = agentsSkillHome(env, osHome);
12865
- const dir2 = join2(agentsHome, "skills");
13309
+ const dir2 = join4(agentsHome, "skills");
12866
13310
  const names = lockPluginNames(agentsHome);
12867
13311
  const flat = skillsUnder(dir2, null);
12868
13312
  const withPkg = flat.flatMap((s2) => {
@@ -12953,12 +13397,12 @@ var init_telegram_operator = __esm({
12953
13397
  });
12954
13398
 
12955
13399
  // ../runner/src/spawn-helper.ts
12956
- import { readdirSync as readdirSync2, existsSync as existsSync4, statSync, chmodSync } from "node:fs";
12957
- import { dirname as dirname4, join as join3 } from "node:path";
13400
+ import { readdirSync as readdirSync2, existsSync as existsSync4, statSync, chmodSync as chmodSync3 } from "node:fs";
13401
+ import { dirname as dirname4, join as join5 } from "node:path";
12958
13402
  function spawnHelperPaths(ptyDir, listDir, exists) {
12959
- const dirs = [join3(ptyDir, "build", "Release")];
12960
- for (const name2 of listDir(join3(ptyDir, "prebuilds"))) dirs.push(join3(ptyDir, "prebuilds", name2));
12961
- return dirs.map((d) => join3(d, "spawn-helper")).filter(exists);
13403
+ const dirs = [join5(ptyDir, "build", "Release")];
13404
+ for (const name2 of listDir(join5(ptyDir, "prebuilds"))) dirs.push(join5(ptyDir, "prebuilds", name2));
13405
+ return dirs.map((d) => join5(d, "spawn-helper")).filter(exists);
12962
13406
  }
12963
13407
  function ensureSpawnHelpersExecutable(paths2, ops) {
12964
13408
  const fixed = [];
@@ -12973,10 +13417,10 @@ function ensureSpawnHelpersExecutable(paths2, ops) {
12973
13417
  }
12974
13418
  return fixed;
12975
13419
  }
12976
- function repairSpawnHelper(resolve21, notify) {
13420
+ function repairSpawnHelper(resolve22, notify) {
12977
13421
  let ptyDir;
12978
13422
  try {
12979
- ptyDir = dirname4(resolve21("node-pty/package.json"));
13423
+ ptyDir = dirname4(resolve22("node-pty/package.json"));
12980
13424
  } catch {
12981
13425
  return [];
12982
13426
  }
@@ -12989,15 +13433,15 @@ function repairSpawnHelper(resolve21, notify) {
12989
13433
  }, existsSync4);
12990
13434
  const fixed = ensureSpawnHelpersExecutable(paths2, {
12991
13435
  mode: (p3) => statSync(p3).mode,
12992
- chmod: (p3, m) => chmodSync(p3, m)
13436
+ chmod: (p3, m) => chmodSync3(p3, m)
12993
13437
  });
12994
13438
  if (fixed.length) notify?.("hanoman \xB7 memperbaiki izin `spawn-helper` node-pty (sekali per instalasi)\n");
12995
13439
  return fixed;
12996
13440
  }
12997
- function ensureSpawnHelperOnce(resolve21, notify) {
13441
+ function ensureSpawnHelperOnce(resolve22, notify) {
12998
13442
  if (repaired) return [];
12999
13443
  repaired = true;
13000
- return repairSpawnHelper(resolve21, notify);
13444
+ return repairSpawnHelper(resolve22, notify);
13001
13445
  }
13002
13446
  var repaired;
13003
13447
  var init_spawn_helper = __esm({
@@ -13008,10 +13452,10 @@ var init_spawn_helper = __esm({
13008
13452
  });
13009
13453
 
13010
13454
  // ../runner/src/config-env.ts
13011
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
13012
- import { join as join4 } from "node:path";
13455
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
13456
+ import { join as join6 } from "node:path";
13013
13457
  function configEnvPath(home3) {
13014
- return join4(home3, CONFIG_ENV_FILE);
13458
+ return join6(home3, CONFIG_ENV_FILE);
13015
13459
  }
13016
13460
  function parseConfigEnv(text) {
13017
13461
  const out4 = {};
@@ -13039,8 +13483,8 @@ function readConfigEnv(home3) {
13039
13483
  function writeConfigEnv(home3, values) {
13040
13484
  mkdirSync2(home3, { recursive: true, mode: 448 });
13041
13485
  const path = configEnvPath(home3);
13042
- writeFileSync(path, formatConfigEnv(values), { mode: 384 });
13043
- chmodSync2(path, 384);
13486
+ writeFileSync3(path, formatConfigEnv(values), { mode: 384 });
13487
+ chmodSync4(path, 384);
13044
13488
  }
13045
13489
  var CONFIG_ENV_FILE;
13046
13490
  var init_config_env = __esm({
@@ -13050,23 +13494,6 @@ var init_config_env = __esm({
13050
13494
  }
13051
13495
  });
13052
13496
 
13053
- // ../runner/src/runtime-profile.ts
13054
- function resolveHardening(env) {
13055
- if (env.HANOMAN_HARDENING === "1") return true;
13056
- return env.HANOMAN_SESSION_SANDBOX === "podman" || filled(env.HANOMAN_PUBLIC_ORIGINS) || filled(env.HANOMAN_TRUST_PROXY);
13057
- }
13058
- function resolveDeployment(env) {
13059
- if (resolveHardening(env)) return "public";
13060
- return env.HANOMAN_DEPLOYMENT === "public" ? "public" : "local";
13061
- }
13062
- var filled;
13063
- var init_runtime_profile = __esm({
13064
- "../runner/src/runtime-profile.ts"() {
13065
- "use strict";
13066
- filled = (v) => !!v && v.trim() !== "";
13067
- }
13068
- });
13069
-
13070
13497
  // ../runner/src/sandbox-probe.ts
13071
13498
  import { execFileSync } from "node:child_process";
13072
13499
  import { accessSync, constants } from "node:fs";
@@ -13179,6 +13606,9 @@ var init_src2 = __esm({
13179
13606
  init_codex_settings();
13180
13607
  init_agent_cli();
13181
13608
  init_custom_agents();
13609
+ init_agent_readonly();
13610
+ init_codex_agent_config();
13611
+ init_custom_agent_eval();
13182
13612
  init_verify_scope();
13183
13613
  init_code_style();
13184
13614
  init_paths();
@@ -13465,9 +13895,9 @@ var init_session_id = __esm({
13465
13895
  });
13466
13896
 
13467
13897
  // src/services/secret-box.ts
13468
- import { chmodSync as chmodSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
13898
+ import { chmodSync as chmodSync5, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
13469
13899
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
13470
- import { dirname as dirname6, join as join5 } from "node:path";
13900
+ import { dirname as dirname6, join as join7 } from "node:path";
13471
13901
  function isEncrypted(value) {
13472
13902
  return value.startsWith(ENC_PREFIX);
13473
13903
  }
@@ -13499,7 +13929,7 @@ function fromEnv(raw) {
13499
13929
  return null;
13500
13930
  }
13501
13931
  function secretKeyPath() {
13502
- return join5(resolveHome(), "secret.key");
13932
+ return join7(resolveHome(), "secret.key");
13503
13933
  }
13504
13934
  function secretKey() {
13505
13935
  if (cached) return cached;
@@ -13519,8 +13949,8 @@ function secretKey() {
13519
13949
  }
13520
13950
  const key = randomBytes(KEY_BYTES);
13521
13951
  mkdirSync4(dirname6(path), { recursive: true });
13522
- writeFileSync2(path, key.toString("base64url"), { mode: 384 });
13523
- chmodSync3(path, 384);
13952
+ writeFileSync4(path, key.toString("base64url"), { mode: 384 });
13953
+ chmodSync5(path, 384);
13524
13954
  cached = key;
13525
13955
  return key;
13526
13956
  }
@@ -13713,7 +14143,7 @@ import { request as httpRequest } from "node:http";
13713
14143
  import { request as httpsRequest } from "node:https";
13714
14144
  import { createGunzip } from "node:zlib";
13715
14145
  async function pinnedRequest(input) {
13716
- return new Promise((resolve21, reject2) => {
14146
+ return new Promise((resolve22, reject2) => {
13717
14147
  const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
13718
14148
  const req = transport({
13719
14149
  protocol: input.url.protocol,
@@ -13735,7 +14165,7 @@ async function pinnedRequest(input) {
13735
14165
  const capTerurai = input.maxDecodedBytes ?? input.maxResponseBytes;
13736
14166
  const chunks = [];
13737
14167
  let kabel = 0, terurai = 0;
13738
- const selesai = () => resolve21({
14168
+ const selesai = () => resolve22({
13739
14169
  status: response.statusCode ?? 0,
13740
14170
  headers: response.headers,
13741
14171
  body: Buffer.concat(chunks)
@@ -13801,7 +14231,7 @@ var init_safe_outbound_request = __esm({
13801
14231
  // src/services/uploads.ts
13802
14232
  import { randomUUID } from "node:crypto";
13803
14233
  import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
13804
- import { join as join6, resolve as resolve8 } from "node:path";
14234
+ import { join as join8, resolve as resolve8 } from "node:path";
13805
14235
  function extFor(mimeType) {
13806
14236
  return EXT[mimeType] ?? ".bin";
13807
14237
  }
@@ -13810,12 +14240,12 @@ function uploadDir() {
13810
14240
  }
13811
14241
  function sessionUploadDir(sessionId2) {
13812
14242
  if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
13813
- return join6(uploadDir(), "terminal", sessionId2);
14243
+ return join8(uploadDir(), "terminal", sessionId2);
13814
14244
  }
13815
14245
  async function saveSessionUpload(sessionId2, buf, mimeType) {
13816
14246
  const dir2 = sessionUploadDir(sessionId2);
13817
14247
  await mkdir(dir2, { recursive: true, mode: 448 });
13818
- const path = join6(dir2, `${randomUUID()}${extFor(mimeType)}`);
14248
+ const path = join8(dir2, `${randomUUID()}${extFor(mimeType)}`);
13819
14249
  await writeFile(path, buf, { mode: 384 });
13820
14250
  return { path, size: buf.length };
13821
14251
  }
@@ -13831,11 +14261,11 @@ async function dropSessionUploads(sessionId2) {
13831
14261
  }
13832
14262
  async function readUpload(storageKey) {
13833
14263
  const safe = storageKey.replace(/[/\\]/g, "");
13834
- return readFile2(join6(uploadDir(), safe));
14264
+ return readFile2(join8(uploadDir(), safe));
13835
14265
  }
13836
14266
  async function readUploadOrFetch(storageKey) {
13837
14267
  const safe = storageKey.replace(/[/\\]/g, "");
13838
- const target2 = join6(uploadDir(), safe);
14268
+ const target2 = join8(uploadDir(), safe);
13839
14269
  try {
13840
14270
  return await readFile2(target2);
13841
14271
  } catch {
@@ -13862,7 +14292,7 @@ async function readUploadOrFetch(storageKey) {
13862
14292
  }
13863
14293
  async function deleteUpload(storageKey) {
13864
14294
  const safe = storageKey.replace(/[/\\]/g, "");
13865
- await unlink(join6(uploadDir(), safe)).catch(() => {
14295
+ await unlink(join8(uploadDir(), safe)).catch(() => {
13866
14296
  });
13867
14297
  }
13868
14298
  var EXT, SESSION_ID;
@@ -14211,11 +14641,15 @@ function sandboxArgv(input) {
14211
14641
  const mounts = ["--volume", `${input.worktree}:/workspace:${input.worktreeMode ?? "rw"}`];
14212
14642
  if (input.phaseFile) mounts.push("--volume", `${input.phaseFile}:${input.phaseFile}:rw`);
14213
14643
  if (input.promptFile) mounts.push("--volume", `${input.promptFile}:${input.promptFile}:ro`);
14644
+ if (input.agentConfigDir)
14645
+ mounts.push("--volume", `${input.agentConfigDir}:${input.agentConfigDir}:ro`);
14646
+ if (input.eventDir)
14647
+ mounts.push("--volume", `${input.eventDir}:${input.eventDir}:rw`);
14214
14648
  if (input.attachmentsDir)
14215
14649
  mounts.push("--volume", `${input.attachmentsDir}:${input.attachmentsDir}:ro`);
14216
14650
  mounts.push("--volume", `${input.credentialDir}:/agent-home:ro`);
14217
14651
  return [
14218
- "podman",
14652
+ input.podmanBin ?? "podman",
14219
14653
  "run",
14220
14654
  "--rm",
14221
14655
  "--read-only",
@@ -14243,6 +14677,7 @@ function sandboxArgv(input) {
14243
14677
  "NO_PROXY=localhost,127.0.0.1,::1",
14244
14678
  "--env",
14245
14679
  "HOME=/agent-home",
14680
+ ...input.eventDir ? ["--env", `HANOMAN_EVENT_DIR=${input.eventDir}`] : [],
14246
14681
  ...mounts,
14247
14682
  input.image,
14248
14683
  "/bin/sh",
@@ -14262,6 +14697,7 @@ function sandboxArgvFromEnv(input) {
14262
14697
  ...input,
14263
14698
  credentialDir,
14264
14699
  proxy,
14700
+ podmanBin: env.HANOMAN_PODMAN_BIN ?? "podman",
14265
14701
  image: env.HANOMAN_SESSION_IMAGE ?? "hanoman-agent:latest",
14266
14702
  network: env.HANOMAN_SESSION_NETWORK ?? "hanoman-egress"
14267
14703
  });
@@ -14280,18 +14716,30 @@ var init_session_sandbox = __esm({
14280
14716
  }
14281
14717
  });
14282
14718
 
14719
+ // src/services/session-event-spool.ts
14720
+ import { tmpdir } from "node:os";
14721
+ import { join as join9 } from "node:path";
14722
+ var sessionEventSpoolRoot, sessionEventDir;
14723
+ var init_session_event_spool = __esm({
14724
+ "src/services/session-event-spool.ts"() {
14725
+ "use strict";
14726
+ sessionEventSpoolRoot = () => join9(tmpdir(), "hanoman-session-events");
14727
+ sessionEventDir = (sessionId2) => join9(sessionEventSpoolRoot(), sessionId2);
14728
+ }
14729
+ });
14730
+
14283
14731
  // src/services/pty.ts
14284
14732
  import { spawn } from "node-pty";
14285
14733
  import { execFile, execFileSync as execFileSync2 } from "node:child_process";
14286
14734
  import { createRequire } from "node:module";
14287
14735
  import { randomUUID as randomUUID2 } from "node:crypto";
14288
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
14736
+ import { chmodSync as chmodSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync5 } from "node:fs";
14289
14737
  import { dirname as dirname7 } from "node:path";
14290
- import { tmpdir } from "node:os";
14738
+ import { tmpdir as tmpdir2 } from "node:os";
14291
14739
  function noTtyPromptEnv() {
14292
14740
  const path = askpassDenyPath();
14293
14741
  mkdirSync5(dirname7(path), { recursive: true, mode: 448 });
14294
- writeFileSync3(path, ASKPASS_DENY, { mode: 448 });
14742
+ writeFileSync5(path, ASKPASS_DENY, { mode: 448 });
14295
14743
  return { SSH_ASKPASS: path, SSH_ASKPASS_REQUIRE: "force", GIT_TERMINAL_PROMPT: "0" };
14296
14744
  }
14297
14745
  function tmux(...args) {
@@ -14310,14 +14758,14 @@ function tmux(...args) {
14310
14758
  }
14311
14759
  }
14312
14760
  function tmuxAsync(...args) {
14313
- return new Promise((resolve21, reject2) => {
14761
+ return new Promise((resolve22, reject2) => {
14314
14762
  execFile(
14315
14763
  "tmux",
14316
14764
  ["-L", socket(), "-f", "/dev/null", ...args],
14317
14765
  { encoding: "utf8" },
14318
14766
  (e, stdout, stderr) => {
14319
14767
  if (!e) {
14320
- resolve21(stdout);
14768
+ resolve22(stdout);
14321
14769
  return;
14322
14770
  }
14323
14771
  const err = e;
@@ -14368,7 +14816,8 @@ function parsePanes(out4) {
14368
14816
  alternate,
14369
14817
  activity,
14370
14818
  eventHook,
14371
- created
14819
+ created,
14820
+ agentRoster
14372
14821
  ] = line.split(" ");
14373
14822
  if (!n2?.startsWith(PREFIX)) return [];
14374
14823
  const exited = dead === "1";
@@ -14398,10 +14847,30 @@ function parsePanes(out4) {
14398
14847
  // SPEC-919 · sesi yang lahir di tmux yang tak menjawab field ini → 0 (epoch).
14399
14848
  startedAt: Number(created) || 0,
14400
14849
  // SPEC-909 · ADR-0146 · sesi yang lahir sebelum pembaruan tak punya opsi ini → false.
14401
- eventHook: eventHook === "1"
14850
+ eventHook: eventHook === "1",
14851
+ agentRoster: parseAgentRoster(agentRoster)
14402
14852
  }];
14403
14853
  });
14404
14854
  }
14855
+ function parseAgentRoster(value) {
14856
+ try {
14857
+ const parsed = JSON.parse(value || "[]");
14858
+ if (!Array.isArray(parsed)) return [];
14859
+ return parsed.flatMap((entry) => {
14860
+ if (!entry || typeof entry !== "object") return [];
14861
+ const row = entry;
14862
+ if (typeof row.name !== "string") return [];
14863
+ return [{
14864
+ name: row.name,
14865
+ ...typeof row.id === "string" ? { id: row.id } : {},
14866
+ ...typeof row.model === "string" ? { model: row.model } : {},
14867
+ ...typeof row.timeoutSeconds === "number" ? { timeoutSeconds: row.timeoutSeconds } : {}
14868
+ }];
14869
+ });
14870
+ } catch {
14871
+ return [];
14872
+ }
14873
+ }
14405
14874
  function sessionEventEnv(sessionId2, env = process.env) {
14406
14875
  const port2 = Number(env.PORT ?? 8787);
14407
14876
  const host2 = controlHost(loadIngressPolicy(env));
@@ -14409,6 +14878,7 @@ function sessionEventEnv(sessionId2, env = process.env) {
14409
14878
  HANOMAN_SESSION_ID: sessionId2,
14410
14879
  HANOMAN_EVENT_URL: `http://127.0.0.1:${port2}/api/session-events`,
14411
14880
  HANOMAN_EVENT_TOKEN: sessionEventToken(sessionId2),
14881
+ HANOMAN_EVENT_DIR: sessionEventDir(sessionId2),
14412
14882
  ...host2 ? { HANOMAN_EVENT_HOST: host2 } : {}
14413
14883
  };
14414
14884
  }
@@ -14418,6 +14888,24 @@ function registerSessionHooks(h) {
14418
14888
  function registerCustomAgentSource(fn) {
14419
14889
  customAgentSource = fn;
14420
14890
  }
14891
+ function registerCodexNativeAgentSupport(fn) {
14892
+ codexNativeAgentSupport = fn;
14893
+ }
14894
+ function collectChangedFiles(cwd, baseSha, run4 = runGitDiff) {
14895
+ try {
14896
+ const files = /* @__PURE__ */ new Set();
14897
+ const add = (args) => {
14898
+ for (const path of run4(cwd, args).split("\0")) if (path) files.add(path);
14899
+ };
14900
+ if (baseSha) add(["diff", "--name-only", "-z", `${baseSha}...HEAD`]);
14901
+ add(["diff", "--name-only", "-z"]);
14902
+ add(["diff", "--cached", "--name-only", "-z"]);
14903
+ add(["ls-files", "--others", "--exclude-standard", "-z"]);
14904
+ return [...files].sort();
14905
+ } catch {
14906
+ return [];
14907
+ }
14908
+ }
14421
14909
  function sessionKind(o, projectId, cwd) {
14422
14910
  if (o.specId) return "spec";
14423
14911
  if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
@@ -14440,15 +14928,62 @@ function createSession(projectId, cwd, opts = {}) {
14440
14928
  const existing = getSession(id);
14441
14929
  if (existing && !existing.exited) return existing;
14442
14930
  if (existing) killSession(id);
14931
+ const eventDir = opts.command ? void 0 : sessionEventDir(id);
14932
+ if (eventDir) {
14933
+ mkdirSync5(eventDir, { recursive: true, mode: 448 });
14934
+ chmodSync6(eventDir, 448);
14935
+ }
14443
14936
  const agentForDefs = opts.agent ?? "claude";
14444
- const customDefs = opts.command ? [] : customAgentsFor(projectId, agentForDefs);
14445
- const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : agentDelegationClause(customDefs);
14937
+ const selectionContext = {
14938
+ projectId,
14939
+ runtime: agentForDefs,
14940
+ flow: opts.flow,
14941
+ cwd,
14942
+ baseSha: opts.env?.HANOMAN_BASE_SHA,
14943
+ prompt: opts.prompt,
14944
+ changedFiles: opts.command ? [] : collectChangedFiles(cwd, opts.env?.HANOMAN_BASE_SHA)
14945
+ };
14946
+ const customDefs = opts.command ? [] : customAgentsFor(selectionContext);
14947
+ let rosterBlock = "";
14948
+ let codexAgentArgs = [];
14949
+ let agentsFile;
14950
+ let agentConfigDir;
14951
+ let liveAgentDefs = [];
14952
+ if (customDefs.length > 0) {
14953
+ const tempDir = agentTempDir(id);
14954
+ agentConfigDir = tempDir;
14955
+ mkdirSync5(tempDir, { recursive: true, mode: 448 });
14956
+ const readOnlyHook = customDefs.some((def2) => def2.workspacePolicy === "read-only") ? writeReadOnlyHook(tempDir) : void 0;
14957
+ if (agentForDefs === "claude") {
14958
+ const json = renderAgentsJson(customDefs, { readOnlyHookCommand: readOnlyHook?.command });
14959
+ if (json) {
14960
+ agentsFile = agentsFilePath(id);
14961
+ writeFileSync5(agentsFile, json, { mode: 384 });
14962
+ rosterBlock = agentDelegationClause(customDefs, "claude");
14963
+ liveAgentDefs = customDefs;
14964
+ }
14965
+ } else {
14966
+ const materialized = materializeCodexAgents(customDefs, tempDir, {
14967
+ readOnlyHookCommand: readOnlyHook?.command,
14968
+ clientVersion: codexNativeAgentSupport().version
14969
+ });
14970
+ codexAgentArgs = materialized.args;
14971
+ rosterBlock = materialized.delegationClause;
14972
+ liveAgentDefs = materialized.liveDefs;
14973
+ for (const warning of materialized.warnings) {
14974
+ process.stderr.write(
14975
+ `hanoman: custom agent ${warning.agentName} tidak dimaterialisasi: ${warning.reason}
14976
+ `
14977
+ );
14978
+ }
14979
+ }
14980
+ }
14446
14981
  let promptArg = "";
14447
14982
  let promptFile;
14448
14983
  if (!opts.command && opts.prompt) {
14449
14984
  promptFile = promptFilePath(id);
14450
14985
  mkdirSync5(dirname7(promptFile), { recursive: true, mode: 448 });
14451
- writeFileSync3(promptFile, opts.prompt + rosterBlock, { mode: 384 });
14986
+ writeFileSync5(promptFile, opts.prompt + rosterBlock, { mode: 384 });
14452
14987
  promptArg = `"$(cat ${sq(promptFile)})"`;
14453
14988
  }
14454
14989
  const agent = opts.agent ?? "claude";
@@ -14460,7 +14995,7 @@ function createSession(projectId, cwd, opts = {}) {
14460
14995
  if (agent === "codex" && opts.goal && opts.flow && opts.specId) {
14461
14996
  goalGate = goalGatePath(id);
14462
14997
  mkdirSync5(dirname7(goalGate), { recursive: true, mode: 448 });
14463
- writeFileSync3(goalGate, codexGoalScript({
14998
+ writeFileSync5(goalGate, codexGoalScript({
14464
14999
  flow: opts.flow,
14465
15000
  specId: opts.specId,
14466
15001
  condition: opts.goal,
@@ -14470,15 +15005,6 @@ function createSession(projectId, cwd, opts = {}) {
14470
15005
  }), { mode: 448 });
14471
15006
  }
14472
15007
  const effort = agent === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort;
14473
- let agentsFile;
14474
- if (agent === "claude") {
14475
- const json = renderAgentsJson(customDefs);
14476
- if (json) {
14477
- agentsFile = agentsFilePath(id);
14478
- mkdirSync5(dirname7(agentsFile), { recursive: true, mode: 448 });
14479
- writeFileSync3(agentsFile, json, { mode: 384 });
14480
- }
14481
- }
14482
15008
  const flags = agentFlags({
14483
15009
  agent,
14484
15010
  model: opts.model,
@@ -14491,7 +15017,8 @@ function createSession(projectId, cwd, opts = {}) {
14491
15017
  eventHook: true
14492
15018
  }).map(sq).join(" ");
14493
15019
  const agentsArg = agentsFile ? `--agents "$(cat ${sq(agentsFile)})"` : "";
14494
- argv = [sq(agentBin(agent)), promptArg, flags, agentsArg].filter(Boolean).join(" ");
15020
+ const nativeAgentArgs = agent === "codex" ? codexAgentArgs.map(sq).join(" ") : "";
15021
+ argv = [sq(agentBin(agent)), promptArg, flags, agentsArg, nativeAgentArgs].filter(Boolean).join(" ");
14495
15022
  }
14496
15023
  const envPairs = [];
14497
15024
  if (!opts.command && agent === "claude") {
@@ -14510,16 +15037,16 @@ function createSession(projectId, cwd, opts = {}) {
14510
15037
  if (opts.attachmentsDir) envPairs.push(`HANOMAN_ATTACHMENTS_DIR=${sq(opts.attachmentsDir)}`);
14511
15038
  for (const [k, v] of Object.entries(opts.env ?? {})) envPairs.push(`${k}=${sq(v)}`);
14512
15039
  let cmd = envPairs.length ? `${envPairs.join(" ")} ${argv}` : argv;
14513
- let sandboxed = false;
14514
15040
  if (!opts.command) {
14515
15041
  const wrapped = sandboxCommand({
14516
15042
  command: cmd,
14517
15043
  worktree: cwd,
14518
15044
  phaseFile: opts.phaseFile,
14519
15045
  promptFile,
15046
+ agentConfigDir,
15047
+ eventDir,
14520
15048
  attachmentsDir: opts.attachmentsDir
14521
15049
  });
14522
- sandboxed = wrapped !== cmd;
14523
15050
  cmd = wrapped;
14524
15051
  }
14525
15052
  if (opts.decisionFile) mkdirSync5(dirname7(opts.decisionFile), { recursive: true });
@@ -14594,9 +15121,18 @@ function createSession(projectId, cwd, opts = {}) {
14594
15121
  if (opts.flow) tmux("set-option", "-t", name(id), "@hanoman_flow", opts.flow);
14595
15122
  if (opts.branch) tmux("set-option", "-t", name(id), "@hanoman_branch", opts.branch);
14596
15123
  tmux("set-option", "-t", name(id), "@hanoman_agent", agent);
15124
+ if (liveAgentDefs.length > 0) {
15125
+ const roster = liveAgentDefs.map((def2) => ({
15126
+ ...def2.id ? { id: def2.id } : {},
15127
+ name: def2.name,
15128
+ ...def2.model ? { model: def2.model } : {},
15129
+ ...def2.timeoutSeconds ? { timeoutSeconds: def2.timeoutSeconds } : {}
15130
+ }));
15131
+ tmux("set-option", "-t", name(id), "@hanoman_agent_roster", JSON.stringify(roster));
15132
+ }
14597
15133
  if (opts.phaseFile) tmux("set-option", "-t", name(id), "@hanoman_phase_file", opts.phaseFile);
14598
15134
  if (opts.decisionFile) tmux("set-option", "-t", name(id), "@hanoman_decision_file", opts.decisionFile);
14599
- if (!opts.command && !sandboxed) tmux("set-option", "-t", name(id), "@hanoman_event_hook", "1");
15135
+ if (!opts.command) tmux("set-option", "-t", name(id), "@hanoman_event_hook", "1");
14600
15136
  if (opts.goal && !opts.command) void armGoalInTui(id, opts.goal, { agent }).catch(() => {
14601
15137
  });
14602
15138
  emitBirth({
@@ -14787,10 +15323,10 @@ function startPoll() {
14787
15323
  poll = setInterval(() => {
14788
15324
  if (polling) return;
14789
15325
  polling = true;
14790
- const snapshot2 = [...attached.entries()];
15326
+ const snapshot3 = [...attached.entries()];
14791
15327
  listPanesAsync().then((panes) => {
14792
15328
  const live = new Map(panes.map((p3) => [p3.id, p3]));
14793
- for (const [id, a] of snapshot2) {
15329
+ for (const [id, a] of snapshot3) {
14794
15330
  if (attached.get(id) !== a) continue;
14795
15331
  const p3 = live.get(id);
14796
15332
  if (!p3) end(id, 0);
@@ -14853,6 +15389,14 @@ function killSession(id) {
14853
15389
  const transcript = captureTranscript(id);
14854
15390
  drop(id);
14855
15391
  tmux("kill-session", "-t", name(id));
15392
+ try {
15393
+ rmSync2(agentTempDir(id), { recursive: true, force: true });
15394
+ } catch {
15395
+ }
15396
+ try {
15397
+ rmSync2(sessionEventDir(id), { recursive: true, force: true });
15398
+ } catch {
15399
+ }
14856
15400
  emitDeath({ sessionId: id, exitCode: p3.exited ? p3.code : null, transcript });
14857
15401
  void dropSessionUploads(id).catch(() => {
14858
15402
  });
@@ -14878,7 +15422,7 @@ function spawnPty(...args) {
14878
15422
  );
14879
15423
  }
14880
15424
  }
14881
- var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, clearMarker, PANE_QUIET_MS, paneQuiet, markerOnset, decisionOnset, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, liveDecisions, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, polling, TERMINAL_QUERY, stripTerminalQueries, detach;
15425
+ var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, clearMarker, PANE_QUIET_MS, paneQuiet, markerOnset, decisionOnset, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentTempDir, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, liveDecisions, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, codexNativeAgentSupport, customAgentsFor, runGitDiff, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, polling, TERMINAL_QUERY, stripTerminalQueries, detach;
14882
15426
  var init_pty = __esm({
14883
15427
  "src/services/pty.ts"() {
14884
15428
  "use strict";
@@ -14892,6 +15436,8 @@ var init_pty = __esm({
14892
15436
  init_ingress_policy();
14893
15437
  init_session_event_token();
14894
15438
  init_session_sandbox();
15439
+ init_session_event_spool();
15440
+ init_session_event_spool();
14895
15441
  socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
14896
15442
  PREFIX = "hanoman-";
14897
15443
  MAX_SCROLLBACK = 256 * 1024;
@@ -14906,7 +15452,7 @@ var init_pty = __esm({
14906
15452
  };
14907
15453
  clearMarker = (f) => {
14908
15454
  try {
14909
- writeFileSync3(f, "");
15455
+ writeFileSync5(f, "");
14910
15456
  } catch {
14911
15457
  }
14912
15458
  };
@@ -14934,11 +15480,12 @@ var init_pty = __esm({
14934
15480
  rootBypassEnv = (uid = process.getuid?.()) => uid === 0 ? { IS_SANDBOX: "1" } : {};
14935
15481
  frame = (f) => JSON.stringify(f);
14936
15482
  name = (id) => PREFIX + id;
14937
- promptFilePath = (id) => `${tmpdir()}/hanoman-prompts/${id}`;
14938
- goalGatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.sh`;
14939
- goalStatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.count`;
14940
- agentsFilePath = (id) => `${tmpdir()}/hanoman-agents/${id}.json`;
14941
- askpassDenyPath = () => `${tmpdir()}/hanoman-askpass/deny.sh`;
15483
+ promptFilePath = (id) => `${tmpdir2()}/hanoman-prompts/${id}`;
15484
+ goalGatePath = (id) => `${tmpdir2()}/hanoman-goal-gates/${id}.sh`;
15485
+ goalStatePath = (id) => `${tmpdir2()}/hanoman-goal-gates/${id}.count`;
15486
+ agentTempDir = (id) => `${tmpdir2()}/hanoman-agents/${id}`;
15487
+ agentsFilePath = (id) => `${agentTempDir(id)}/claude.json`;
15488
+ askpassDenyPath = () => `${tmpdir2()}/hanoman-askpass/deny.sh`;
14942
15489
  ASKPASS_DENY = `#!/bin/sh
14943
15490
  echo "hanoman: tak ada manusia di pane ini \u2014 permintaan ketikan ditolak: $1" >&2
14944
15491
  echo "hanoman: buka kuncinya di luar sesi (mis. ssh-add ~/.ssh/id_rsa), lalu ulangi." >&2
@@ -14970,7 +15517,8 @@ exit 1
14970
15517
  // `pty-parse.test.ts` mengunci panjang FMT terhadap destructuring `parsePanes`.
14971
15518
  "#{window_activity}",
14972
15519
  "#{@hanoman_event_hook}",
14973
- "#{session_created}"
15520
+ "#{session_created}",
15521
+ "#{@hanoman_agent_roster}"
14974
15522
  ].join(" ");
14975
15523
  toSessionInfo = ({
14976
15524
  id,
@@ -15028,13 +15576,18 @@ exit 1
15028
15576
  }
15029
15577
  };
15030
15578
  customAgentSource = () => [];
15031
- customAgentsFor = (projectId, agent) => {
15579
+ codexNativeAgentSupport = () => ({ version: "0.151.0", ok: true });
15580
+ customAgentsFor = (context) => {
15032
15581
  try {
15033
- return customAgentSource(projectId, agent);
15582
+ return customAgentSource(context);
15034
15583
  } catch {
15035
15584
  return [];
15036
15585
  }
15037
15586
  };
15587
+ runGitDiff = (cwd, args) => execFileSync2("git", ["-C", cwd, ...args], {
15588
+ encoding: "utf8",
15589
+ stdio: ["ignore", "pipe", "ignore"]
15590
+ });
15038
15591
  sleep = (ms) => new Promise((r) => {
15039
15592
  setTimeout(r, ms);
15040
15593
  });
@@ -15260,6 +15813,11 @@ function validateSyncData(entity, data, options2 = {}) {
15260
15813
  if (!Number.isSafeInteger(value)) throw new Error(`sync tipe invalid: ${entity}.${field}`);
15261
15814
  continue;
15262
15815
  }
15816
+ if (NULLABLE_NUMBER_FIELDS.has(key)) {
15817
+ if (value !== null && !Number.isSafeInteger(value))
15818
+ throw new Error(`sync tipe invalid: ${entity}.${field}`);
15819
+ continue;
15820
+ }
15263
15821
  if (BOOLEAN_FIELDS.has(key)) {
15264
15822
  if (typeof value !== "boolean") throw new Error(`sync tipe invalid: ${entity}.${field}`);
15265
15823
  continue;
@@ -15527,7 +16085,7 @@ async function upsertLocal(entity, id, version, data) {
15527
16085
  function setAcceptedHook(hook) {
15528
16086
  onAccepted = hook;
15529
16087
  }
15530
- var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, FLOAT_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, PULL_MAX_BYTES, BOOTSTRAP_ORDER, onAccepted, __FIELDS_FOR_TEST;
16088
+ var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, NULLABLE_NUMBER_FIELDS, FLOAT_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, PULL_MAX_BYTES, BOOTSTRAP_ORDER, onAccepted, __FIELDS_FOR_TEST;
15531
16089
  var init_sync = __esm({
15532
16090
  "src/services/sync.ts"() {
15533
16091
  "use strict";
@@ -15584,7 +16142,24 @@ var init_sync = __esm({
15584
16142
  // SPEC-484 · ADR-0101 · `runtime` ikut: ia menentukan sesi mesin mana yang memakai persona ini,
15585
16143
  // dan kolom yang terlewat di sini mendarat sebagai default palsu (= "warisi") di setiap mesin
15586
16144
  // lain tanpa satu pun error.
15587
- customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "runtime", "enabled", "createdAt", "updatedAt"],
16145
+ customAgent: [
16146
+ "projectId",
16147
+ "name",
16148
+ "description",
16149
+ "instructions",
16150
+ "tools",
16151
+ "model",
16152
+ "mentions",
16153
+ "runtime",
16154
+ "activation",
16155
+ "effort",
16156
+ "workspacePolicy",
16157
+ "maxTurns",
16158
+ "timeoutSeconds",
16159
+ "enabled",
16160
+ "createdAt",
16161
+ "updatedAt"
16162
+ ],
15588
16163
  // SPEC-471 · ADR-0095 · SELURUH kolom bermakna ikut. `status`/`specId` termasuk: keputusan
15589
16164
  // triase adalah bagian keadaan yang harus dilihat sama oleh semua mesin — tanpa itu satu
15590
16165
  // mesin bisa menerima ulang issue yang di mesin lain sudah jadi backlog.
@@ -15663,6 +16238,7 @@ var init_sync = __esm({
15663
16238
  "ticketAttachment:size",
15664
16239
  "githubIssue:number"
15665
16240
  ]);
16241
+ NULLABLE_NUMBER_FIELDS = /* @__PURE__ */ new Set(["customAgent:maxTurns", "customAgent:timeoutSeconds"]);
15666
16242
  FLOAT_FIELDS = /* @__PURE__ */ new Set(["task:order"]);
15667
16243
  BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["vps:hardened", "customAgent:enabled", "member:active"]);
15668
16244
  JSON_FIELDS = /* @__PURE__ */ new Set([
@@ -15697,6 +16273,33 @@ var init_sync = __esm({
15697
16273
  }
15698
16274
  });
15699
16275
 
16276
+ // src/services/sync-notify.ts
16277
+ async function notifySynced(entity, id) {
16278
+ try {
16279
+ if (!isEntity(entity)) return;
16280
+ await consumeTombstoneOnRecreate(entity, id);
16281
+ if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
16282
+ else await publishLocal(entity, id);
16283
+ } catch {
16284
+ }
16285
+ }
16286
+ async function notifyDeleted(entity, id) {
16287
+ try {
16288
+ if (!isEntity(entity)) return;
16289
+ if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
16290
+ else await publishDelete(entity, id);
16291
+ } catch {
16292
+ }
16293
+ }
16294
+ var init_sync_notify = __esm({
16295
+ "src/services/sync-notify.ts"() {
16296
+ "use strict";
16297
+ init_config2();
16298
+ init_outbox();
16299
+ init_sync();
16300
+ }
16301
+ });
16302
+
15700
16303
  // src/services/settings.ts
15701
16304
  async function getSetting() {
15702
16305
  const raw = (await prisma.setting.findUnique({ where: { id: 1 } }))?.data;
@@ -15777,8 +16380,10 @@ var init_settings3 = __esm({
15777
16380
  // SPEC-518 · agen pembuat changelog (opt-in, mati)
15778
16381
  portalChat: PORTAL_CHAT_DEFAULTS,
15779
16382
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
15780
- builtinAgents: {}
16383
+ builtinAgents: {},
15781
16384
  // SPEC-881 · ADR-0136 · sidik jari seed (lokal, tak disync)
16385
+ builtinAgentPolicies: {}
16386
+ // SPEC-950 · marker safety policy sekali-jalan (lokal)
15782
16387
  };
15783
16388
  RETIRED_MODELS = { "claude-opus-4-8": "claude-opus-5" };
15784
16389
  }
@@ -16184,12 +16789,12 @@ var require_common = __commonJS({
16184
16789
  createDebug.skips = [];
16185
16790
  createDebug.formatters = {};
16186
16791
  function selectColor(namespace) {
16187
- let hash2 = 0;
16792
+ let hash3 = 0;
16188
16793
  for (let i = 0; i < namespace.length; i++) {
16189
- hash2 = (hash2 << 5) - hash2 + namespace.charCodeAt(i);
16190
- hash2 |= 0;
16794
+ hash3 = (hash3 << 5) - hash3 + namespace.charCodeAt(i);
16795
+ hash3 |= 0;
16191
16796
  }
16192
- return createDebug.colors[Math.abs(hash2) % createDebug.colors.length];
16797
+ return createDebug.colors[Math.abs(hash3) % createDebug.colors.length];
16193
16798
  }
16194
16799
  createDebug.selectColor = selectColor;
16195
16800
  function createDebug(namespace) {
@@ -17335,7 +17940,7 @@ var init_gateway = __esm({
17335
17940
  const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
17336
17941
  updateTelegramRuntimeStatus({ running: false, readiness: "error", lastError: message });
17337
17942
  if (error instanceof TelegramApiError && error.code === 409) return;
17338
- await new Promise((resolve21) => setTimeout(resolve21, 1e3));
17943
+ await new Promise((resolve22) => setTimeout(resolve22, 1e3));
17339
17944
  }
17340
17945
  }
17341
17946
  }
@@ -17498,9 +18103,9 @@ ${input.text}`;
17498
18103
  }
17499
18104
  const engine = await this.deps.defaults();
17500
18105
  await this.deps.store.setChatEngine(input.chatId, engine);
17501
- const hash2 = chatHash(input.chatId);
17502
- const projectId = `telegram:${hash2}`;
17503
- const cwd = `${this.deps.home.replace(/\/$/, "")}/telegram/${hash2}`;
18106
+ const hash3 = chatHash(input.chatId);
18107
+ const projectId = `telegram:${hash3}`;
18108
+ const cwd = `${this.deps.home.replace(/\/$/, "")}/telegram/${hash3}`;
17504
18109
  this.deps.ensureDir(cwd);
17505
18110
  if (engine.agent === "codex") this.deps.ensureCodexTrust(cwd);
17506
18111
  const personality = await this.deps.personality(context.personalityAgentId, context.activeProjectId);
@@ -18049,6 +18654,57 @@ var init_bootstrap = __esm({
18049
18654
  }
18050
18655
  });
18051
18656
 
18657
+ // src/services/codex-version.ts
18658
+ import { execFile as execFile14 } from "node:child_process";
18659
+ import { promisify as promisify13 } from "node:util";
18660
+ async function probeCodexVersion(env = process.env, execute = runVersion) {
18661
+ const probe = codexNativeVersionProbe(env, codexBin2());
18662
+ try {
18663
+ return parseCodexVersion((await execute(probe.bin, probe.args)).stdout);
18664
+ } catch {
18665
+ return null;
18666
+ }
18667
+ }
18668
+ function parseCodexVersion(out4) {
18669
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(out4);
18670
+ return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
18671
+ }
18672
+ async function getCodexVersion(now = Date.now()) {
18673
+ if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
18674
+ const version = await probeCodexVersion();
18675
+ cache3 = { at: now, version };
18676
+ return version;
18677
+ }
18678
+ async function codexVersionInfo() {
18679
+ const version = await getCodexVersion();
18680
+ return {
18681
+ version,
18682
+ minRequired: CODEX_MIN_CLIENT,
18683
+ ok: version === null || cmpVersion(version, CODEX_MIN_CLIENT) >= 0
18684
+ };
18685
+ }
18686
+ function _resetCodexVersionCache() {
18687
+ cache3 = null;
18688
+ }
18689
+ var run3, CODEX_MIN_CLIENT, TTL_MS4, cache3, codexBin2, runVersion;
18690
+ var init_codex_version = __esm({
18691
+ "src/services/codex-version.ts"() {
18692
+ "use strict";
18693
+ init_src();
18694
+ init_src2();
18695
+ init_config2();
18696
+ run3 = promisify13(execFile14);
18697
+ CODEX_MIN_CLIENT = "0.144.0";
18698
+ TTL_MS4 = 5 * 6e4;
18699
+ cache3 = null;
18700
+ codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
18701
+ runVersion = async (bin, args) => {
18702
+ const { stdout } = await run3(bin, args, { timeout: 1e4 });
18703
+ return { stdout };
18704
+ };
18705
+ }
18706
+ });
18707
+
18052
18708
  // src/services/presence/snapshot.ts
18053
18709
  function paneToPresence(p3, phase) {
18054
18710
  return {
@@ -20448,7 +21104,7 @@ var require_websocket = __commonJS({
20448
21104
  var http = __require("http");
20449
21105
  var net = __require("net");
20450
21106
  var tls = __require("tls");
20451
- var { randomBytes: randomBytes11, createHash: createHash11 } = __require("crypto");
21107
+ var { randomBytes: randomBytes11, createHash: createHash12 } = __require("crypto");
20452
21108
  var { Duplex, Readable } = __require("stream");
20453
21109
  var { URL: URL2 } = __require("url");
20454
21110
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -21116,8 +21772,8 @@ var require_websocket = __commonJS({
21116
21772
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
21117
21773
  return;
21118
21774
  }
21119
- const digest = createHash11("sha1").update(key + GUID).digest("base64");
21120
- if (res.headers["sec-websocket-accept"] !== digest) {
21775
+ const digest2 = createHash12("sha1").update(key + GUID).digest("base64");
21776
+ if (res.headers["sec-websocket-accept"] !== digest2) {
21121
21777
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
21122
21778
  return;
21123
21779
  }
@@ -21397,7 +22053,7 @@ var require_stream = __commonJS({
21397
22053
  };
21398
22054
  duplex._final = function(callback) {
21399
22055
  if (ws2.readyState === ws2.CONNECTING) {
21400
- ws2.once("open", function open4() {
22056
+ ws2.once("open", function open5() {
21401
22057
  duplex._final(callback);
21402
22058
  });
21403
22059
  return;
@@ -21418,7 +22074,7 @@ var require_stream = __commonJS({
21418
22074
  };
21419
22075
  duplex._write = function(chunk, encoding, callback) {
21420
22076
  if (ws2.readyState === ws2.CONNECTING) {
21421
- ws2.once("open", function open4() {
22077
+ ws2.once("open", function open5() {
21422
22078
  duplex._write(chunk, encoding, callback);
21423
22079
  });
21424
22080
  return;
@@ -21485,7 +22141,7 @@ var require_websocket_server = __commonJS({
21485
22141
  var EventEmitter = __require("events");
21486
22142
  var http = __require("http");
21487
22143
  var { Duplex } = __require("stream");
21488
- var { createHash: createHash11 } = __require("crypto");
22144
+ var { createHash: createHash12 } = __require("crypto");
21489
22145
  var extension2 = require_extension();
21490
22146
  var PerMessageDeflate2 = require_permessage_deflate();
21491
22147
  var subprotocol2 = require_subprotocol();
@@ -21792,12 +22448,12 @@ var require_websocket_server = __commonJS({
21792
22448
  );
21793
22449
  }
21794
22450
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
21795
- const digest = createHash11("sha1").update(key + GUID).digest("base64");
22451
+ const digest2 = createHash12("sha1").update(key + GUID).digest("base64");
21796
22452
  const headers = [
21797
22453
  "HTTP/1.1 101 Switching Protocols",
21798
22454
  "Upgrade: websocket",
21799
22455
  "Connection: Upgrade",
21800
- `Sec-WebSocket-Accept: ${digest}`
22456
+ `Sec-WebSocket-Accept: ${digest2}`
21801
22457
  ];
21802
22458
  const ws2 = new this.options.WebSocket(null, void 0, this.options);
21803
22459
  if (protocols.size) {
@@ -22343,6 +22999,382 @@ var init_sync_client = __esm({
22343
22999
  }
22344
23000
  });
22345
23001
 
23002
+ // src/services/agent-tool-catalog.ts
23003
+ import { readFileSync as readFileSync17 } from "node:fs";
23004
+ import { homedir as homedir10 } from "node:os";
23005
+ import { join as join29 } from "node:path";
23006
+ function mcpServerNames(repoDir) {
23007
+ const names = [];
23008
+ const claudeJson = readJson2(join29(home(), ".claude.json"));
23009
+ names.push(...serversOf(claudeJson));
23010
+ if (repoDir) {
23011
+ const projects = claudeJson?.projects;
23012
+ if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
23013
+ names.push(...serversOf(readJson2(join29(repoDir, ".mcp.json"))));
23014
+ }
23015
+ names.push(...codexServers());
23016
+ return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
23017
+ }
23018
+ function agentToolCatalog(repoDir) {
23019
+ return [ALL_TOOLS_ENTRY, ...BUILTIN_AGENT_TOOLS, ...mcpServerNames(repoDir).map(mcpToolEntry)];
23020
+ }
23021
+ var home, readJson2, serversOf, codexServers, agentToolIds;
23022
+ var init_agent_tool_catalog = __esm({
23023
+ "src/services/agent-tool-catalog.ts"() {
23024
+ "use strict";
23025
+ init_src();
23026
+ home = () => process.env.HOME || homedir10();
23027
+ readJson2 = (path) => {
23028
+ try {
23029
+ return JSON.parse(readFileSync17(path, "utf8"));
23030
+ } catch {
23031
+ return null;
23032
+ }
23033
+ };
23034
+ serversOf = (node) => {
23035
+ const ms = node?.mcpServers;
23036
+ if (!ms || typeof ms !== "object" || Array.isArray(ms)) return [];
23037
+ return Object.keys(ms);
23038
+ };
23039
+ codexServers = () => {
23040
+ let text;
23041
+ try {
23042
+ text = readFileSync17(join29(home(), ".codex", "config.toml"), "utf8");
23043
+ } catch {
23044
+ return [];
23045
+ }
23046
+ const out4 = [];
23047
+ for (const m of text.matchAll(/^\s*\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.[^\]]*)?\]/gm)) {
23048
+ const name2 = m[1] ?? m[2];
23049
+ if (name2) out4.push(name2);
23050
+ }
23051
+ return out4;
23052
+ };
23053
+ agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
23054
+ }
23055
+ });
23056
+
23057
+ // src/services/builtin-agents.ts
23058
+ import { createHash as createHash9 } from "node:crypto";
23059
+ async function seedBuiltinAgents() {
23060
+ try {
23061
+ const setting = await getSetting();
23062
+ const stamps = { ...setting.builtinAgents };
23063
+ const policies = { ...setting.builtinAgentPolicies };
23064
+ let changed = false;
23065
+ for (const a of BUILTIN_AGENTS) {
23066
+ const id = customAgentId(null, a.name);
23067
+ const fp = builtinFingerprint(a);
23068
+ const row = await prisma.customAgent.findUnique({ where: { id } });
23069
+ const qaPolicyPending = a.name === "qa-verifier" && policies[a.name] !== QA_SAFETY_POLICY;
23070
+ if (!row) {
23071
+ if (await findTombstone("customAgent", id)) {
23072
+ if (qaPolicyPending) {
23073
+ policies[a.name] = QA_SAFETY_POLICY;
23074
+ changed = true;
23075
+ }
23076
+ continue;
23077
+ }
23078
+ await prisma.customAgent.create({ data: {
23079
+ id,
23080
+ projectId: null,
23081
+ name: a.name,
23082
+ description: a.description,
23083
+ instructions: a.instructions,
23084
+ tools: [...a.tools],
23085
+ model: null,
23086
+ mentions: [],
23087
+ runtime: null,
23088
+ activation: a.activation,
23089
+ effort: a.effort,
23090
+ workspacePolicy: a.workspacePolicy,
23091
+ maxTurns: a.maxTurns,
23092
+ timeoutSeconds: a.timeoutSeconds,
23093
+ enabled: a.enabledByDefault
23094
+ } });
23095
+ await notifySynced("customAgent", id);
23096
+ stamps[a.name] = fp;
23097
+ changed = true;
23098
+ if (qaPolicyPending) policies[a.name] = QA_SAFETY_POLICY;
23099
+ continue;
23100
+ }
23101
+ const stamped = stamps[a.name];
23102
+ const unedited = Boolean(stamped) && (stamped === rowFingerprint(row) || stamped === legacyRowFingerprint(row));
23103
+ const data = {};
23104
+ if (unedited && stamped !== fp) {
23105
+ Object.assign(data, {
23106
+ description: a.description,
23107
+ instructions: a.instructions,
23108
+ tools: [...a.tools],
23109
+ activation: a.activation,
23110
+ effort: a.effort,
23111
+ workspacePolicy: a.workspacePolicy,
23112
+ maxTurns: a.maxTurns,
23113
+ timeoutSeconds: a.timeoutSeconds
23114
+ });
23115
+ stamps[a.name] = fp;
23116
+ changed = true;
23117
+ }
23118
+ if (qaPolicyPending && unedited) data.enabled = false;
23119
+ if (Object.keys(data).length > 0) {
23120
+ await prisma.customAgent.update({ where: { id }, data });
23121
+ await notifySynced("customAgent", id);
23122
+ }
23123
+ if (qaPolicyPending) {
23124
+ policies[a.name] = QA_SAFETY_POLICY;
23125
+ changed = true;
23126
+ }
23127
+ }
23128
+ if (changed) {
23129
+ const data = { ...setting, builtinAgents: stamps, builtinAgentPolicies: policies };
23130
+ await prisma.setting.upsert({
23131
+ where: { id: 1 },
23132
+ update: { data },
23133
+ create: { id: 1, data }
23134
+ });
23135
+ }
23136
+ } catch {
23137
+ }
23138
+ }
23139
+ var digest, legacyFingerprint, fingerprint, builtinFingerprint, rowFingerprint, legacyRowFingerprint, QA_SAFETY_POLICY;
23140
+ var init_builtin_agents2 = __esm({
23141
+ "src/services/builtin-agents.ts"() {
23142
+ "use strict";
23143
+ init_src();
23144
+ init_db();
23145
+ init_settings3();
23146
+ init_tombstone();
23147
+ init_sync_notify();
23148
+ digest = (parts) => createHash9("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
23149
+ legacyFingerprint = (name2, description, instructions, tools) => digest([name2, description, instructions, [...tools].join(",")]);
23150
+ fingerprint = (a) => digest([
23151
+ a.name,
23152
+ a.description,
23153
+ a.instructions,
23154
+ (toolsOf(a.tools) ?? []).join(","),
23155
+ activationOf(a.activation),
23156
+ effortOf(a.effort) ?? "",
23157
+ workspacePolicyOf(a.workspacePolicy),
23158
+ String(maxTurnsOf(a.maxTurns) ?? ""),
23159
+ String(timeoutSecondsOf(a.timeoutSeconds) ?? "")
23160
+ ]);
23161
+ builtinFingerprint = (a) => fingerprint(a);
23162
+ rowFingerprint = (r) => fingerprint(r);
23163
+ legacyRowFingerprint = (r) => legacyFingerprint(r.name, r.description, r.instructions, toolsOf(r.tools) ?? []);
23164
+ QA_SAFETY_POLICY = "disable-unedited-v1";
23165
+ }
23166
+ });
23167
+
23168
+ // src/services/custom-agents.ts
23169
+ var custom_agents_exports = {};
23170
+ __export(custom_agents_exports, {
23171
+ agentDefsFor: () => agentDefsFor,
23172
+ collectChangedFiles: () => collectChangedFiles,
23173
+ currentCustomAgentRuntimeSupport: () => currentCustomAgentRuntimeSupport,
23174
+ installCustomAgents: () => installCustomAgents,
23175
+ loadCustomAgents: () => loadCustomAgents,
23176
+ refreshCustomAgentRuntimeSupport: () => refreshCustomAgentRuntimeSupport,
23177
+ selectAgentRows: () => selectAgentRows,
23178
+ toDef: () => toDef,
23179
+ unknownMentions: () => unknownMentions,
23180
+ validateGraph: () => validateGraph
23181
+ });
23182
+ function smartBuiltinSelected(row, context) {
23183
+ switch (row.name) {
23184
+ case "scout":
23185
+ return hasPhase(context, "Plan") || hasPhase(context, "Execute") || hasPhase(context, "Audit") || context.changedFiles.length === 0;
23186
+ case "blast-radius":
23187
+ return hasPhase(context, "Execute") || hasPhase(context, "Audit") || context.changedFiles.length > 0;
23188
+ case "security-reviewer":
23189
+ return (hasPhase(context, "Execute") || hasPhase(context, "Audit")) && touchesExternalInput(context);
23190
+ case "spec-auditor":
23191
+ return hasPhase(context, "Plan") || hasPhase(context, "Execute");
23192
+ case "dep-auditor":
23193
+ return touchesDependency(context.changedFiles);
23194
+ case "root-causer":
23195
+ return hasPhase(context, "Audit");
23196
+ case "qa-verifier":
23197
+ return context.runtime === "claude" && hasPhase(context, "Execute") && workspacePolicyOf(row.workspacePolicy) === "isolated-worktree" && touchesExecutableWork(context.changedFiles);
23198
+ case "edge-case-hunter":
23199
+ return context.runtime === "claude" && hasPhase(context, "Execute") && workspacePolicyOf(row.workspacePolicy) === "isolated-worktree";
23200
+ default:
23201
+ return true;
23202
+ }
23203
+ }
23204
+ function selectAgentRows(rows, context) {
23205
+ return rows.filter((row) => {
23206
+ if (!row.enabled) return false;
23207
+ const runtime = runtimeOf(row.runtime);
23208
+ if (runtime !== null && runtime !== context.runtime) return false;
23209
+ if (workspacePolicyOf(row.workspacePolicy) === "isolated-worktree" && context.runtime !== "claude") return false;
23210
+ if (activationOf(row.activation) === "always") return true;
23211
+ const builtin = row.projectId === null && BUILTIN_AGENT_NAMES.includes(row.name);
23212
+ return builtin ? smartBuiltinSelected(row, context) : true;
23213
+ });
23214
+ }
23215
+ function currentCustomAgentRuntimeSupport() {
23216
+ return { ...codexNativeSupport };
23217
+ }
23218
+ function toDef(r) {
23219
+ return {
23220
+ id: r.id,
23221
+ name: r.name,
23222
+ description: r.description,
23223
+ instructions: r.instructions,
23224
+ tools: toolsOf(r.tools),
23225
+ model: r.model,
23226
+ mentions: mentionsOf(r.mentions),
23227
+ activation: activationOf(r.activation),
23228
+ effort: effortOf(r.effort),
23229
+ workspacePolicy: workspacePolicyOf(r.workspacePolicy),
23230
+ maxTurns: maxTurnsOf(r.maxTurns),
23231
+ timeoutSeconds: timeoutSecondsOf(r.timeoutSeconds)
23232
+ };
23233
+ }
23234
+ function recommendedModel(row, runtime) {
23235
+ if (row.model) return row.model;
23236
+ if (row.projectId !== null) return null;
23237
+ const builtin = BUILTIN_AGENTS.find((agent) => agent.name === row.name);
23238
+ if (!builtin) return null;
23239
+ const model = builtin.models[runtime];
23240
+ if (runtime === "claude" && (model === "haiku" || model === "sonnet")) return model;
23241
+ return modelsForRuntime(runtime).some((entry) => entry.id === model) ? model : null;
23242
+ }
23243
+ function toRuntimeDef(row, runtime) {
23244
+ return { ...toDef(row), model: recommendedModel(row, runtime) };
23245
+ }
23246
+ async function loadCustomAgents() {
23247
+ try {
23248
+ cache4 = await prisma.customAgent.findMany();
23249
+ const projects = await prisma.project.findMany({ select: { id: true, repoDir: true } });
23250
+ const bindings = await prisma.localBinding.findMany({ select: { projectId: true, repoDir: true } });
23251
+ const next = /* @__PURE__ */ new Map();
23252
+ for (const p3 of projects) next.set(p3.id, p3.repoDir ?? null);
23253
+ for (const b of bindings) next.set(b.projectId, b.repoDir ?? null);
23254
+ repoDirCache = next;
23255
+ } catch {
23256
+ cache4 = [];
23257
+ repoDirCache = /* @__PURE__ */ new Map();
23258
+ }
23259
+ }
23260
+ function agentDefsFor(contextOrProjectId, legacyAgent) {
23261
+ const legacy = typeof contextOrProjectId === "string";
23262
+ const context = legacy ? {
23263
+ projectId: contextOrProjectId,
23264
+ runtime: legacyAgent ?? "claude",
23265
+ cwd: "",
23266
+ changedFiles: []
23267
+ } : contextOrProjectId;
23268
+ const { projectId } = context;
23269
+ const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
23270
+ const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
23271
+ const effectiveIds = new Set(effectiveAgents(globals, project).map((agent) => agent.id));
23272
+ const effectiveRows = cache4.filter((row) => effectiveIds.has(row.id));
23273
+ const eff = legacy ? effectiveRows.filter((row) => row.enabled && (runtimeOf(row.runtime) === null || runtimeOf(row.runtime) === context.runtime) && !(workspacePolicyOf(row.workspacePolicy) === "isolated-worktree" && context.runtime !== "claude")) : selectAgentRows(effectiveRows, context);
23274
+ const needsCatalog = eff.some((row) => (toolsOf(row.tools) ?? []).includes(ALL_TOOLS));
23275
+ const catalogIds = needsCatalog ? agentToolIds(repoDirCache.get(projectId) ?? null) : [];
23276
+ return eff.map((row) => {
23277
+ const a = asCustomAgent(row);
23278
+ return {
23279
+ ...toRuntimeDef(row, context.runtime),
23280
+ // Ekspansi terjadi DI SINI, sebelum `resolveTools` di runner: meneruskan `"*"` apa adanya
23281
+ // membuat claude membuangnya senyap (agen tanpa alat), sementara menerjemahkannya jadi `null`
23282
+ // membuat agen mewarisi SELURUH tool termasuk `Task` — lapis 2 anti-loop lenyap tanpa jejak.
23283
+ tools: expandTools(a.tools, catalogIds),
23284
+ mentions: a.mentions ?? []
23285
+ };
23286
+ });
23287
+ }
23288
+ function validateGraph(rows) {
23289
+ const projectScopes = [...new Set(rows.map((r) => r.projectId).filter((p3) => p3 !== null))];
23290
+ const globals = rows.filter((r) => r.projectId === null).map(asCustomAgent);
23291
+ for (const scope of [null, ...projectScopes]) {
23292
+ const project = scope === null ? [] : rows.filter((r) => r.projectId === scope).map(asCustomAgent);
23293
+ const nodes = effectiveAgents(globals, project).map((a) => ({ name: a.name, mentions: a.mentions ?? [] }));
23294
+ const cycle = detectCycle(nodes);
23295
+ if (cycle) return { scope: scope ?? GLOBAL_SCOPE, cycle };
23296
+ }
23297
+ return null;
23298
+ }
23299
+ function unknownMentions(row, all) {
23300
+ const visible = new Set(
23301
+ all.filter((r) => r.projectId === null || row.projectId !== null && r.projectId === row.projectId).map((r) => r.name)
23302
+ );
23303
+ return mentionsOf(row.mentions).filter((m) => !visible.has(m));
23304
+ }
23305
+ async function refreshCustomAgentRuntimeSupport(probe = defaultCodexSupportProbe) {
23306
+ const refresh = codexSupportRefreshTail.then(async () => {
23307
+ const version = await probe();
23308
+ codexNativeSupport = { version, ok: codexNativeAgentsSupported(version) };
23309
+ });
23310
+ codexSupportRefreshTail = refresh.catch(() => {
23311
+ });
23312
+ return refresh;
23313
+ }
23314
+ async function installCustomAgents() {
23315
+ await seedBuiltinAgents();
23316
+ await loadCustomAgents();
23317
+ await refreshCustomAgentRuntimeSupport();
23318
+ registerCodexNativeAgentSupport(() => codexNativeSupport);
23319
+ registerCustomAgentSource((context) => agentDefsFor(context));
23320
+ if (!codexSupportRefreshTimer) {
23321
+ codexSupportRefreshTimer = setInterval(() => {
23322
+ void refreshCustomAgentRuntimeSupport().catch((error) => console.error("custom agent: probe Codex gagal:", error));
23323
+ }, 5 * 6e4);
23324
+ codexSupportRefreshTimer.unref();
23325
+ }
23326
+ }
23327
+ var phasesOf, hasPhase, touchesDependency, touchesExternalInput, touchesExecutableWork, cache4, codexNativeSupport, codexSupportRefreshTimer, codexSupportRefreshTail, repoDirCache, asCustomAgent, defaultCodexSupportProbe;
23328
+ var init_custom_agents2 = __esm({
23329
+ "src/services/custom-agents.ts"() {
23330
+ "use strict";
23331
+ init_db();
23332
+ init_src();
23333
+ init_src2();
23334
+ init_pty();
23335
+ init_agent_tool_catalog();
23336
+ init_builtin_agents2();
23337
+ init_codex_version();
23338
+ phasesOf = (flow) => flow ? PIPELINES[flow] : [];
23339
+ hasPhase = (context, name2) => phasesOf(context.flow).includes(name2);
23340
+ 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));
23341
+ touchesExternalInput = (context) => {
23342
+ const surface = [context.prompt ?? "", ...context.changedFiles].join("\n");
23343
+ return /(?:^|[\W_/.-])(route|routes|handler|auth|oauth|api|cli|config|filesystem|upload|webhook|input)(?:$|[\W_/.-])/i.test(surface);
23344
+ };
23345
+ 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));
23346
+ cache4 = [];
23347
+ codexNativeSupport = { version: null, ok: false };
23348
+ codexSupportRefreshTimer = null;
23349
+ codexSupportRefreshTail = Promise.resolve();
23350
+ repoDirCache = /* @__PURE__ */ new Map();
23351
+ asCustomAgent = (r) => ({
23352
+ id: r.id,
23353
+ projectId: r.projectId,
23354
+ name: r.name,
23355
+ description: r.description,
23356
+ instructions: r.instructions,
23357
+ tools: toolsOf(r.tools),
23358
+ model: r.model,
23359
+ mentions: mentionsOf(r.mentions),
23360
+ runtime: runtimeOf(r.runtime),
23361
+ activation: activationOf(r.activation),
23362
+ effort: effortOf(r.effort),
23363
+ workspacePolicy: workspacePolicyOf(r.workspacePolicy),
23364
+ maxTurns: maxTurnsOf(r.maxTurns),
23365
+ timeoutSeconds: timeoutSecondsOf(r.timeoutSeconds),
23366
+ enabled: r.enabled,
23367
+ createdAt: "",
23368
+ updatedAt: ""
23369
+ // tak dipakai lapis ini
23370
+ });
23371
+ defaultCodexSupportProbe = async () => {
23372
+ _resetCodexVersionCache();
23373
+ return getCodexVersion();
23374
+ };
23375
+ }
23376
+ });
23377
+
22346
23378
  // src/services/config-apply.ts
22347
23379
  var config_apply_exports = {};
22348
23380
  __export(config_apply_exports, {
@@ -22370,6 +23402,10 @@ async function applyConfigSideEffect(key) {
22370
23402
  return;
22371
23403
  }
22372
23404
  if (configEntry(key)?.inheritEnv) mirrorInheritEnv(key);
23405
+ if (key === "HANOMAN_CODEX_BIN") {
23406
+ const { refreshCustomAgentRuntimeSupport: refreshCustomAgentRuntimeSupport2 } = await Promise.resolve().then(() => (init_custom_agents2(), custom_agents_exports));
23407
+ await refreshCustomAgentRuntimeSupport2();
23408
+ }
22373
23409
  }
22374
23410
  async function rotateSyncOrigin(input) {
22375
23411
  const url2 = new URL(input);
@@ -24854,14 +25890,14 @@ var require_stream_consumer = __commonJS({
24854
25890
  "../node_modules/.pnpm/@fastify+multipart@10.1.0/node_modules/@fastify/multipart/lib/stream-consumer.js"(exports, module) {
24855
25891
  "use strict";
24856
25892
  module.exports = function streamToNull(stream) {
24857
- return new Promise((resolve21, reject2) => {
25893
+ return new Promise((resolve22, reject2) => {
24858
25894
  stream.on("data", () => {
24859
25895
  });
24860
25896
  stream.on("close", () => {
24861
- resolve21();
25897
+ resolve22();
24862
25898
  });
24863
25899
  stream.on("end", () => {
24864
- resolve21();
25900
+ resolve22();
24865
25901
  });
24866
25902
  stream.on("error", (error) => {
24867
25903
  reject2(error);
@@ -25310,10 +26346,10 @@ var require_multipart2 = __commonJS({
25310
26346
  }
25311
26347
  };
25312
26348
  const parts = () => {
25313
- return new Promise((resolve21, reject2) => {
26349
+ return new Promise((resolve22, reject2) => {
25314
26350
  handle((val) => {
25315
26351
  if (val instanceof Error) return reject2(val);
25316
- resolve21(val);
26352
+ resolve22(val);
25317
26353
  });
25318
26354
  });
25319
26355
  };
@@ -25494,7 +26530,7 @@ var require_multipart2 = __commonJS({
25494
26530
  parts = this.parts(options3);
25495
26531
  }
25496
26532
  this.savedRequestFiles = [];
25497
- const tmpdir6 = options3?.tmpdir || os.tmpdir();
26533
+ const tmpdir7 = options3?.tmpdir || os.tmpdir();
25498
26534
  this.tmpUploads = [];
25499
26535
  let i = 0;
25500
26536
  for await (const part of parts) {
@@ -25502,7 +26538,7 @@ var require_multipart2 = __commonJS({
25502
26538
  if (!part.file) {
25503
26539
  continue;
25504
26540
  }
25505
- const filepath = path.join(tmpdir6, generateId() + path.extname(part.filename || "file" + i++));
26541
+ const filepath = path.join(tmpdir7, generateId() + path.extname(part.filename || "file" + i++));
25506
26542
  const target2 = createWriteStream2(filepath);
25507
26543
  try {
25508
26544
  this.tmpUploads.push(filepath);
@@ -25679,15 +26715,15 @@ init_stage_machine();
25679
26715
  init_src();
25680
26716
  import { execFile as execFile2 } from "node:child_process";
25681
26717
  import { promisify } from "node:util";
25682
- import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
26718
+ import { existsSync as existsSync6, rmSync as rmSync3 } from "node:fs";
25683
26719
  import { resolve as resolve10 } from "node:path";
25684
26720
 
25685
26721
  // src/services/safe-repo-path.ts
25686
26722
  import { constants as constants2 } from "node:fs";
25687
26723
  import { lstat, mkdir as mkdir2, open as open2, realpath, rename, unlink as unlink2 } from "node:fs/promises";
25688
- import { isAbsolute as isAbsolute4, join as join7, relative, resolve as resolve9, sep as sep2 } from "node:path";
26724
+ import { isAbsolute as isAbsolute4, join as join10, relative, resolve as resolve9, sep as sep2 } from "node:path";
25689
26725
  import { randomUUID as randomUUID3 } from "node:crypto";
25690
- import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
26726
+ import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
25691
26727
  var PathContainmentError = class extends Error {
25692
26728
  code = "PATH_CONTAINMENT";
25693
26729
  };
@@ -25715,7 +26751,7 @@ async function ensureRepoParents(root, rel) {
25715
26751
  const parts = components(rel).slice(0, -1);
25716
26752
  let current = base2;
25717
26753
  for (const part of parts) {
25718
- current = join7(current, part);
26754
+ current = join10(current, part);
25719
26755
  try {
25720
26756
  await mkdir2(current, { mode: 448 });
25721
26757
  } catch (error) {
@@ -25732,7 +26768,7 @@ function ensureRepoParentsSync(root, rel) {
25732
26768
  const parts = components(rel).slice(0, -1);
25733
26769
  let current = base2;
25734
26770
  for (const part of parts) {
25735
- current = join7(current, part);
26771
+ current = join10(current, part);
25736
26772
  try {
25737
26773
  mkdirSync6(current, { mode: 448 });
25738
26774
  } catch (error) {
@@ -25748,7 +26784,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
25748
26784
  const parts = components(rel);
25749
26785
  let current = base2;
25750
26786
  for (let i = 0; i < parts.length; i++) {
25751
- current = join7(current, parts[i]);
26787
+ current = join10(current, parts[i]);
25752
26788
  const stat4 = await lstat(current).catch((error) => {
25753
26789
  if (error.code === "ENOENT" && opts.allowMissingTail) return null;
25754
26790
  if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
@@ -25769,7 +26805,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
25769
26805
  const parts = components(rel);
25770
26806
  let current = base2;
25771
26807
  for (let i = 0; i < parts.length; i++) {
25772
- current = join7(current, parts[i]);
26808
+ current = join10(current, parts[i]);
25773
26809
  try {
25774
26810
  const stat4 = lstatSync(current);
25775
26811
  if (stat4.isSymbolicLink()) denied("symlink");
@@ -25799,10 +26835,10 @@ function writeRepoFileAtomicSync(root, rel, data) {
25799
26835
  const parent = resolve9(path, "..");
25800
26836
  const parentRel = relative(realpathSync2(root), parent);
25801
26837
  if (parentRel) assertSafeRepoPathSync(root, parentRel);
25802
- const temp = join7(parent, `.hanoman-${randomUUID3()}.tmp`);
26838
+ const temp = join10(parent, `.hanoman-${randomUUID3()}.tmp`);
25803
26839
  const fd = openSync(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
25804
26840
  try {
25805
- writeFileSync4(fd, data);
26841
+ writeFileSync6(fd, data);
25806
26842
  } finally {
25807
26843
  closeSync(fd);
25808
26844
  }
@@ -25846,7 +26882,7 @@ async function writeRepoFileAtomic(root, rel, data) {
25846
26882
  await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
25847
26883
  if (error.code !== "EEXIST") throw error;
25848
26884
  });
25849
- const temp = join7(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
26885
+ const temp = join10(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
25850
26886
  const handle = await open2(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
25851
26887
  try {
25852
26888
  await handle.writeFile(data);
@@ -25945,7 +26981,7 @@ function writeDocFile(repoDir, rel, content) {
25945
26981
  function deleteDocFile(repoDir, rel) {
25946
26982
  const abs = docAbsPath(repoDir, rel);
25947
26983
  if (!existsSync6(abs)) return false;
25948
- rmSync2(abs);
26984
+ rmSync3(abs);
25949
26985
  return true;
25950
26986
  }
25951
26987
 
@@ -26003,9 +27039,9 @@ async function toProjectView(p3, sessions, devices2) {
26003
27039
  const specs = await prisma.spec.findMany({ where: { projectId: p3.id } });
26004
27040
  const { coverage } = await scanRepoDocs(await resolveRepoDir(p3.id));
26005
27041
  const binding = await getBinding(p3.id);
26006
- const open4 = specs.filter((s2) => s2.stage !== "done");
27042
+ const open5 = specs.filter((s2) => s2.stage !== "done");
26007
27043
  const { session, commit } = sessionOf(p3.id, sessions);
26008
- const topStage = open4.length ? open4.map((s2) => s2.stage).sort((a, b) => STAGES.indexOf(b) - STAGES.indexOf(a))[0] : "spec";
27044
+ const topStage = open5.length ? open5.map((s2) => s2.stage).sort((a, b) => STAGES.indexOf(b) - STAGES.indexOf(a))[0] : "spec";
26009
27045
  return {
26010
27046
  id: p3.id,
26011
27047
  name: p3.name,
@@ -26018,7 +27054,7 @@ async function toProjectView(p3, sessions, devices2) {
26018
27054
  docStatus: docStatusFor(coverage),
26019
27055
  coverage,
26020
27056
  createdAt: p3.createdAt.toISOString(),
26021
- backlog: open4.length,
27057
+ backlog: open5.length,
26022
27058
  topStage,
26023
27059
  session,
26024
27060
  activity: session.status === "running" ? `running \xB7 ${session.flow ?? "sesi"}` : "idle",
@@ -26037,31 +27073,13 @@ async function toProjectView(p3, sessions, devices2) {
26037
27073
  };
26038
27074
  }
26039
27075
 
26040
- // src/services/sync-notify.ts
26041
- init_config2();
26042
- init_outbox();
26043
- init_sync();
26044
- async function notifySynced(entity, id) {
26045
- try {
26046
- if (!isEntity(entity)) return;
26047
- await consumeTombstoneOnRecreate(entity, id);
26048
- if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
26049
- else await publishLocal(entity, id);
26050
- } catch {
26051
- }
26052
- }
26053
- async function notifyDeleted(entity, id) {
26054
- try {
26055
- if (!isEntity(entity)) return;
26056
- if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
26057
- else await publishDelete(entity, id);
26058
- } catch {
26059
- }
26060
- }
27076
+ // src/routes/projects.ts
27077
+ init_sync_notify();
26061
27078
 
26062
27079
  // src/services/sync-delete.ts
26063
27080
  init_sync();
26064
27081
  init_tombstone();
27082
+ init_sync_notify();
26065
27083
  init_outbox();
26066
27084
  async function deleteSynced(entity, id, deviceId) {
26067
27085
  const snap = await snapshot(entity, id);
@@ -26305,8 +27323,8 @@ import { existsSync as existsSync9 } from "node:fs";
26305
27323
  // src/services/integrate.ts
26306
27324
  import { execFile as execFile4 } from "node:child_process";
26307
27325
  import { promisify as promisify3 } from "node:util";
26308
- import { rmSync as rmSync3 } from "node:fs";
26309
- import { join as join8 } from "node:path";
27326
+ import { rmSync as rmSync4 } from "node:fs";
27327
+ import { join as join11 } from "node:path";
26310
27328
  var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
26311
27329
  var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
26312
27330
  var exec3 = promisify3(execFile4);
@@ -26342,7 +27360,7 @@ async function resolveTarget(repoDir, target2) {
26342
27360
  async function reclaim(repoDir, wt) {
26343
27361
  await sh(repoDir, ["worktree", "remove", "--force", wt]);
26344
27362
  await sh(repoDir, ["worktree", "prune"]);
26345
- rmSync3(wt, { recursive: true, force: true });
27363
+ rmSync4(wt, { recursive: true, force: true });
26346
27364
  }
26347
27365
  async function integrateBranch(repoDir, src, op, target2) {
26348
27366
  const source = await resolveSource(repoDir, src.branch);
@@ -26350,7 +27368,7 @@ async function integrateBranch(repoDir, src, op, target2) {
26350
27368
  const tgt = await resolveTarget(repoDir, target2);
26351
27369
  if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
26352
27370
  await sh(repoDir, ["fetch", "origin"]);
26353
- const wt = join8(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
27371
+ const wt = join11(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
26354
27372
  await reclaim(repoDir, wt);
26355
27373
  const baseRef = op === "merge" ? tgt.ref : source;
26356
27374
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
@@ -26411,7 +27429,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
26411
27429
  const src = await resolveGraphSource(repoDir, source);
26412
27430
  if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
26413
27431
  await sh(repoDir, ["fetch", "origin"]);
26414
- const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
27432
+ const wt = join11(repoDir, ".worktrees", `merge-${sanitize(current)}`);
26415
27433
  await reclaim(repoDir, wt);
26416
27434
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
26417
27435
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -26449,7 +27467,7 @@ async function replayCurrent(repoDir, source, cmd) {
26449
27467
  const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
26450
27468
  if (!current || current === "HEAD")
26451
27469
  return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
26452
- const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
27470
+ const wt = join11(repoDir, ".worktrees", `merge-${sanitize(current)}`);
26453
27471
  await reclaim(repoDir, wt);
26454
27472
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
26455
27473
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -26497,16 +27515,16 @@ init_db();
26497
27515
  import { execFile as execFile5 } from "node:child_process";
26498
27516
  import { promisify as promisify4 } from "node:util";
26499
27517
  import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
26500
- import { tmpdir as tmpdir2 } from "node:os";
26501
- import { join as join9, resolve as resolve11 } from "node:path";
27518
+ import { tmpdir as tmpdir3 } from "node:os";
27519
+ import { join as join12, resolve as resolve11 } from "node:path";
26502
27520
  var exec4 = promisify4(execFile5);
26503
27521
  var GIT3 = { maxBuffer: 1 << 24 };
26504
27522
  var MAX = 256 * 1024;
26505
- var worktreeDir = (repoDir, specId) => join9(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
27523
+ var worktreeDir = (repoDir, specId) => join12(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
26506
27524
  async function withTempIndex(wt, fn) {
26507
27525
  const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
26508
- const dir2 = await mkdtemp(join9(tmpdir2(), "hanoman-idx-"));
26509
- const tmp = join9(dir2, "index");
27526
+ const dir2 = await mkdtemp(join12(tmpdir3(), "hanoman-idx-"));
27527
+ const tmp = join12(dir2, "index");
26510
27528
  await copyFile(resolve11(wt, idx), tmp);
26511
27529
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
26512
27530
  try {
@@ -26705,7 +27723,7 @@ function appendSourceHistory(current, entry) {
26705
27723
  // src/services/spec-reset.ts
26706
27724
  import { execFile as execFile8 } from "node:child_process";
26707
27725
  import { existsSync as existsSync8 } from "node:fs";
26708
- import { join as join11 } from "node:path";
27726
+ import { join as join14 } from "node:path";
26709
27727
  import { promisify as promisify7 } from "node:util";
26710
27728
 
26711
27729
  // src/services/stage-artifacts.ts
@@ -26754,7 +27772,7 @@ init_db();
26754
27772
  import { execFile as execFile6 } from "node:child_process";
26755
27773
  import { readdir, rm as rm3 } from "node:fs/promises";
26756
27774
  import { promisify as promisify5 } from "node:util";
26757
- import { join as join10, resolve as resolve12 } from "node:path";
27775
+ import { join as join13, resolve as resolve12 } from "node:path";
26758
27776
  init_notifications2();
26759
27777
  var TICK_MS = 6e4;
26760
27778
  var trashDirOf = (repoDir) => resolve12(repoDir, ".worktrees", ".trash");
@@ -26814,7 +27832,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
26814
27832
  }
26815
27833
  let removed = 0;
26816
27834
  for (const entry of entries3) {
26817
- const path = join10(dir2, entry);
27835
+ const path = join13(dir2, entry);
26818
27836
  const known = pending.get(path);
26819
27837
  const row = known ?? { path, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
26820
27838
  pending.set(path, row);
@@ -27370,7 +28388,7 @@ async function planSpecReset(spec) {
27370
28388
  const repoDir = await resolveRepoDir(spec.projectId);
27371
28389
  if (!repoDir) return EMPTY;
27372
28390
  const sid = sessionIdForSpec(spec.id);
27373
- const wt = join11(repoDir, ".worktrees", sid);
28391
+ const wt = join14(repoDir, ".worktrees", sid);
27374
28392
  const branch = `hanoman/${sid}`;
27375
28393
  const hasBranch = await shaResolvable(repoDir, `refs/heads/${branch}`);
27376
28394
  return {
@@ -27401,6 +28419,7 @@ async function applySpecReset(spec, plan) {
27401
28419
 
27402
28420
  // src/routes/specs.ts
27403
28421
  init_notifications2();
28422
+ init_sync_notify();
27404
28423
 
27405
28424
  // src/services/spec-complete.ts
27406
28425
  init_db();
@@ -27408,6 +28427,7 @@ init_notifications2();
27408
28427
 
27409
28428
  // src/services/session-result.ts
27410
28429
  init_db();
28430
+ init_sync_notify();
27411
28431
  import { randomUUID as randomUUID4 } from "node:crypto";
27412
28432
  var WHITELIST = [
27413
28433
  "projectId",
@@ -27433,6 +28453,7 @@ async function recordSessionResult(input) {
27433
28453
  }
27434
28454
 
27435
28455
  // src/services/spec-complete.ts
28456
+ init_sync_notify();
27436
28457
  async function completeSpecManually(spec, input) {
27437
28458
  const at = input.at ?? /* @__PURE__ */ new Date();
27438
28459
  const manualDone = {
@@ -27468,7 +28489,7 @@ import { extname } from "node:path";
27468
28489
  import { randomUUID as randomUUID5 } from "node:crypto";
27469
28490
  import { spawn as spawn2 } from "node:child_process";
27470
28491
  import { mkdir as mkdir3, rename as rename2, unlink as unlink3, writeFile as writeFile2 } from "node:fs/promises";
27471
- import { basename as basename3, isAbsolute as isAbsolute5, join as join12 } from "node:path";
28492
+ import { basename as basename3, isAbsolute as isAbsolute5, join as join15 } from "node:path";
27472
28493
 
27473
28494
  // ../node_modules/.pnpm/strtok3@10.3.5/node_modules/strtok3/lib/stream/Errors.js
27474
28495
  var defaultMessages = "End-Of-Stream";
@@ -29581,7 +30602,7 @@ function readByobReaderWithSignal(reader, buffer, signal) {
29581
30602
  return reader.read(buffer);
29582
30603
  }
29583
30604
  signal.throwIfAborted();
29584
- return new Promise((resolve21, reject2) => {
30605
+ return new Promise((resolve22, reject2) => {
29585
30606
  const cleanup = () => {
29586
30607
  signal.removeEventListener("abort", onAbort);
29587
30608
  };
@@ -29601,7 +30622,7 @@ function readByobReaderWithSignal(reader, buffer, signal) {
29601
30622
  try {
29602
30623
  const result = await reader.read(buffer);
29603
30624
  cleanup();
29604
- resolve21(result);
30625
+ resolve22(result);
29605
30626
  } catch (error) {
29606
30627
  cleanup();
29607
30628
  reject2(error);
@@ -31360,11 +32381,11 @@ var UploadError = class extends Error {
31360
32381
  }
31361
32382
  };
31362
32383
  function timeout(promise, ms, code) {
31363
- return new Promise((resolve21, reject2) => {
32384
+ return new Promise((resolve22, reject2) => {
31364
32385
  const timer9 = setTimeout(() => reject2(new UploadError(code, "upload operation timed out")), ms);
31365
32386
  promise.then((value) => {
31366
32387
  clearTimeout(timer9);
31367
- resolve21(value);
32388
+ resolve22(value);
31368
32389
  }, (error) => {
31369
32390
  clearTimeout(timer9);
31370
32391
  reject2(error);
@@ -31387,7 +32408,7 @@ function scannerFromEnv(path) {
31387
32408
  return Promise.resolve();
31388
32409
  }
31389
32410
  if (!isAbsolute5(command)) return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner path must be absolute"));
31390
- return new Promise((resolve21, reject2) => {
32411
+ return new Promise((resolve22, reject2) => {
31391
32412
  const child = spawn2(command, [path], { shell: false, stdio: "ignore" });
31392
32413
  const timer9 = setTimeout(() => {
31393
32414
  child.kill("SIGKILL");
@@ -31399,27 +32420,27 @@ function scannerFromEnv(path) {
31399
32420
  });
31400
32421
  child.once("exit", (code) => {
31401
32422
  clearTimeout(timer9);
31402
- if (code === 0) resolve21();
32423
+ if (code === 0) resolve22();
31403
32424
  else reject2(new UploadError("UPLOAD_SCAN", `scanner exit ${code}`));
31404
32425
  });
31405
32426
  });
31406
32427
  }
31407
- function safeFilename(input, extension2) {
32428
+ function safeFilename2(input, extension2) {
31408
32429
  const stem = basename3(input).replace(/\.[^.]*$/, "").replace(/[^a-zA-Z0-9._ -]/g, "_").slice(0, 180).trim();
31409
32430
  return `${stem || "upload"}${extension2}`;
31410
32431
  }
31411
32432
  async function commitToStorage(buffer, extension2, deps, beforePromote) {
31412
32433
  const storageDir = deps.storageDir ?? uploadDir();
31413
- const quarantineDir = join12(storageDir, ".quarantine");
32434
+ const quarantineDir = join15(storageDir, ".quarantine");
31414
32435
  await mkdir3(quarantineDir, { recursive: true, mode: 448 });
31415
32436
  await mkdir3(storageDir, { recursive: true, mode: 448 });
31416
- const quarantine = join12(quarantineDir, `${randomUUID5()}.upload`);
32437
+ const quarantine = join15(quarantineDir, `${randomUUID5()}.upload`);
31417
32438
  const storageKey = `${randomUUID5()}${extension2}`;
31418
32439
  await writeFile2(quarantine, buffer, { mode: 384, flag: "wx" });
31419
32440
  try {
31420
32441
  await timeout((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
31421
32442
  await beforePromote?.();
31422
- await rename2(quarantine, join12(storageDir, storageKey));
32443
+ await rename2(quarantine, join15(storageDir, storageKey));
31423
32444
  } catch (error) {
31424
32445
  await unlink3(quarantine).catch(() => {
31425
32446
  });
@@ -31466,7 +32487,7 @@ async function processUpload(input, deps = {}) {
31466
32487
  });
31467
32488
  return {
31468
32489
  storageKey,
31469
- filename: safeFilename(input.clientName, type.extension),
32490
+ filename: safeFilename2(input.clientName, type.extension),
31470
32491
  mimeType,
31471
32492
  extension: type.extension,
31472
32493
  size: normalized.byteLength,
@@ -31506,7 +32527,7 @@ async function processDocumentUpload(input, deps = {}) {
31506
32527
  const storageKey = await commitToStorage(input.buffer, extension2, deps);
31507
32528
  return {
31508
32529
  storageKey,
31509
- filename: safeFilename(input.clientName, extension2),
32530
+ filename: safeFilename2(input.clientName, extension2),
31510
32531
  mimeType: input.clientMime,
31511
32532
  extension: extension2,
31512
32533
  size: input.buffer.byteLength
@@ -31620,10 +32641,10 @@ async function dropSpecAttachments(specId) {
31620
32641
  // src/services/spec-attachment-dir.ts
31621
32642
  init_db();
31622
32643
  import { mkdir as mkdir4, readdir as readdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
31623
- import { join as join13 } from "node:path";
32644
+ import { join as join16 } from "node:path";
31624
32645
  init_session_id();
31625
32646
  init_uploads();
31626
- var specAttachmentsDir = (repoDir, sessionId2) => join13(repoDir, ".worktrees", ".attachments", sessionId2);
32647
+ var specAttachmentsDir = (repoDir, sessionId2) => join16(repoDir, ".worktrees", ".attachments", sessionId2);
31627
32648
  var INDEX = "INDEX.md";
31628
32649
  var humanSize = (n2) => n2 >= 1024 * 1024 ? `${(n2 / 1024 / 1024).toFixed(1)} MB` : n2 >= 1024 ? `${Math.round(n2 / 1024)} KB` : `${n2} B`;
31629
32650
  function uniqueName(taken, filename) {
@@ -31673,19 +32694,19 @@ async function syncSpecAttachmentsDir(specId, projectId) {
31673
32694
  for (const a of rows) {
31674
32695
  let bytes;
31675
32696
  try {
31676
- bytes = await readFile3(join13(uploadDir(), a.storageKey));
32697
+ bytes = await readFile3(join16(uploadDir(), a.storageKey));
31677
32698
  } catch {
31678
32699
  continue;
31679
32700
  }
31680
32701
  const filename = uniqueName(taken, a.filename);
31681
- const path = join13(dir2, filename);
32702
+ const path = join16(dir2, filename);
31682
32703
  await writeFile3(path, bytes, { mode: 384 });
31683
32704
  items.push({ filename, mimeType: a.mimeType, size: a.size, path });
31684
32705
  }
31685
- await writeFile3(join13(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
32706
+ await writeFile3(join16(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
31686
32707
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
31687
32708
  for (const name2 of await readdir3(dir2)) {
31688
- if (!keep.has(name2)) await rm4(join13(dir2, name2), { recursive: true, force: true }).catch(() => {
32709
+ if (!keep.has(name2)) await rm4(join16(dir2, name2), { recursive: true, force: true }).catch(() => {
31689
32710
  });
31690
32711
  }
31691
32712
  return items;
@@ -34242,7 +35263,7 @@ function renderDocPdf(text, name2, meta) {
34242
35263
  });
34243
35264
  const chunks = [];
34244
35265
  doc.on("data", (c) => chunks.push(c));
34245
- const done = new Promise((resolve21) => doc.on("end", () => resolve21(Buffer.concat(chunks))));
35266
+ const done = new Promise((resolve22) => doc.on("end", () => resolve22(Buffer.concat(chunks))));
34246
35267
  doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
34247
35268
  doc.moveDown(0.25);
34248
35269
  doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
@@ -34317,6 +35338,7 @@ init_pty();
34317
35338
  init_session_phases();
34318
35339
  init_stage_machine();
34319
35340
  init_notifications2();
35341
+ init_sync_notify();
34320
35342
 
34321
35343
  // src/services/spec-head.ts
34322
35344
  init_db();
@@ -34337,11 +35359,12 @@ async function recordHeadSha(specId, worktree, read2 = readHead) {
34337
35359
  }
34338
35360
 
34339
35361
  // src/services/live-specs.ts
35362
+ var specNum = (id) => Number.parseInt(id.match(/\d+/)?.[0] ?? "0", 10);
34340
35363
  async function liveSpecs(filter = {}) {
34341
- const specs = await prisma.spec.findMany({
35364
+ const specs = (await prisma.spec.findMany({
34342
35365
  where: { projectId: filter.project, source: filter.source },
34343
35366
  orderBy: { id: "desc" }
34344
- });
35367
+ })).sort((a, b) => specNum(b.id) - specNum(a.id));
34345
35368
  const live = sessionPhasesBySpec();
34346
35369
  if (live.size === 0) return decorateBlocked(specs);
34347
35370
  const advanced = [];
@@ -35226,7 +36249,7 @@ async function deleteBranches(repoDir, names, opts) {
35226
36249
  import { execFile as execFile11 } from "node:child_process";
35227
36250
  import { realpathSync as realpathSync4 } from "node:fs";
35228
36251
  import { stat } from "node:fs/promises";
35229
- import { basename as basename4, join as join14, resolve as resolve15, sep as sep4 } from "node:path";
36252
+ import { basename as basename4, join as join17, resolve as resolve15, sep as sep4 } from "node:path";
35230
36253
  import { promisify as promisify10 } from "node:util";
35231
36254
  var exec10 = promisify10(execFile11);
35232
36255
  var GIT7 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
@@ -35271,7 +36294,7 @@ var real = (p3) => {
35271
36294
  };
35272
36295
  async function bornAt(path) {
35273
36296
  try {
35274
- const st = await stat(join14(path, ".git"));
36297
+ const st = await stat(join17(path, ".git"));
35275
36298
  const ms = st.birthtimeMs > 0 ? st.birthtimeMs : st.mtimeMs;
35276
36299
  return new Date(ms).toISOString();
35277
36300
  } catch {
@@ -35436,7 +36459,7 @@ init_session_id();
35436
36459
  // src/services/repo-fs.ts
35437
36460
  import { lstat as lstat2, rename as rename3, rm as rm5 } from "node:fs/promises";
35438
36461
  import { createWriteStream } from "node:fs";
35439
- import { join as join15 } from "node:path";
36462
+ import { join as join18 } from "node:path";
35440
36463
  import { randomUUID as randomUUID6 } from "node:crypto";
35441
36464
  import { pipeline } from "node:stream/promises";
35442
36465
  var EntryExistsError = class extends Error {
@@ -35497,7 +36520,7 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
35497
36520
  if (current && !opts.overwrite) return { status: "exists" };
35498
36521
  if (current && !current.isFile())
35499
36522
  throw new PathContainmentError("repository path ditolak: target bukan file regular");
35500
- const temp = join15(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
36523
+ const temp = join18(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
35501
36524
  try {
35502
36525
  await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
35503
36526
  if (opts.isTruncated?.()) {
@@ -35515,13 +36538,13 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
35515
36538
  // src/routes/ide.ts
35516
36539
  var activeSessions = (id) => listSessions().filter((s2) => s2.projectId === id && !s2.exited).length;
35517
36540
  async function lockInputs(id) {
35518
- const open4 = await prisma.spec.findMany({
36541
+ const open5 = await prisma.spec.findMany({
35519
36542
  where: { projectId: id, stage: { not: "done" } },
35520
36543
  select: { id: true }
35521
36544
  });
35522
36545
  const sessions = listSessions().filter((s2) => s2.projectId === id && !s2.exited).map((s2) => s2.branch || (s2.specId ? `hanoman/${s2.id}` : "")).filter(Boolean);
35523
36546
  return {
35524
- openSpecBranches: new Set(open4.map((s2) => sourceBranch(s2.id))),
36547
+ openSpecBranches: new Set(open5.map((s2) => sourceBranch(s2.id))),
35525
36548
  sessionBranches: new Set(sessions)
35526
36549
  };
35527
36550
  }
@@ -36013,14 +37036,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
36013
37036
  // src/routes/fs.ts
36014
37037
  import { readdir as readdir4 } from "node:fs/promises";
36015
37038
  import { homedir as homedir4 } from "node:os";
36016
- import { resolve as resolve17, dirname as dirname9, join as join16 } from "node:path";
37039
+ import { resolve as resolve17, dirname as dirname9, join as join19 } from "node:path";
36017
37040
  async function fs_default(app2) {
36018
37041
  app2.get("/fs/browse", async (req, reply) => {
36019
37042
  const q = req.query.path;
36020
37043
  const dir2 = q && q.trim() ? resolve17(q.trim()) : homedir4();
36021
37044
  try {
36022
37045
  const ents = await readdir4(dir2, { withFileTypes: true });
36023
- const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join16(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
37046
+ const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join19(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
36024
37047
  const parent = dirname9(dir2);
36025
37048
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries3 };
36026
37049
  } catch {
@@ -36502,14 +37525,14 @@ function installCommand(m, agent, shell = shellBin()) {
36502
37525
  }
36503
37526
 
36504
37527
  // src/services/terminal-diag.ts
36505
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync4 } from "node:fs";
36506
- import { join as join17 } from "node:path";
37528
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync5 } from "node:fs";
37529
+ import { join as join20 } from "node:path";
36507
37530
  var DIAG_MAX_BYTES = 2 * 1024 * 1024;
36508
37531
  var KINDS = /* @__PURE__ */ new Set(["key", "comp", "data", "ack", "pred"]);
36509
37532
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
36510
37533
  function diagFile(home3, sessionId2) {
36511
37534
  if (!ID.test(sessionId2)) throw new Error(`id sesi tak sah untuk diag: ${sessionId2}`);
36512
- return join17(home3, "diag", `${sessionId2}.jsonl`);
37535
+ return join20(home3, "diag", `${sessionId2}.jsonl`);
36513
37536
  }
36514
37537
  function usable(ev) {
36515
37538
  if (!ev || typeof ev !== "object") return false;
@@ -36520,9 +37543,9 @@ function appendDiag(home3, sessionId2, events) {
36520
37543
  const file = diagFile(home3, sessionId2);
36521
37544
  const rows = events.filter(usable);
36522
37545
  if (!rows.length) return;
36523
- mkdirSync9(join17(home3, "diag"), { recursive: true });
37546
+ mkdirSync9(join20(home3, "diag"), { recursive: true });
36524
37547
  try {
36525
- if (statSync3(file).size > DIAG_MAX_BYTES) rmSync4(file);
37548
+ if (statSync3(file).size > DIAG_MAX_BYTES) rmSync5(file);
36526
37549
  } catch {
36527
37550
  }
36528
37551
  appendFileSync2(file, rows.map((r) => JSON.stringify(r)).join("\n") + "\n");
@@ -37077,9 +38100,9 @@ init_pty();
37077
38100
  init_session_sandbox();
37078
38101
  import { execFile as execFile13 } from "node:child_process";
37079
38102
  import { randomUUID as randomUUID7 } from "node:crypto";
37080
- import { mkdirSync as mkdirSync10, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
37081
- import { tmpdir as tmpdir3 } from "node:os";
37082
- import { join as join18 } from "node:path";
38103
+ import { mkdirSync as mkdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
38104
+ import { tmpdir as tmpdir4 } from "node:os";
38105
+ import { join as join21 } from "node:path";
37083
38106
  var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
37084
38107
  function leadArgv(o) {
37085
38108
  if (o.agent === "codex") {
@@ -37100,22 +38123,22 @@ function leadArgv(o) {
37100
38123
  ];
37101
38124
  }
37102
38125
  var leadEnv = (agent, base2 = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base2 } : { ...base2 };
37103
- var shellQuote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
38126
+ var shellQuote3 = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
37104
38127
  function leadProcess(prompt, o, env = process.env) {
37105
38128
  const file = binFor(o.agent);
37106
38129
  const directArgs = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
37107
38130
  const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
37108
38131
  if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
37109
38132
  } };
37110
- const promptDir = join18(tmpdir3(), "hanoman-prompts");
38133
+ const promptDir = join21(tmpdir4(), "hanoman-prompts");
37111
38134
  mkdirSync10(promptDir, { recursive: true, mode: 448 });
37112
- const promptFile = join18(promptDir, `oneshot-${randomUUID7()}`);
37113
- writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
37114
- const workspace = o.cwd ?? join18(tmpdir3(), `hanoman-oneshot-${randomUUID7()}`);
38135
+ const promptFile = join21(promptDir, `oneshot-${randomUUID7()}`);
38136
+ writeFileSync7(promptFile, prompt, { flag: "wx", mode: 384 });
38137
+ const workspace = o.cwd ?? join21(tmpdir4(), `hanoman-oneshot-${randomUUID7()}`);
37115
38138
  if (!o.cwd) mkdirSync10(workspace, { recursive: false, mode: 448 });
37116
38139
  try {
37117
38140
  const argsWithoutPrompt = directArgs.slice(0, -1);
37118
- const command = [file, ...argsWithoutPrompt].map(shellQuote).join(" ") + ` "$(cat ${shellQuote(promptFile)})"`;
38141
+ const command = [file, ...argsWithoutPrompt].map(shellQuote3).join(" ") + ` "$(cat ${shellQuote3(promptFile)})"`;
37119
38142
  const sandbox = sandboxArgvFromEnv({
37120
38143
  command,
37121
38144
  worktree: workspace,
@@ -37129,13 +38152,13 @@ function leadProcess(prompt, o, env = process.env) {
37129
38152
  args: sandbox.slice(1),
37130
38153
  promptFile,
37131
38154
  cleanup: () => {
37132
- rmSync5(promptFile, { force: true });
37133
- if (!o.cwd) rmSync5(workspace, { recursive: true, force: true });
38155
+ rmSync6(promptFile, { force: true });
38156
+ if (!o.cwd) rmSync6(workspace, { recursive: true, force: true });
37134
38157
  }
37135
38158
  };
37136
38159
  } catch (error) {
37137
- rmSync5(promptFile, { force: true });
37138
- if (!o.cwd) rmSync5(workspace, { recursive: true, force: true });
38160
+ rmSync6(promptFile, { force: true });
38161
+ if (!o.cwd) rmSync6(workspace, { recursive: true, force: true });
37139
38162
  throw error;
37140
38163
  }
37141
38164
  }
@@ -37153,7 +38176,7 @@ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
37153
38176
  }
37154
38177
  function think(prompt, o) {
37155
38178
  const process2 = leadProcess(prompt, o);
37156
- return new Promise((resolve21, reject2) => {
38179
+ return new Promise((resolve22, reject2) => {
37157
38180
  const child = execFile13(process2.file, process2.args, {
37158
38181
  cwd: process2.cwd,
37159
38182
  timeout: o.timeoutMs,
@@ -37167,7 +38190,7 @@ function think(prompt, o) {
37167
38190
  reject2(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
37168
38191
  return;
37169
38192
  }
37170
- resolve21(stdout);
38193
+ resolve22(stdout);
37171
38194
  });
37172
38195
  child.stdin?.end();
37173
38196
  });
@@ -37204,9 +38227,9 @@ function acquire(cap, waitMs) {
37204
38227
  return Promise.resolve();
37205
38228
  }
37206
38229
  const startedAt = Date.now();
37207
- return new Promise((resolve21, reject2) => {
38230
+ return new Promise((resolve22, reject2) => {
37208
38231
  const w = {
37209
- grant: resolve21,
38232
+ grant: resolve22,
37210
38233
  deny: reject2,
37211
38234
  timer: setTimeout(() => {
37212
38235
  const i = queue.indexOf(w);
@@ -38602,9 +39625,9 @@ import { readFileSync as readFileSync10 } from "node:fs";
38602
39625
  // src/services/vps-ssh.ts
38603
39626
  init_config2();
38604
39627
  import { spawn as spawn4 } from "node:child_process";
38605
- import { mkdtempSync, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
38606
- import { tmpdir as tmpdir4 } from "node:os";
38607
- import { dirname as dirname10, join as join19 } from "node:path";
39628
+ import { mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
39629
+ import { tmpdir as tmpdir5 } from "node:os";
39630
+ import { dirname as dirname10, join as join22 } from "node:path";
38608
39631
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
38609
39632
  function consoleArgv(t) {
38610
39633
  return [
@@ -38619,9 +39642,9 @@ function consoleArgv(t) {
38619
39642
  ];
38620
39643
  }
38621
39644
  function askpassScript() {
38622
- const dir2 = mkdtempSync(join19(tmpdir4(), "hanoman-askpass-"));
38623
- const path = join19(dir2, "askpass.sh");
38624
- writeFileSync6(path, `#!/bin/sh
39645
+ const dir2 = mkdtempSync(join22(tmpdir5(), "hanoman-askpass-"));
39646
+ const path = join22(dir2, "askpass.sh");
39647
+ writeFileSync8(path, `#!/bin/sh
38625
39648
  printf '%s' "$HANOMAN_SSH_PASSWORD"
38626
39649
  `, { mode: 448 });
38627
39650
  return path;
@@ -38654,14 +39677,14 @@ function sshExec(t, remoteCmd, opts = {}) {
38654
39677
  SSH_ASKPASS_REQUIRE: "force",
38655
39678
  HANOMAN_SSH_PASSWORD: opts.password
38656
39679
  } : process.env;
38657
- return new Promise((resolve21) => {
39680
+ return new Promise((resolve22) => {
38658
39681
  const p3 = spawn4(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
38659
39682
  let out4 = "";
38660
39683
  const timer9 = setTimeout(() => p3.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
38661
39684
  const done = (r) => {
38662
39685
  clearTimeout(timer9);
38663
- if (askpass) rmSync6(dirname10(askpass), { recursive: true, force: true });
38664
- resolve21(r);
39686
+ if (askpass) rmSync7(dirname10(askpass), { recursive: true, force: true });
39687
+ resolve22(r);
38665
39688
  };
38666
39689
  p3.stdout.on("data", (d) => {
38667
39690
  out4 += d;
@@ -38681,16 +39704,16 @@ function sshExec(t, remoteCmd, opts = {}) {
38681
39704
  // src/services/vps-audit.ts
38682
39705
  init_db();
38683
39706
  import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
38684
- import { join as join21 } from "node:path";
39707
+ import { join as join24 } from "node:path";
38685
39708
  import { fileURLToPath as fileURLToPath3 } from "node:url";
38686
39709
 
38687
39710
  // src/runner/deps.ts
38688
39711
  import { existsSync as existsSync13 } from "node:fs";
38689
- import { dirname as dirname11, join as join20 } from "node:path";
39712
+ import { dirname as dirname11, join as join23 } from "node:path";
38690
39713
  function repoRootFrom(startDir) {
38691
39714
  let dir2 = startDir;
38692
39715
  for (let i = 0; i < 8; i++) {
38693
- if (existsSync13(join20(dir2, "pnpm-workspace.yaml"))) return dir2;
39716
+ if (existsSync13(join23(dir2, "pnpm-workspace.yaml"))) return dir2;
38694
39717
  const parent = dirname11(dir2);
38695
39718
  if (parent === dir2) break;
38696
39719
  dir2 = parent;
@@ -38699,6 +39722,9 @@ function repoRootFrom(startDir) {
38699
39722
  }
38700
39723
  var repoRoot = (startDir = process.cwd()) => repoRootFrom(startDir);
38701
39724
 
39725
+ // src/services/vps-audit.ts
39726
+ init_sync_notify();
39727
+
38702
39728
  // src/vps/scoring.ts
38703
39729
  var pct = (fulfilled, applicable) => applicable === 0 ? 100 : Math.round(fulfilled / applicable * 100);
38704
39730
  function scoreCompliance(probeStatus, states, items = CATALOG) {
@@ -38800,7 +39826,7 @@ var packagedScript = (f) => fileURLToPath3(new URL(`../scripts/vps/${f}`, import
38800
39826
  var moduleDir = () => fileURLToPath3(new URL(".", import.meta.url));
38801
39827
  var scriptPath = (f) => {
38802
39828
  const packed = packagedScript(f);
38803
- return existsSync14(packed) ? packed : join21(repoRoot(moduleDir()), "server", "scripts", "vps", f);
39829
+ return existsSync14(packed) ? packed : join24(repoRoot(moduleDir()), "server", "scripts", "vps", f);
38804
39830
  };
38805
39831
  async function itemStatesOf(vpsId) {
38806
39832
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
@@ -39008,27 +40034,27 @@ async function buildChecklist(vpsId) {
39008
40034
  init_src2();
39009
40035
  init_config2();
39010
40036
  import { execFileSync as execFileSync3 } from "node:child_process";
39011
- import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
40037
+ import { chmodSync as chmodSync7, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
39012
40038
  import { homedir as homedir5 } from "node:os";
39013
- import { join as join22 } from "node:path";
40039
+ import { join as join25 } from "node:path";
39014
40040
  var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? resolveDataDirs().sshKeys;
39015
40041
  var KEY_FILES = ["id_ed25519", "id_ed25519.pub"];
39016
40042
  function adoptLegacyKey(dir2) {
39017
40043
  if (effectiveStr("HANOMAN_SSH_KEY_DIR")) return;
39018
- const legacy = join22(homedir5(), ".hanoman");
40044
+ const legacy = join25(homedir5(), ".hanoman");
39019
40045
  if (legacy === dir2) return;
39020
- if (!KEY_FILES.every((f) => existsSync15(join22(legacy, f)))) return;
40046
+ if (!KEY_FILES.every((f) => existsSync15(join25(legacy, f)))) return;
39021
40047
  mkdirSync11(dir2, { recursive: true, mode: 448 });
39022
40048
  for (const f of KEY_FILES) {
39023
- copyFileSync(join22(legacy, f), join22(dir2, f));
39024
- chmodSync4(join22(dir2, f), 384);
40049
+ copyFileSync(join25(legacy, f), join25(dir2, f));
40050
+ chmodSync7(join25(dir2, f), 384);
39025
40051
  }
39026
- for (const f of KEY_FILES) unlinkSync2(join22(legacy, f));
40052
+ for (const f of KEY_FILES) unlinkSync2(join25(legacy, f));
39027
40053
  console.log(`vps: key SSH dipindah dari ${legacy} ke ${dir2} (SPEC-846 \u2014 satu batas backup)`);
39028
40054
  }
39029
40055
  function ensureHanomanKey() {
39030
40056
  const dir2 = keyDir();
39031
- const privPath = join22(dir2, KEY_FILES[0]);
40057
+ const privPath = join25(dir2, KEY_FILES[0]);
39032
40058
  const pubPath = `${privPath}.pub`;
39033
40059
  if (!existsSync15(privPath)) adoptLegacyKey(dir2);
39034
40060
  if (!existsSync15(privPath)) {
@@ -39057,6 +40083,7 @@ async function bootstrapKey(t, password) {
39057
40083
  // src/routes/vps.ts
39058
40084
  init_pty();
39059
40085
  init_settings3();
40086
+ init_sync_notify();
39060
40087
  function keyMissing(v) {
39061
40088
  return !!v.keyPath && !existsSync16(v.keyPath);
39062
40089
  }
@@ -39340,7 +40367,7 @@ init_config2();
39340
40367
  import { execFileSync as execFileSync4 } from "node:child_process";
39341
40368
  import { readFileSync as readFileSync14 } from "node:fs";
39342
40369
  import { homedir as homedir7 } from "node:os";
39343
- import { join as join23 } from "node:path";
40370
+ import { join as join26 } from "node:path";
39344
40371
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
39345
40372
  var TTL_MS2 = 3e4;
39346
40373
  var lastOk = null;
@@ -39353,7 +40380,7 @@ var LABELS = {
39353
40380
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
39354
40381
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
39355
40382
  function credsFile() {
39356
- return join23(effectiveStr("CLAUDE_CONFIG_DIR") ?? join23(homedir7(), ".claude"), ".credentials.json");
40383
+ return join26(effectiveStr("CLAUDE_CONFIG_DIR") ?? join26(homedir7(), ".claude"), ".credentials.json");
39357
40384
  }
39358
40385
  function readAccessToken() {
39359
40386
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
@@ -39427,14 +40454,14 @@ async function getLimits() {
39427
40454
  // src/services/codex-limits.ts
39428
40455
  import { open as open3, readdir as readdir5, stat as stat2 } from "node:fs/promises";
39429
40456
  import { homedir as homedir8 } from "node:os";
39430
- import { join as join24 } from "node:path";
40457
+ import { join as join27 } from "node:path";
39431
40458
  var TTL_MS3 = 3e4;
39432
40459
  var STALE_AFTER_MS = 12 * 36e5;
39433
40460
  var TAIL_BYTES = 512 * 1024;
39434
40461
  var MAX_FILES = 8;
39435
40462
  var cache2 = null;
39436
40463
  var freshUntil2 = 0;
39437
- var codexSessionsDir = () => join24(process.env.CODEX_HOME ?? join24(homedir8(), ".codex"), "sessions");
40464
+ var codexSessionsDir = () => join27(process.env.CODEX_HOME ?? join27(homedir8(), ".codex"), "sessions");
39438
40465
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
39439
40466
  function windowLabel(minutes) {
39440
40467
  if (minutes === 300) return "Sesi 5 jam";
@@ -39467,7 +40494,7 @@ async function recentRollouts(dir2) {
39467
40494
  return [];
39468
40495
  }
39469
40496
  const stamped = await Promise.all(entries3.map(async (rel) => {
39470
- const full = join24(dir2, rel);
40497
+ const full = join27(dir2, rel);
39471
40498
  try {
39472
40499
  return { full, mtime: (await stat2(full)).mtimeMs };
39473
40500
  } catch {
@@ -39544,41 +40571,8 @@ async function limits(app2) {
39544
40571
  app2.get("/limits/codex", async () => getCodexLimits());
39545
40572
  }
39546
40573
 
39547
- // src/services/codex-version.ts
39548
- init_src();
39549
- init_config2();
39550
- import { execFile as execFile14 } from "node:child_process";
39551
- import { promisify as promisify13 } from "node:util";
39552
- var run3 = promisify13(execFile14);
39553
- var CODEX_MIN_CLIENT = "0.144.0";
39554
- var TTL_MS4 = 5 * 6e4;
39555
- var cache3 = null;
39556
- var codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
39557
- function parseCodexVersion(out4) {
39558
- const m = /(\d+)\.(\d+)\.(\d+)/.exec(out4);
39559
- return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
39560
- }
39561
- async function getCodexVersion(now = Date.now()) {
39562
- if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
39563
- let version = null;
39564
- try {
39565
- const { stdout } = await run3(codexBin2(), ["--version"], { timeout: 5e3 });
39566
- version = parseCodexVersion(stdout);
39567
- } catch {
39568
- }
39569
- cache3 = { at: now, version };
39570
- return version;
39571
- }
39572
- async function codexVersionInfo() {
39573
- const version = await getCodexVersion();
39574
- return {
39575
- version,
39576
- minRequired: CODEX_MIN_CLIENT,
39577
- ok: version === null || cmpVersion(version, CODEX_MIN_CLIENT) >= 0
39578
- };
39579
- }
39580
-
39581
40574
  // src/routes/codex.ts
40575
+ init_codex_version();
39582
40576
  async function codex(app2) {
39583
40577
  app2.get("/codex/version", async () => codexVersionInfo());
39584
40578
  }
@@ -41179,7 +42173,7 @@ init_config2();
41179
42173
  init_src2();
41180
42174
  import { randomUUID as randomUUID8 } from "node:crypto";
41181
42175
  import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir6, stat as stat3 } from "node:fs/promises";
41182
- import { join as join25, resolve as resolve19, basename as basename6 } from "node:path";
42176
+ import { join as join28, resolve as resolve19, basename as basename6 } from "node:path";
41183
42177
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
41184
42178
  function transcriptDir() {
41185
42179
  return resolve19(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
@@ -41200,13 +42194,13 @@ async function saveTranscript(text) {
41200
42194
  const dir2 = transcriptDir();
41201
42195
  await mkdir5(dir2, { recursive: true, mode: 448 });
41202
42196
  const key = `${randomUUID8()}.log`;
41203
- await writeFile4(join25(dir2, key), body, { encoding: "utf8", mode: 384 });
42197
+ await writeFile4(join28(dir2, key), body, { encoding: "utf8", mode: 384 });
41204
42198
  return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
41205
42199
  }
41206
42200
  async function readTranscript(key) {
41207
42201
  if (!key) return null;
41208
42202
  try {
41209
- return await readFile4(join25(transcriptDir(), basename6(key)), "utf8");
42203
+ return await readFile4(join28(transcriptDir(), basename6(key)), "utf8");
41210
42204
  } catch {
41211
42205
  return null;
41212
42206
  }
@@ -41214,7 +42208,7 @@ async function readTranscript(key) {
41214
42208
  async function deleteTranscript(key) {
41215
42209
  if (!key) return;
41216
42210
  try {
41217
- await unlink4(join25(transcriptDir(), basename6(key)));
42211
+ await unlink4(join28(transcriptDir(), basename6(key)));
41218
42212
  } catch (e) {
41219
42213
  if (e.code !== "ENOENT") throw e;
41220
42214
  }
@@ -41232,7 +42226,7 @@ async function listTranscripts() {
41232
42226
  for (const name2 of names) {
41233
42227
  if (!name2.endsWith(".log")) continue;
41234
42228
  try {
41235
- rows.push({ key: name2, mtimeMs: (await stat3(join25(dir2, name2))).mtimeMs });
42229
+ rows.push({ key: name2, mtimeMs: (await stat3(join28(dir2, name2))).mtimeMs });
41236
42230
  } catch {
41237
42231
  }
41238
42232
  }
@@ -41286,11 +42280,11 @@ async function beginSession(b) {
41286
42280
  });
41287
42281
  }
41288
42282
  async function finishSession(d) {
41289
- const open4 = await prisma.sessionHistory.findFirst({
42283
+ const open5 = await prisma.sessionHistory.findFirst({
41290
42284
  where: { sessionId: d.sessionId, endedAt: null },
41291
42285
  orderBy: { startedAt: "desc" }
41292
42286
  });
41293
- if (!open4) return;
42287
+ if (!open5) return;
41294
42288
  let t = { key: "", bytes: 0 };
41295
42289
  if (d.transcript) {
41296
42290
  try {
@@ -41300,7 +42294,7 @@ async function finishSession(d) {
41300
42294
  }
41301
42295
  }
41302
42296
  await prisma.sessionHistory.update({
41303
- where: { id: open4.id },
42297
+ where: { id: open5.id },
41304
42298
  data: {
41305
42299
  endedAt: /* @__PURE__ */ new Date(),
41306
42300
  endedReason: CLOSED,
@@ -41415,14 +42409,14 @@ async function reconcileTranscripts(opts = {}) {
41415
42409
  return report;
41416
42410
  }
41417
42411
  async function reconcileHistory(liveSessionIds) {
41418
- const open4 = await prisma.sessionHistory.findMany({
42412
+ const open5 = await prisma.sessionHistory.findMany({
41419
42413
  where: { endedAt: null },
41420
42414
  select: { id: true, sessionId: true, updatedAt: true }
41421
42415
  });
41422
42416
  const live = new Set(liveSessionIds);
41423
42417
  const at = /* @__PURE__ */ new Date();
41424
42418
  let closed = 0;
41425
- for (const r of open4) {
42419
+ for (const r of open5) {
41426
42420
  if (live.has(r.sessionId)) continue;
41427
42421
  await prisma.sessionHistory.update({
41428
42422
  where: { id: r.id },
@@ -41473,7 +42467,285 @@ async function session_history_default(app2) {
41473
42467
  init_src();
41474
42468
  init_session_event_token();
41475
42469
  init_pty();
42470
+
42471
+ // src/services/agent-invocations.ts
42472
+ init_src();
42473
+ init_db();
42474
+ import { createHash as createHash8, randomUUID as randomUUID10 } from "node:crypto";
42475
+ import { execFileSync as execFileSync5 } from "node:child_process";
42476
+ import { homedir as homedir9 } from "node:os";
42477
+ import { readFileSync as readFileSync16, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
42478
+ import { isAbsolute as isAbsolute7, relative as relative3, resolve as resolve20 } from "node:path";
42479
+ var MAX_EXCERPT_BYTES = 4096;
42480
+ var MAX_TRANSCRIPT_BYTES2 = 10 * 1024 * 1024;
42481
+ var ANSI2 = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
42482
+ var snapshotHashes = /* @__PURE__ */ new Map();
42483
+ var keyOf = (x) => `${x.sessionId}\0${x.runtimeInvocationId}`;
42484
+ var hash2 = (value) => createHash8("sha256").update(value).digest("hex");
42485
+ var defaultGitStatus = (cwd) => {
42486
+ try {
42487
+ return execFileSync5("git", ["-C", cwd, "status", "--porcelain=v1", "-z"], {
42488
+ encoding: "utf8",
42489
+ stdio: ["ignore", "pipe", "ignore"]
42490
+ });
42491
+ } catch {
42492
+ return null;
42493
+ }
42494
+ };
42495
+ function snapshot2(cwd, run4 = defaultGitStatus) {
42496
+ try {
42497
+ const value = run4(cwd);
42498
+ return value === null ? null : hash2(value);
42499
+ } catch {
42500
+ return null;
42501
+ }
42502
+ }
42503
+ var stripAnsi = (value) => value.replace(ANSI2, "");
42504
+ var utf8Prefix = (value, maxBytes) => {
42505
+ let out4 = "", bytes = 0;
42506
+ for (const char of value) {
42507
+ const next = Buffer.byteLength(char, "utf8");
42508
+ if (bytes + next > maxBytes) break;
42509
+ out4 += char;
42510
+ bytes += next;
42511
+ }
42512
+ return out4;
42513
+ };
42514
+ var transcriptRoots = () => [
42515
+ resolve20(process.env.CLAUDE_CONFIG_DIR ?? `${homedir9()}/.claude`),
42516
+ resolve20(process.env.CODEX_HOME ?? `${homedir9()}/.codex`)
42517
+ ];
42518
+ var inside = (path, root) => {
42519
+ const rel = relative3(root, path);
42520
+ return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
42521
+ };
42522
+ var EMPTY_USAGE = { inputTokens: null, outputTokens: null, cachedTokens: null };
42523
+ var nonnegativeInt = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
42524
+ function usageFromText(text) {
42525
+ const found = [];
42526
+ const inspect = (value) => {
42527
+ if (!value || typeof value !== "object") return;
42528
+ const record2 = value;
42529
+ const usage2 = record2.usage;
42530
+ if (usage2 && typeof usage2 === "object") {
42531
+ const u = usage2;
42532
+ found.push([
42533
+ nonnegativeInt(u.input_tokens ?? u.inputTokens) ?? -1,
42534
+ nonnegativeInt(u.output_tokens ?? u.outputTokens) ?? -1,
42535
+ nonnegativeInt(u.cached_tokens ?? u.cachedTokens ?? u.cache_read_input_tokens ?? u.cacheReadInputTokens) ?? -1
42536
+ ]);
42537
+ }
42538
+ };
42539
+ for (const line of text.split("\n")) {
42540
+ if (!line.trim()) continue;
42541
+ try {
42542
+ inspect(JSON.parse(line));
42543
+ } catch {
42544
+ }
42545
+ }
42546
+ if (found.length === 0) return EMPTY_USAGE;
42547
+ const max = (index) => {
42548
+ const values = found.map((entry) => entry[index]).filter((n2) => n2 >= 0);
42549
+ return values.length ? Math.max(...values) : null;
42550
+ };
42551
+ return { inputTokens: max(0), outputTokens: max(1), cachedTokens: max(2) };
42552
+ }
42553
+ function transcriptUsage(path, roots = transcriptRoots()) {
42554
+ if (!path) return EMPTY_USAGE;
42555
+ try {
42556
+ const real2 = realpathSync5(path);
42557
+ const safeRoots = roots.map((root) => realpathSync5(root));
42558
+ if (!safeRoots.some((root) => inside(real2, root))) return EMPTY_USAGE;
42559
+ const info = statSync4(real2);
42560
+ if (!info.isFile() || info.size > MAX_TRANSCRIPT_BYTES2) return EMPTY_USAGE;
42561
+ return usageFromText(readFileSync16(real2, "utf8"));
42562
+ } catch {
42563
+ return EMPTY_USAGE;
42564
+ }
42565
+ }
42566
+ async function startAgentInvocation(input, io = {}) {
42567
+ const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
42568
+ const existing = await prisma.agentInvocation.findUnique({
42569
+ where: { sessionId_runtimeInvocationId: {
42570
+ sessionId: input.sessionId,
42571
+ runtimeInvocationId: input.runtimeInvocationId
42572
+ } }
42573
+ });
42574
+ const row = await prisma.agentInvocation.upsert({
42575
+ where: { sessionId_runtimeInvocationId: {
42576
+ sessionId: input.sessionId,
42577
+ runtimeInvocationId: input.runtimeInvocationId
42578
+ } },
42579
+ update: {},
42580
+ create: {
42581
+ id: randomUUID10(),
42582
+ sessionId: input.sessionId,
42583
+ projectId: input.projectId,
42584
+ specId: input.specId ?? null,
42585
+ runtime: input.runtime,
42586
+ runtimeInvocationId: input.runtimeInvocationId,
42587
+ customAgentId: input.customAgentId ?? null,
42588
+ agentName: input.agentName,
42589
+ model: input.model ?? null,
42590
+ status: "running",
42591
+ startedAt
42592
+ }
42593
+ });
42594
+ const before2 = snapshot2(input.cwd, io.gitStatus);
42595
+ if (before2 !== null) snapshotHashes.set(keyOf(input), before2);
42596
+ return { row, duplicate: existing !== null };
42597
+ }
42598
+ async function stopAgentInvocation(input, io = {}) {
42599
+ const unique = { sessionId_runtimeInvocationId: {
42600
+ sessionId: input.sessionId,
42601
+ runtimeInvocationId: input.runtimeInvocationId
42602
+ } };
42603
+ const existing = await prisma.agentInvocation.findUnique({ where: unique });
42604
+ if (existing?.endedAt) return { row: existing, duplicate: true };
42605
+ const endedAt = input.endedAt ?? /* @__PURE__ */ new Date();
42606
+ const startedAt = existing?.startedAt ?? endedAt;
42607
+ const cleanResult = input.result === void 0 ? null : stripAnsi(input.result);
42608
+ const usage2 = transcriptUsage(input.transcriptPath, io.transcriptRoots);
42609
+ const before2 = snapshotHashes.get(keyOf(input));
42610
+ const after = snapshot2(input.cwd, io.gitStatus);
42611
+ snapshotHashes.delete(keyOf(input));
42612
+ const evidence = {
42613
+ // Stop tanpa start lazim setelah restart server di tengah invocation. Waktu start dan status
42614
+ // runtime sudah hilang; simpan baris sintetis yang dapat diaudit tanpa mengarang durasi 0 ms.
42615
+ status: existing ? input.status ?? "completed" : "completed",
42616
+ endedAt,
42617
+ durationMs: existing ? Math.max(0, endedAt.getTime() - startedAt.getTime()) : null,
42618
+ ...usage2,
42619
+ resultExcerpt: cleanResult === null ? null : utf8Prefix(cleanResult, MAX_EXCERPT_BYTES),
42620
+ resultHash: cleanResult === null ? null : hash2(cleanResult),
42621
+ workspaceChanged: before2 !== void 0 && after !== null && before2 !== after
42622
+ };
42623
+ if (existing) {
42624
+ const row2 = await prisma.agentInvocation.update({ where: { id: existing.id }, data: evidence });
42625
+ return { row: row2, duplicate: false };
42626
+ }
42627
+ const row = await prisma.agentInvocation.create({
42628
+ data: {
42629
+ id: randomUUID10(),
42630
+ sessionId: input.sessionId,
42631
+ projectId: input.projectId,
42632
+ specId: input.specId ?? null,
42633
+ runtime: input.runtime,
42634
+ runtimeInvocationId: input.runtimeInvocationId,
42635
+ customAgentId: input.customAgentId ?? null,
42636
+ agentName: input.agentName,
42637
+ model: input.model ?? null,
42638
+ startedAt,
42639
+ ...evidence
42640
+ }
42641
+ });
42642
+ return { row, duplicate: false };
42643
+ }
42644
+ async function reconcileAgentInvocations(liveSessionIds) {
42645
+ const live = new Set(liveSessionIds);
42646
+ const open5 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
42647
+ let changed = 0;
42648
+ const endedAt = /* @__PURE__ */ new Date();
42649
+ for (const row of open5) {
42650
+ if (live.has(row.sessionId)) continue;
42651
+ await prisma.agentInvocation.update({
42652
+ where: { id: row.id },
42653
+ data: {
42654
+ status: "abandoned",
42655
+ endedAt,
42656
+ durationMs: Math.max(0, endedAt.getTime() - row.startedAt.getTime())
42657
+ }
42658
+ });
42659
+ changed++;
42660
+ }
42661
+ return changed;
42662
+ }
42663
+ var agentInvocationView = (row) => ({
42664
+ id: row.id,
42665
+ sessionId: row.sessionId,
42666
+ projectId: row.projectId,
42667
+ specId: row.specId,
42668
+ runtime: row.runtime === "codex" ? "codex" : "claude",
42669
+ customAgentId: row.customAgentId,
42670
+ agentName: row.agentName,
42671
+ model: row.model,
42672
+ status: row.status,
42673
+ startedAt: row.startedAt.toISOString(),
42674
+ endedAt: row.endedAt?.toISOString() ?? null,
42675
+ durationMs: row.durationMs,
42676
+ inputTokens: row.inputTokens,
42677
+ outputTokens: row.outputTokens,
42678
+ cachedTokens: row.cachedTokens,
42679
+ resultExcerpt: row.resultExcerpt,
42680
+ resultHash: row.resultHash,
42681
+ workspaceChanged: row.workspaceChanged,
42682
+ disposition: AGENT_DISPOSITIONS.includes(row.disposition) ? row.disposition : "pending",
42683
+ dispositionNote: row.dispositionNote,
42684
+ evaluatedAt: row.evaluatedAt?.toISOString() ?? null
42685
+ });
42686
+ var median = (values) => {
42687
+ if (values.length === 0) return null;
42688
+ values.sort((a, b) => a - b);
42689
+ const middle = Math.floor(values.length / 2);
42690
+ return values.length % 2 === 1 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
42691
+ };
42692
+ var availableSum = (values) => {
42693
+ const known = values.filter((value) => value !== null);
42694
+ return known.length ? known.reduce((sum, value) => sum + value, 0) : null;
42695
+ };
42696
+ async function agentMetrics(query2) {
42697
+ const where = {
42698
+ ...query2.projectId ? { projectId: query2.projectId } : {},
42699
+ ...query2.from || query2.to ? { startedAt: {
42700
+ ...query2.from ? { gte: query2.from } : {},
42701
+ ...query2.to ? { lte: query2.to } : {}
42702
+ } } : {}
42703
+ };
42704
+ const rows = await prisma.agentInvocation.findMany({ where, orderBy: { startedAt: "desc" } });
42705
+ const groups = /* @__PURE__ */ new Map();
42706
+ for (const row of rows) groups.set(row.agentName, [...groups.get(row.agentName) ?? [], row]);
42707
+ const agents = [...groups.entries()].map(([agentName, invocations]) => {
42708
+ const dispositions = { pending: 0, accepted: 0, partial: 0, rejected: 0, falsePositive: 0 };
42709
+ for (const row of invocations) {
42710
+ if (row.disposition === "accepted") dispositions.accepted++;
42711
+ else if (row.disposition === "partial") dispositions.partial++;
42712
+ else if (row.disposition === "rejected") dispositions.rejected++;
42713
+ else if (row.disposition === "false-positive") dispositions.falsePositive++;
42714
+ else dispositions.pending++;
42715
+ }
42716
+ const evaluated = dispositions.accepted + dispositions.partial + dispositions.rejected + dispositions.falsePositive;
42717
+ return {
42718
+ agentName,
42719
+ invocationCount: invocations.length,
42720
+ medianDurationMs: median(invocations.flatMap((row) => row.durationMs === null ? [] : [row.durationMs])),
42721
+ inputTokens: availableSum(invocations.map((row) => row.inputTokens)),
42722
+ outputTokens: availableSum(invocations.map((row) => row.outputTokens)),
42723
+ cachedTokens: availableSum(invocations.map((row) => row.cachedTokens)),
42724
+ dispositions,
42725
+ operationalPrecision: evaluated ? (dispositions.accepted + dispositions.partial) / evaluated : null,
42726
+ workspaceChanged: invocations.some((row) => row.workspaceChanged)
42727
+ };
42728
+ }).sort((a, b) => a.agentName.localeCompare(b.agentName));
42729
+ return { agents, recent: rows.slice(0, 100).map(agentInvocationView) };
42730
+ }
42731
+ async function updateAgentInvocationDisposition(id, disposition, note) {
42732
+ const exists = await prisma.agentInvocation.findUnique({ where: { id } });
42733
+ if (!exists) return null;
42734
+ const row = await prisma.agentInvocation.update({
42735
+ where: { id },
42736
+ data: {
42737
+ disposition,
42738
+ dispositionNote: note?.trim() || null,
42739
+ evaluatedAt: /* @__PURE__ */ new Date()
42740
+ }
42741
+ });
42742
+ return agentInvocationView(row);
42743
+ }
42744
+
42745
+ // src/routes/session-events.ts
41476
42746
  var bearer = (h) => /^Bearer (.+)$/.exec(h ?? "")?.[1] ?? "";
42747
+ var recordOf = (value) => value && typeof value === "object" ? value : null;
42748
+ var boundedString = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max ? value : void 0;
41477
42749
  async function session_events_default(app2) {
41478
42750
  app2.post("/session-events", async (req, reply) => {
41479
42751
  const sessionId2 = String(req.headers["x-hanoman-session"] ?? "");
@@ -41482,6 +42754,38 @@ async function session_events_default(app2) {
41482
42754
  return reply.code(401).send({ error: "unauthorized" });
41483
42755
  const s2 = await getSessionAsync(sessionId2);
41484
42756
  if (!s2 || s2.exited) return reply.code(404).send({ error: "live session not found" });
42757
+ const body = recordOf(req.body);
42758
+ const lifecycle = body?.hook_event_name;
42759
+ if (body && (lifecycle === "SubagentStart" || lifecycle === "SubagentStop")) {
42760
+ const runtimeInvocationId = boundedString(
42761
+ body.agent_id ?? body.subagent_id ?? body.thread_id,
42762
+ 500
42763
+ );
42764
+ const agentName = boundedString(body.agent_type ?? body.agent_name, 200);
42765
+ const meta = agentName ? (s2.agentRoster ?? []).find((agent) => agent.name === agentName) : void 0;
42766
+ if (!runtimeInvocationId || !meta) return reply.code(202).send({ ignored: true });
42767
+ const identity = {
42768
+ sessionId: sessionId2,
42769
+ projectId: s2.projectId,
42770
+ specId: s2.specId,
42771
+ runtime: s2.agent,
42772
+ runtimeInvocationId,
42773
+ customAgentId: meta.id,
42774
+ agentName: meta.name,
42775
+ model: meta.model,
42776
+ cwd: s2.cwd
42777
+ };
42778
+ const outcome = lifecycle === "SubagentStart" ? await startAgentInvocation(identity) : await stopAgentInvocation({
42779
+ ...identity,
42780
+ status: body.status === "interrupted" ? "interrupted" : "completed",
42781
+ result: boundedString(body.last_assistant_message ?? body.result, 1e6),
42782
+ transcriptPath: boundedString(
42783
+ body.agent_transcript_path ?? body.transcript_path,
42784
+ 4096
42785
+ )
42786
+ });
42787
+ return reply.code(202).send(outcome.duplicate ? { duplicate: true } : { accepted: true });
42788
+ }
41485
42789
  const event = parseHookEvent(req.body);
41486
42790
  if (!event) return reply.code(202).send({ ignored: true });
41487
42791
  const r = await intakeAsk({
@@ -41575,9 +42879,9 @@ init_db();
41575
42879
  init_db();
41576
42880
  init_uploads();
41577
42881
  init_src();
41578
- import { createHash as createHash8, randomBytes as randomBytes6 } from "node:crypto";
42882
+ import { createHash as createHash10, randomBytes as randomBytes6 } from "node:crypto";
41579
42883
  function hashAccessKey(key) {
41580
- return createHash8("sha256").update(key).digest("hex");
42884
+ return createHash10("sha256").update(key).digest("hex");
41581
42885
  }
41582
42886
  function generateAccessKey() {
41583
42887
  const key = "hnm_tkt_" + randomBytes6(24).toString("hex");
@@ -41587,13 +42891,13 @@ function generateShareToken() {
41587
42891
  return "hnm_shr_" + randomBytes6(24).toString("hex");
41588
42892
  }
41589
42893
  async function createTicket(input) {
41590
- const { key, hash: hash2 } = generateAccessKey();
42894
+ const { key, hash: hash3 } = generateAccessKey();
41591
42895
  for (let attempt = 0; attempt < 3; attempt++) {
41592
42896
  const max = await prisma.ticket.aggregate({ where: { projectId: input.projectId }, _max: { number: true } });
41593
42897
  const number = (max._max.number ?? 0) + 1;
41594
42898
  try {
41595
42899
  const ticket = await prisma.ticket.create({
41596
- data: { ...input, number, accessKeyHash: hash2, shareToken: generateShareToken(), status: "new" }
42900
+ data: { ...input, number, accessKeyHash: hash3, shareToken: generateShareToken(), status: "new" }
41597
42901
  });
41598
42902
  return { ticket, key };
41599
42903
  } catch (e) {
@@ -41620,6 +42924,7 @@ async function pruneOldTickets(now = Date.now()) {
41620
42924
  // src/services/ticket-intake.ts
41621
42925
  init_db();
41622
42926
  init_notifications2();
42927
+ init_sync_notify();
41623
42928
  init_uploads();
41624
42929
  var TICKET_UPLOAD = { MAX_FILES: 3 };
41625
42930
  async function parseTicketUpload(req) {
@@ -41783,10 +43088,12 @@ async function help_default(app2) {
41783
43088
 
41784
43089
  // src/routes/tickets.ts
41785
43090
  init_db();
43091
+ init_sync_notify();
41786
43092
  init_uploads();
41787
43093
 
41788
43094
  // src/services/ticket-accept.ts
41789
43095
  init_db();
43096
+ init_sync_notify();
41790
43097
  var attachmentData = (t, atts) => {
41791
43098
  if (atts.length === 0) return "Tanpa lampiran.";
41792
43099
  const list2 = atts.map(
@@ -42701,201 +44008,28 @@ async function changelog_default(app2) {
42701
44008
  // src/routes/custom-agents.ts
42702
44009
  init_src();
42703
44010
  init_db();
42704
-
42705
- // src/services/agent-tool-catalog.ts
42706
- init_src();
42707
- import { readFileSync as readFileSync16 } from "node:fs";
42708
- import { homedir as homedir9 } from "node:os";
42709
- import { join as join26 } from "node:path";
42710
- var home = () => process.env.HOME || homedir9();
42711
- var readJson2 = (path) => {
42712
- try {
42713
- return JSON.parse(readFileSync16(path, "utf8"));
42714
- } catch {
42715
- return null;
42716
- }
42717
- };
42718
- var serversOf = (node) => {
42719
- const ms = node?.mcpServers;
42720
- if (!ms || typeof ms !== "object" || Array.isArray(ms)) return [];
42721
- return Object.keys(ms);
42722
- };
42723
- var codexServers = () => {
42724
- let text;
42725
- try {
42726
- text = readFileSync16(join26(home(), ".codex", "config.toml"), "utf8");
42727
- } catch {
42728
- return [];
42729
- }
42730
- const out4 = [];
42731
- for (const m of text.matchAll(/^\s*\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.[^\]]*)?\]/gm)) {
42732
- const name2 = m[1] ?? m[2];
42733
- if (name2) out4.push(name2);
42734
- }
42735
- return out4;
42736
- };
42737
- function mcpServerNames(repoDir) {
42738
- const names = [];
42739
- const claudeJson = readJson2(join26(home(), ".claude.json"));
42740
- names.push(...serversOf(claudeJson));
42741
- if (repoDir) {
42742
- const projects = claudeJson?.projects;
42743
- if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
42744
- names.push(...serversOf(readJson2(join26(repoDir, ".mcp.json"))));
42745
- }
42746
- names.push(...codexServers());
42747
- return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
42748
- }
42749
- function agentToolCatalog(repoDir) {
42750
- return [ALL_TOOLS_ENTRY, ...BUILTIN_AGENT_TOOLS, ...mcpServerNames(repoDir).map(mcpToolEntry)];
42751
- }
42752
- var agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
42753
-
42754
- // src/routes/custom-agents.ts
42755
- init_settings3();
42756
-
42757
- // src/services/builtin-agents.ts
42758
- init_src();
42759
- init_db();
44011
+ init_sync_notify();
44012
+ init_agent_tool_catalog();
42760
44013
  init_settings3();
42761
- init_tombstone();
42762
- import { createHash as createHash9 } from "node:crypto";
42763
- var fingerprint = (name2, description, instructions, tools) => createHash9("sha256").update([name2, description, instructions, [...tools].join(",")].join(" ")).digest("hex").slice(0, 16);
42764
- var builtinFingerprint = (a) => fingerprint(a.name, a.description, a.instructions, a.tools);
42765
- var rowFingerprint = (r) => fingerprint(r.name, r.description, r.instructions, toolsOf(r.tools) ?? []);
42766
- async function seedBuiltinAgents() {
42767
- try {
42768
- const setting = await getSetting();
42769
- const stamps = { ...setting.builtinAgents };
42770
- let changed = false;
42771
- for (const a of BUILTIN_AGENTS) {
42772
- const id = customAgentId(null, a.name);
42773
- const fp = builtinFingerprint(a);
42774
- const row = await prisma.customAgent.findUnique({ where: { id } });
42775
- if (!row) {
42776
- if (await findTombstone("customAgent", id)) continue;
42777
- await prisma.customAgent.create({ data: {
42778
- id,
42779
- projectId: null,
42780
- name: a.name,
42781
- description: a.description,
42782
- instructions: a.instructions,
42783
- tools: [...a.tools],
42784
- model: null,
42785
- mentions: [],
42786
- runtime: null,
42787
- enabled: a.enabledByDefault
42788
- } });
42789
- await notifySynced("customAgent", id);
42790
- stamps[a.name] = fp;
42791
- changed = true;
42792
- continue;
42793
- }
42794
- const stamped = stamps[a.name];
42795
- if (!stamped || stamped === fp) continue;
42796
- if (stamped !== rowFingerprint(row)) continue;
42797
- await prisma.customAgent.update({ where: { id }, data: {
42798
- description: a.description,
42799
- instructions: a.instructions,
42800
- tools: [...a.tools]
42801
- // `enabled` TIDAK di sini. Sengaja.
42802
- } });
42803
- await notifySynced("customAgent", id);
42804
- stamps[a.name] = fp;
42805
- changed = true;
42806
- }
42807
- if (changed) {
42808
- const data = { ...setting, builtinAgents: stamps };
42809
- await prisma.setting.upsert({
42810
- where: { id: 1 },
42811
- update: { data },
42812
- create: { id: 1, data }
42813
- });
42814
- }
42815
- } catch {
42816
- }
42817
- }
42818
-
42819
- // src/services/custom-agents.ts
42820
- init_db();
42821
- init_src();
42822
- init_pty();
42823
- var cache4 = [];
42824
- var repoDirCache = /* @__PURE__ */ new Map();
42825
- var asCustomAgent = (r) => ({
42826
- id: r.id,
42827
- projectId: r.projectId,
42828
- name: r.name,
42829
- description: r.description,
42830
- instructions: r.instructions,
42831
- tools: toolsOf(r.tools),
42832
- model: r.model,
42833
- mentions: mentionsOf(r.mentions),
42834
- runtime: runtimeOf(r.runtime),
42835
- enabled: r.enabled,
42836
- createdAt: "",
42837
- updatedAt: ""
42838
- // tak dipakai lapis ini
42839
- });
42840
- async function loadCustomAgents() {
42841
- try {
42842
- cache4 = await prisma.customAgent.findMany();
42843
- const projects = await prisma.project.findMany({ select: { id: true, repoDir: true } });
42844
- const bindings = await prisma.localBinding.findMany({ select: { projectId: true, repoDir: true } });
42845
- const next = /* @__PURE__ */ new Map();
42846
- for (const p3 of projects) next.set(p3.id, p3.repoDir ?? null);
42847
- for (const b of bindings) next.set(b.projectId, b.repoDir ?? null);
42848
- repoDirCache = next;
42849
- } catch {
42850
- cache4 = [];
42851
- repoDirCache = /* @__PURE__ */ new Map();
42852
- }
42853
- }
42854
- function agentDefsFor(projectId, agent) {
42855
- const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
42856
- const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
42857
- const eff = effectiveAgents(globals, project).filter((a) => a.runtime === null || a.runtime === agent);
42858
- const needsCatalog = eff.some((a) => (a.tools ?? []).includes(ALL_TOOLS));
42859
- const catalogIds = needsCatalog ? agentToolIds(repoDirCache.get(projectId) ?? null) : [];
42860
- return eff.map((a) => ({
42861
- name: a.name,
42862
- description: a.description,
42863
- instructions: a.instructions,
42864
- // Ekspansi terjadi DI SINI, sebelum `resolveTools` di runner: meneruskan `"*"` apa adanya
42865
- // membuat claude membuangnya senyap (agen tanpa alat), sementara menerjemahkannya jadi `null`
42866
- // membuat agen mewarisi SELURUH tool termasuk `Task` — lapis 2 anti-loop lenyap tanpa jejak.
42867
- tools: expandTools(a.tools, catalogIds),
42868
- model: a.model,
42869
- mentions: a.mentions ?? []
42870
- }));
42871
- }
42872
- function validateGraph(rows) {
42873
- const projectScopes = [...new Set(rows.map((r) => r.projectId).filter((p3) => p3 !== null))];
42874
- const globals = rows.filter((r) => r.projectId === null).map(asCustomAgent);
42875
- for (const scope of [null, ...projectScopes]) {
42876
- const project = scope === null ? [] : rows.filter((r) => r.projectId === scope).map(asCustomAgent);
42877
- const nodes = effectiveAgents(globals, project).map((a) => ({ name: a.name, mentions: a.mentions ?? [] }));
42878
- const cycle = detectCycle(nodes);
42879
- if (cycle) return { scope: scope ?? GLOBAL_SCOPE, cycle };
42880
- }
42881
- return null;
42882
- }
42883
- function unknownMentions(row, all) {
42884
- const visible = new Set(
42885
- all.filter((r) => r.projectId === null || row.projectId !== null && r.projectId === row.projectId).map((r) => r.name)
42886
- );
42887
- return mentionsOf(row.mentions).filter((m) => !visible.has(m));
42888
- }
42889
- async function installCustomAgents() {
42890
- await seedBuiltinAgents();
42891
- await loadCustomAgents();
42892
- registerCustomAgentSource((projectId, agent) => agentDefsFor(projectId, agent));
42893
- }
42894
-
42895
- // src/routes/custom-agents.ts
44014
+ init_builtin_agents2();
44015
+ init_custom_agents2();
42896
44016
  var rowsOf = async () => await prisma.customAgent.findMany();
42897
44017
  var stampsOf = async () => (await getSetting()).builtinAgents;
42898
- var view8 = (r, projectId, stamps = {}) => {
44018
+ var availabilityOf = (r, requestedRuntime) => {
44019
+ const configuredRuntime = runtimeOf(r.runtime);
44020
+ if (requestedRuntime && configuredRuntime && configuredRuntime !== requestedRuntime) {
44021
+ return { available: false, availabilityReason: `hanya tersedia untuk runtime ${configuredRuntime}` };
44022
+ }
44023
+ const effectiveRuntime = requestedRuntime ?? configuredRuntime;
44024
+ if (workspacePolicyOf(r.workspacePolicy) === "isolated-worktree" && effectiveRuntime === "codex") {
44025
+ return {
44026
+ available: false,
44027
+ availabilityReason: "isolated-worktree belum tersedia untuk subagent Codex"
44028
+ };
44029
+ }
44030
+ return { available: true };
44031
+ };
44032
+ var view8 = (r, projectId, stamps = {}, requestedRuntime) => {
42899
44033
  const builtin = r.projectId === null && BUILTIN_AGENT_NAMES.includes(r.name);
42900
44034
  return {
42901
44035
  id: r.id,
@@ -42907,12 +44041,18 @@ var view8 = (r, projectId, stamps = {}) => {
42907
44041
  model: r.model,
42908
44042
  mentions: mentionsOf(r.mentions),
42909
44043
  runtime: runtimeOf(r.runtime),
44044
+ activation: activationOf(r.activation),
44045
+ effort: effortOf(r.effort),
44046
+ workspacePolicy: workspacePolicyOf(r.workspacePolicy),
44047
+ maxTurns: maxTurnsOf(r.maxTurns),
44048
+ timeoutSeconds: timeoutSecondsOf(r.timeoutSeconds),
42910
44049
  enabled: r.enabled,
42911
44050
  builtin,
42912
44051
  // Sidik jari yang tak tercatat (baris menyeberang sync dari mesin lain, seed di sini belum
42913
44052
  // pernah menyentuhnya) dibaca sebagai "disunting" — lebih baik menandai berlebih daripada
42914
44053
  // menjanjikan "asli bawaan" untuk isi yang tak bisa kita buktikan.
42915
44054
  builtinEdited: builtin ? stamps[r.name] !== rowFingerprint(r) : false,
44055
+ ...availabilityOf(r, requestedRuntime),
42916
44056
  ...projectId ? { inherited: r.projectId === null } : {}
42917
44057
  };
42918
44058
  };
@@ -42930,6 +44070,11 @@ function modelProblem(model, runtime) {
42930
44070
  const ok2 = modelsForRuntime(runtime).some((m) => m.id === model);
42931
44071
  return ok2 ? null : { error: "model tak dikenal untuk runtime ini", model, runtime };
42932
44072
  }
44073
+ function effortProblem(effort, runtime, model) {
44074
+ if (!effort) return null;
44075
+ const ok2 = effortsForRuntimeModel(runtime, model).includes(effort);
44076
+ return ok2 ? null : { error: "effort tak didukung runtime/model ini", effort, runtime, model };
44077
+ }
42933
44078
  async function custom_agents_default(app2) {
42934
44079
  app2.get("/custom-agents/catalog", async (req) => {
42935
44080
  const projectId = req.query.projectId;
@@ -42939,8 +44084,13 @@ async function custom_agents_default(app2) {
42939
44084
  runtimes: AGENT_RUNTIMES.map((id) => ({ id, label: AGENT_RUNTIME_LABELS[id] }))
42940
44085
  };
42941
44086
  });
42942
- app2.get("/custom-agents", async (req) => {
42943
- const projectId = req.query.projectId;
44087
+ app2.get("/custom-agents", async (req, reply) => {
44088
+ const query2 = req.query;
44089
+ const projectId = query2.projectId;
44090
+ const requestedRuntime = query2.runtime;
44091
+ if (requestedRuntime && !AGENT_RUNTIMES.includes(requestedRuntime)) {
44092
+ return reply.code(400).send({ error: "runtime harus claude atau codex" });
44093
+ }
42944
44094
  const rows = await prisma.customAgent.findMany({
42945
44095
  where: projectId ? { OR: [{ projectId: null }, { projectId }] } : { projectId: null },
42946
44096
  orderBy: { name: "asc" }
@@ -42949,7 +44099,7 @@ async function custom_agents_default(app2) {
42949
44099
  for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
42950
44100
  for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
42951
44101
  const stamps = await stampsOf();
42952
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view8(r, projectId, stamps));
44102
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view8(r, projectId, stamps, requestedRuntime));
42953
44103
  });
42954
44104
  app2.post("/custom-agents", async (req, reply) => {
42955
44105
  const parsed = zCreateCustomAgent.safeParse(req.body);
@@ -42962,6 +44112,8 @@ async function custom_agents_default(app2) {
42962
44112
  if (tp) return reply.code(400).send(tp);
42963
44113
  const mp = modelProblem(p3.model ?? null, p3.runtime ?? null);
42964
44114
  if (mp) return reply.code(400).send(mp);
44115
+ const ep = effortProblem(p3.effort ?? null, p3.runtime ?? null, p3.model ?? null);
44116
+ if (ep) return reply.code(400).send(ep);
42965
44117
  const id = customAgentId(projectId, p3.name);
42966
44118
  if (await prisma.customAgent.findUnique({ where: { id } }))
42967
44119
  return reply.code(409).send({ error: "nama sudah dipakai di scope ini", id });
@@ -42975,6 +44127,11 @@ async function custom_agents_default(app2) {
42975
44127
  model: p3.model ?? null,
42976
44128
  mentions: p3.mentions ?? [],
42977
44129
  runtime: p3.runtime ?? null,
44130
+ activation: p3.activation ?? "always",
44131
+ effort: p3.effort ?? null,
44132
+ workspacePolicy: p3.workspacePolicy ?? "inherit",
44133
+ maxTurns: p3.maxTurns ?? null,
44134
+ timeoutSeconds: p3.timeoutSeconds ?? null,
42978
44135
  enabled: p3.enabled ?? true
42979
44136
  };
42980
44137
  const all = [...await rowsOf(), candidate];
@@ -42992,6 +44149,11 @@ async function custom_agents_default(app2) {
42992
44149
  model: candidate.model,
42993
44150
  mentions: candidate.mentions,
42994
44151
  runtime: candidate.runtime,
44152
+ activation: candidate.activation,
44153
+ effort: candidate.effort,
44154
+ workspacePolicy: candidate.workspacePolicy,
44155
+ maxTurns: candidate.maxTurns,
44156
+ timeoutSeconds: candidate.timeoutSeconds,
42995
44157
  enabled: candidate.enabled
42996
44158
  } });
42997
44159
  await loadCustomAgents();
@@ -43009,6 +44171,12 @@ async function custom_agents_default(app2) {
43009
44171
  if (!existing) return reply.code(404).send({ error: "not found" });
43010
44172
  const before2 = existing;
43011
44173
  const effRuntime = "runtime" in parsed.data ? parsed.data.runtime ?? null : runtimeOf(before2.runtime);
44174
+ const effWorkspacePolicy = "workspacePolicy" in parsed.data ? parsed.data.workspacePolicy ?? "inherit" : workspacePolicyOf(before2.workspacePolicy);
44175
+ if (("runtime" in parsed.data || "workspacePolicy" in parsed.data) && effWorkspacePolicy === "isolated-worktree" && effRuntime !== "claude") {
44176
+ return reply.code(400).send({
44177
+ error: "isolated-worktree hanya tersedia untuk agen ber-runtime Claude Code"
44178
+ });
44179
+ }
43012
44180
  if (parsed.data.tools !== void 0) {
43013
44181
  const tp = toolsProblem(parsed.data.tools, agentToolIds(await repoDirOf2(before2.projectId)));
43014
44182
  if (tp) return reply.code(400).send(tp);
@@ -43020,12 +44188,25 @@ async function custom_agents_default(app2) {
43020
44188
  );
43021
44189
  if (mp) return reply.code(400).send(mp);
43022
44190
  }
44191
+ if (parsed.data.effort !== void 0 || parsed.data.model !== void 0 || "runtime" in parsed.data) {
44192
+ const ep = effortProblem(
44193
+ parsed.data.effort !== void 0 ? parsed.data.effort : effortOf(before2.effort),
44194
+ effRuntime,
44195
+ parsed.data.model !== void 0 ? parsed.data.model : before2.model
44196
+ );
44197
+ if (ep) return reply.code(400).send(ep);
44198
+ }
43023
44199
  const candidate = {
43024
44200
  ...before2,
43025
44201
  ...parsed.data,
43026
44202
  mentions: parsed.data.mentions ?? mentionsOf(before2.mentions),
43027
44203
  tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before2.tools),
43028
- runtime: effRuntime
44204
+ runtime: effRuntime,
44205
+ activation: parsed.data.activation ?? activationOf(before2.activation),
44206
+ effort: parsed.data.effort !== void 0 ? parsed.data.effort : effortOf(before2.effort),
44207
+ workspacePolicy: effWorkspacePolicy,
44208
+ maxTurns: parsed.data.maxTurns !== void 0 ? parsed.data.maxTurns : maxTurnsOf(before2.maxTurns),
44209
+ timeoutSeconds: parsed.data.timeoutSeconds !== void 0 ? parsed.data.timeoutSeconds : timeoutSecondsOf(before2.timeoutSeconds)
43029
44210
  };
43030
44211
  const all = (await rowsOf()).map((r) => r.id === id ? candidate : r);
43031
44212
  const unknown = unknownMentions(candidate, all);
@@ -43039,6 +44220,11 @@ async function custom_agents_default(app2) {
43039
44220
  model: candidate.model,
43040
44221
  mentions: candidate.mentions,
43041
44222
  runtime: candidate.runtime,
44223
+ activation: candidate.activation,
44224
+ effort: candidate.effort,
44225
+ workspacePolicy: candidate.workspacePolicy,
44226
+ maxTurns: candidate.maxTurns,
44227
+ timeoutSeconds: candidate.timeoutSeconds,
43042
44228
  enabled: candidate.enabled
43043
44229
  } });
43044
44230
  await loadCustomAgents();
@@ -43065,9 +44251,39 @@ async function custom_agents_default(app2) {
43065
44251
  });
43066
44252
  }
43067
44253
 
44254
+ // src/routes/custom-agent-metrics.ts
44255
+ init_zod();
44256
+ var zQuery = external_exports.object({
44257
+ projectId: external_exports.string().min(1).optional(),
44258
+ from: external_exports.string().datetime({ offset: true }).optional(),
44259
+ to: external_exports.string().datetime({ offset: true }).optional()
44260
+ });
44261
+ var zPatch = external_exports.object({
44262
+ disposition: external_exports.enum(["accepted", "partial", "rejected", "false-positive"]),
44263
+ note: external_exports.string().max(500).nullable().optional()
44264
+ }).strict();
44265
+ async function custom_agent_metrics_default(app2) {
44266
+ app2.get("/custom-agents/metrics", async (req, reply) => {
44267
+ const parsed = zQuery.safeParse(req.query);
44268
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message });
44269
+ const from = parsed.data.from ? new Date(parsed.data.from) : void 0;
44270
+ const to = parsed.data.to ? new Date(parsed.data.to) : void 0;
44271
+ if (from && to && from > to) return reply.code(400).send({ error: "from harus sebelum to" });
44272
+ return agentMetrics({ projectId: parsed.data.projectId, from, to });
44273
+ });
44274
+ app2.patch("/custom-agents/invocations/:id", async (req, reply) => {
44275
+ const body = zPatch.safeParse(req.body);
44276
+ if (!body.success) return reply.code(400).send({ error: body.error.issues[0]?.message });
44277
+ const id = String(req.params.id ?? "");
44278
+ const view13 = await updateAgentInvocationDisposition(id, body.data.disposition, body.data.note);
44279
+ return view13 ?? reply.code(404).send({ error: "invocation tidak ditemukan" });
44280
+ });
44281
+ }
44282
+
43068
44283
  // src/routes/members.ts
43069
44284
  init_src();
43070
44285
  init_db();
44286
+ init_sync_notify();
43071
44287
  var view9 = (m) => ({
43072
44288
  id: m.id,
43073
44289
  name: m.name,
@@ -43124,10 +44340,12 @@ async function members_default(app2) {
43124
44340
  // src/routes/tasks.ts
43125
44341
  init_src();
43126
44342
  init_db();
44343
+ init_sync_notify();
43127
44344
 
43128
44345
  // src/services/task-escalate.ts
43129
44346
  init_src();
43130
44347
  init_db();
44348
+ init_sync_notify();
43131
44349
  var day = (d) => d ? d.toISOString().slice(0, 10) : null;
43132
44350
  function contextOf(task, member, backlink) {
43133
44351
  const lines2 = [
@@ -43408,7 +44626,7 @@ function issuesFromRest(raw) {
43408
44626
  }
43409
44627
  var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
43410
44628
  var API2 = "https://api.github.com";
43411
- var defaultRunGh = (args, env) => new Promise((resolve21, reject2) => {
44629
+ var defaultRunGh = (args, env) => new Promise((resolve22, reject2) => {
43412
44630
  execFile16(
43413
44631
  args[0],
43414
44632
  args.slice(1),
@@ -43416,7 +44634,7 @@ var defaultRunGh = (args, env) => new Promise((resolve21, reject2) => {
43416
44634
  (err, stdout, stderr) => {
43417
44635
  const e = err;
43418
44636
  if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject2(e);
43419
- resolve21({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
44637
+ resolve22({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
43420
44638
  }
43421
44639
  );
43422
44640
  });
@@ -43511,6 +44729,7 @@ async function fetchIssues(repo, opts, deps = {}) {
43511
44729
  }
43512
44730
 
43513
44731
  // src/services/github-issues.ts
44732
+ init_sync_notify();
43514
44733
  var issueRowId = (projectId, slug, number) => `${projectId}:${slug}#${number}`;
43515
44734
  async function pullIssues(projectId, opts = {}, deps = {}) {
43516
44735
  const resolved = await resolveGithubRepo(projectId);
@@ -43559,6 +44778,7 @@ async function pullIssues(projectId, opts = {}, deps = {}) {
43559
44778
  // src/services/github-accept.ts
43560
44779
  init_src();
43561
44780
  init_db();
44781
+ init_sync_notify();
43562
44782
  var backlinkOf = (i) => `Dari GitHub issue ${i.repoSlug}#${i.number} (${i.url}).`;
43563
44783
  async function acceptGithubIssue(issue2, opts) {
43564
44784
  if (issue2.specId) {
@@ -43615,6 +44835,7 @@ ${backlink}`;
43615
44835
  }
43616
44836
 
43617
44837
  // src/routes/github-issues.ts
44838
+ init_sync_notify();
43618
44839
  var zPull = external_exports.object({
43619
44840
  state: external_exports.enum(["open", "all"]).optional(),
43620
44841
  limit: external_exports.number().int().min(1).max(1e3).optional()
@@ -44451,10 +45672,10 @@ function sanitizeClientText(text) {
44451
45672
  return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").slice(0, MAX_PESAN);
44452
45673
  }
44453
45674
  function wrapClientMessage(text, nonce) {
44454
- const open4 = `<pesan-klien-${nonce}>`;
45675
+ const open5 = `<pesan-klien-${nonce}>`;
44455
45676
  const close = `</pesan-klien-${nonce}>`;
44456
45677
  const jinak = sanitizeClientText(text).replaceAll("</pesan-klien", "<\u200B/pesan-klien").replaceAll("<pesan-klien", "<\u200Bpesan-klien");
44457
- return `${open4}
45678
+ return `${open5}
44458
45679
  ${jinak}
44459
45680
  ${close}`;
44460
45681
  }
@@ -44553,9 +45774,9 @@ ${baru}`;
44553
45774
  // src/services/portal-chat/workspace.ts
44554
45775
  init_src();
44555
45776
  init_db();
44556
- import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "node:fs";
44557
- import { tmpdir as tmpdir5 } from "node:os";
44558
- import { join as join27 } from "node:path";
45777
+ import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
45778
+ import { tmpdir as tmpdir6 } from "node:os";
45779
+ import { join as join30 } from "node:path";
44559
45780
  var STAGE_LABEL = {
44560
45781
  brainstorming: "Dirumuskan",
44561
45782
  objective: "Dirumuskan",
@@ -44610,7 +45831,7 @@ ${c.body}
44610
45831
  ${baris.join("\n")}`;
44611
45832
  }
44612
45833
  async function buildChatWorkspace(projectId) {
44613
- const dir2 = mkdtempSync2(join27(tmpdir5(), "hanoman-portal-chat-"));
45834
+ const dir2 = mkdtempSync2(join30(tmpdir6(), "hanoman-portal-chat-"));
44614
45835
  try {
44615
45836
  const project = await prisma.project.findUnique({
44616
45837
  where: { id: projectId },
@@ -44631,7 +45852,7 @@ async function buildChatWorkspace(projectId) {
44631
45852
  });
44632
45853
  const files = [];
44633
45854
  const tulis = (rel, isi) => {
44634
- writeFileSync7(join27(dir2, rel), isi, { mode: 384 });
45855
+ writeFileSync9(join30(dir2, rel), isi, { mode: 384 });
44635
45856
  files.push(rel);
44636
45857
  };
44637
45858
  tulis("project.md", renderProjectDoc(project));
@@ -44640,16 +45861,16 @@ async function buildChatWorkspace(projectId) {
44640
45861
  tulis("catatan-rilis.md", renderChangelogDoc(changelogs));
44641
45862
  const prds = await listPrds(projectId);
44642
45863
  if (prds.length) {
44643
- mkdirSync12(join27(dir2, "dokumen"), { mode: 448 });
45864
+ mkdirSync12(join30(dir2, "dokumen"), { mode: 448 });
44644
45865
  for (const prd of prds) {
44645
45866
  const isi = await readPrd(projectId, prd.path);
44646
45867
  if (!isi) continue;
44647
- tulis(join27("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
45868
+ tulis(join30("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
44648
45869
  }
44649
45870
  }
44650
- return { dir: dir2, files, cleanup: () => rmSync7(dir2, { recursive: true, force: true }) };
45871
+ return { dir: dir2, files, cleanup: () => rmSync8(dir2, { recursive: true, force: true }) };
44651
45872
  } catch (error) {
44652
- rmSync7(dir2, { recursive: true, force: true });
45873
+ rmSync8(dir2, { recursive: true, force: true });
44653
45874
  throw error;
44654
45875
  }
44655
45876
  }
@@ -44683,7 +45904,7 @@ function portalChatArgv(o) {
44683
45904
  o.prompt
44684
45905
  ];
44685
45906
  }
44686
- var shellQuote2 = (v) => `'${v.replace(/'/g, `'"'"'`)}'`;
45907
+ var shellQuote4 = (v) => `'${v.replace(/'/g, `'"'"'`)}'`;
44687
45908
  function portalChatProcess(o, env = process.env) {
44688
45909
  const file = effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
44689
45910
  const args = portalChatArgv(o);
@@ -44693,7 +45914,7 @@ function portalChatProcess(o, env = process.env) {
44693
45914
  throw new Error("chat portal menolak jalan: sandbox sesi wajib saat hardening menyala");
44694
45915
  return { file, args, cwd: o.workspace };
44695
45916
  }
44696
- const command = [file, ...args].map(shellQuote2).join(" ");
45917
+ const command = [file, ...args].map(shellQuote4).join(" ");
44697
45918
  const sandbox = sandboxArgvFromEnv({
44698
45919
  command,
44699
45920
  worktree: o.workspace,
@@ -44727,7 +45948,7 @@ function chatFailureReason(err, stdout, stderr, timeoutMs) {
44727
45948
  return `chat portal gagal (${bagaimana}): ${detail}`;
44728
45949
  }
44729
45950
  function runProcess(p3, timeoutMs) {
44730
- return new Promise((resolve21, reject2) => {
45951
+ return new Promise((resolve22, reject2) => {
44731
45952
  const child = execFile17(p3.file, p3.args, {
44732
45953
  cwd: p3.cwd,
44733
45954
  timeout: timeoutMs,
@@ -44739,7 +45960,7 @@ function runProcess(p3, timeoutMs) {
44739
45960
  stdout,
44740
45961
  stderr,
44741
45962
  timeoutMs
44742
- ))) : resolve21(stdout));
45963
+ ))) : resolve22(stdout));
44743
45964
  child.stdin?.end();
44744
45965
  });
44745
45966
  }
@@ -44963,7 +46184,7 @@ init_zod();
44963
46184
  init_db();
44964
46185
  init_settings3();
44965
46186
  var zPrd = external_exports.object({ slug: external_exports.string().regex(/^[a-z0-9]([a-z0-9-]{0,60}[a-z0-9])?$/) });
44966
- var zQuery = external_exports.object({ project: external_exports.string().min(1) });
46187
+ var zQuery2 = external_exports.object({ project: external_exports.string().min(1) });
44967
46188
  var sessionRow = (s2) => ({
44968
46189
  id: s2.id,
44969
46190
  projectId: s2.projectId,
@@ -44991,7 +46212,7 @@ var messageRow = (m) => ({
44991
46212
  });
44992
46213
  async function portal_chat_admin_default(app2) {
44993
46214
  app2.get("/portal-chat/sessions", async (req, reply) => {
44994
- const parsed = zQuery.safeParse(req.query);
46215
+ const parsed = zQuery2.safeParse(req.query);
44995
46216
  if (!parsed.success) return reply.code(400).send({ error: "project wajib" });
44996
46217
  const { page, limit } = req.query;
44997
46218
  const rows = await prisma.portalChatSession.findMany({
@@ -45035,7 +46256,7 @@ async function portal_chat_admin_default(app2) {
45035
46256
  return reply.code(201).send({ path });
45036
46257
  });
45037
46258
  app2.get("/portal-chat/export", async (req, reply) => {
45038
- const parsed = zQuery.safeParse(req.query);
46259
+ const parsed = zQuery2.safeParse(req.query);
45039
46260
  if (!parsed.success) return reply.code(400).send({ error: "project wajib" });
45040
46261
  const { from, to } = req.query;
45041
46262
  const rows = await prisma.portalChatSession.findMany({
@@ -45155,15 +46376,15 @@ init_src();
45155
46376
  init_db();
45156
46377
 
45157
46378
  // src/services/bootstrap.ts
45158
- import { createHash as createHash10, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
46379
+ import { createHash as createHash11, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
45159
46380
  import { chmod, lstat as lstat3, mkdir as mkdir6, readFile as readFile5, unlink as unlink5, writeFile as writeFile5 } from "node:fs/promises";
45160
- import { join as join28 } from "node:path";
46381
+ import { join as join31 } from "node:path";
45161
46382
  var TTL_MS5 = 15 * 6e4;
45162
46383
  var SETUP_TOKEN_FILE = "setup.token";
45163
46384
  var BootstrapError = class extends Error {
45164
46385
  code = "BOOTSTRAP_PROOF";
45165
46386
  };
45166
- var tokenPath = (home3) => join28(home3, SETUP_TOKEN_FILE);
46387
+ var tokenPath = (home3) => join31(home3, SETUP_TOKEN_FILE);
45167
46388
  async function readStored(home3) {
45168
46389
  try {
45169
46390
  const info = await lstat3(tokenPath(home3));
@@ -45208,8 +46429,8 @@ ${new Date(expiresAt).toISOString()}
45208
46429
  }
45209
46430
  async function verifySetupToken(candidate, home3, now = Date.now()) {
45210
46431
  const stored = await readStored(home3);
45211
- const got = createHash10("sha256").update(candidate).digest();
45212
- const want = createHash10("sha256").update(stored.token).digest();
46432
+ const got = createHash11("sha256").update(candidate).digest();
46433
+ const want = createHash11("sha256").update(stored.token).digest();
45213
46434
  if (stored.expiresAt <= now || !timingSafeEqual4(got, want)) throw new BootstrapError("invalid setup proof");
45214
46435
  }
45215
46436
  async function consumeSetupToken(home3) {
@@ -45488,7 +46709,10 @@ function capabilityForRoute(method, path) {
45488
46709
  return rw("settings");
45489
46710
  }
45490
46711
  if (top === "lead") return rw("lead");
45491
- if (top === "custom-agents") return rw("agents");
46712
+ if (top === "custom-agents") {
46713
+ if (seg[1] === "metrics" || seg[1] === "invocations") return "COOKIE_ONLY";
46714
+ return rw("agents");
46715
+ }
45492
46716
  if (top === "telegram") {
45493
46717
  const sub = seg[1] ?? "";
45494
46718
  if (sub === "settings" || sub === "test" || sub === "credentials") return "COOKIE_ONLY";
@@ -45775,6 +46999,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
45775
46999
  await api.register(methods);
45776
47000
  await api.register(lead_default);
45777
47001
  await api.register(custom_agents_default);
47002
+ await api.register(custom_agent_metrics_default);
45778
47003
  await api.register(githubIssues);
45779
47004
  await api.register(telegramRoutes);
45780
47005
  await api.register(webhooks_default);
@@ -45975,6 +47200,7 @@ init_db();
45975
47200
  init_src2();
45976
47201
  init_notifications2();
45977
47202
  init_stage_machine();
47203
+ init_sync_notify();
45978
47204
  init_pty();
45979
47205
  init_session_phases();
45980
47206
  var FAIL_REASON = "sesi berakhir sebelum mencapai done (gagal/limit)";
@@ -46141,6 +47367,7 @@ function registerTriaseSource() {
46141
47367
  }
46142
47368
 
46143
47369
  // src/server.ts
47370
+ init_custom_agents2();
46144
47371
  init_pty();
46145
47372
  init_bootstrap();
46146
47373
 
@@ -46150,7 +47377,7 @@ init_tap();
46150
47377
  // src/services/webhooks/emit.ts
46151
47378
  init_src();
46152
47379
  init_db();
46153
- import { randomUUID as randomUUID10 } from "node:crypto";
47380
+ import { randomUUID as randomUUID11 } from "node:crypto";
46154
47381
  function skipped(def2, row) {
46155
47382
  if (!def2.skipWhen || !row) return false;
46156
47383
  return row[def2.skipWhen.field] === def2.skipWhen.equals;
@@ -46216,7 +47443,7 @@ async function emitWebhook(i) {
46216
47443
  const targets = matchingEndpoints(type, projectId);
46217
47444
  if (!targets.length) return;
46218
47445
  const name2 = projectId ? (await prisma.project.findUnique({ where: { id: projectId }, select: { name: true } }))?.name ?? null : null;
46219
- const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID10().replace(/-/g, "")}`);
47446
+ const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID11().replace(/-/g, "")}`);
46220
47447
  if (!env) return;
46221
47448
  await enqueueEnvelope(env, targets);
46222
47449
  } catch (e) {
@@ -46529,13 +47756,13 @@ init_src2();
46529
47756
 
46530
47757
  // src/services/secure-home.ts
46531
47758
  import { chmod as chmod2, lstat as lstat4, mkdir as mkdir7 } from "node:fs/promises";
46532
- import { isAbsolute as isAbsolute7, resolve as resolve20 } from "node:path";
47759
+ import { isAbsolute as isAbsolute8, resolve as resolve21 } from "node:path";
46533
47760
  var HomePermissionError = class extends Error {
46534
47761
  code = "HOME_SYMLINK";
46535
47762
  };
46536
47763
  async function assertNoSymlink(path, allowMissing) {
46537
47764
  try {
46538
- const info = await lstat4(resolve20(path));
47765
+ const info = await lstat4(resolve21(path));
46539
47766
  if (info.isSymbolicLink()) throw new HomePermissionError(`symlink ditolak: ${path}`);
46540
47767
  } catch (error) {
46541
47768
  if (error.code === "ENOENT" && allowMissing) return;
@@ -46543,7 +47770,7 @@ async function assertNoSymlink(path, allowMissing) {
46543
47770
  }
46544
47771
  }
46545
47772
  async function secureHanomanHome(opts) {
46546
- if (!isAbsolute7(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
47773
+ if (!isAbsolute8(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
46547
47774
  await assertNoSymlink(opts.home, true);
46548
47775
  await mkdir7(opts.home, { recursive: true, mode: 448 });
46549
47776
  await assertNoSymlink(opts.home, false);
@@ -46733,10 +47960,106 @@ function startRetentionSweep() {
46733
47960
 
46734
47961
  // src/server.ts
46735
47962
  init_uploads();
47963
+
47964
+ // src/services/session-event-relay.ts
47965
+ init_session_event_token();
47966
+ init_session_event_spool();
47967
+ import { constants as constants3 } from "node:fs";
47968
+ import { mkdir as mkdir8, open as open4, readdir as readdir7, rm as rm6 } from "node:fs/promises";
47969
+ import { join as join32 } from "node:path";
47970
+ var MAX_EVENT_BYTES = 1e6;
47971
+ var MAX_FILES_PER_DRAIN = 1e3;
47972
+ var SESSION_ID_RE = /^[a-z0-9_-]+$/;
47973
+ async function drainSessionEventSpool(app2, root = sessionEventSpoolRoot()) {
47974
+ await mkdir8(root, { recursive: true, mode: 448 });
47975
+ let delivered = 0;
47976
+ let examined = 0;
47977
+ let readBuffer;
47978
+ for (const session of await readdir7(root, { withFileTypes: true })) {
47979
+ if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
47980
+ const dir2 = join32(root, session.name);
47981
+ let entries3;
47982
+ try {
47983
+ entries3 = await readdir7(dir2, { withFileTypes: true });
47984
+ } catch {
47985
+ continue;
47986
+ }
47987
+ for (const entry of entries3) {
47988
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
47989
+ if (++examined > MAX_FILES_PER_DRAIN) return delivered;
47990
+ const path = join32(dir2, entry.name);
47991
+ let payload;
47992
+ let handle;
47993
+ try {
47994
+ handle = await open4(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
47995
+ const stat4 = await handle.stat();
47996
+ if (!stat4.isFile() || stat4.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
47997
+ readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
47998
+ const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
47999
+ if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
48000
+ const parsed = JSON.parse(readBuffer.subarray(0, bytesRead).toString("utf8"));
48001
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
48002
+ throw new Error("payload event bukan object");
48003
+ }
48004
+ payload = parsed;
48005
+ } catch {
48006
+ await rm6(path, { force: true }).catch(() => {
48007
+ });
48008
+ continue;
48009
+ } finally {
48010
+ await handle?.close().catch(() => {
48011
+ });
48012
+ }
48013
+ try {
48014
+ const response = await app2.inject({
48015
+ method: "POST",
48016
+ url: "/api/session-events",
48017
+ headers: {
48018
+ authorization: `Bearer ${sessionEventToken(session.name)}`,
48019
+ "x-hanoman-session": session.name
48020
+ },
48021
+ payload
48022
+ });
48023
+ if (response.statusCode === 429 || response.statusCode >= 500) continue;
48024
+ await rm6(path, { force: true }).catch(() => {
48025
+ });
48026
+ if (response.statusCode >= 200 && response.statusCode < 300) delivered++;
48027
+ } catch {
48028
+ }
48029
+ }
48030
+ }
48031
+ return delivered;
48032
+ }
48033
+ function startSessionEventRelay(app2, options2 = {}) {
48034
+ const root = options2.root ?? sessionEventSpoolRoot();
48035
+ let running = false;
48036
+ const tick6 = async () => {
48037
+ if (running) return;
48038
+ running = true;
48039
+ try {
48040
+ await drainSessionEventSpool({ inject: async (request) => app2.inject(request) }, root);
48041
+ } catch (error) {
48042
+ console.error("session event relay gagal:", error);
48043
+ } finally {
48044
+ running = false;
48045
+ }
48046
+ };
48047
+ const timer9 = setInterval(() => {
48048
+ void tick6();
48049
+ }, options2.intervalMs ?? 250);
48050
+ timer9.unref();
48051
+ app2.addHook("onClose", async () => {
48052
+ clearInterval(timer9);
48053
+ });
48054
+ void tick6();
48055
+ }
48056
+
48057
+ // src/server.ts
46736
48058
  var app = buildApp();
46737
48059
  var port = Number(process.env.PORT ?? 8787);
46738
48060
  var host = process.env.HOST ?? "127.0.0.1";
46739
48061
  assertRuntimeBoundary(process.env, { uid: process.getuid?.(), host });
48062
+ startSessionEventRelay(app);
46740
48063
  process.on("unhandledRejection", (err) => console.error("unhandledRejection:", err));
46741
48064
  process.on("uncaughtException", (err) => console.error("uncaughtException:", err));
46742
48065
  async function shutdown(sig) {
@@ -46765,9 +48088,19 @@ var bootstrapReady = secureHanomanHome({
46765
48088
  const proof = await ensureSetupToken(resolveHome());
46766
48089
  console.log(`setup admin memerlukan token di ${proof.path}; kedaluwarsa ${new Date(proof.expiresAt).toISOString()}`);
46767
48090
  });
46768
- bootstrapReady.then(() => app.listen({ port, host })).then(async () => {
46769
- console.log(`hanoman api ${host}:${port}`);
48091
+ bootstrapReady.then(async () => {
48092
+ try {
48093
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
48094
+ const { applyConfigOnBoot: applyConfigOnBoot2 } = await Promise.resolve().then(() => (init_config_apply(), config_apply_exports));
48095
+ await loadConfig2();
48096
+ await applyConfigOnBoot2();
48097
+ } catch (e) {
48098
+ console.error("config runtime gagal dimuat \u2014 memakai env/default:", e);
48099
+ }
46770
48100
  await installCustomAgents();
48101
+ return app.listen({ port, host });
48102
+ }).then(async () => {
48103
+ console.log(`hanoman api ${host}:${port}`);
46771
48104
  await installWebhooks();
46772
48105
  installSessionHistory();
46773
48106
  try {
@@ -46775,17 +48108,12 @@ bootstrapReady.then(() => app.listen({ port, host })).then(async () => {
46775
48108
  void reconcileHistory(liveIds).then((n2) => {
46776
48109
  if (n2) console.log(`riwayat sesi: ${n2} baris berjalan direkonsiliasi`);
46777
48110
  }).catch((e) => console.error("rekonsiliasi riwayat sesi:", e));
48111
+ void reconcileAgentInvocations(liveIds).then((n2) => {
48112
+ if (n2) console.log(`custom agent: ${n2} invocation ditandai abandoned`);
48113
+ }).catch((e) => console.error("rekonsiliasi invocation custom agent:", e));
46778
48114
  } catch (e) {
46779
48115
  console.error("rekonsiliasi riwayat sesi dilewati \u2014 tmux tak terbaca:", e);
46780
48116
  }
46781
- try {
46782
- const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
46783
- const { applyConfigOnBoot: applyConfigOnBoot2 } = await Promise.resolve().then(() => (init_config_apply(), config_apply_exports));
46784
- await loadConfig2();
46785
- await applyConfigOnBoot2();
46786
- } catch (e) {
46787
- console.error("config runtime gagal dimuat \u2014 memakai env/default:", e);
46788
- }
46789
48117
  const boundPort = app.server.address().port;
46790
48118
  await installTelegramGateway(app, { apiBase: `http://127.0.0.1:${boundPort}` });
46791
48119
  startVpsMonitor();