hanoman 0.2.3 → 0.2.5

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)
@@ -11651,6 +11775,16 @@ var init_presence = __esm({
11651
11775
  }
11652
11776
  });
11653
11777
 
11778
+ // ../shared/src/pending.ts
11779
+ var EMPTY_PENDING, OPEN_LEAD_FLOW_STATUSES;
11780
+ var init_pending = __esm({
11781
+ "../shared/src/pending.ts"() {
11782
+ "use strict";
11783
+ EMPTY_PENDING = { triage: 0, backlog: 0, prd: 0, lead: 0 };
11784
+ OPEN_LEAD_FLOW_STATUSES = ["menunggu", "sebagian"];
11785
+ }
11786
+ });
11787
+
11654
11788
  // ../shared/src/index.ts
11655
11789
  var init_src = __esm({
11656
11790
  "../shared/src/index.ts"() {
@@ -11691,6 +11825,7 @@ var init_src = __esm({
11691
11825
  init_session_dialog();
11692
11826
  init_session_ask();
11693
11827
  init_presence();
11828
+ init_pending();
11694
11829
  init_team();
11695
11830
  }
11696
11831
  });
@@ -12321,8 +12456,8 @@ var init_git = __esm({
12321
12456
  // di dalam repo induk, yang menjawab "true" untuk pertanyaan pertama. cwd yang tak ada membuat
12322
12457
  // spawnSync gagal (`status` null), dan itu sudah tertangkap `!== 0`.
12323
12458
  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;
12459
+ const inside2 = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: path, encoding: "utf8" });
12460
+ if (inside2.status !== 0 || inside2.stdout.trim() !== "true") return false;
12326
12461
  const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: path, encoding: "utf8" });
12327
12462
  return top.status === 0 && samePath(top.stdout.trim(), path);
12328
12463
  },
@@ -12353,17 +12488,27 @@ var init_git = __esm({
12353
12488
  });
12354
12489
 
12355
12490
  // ../runner/src/settings.ts
12356
- var EVENT_HOOK_COMMAND, guardSettings;
12491
+ var EVENT_SPOOL_SCRIPT, EVENT_HOOK_COMMAND, guardSettings;
12357
12492
  var init_settings2 = __esm({
12358
12493
  "../runner/src/settings.ts"() {
12359
12494
  "use strict";
12495
+ EVENT_SPOOL_SCRIPT = [
12496
+ 'const fs=require("node:fs"),p=require("node:path"),c=require("node:crypto")',
12497
+ 'let d=""',
12498
+ 'process.stdin.setEncoding("utf8")',
12499
+ 'process.stdin.on("data",x=>d+=x)',
12500
+ '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{}}})'
12501
+ ].join(";");
12360
12502
  EVENT_HOOK_COMMAND = [
12503
+ 'if [ -n "${HANOMAN_EVENT_DIR:-}" ]; then',
12504
+ `node -e '${EVENT_SPOOL_SCRIPT}' >/dev/null 2>&1;`,
12505
+ "else",
12361
12506
  'curl -sS -m 2 -X POST "$HANOMAN_EVENT_URL"',
12362
12507
  "-H 'content-type: application/json'",
12363
12508
  '-H "authorization: Bearer $HANOMAN_EVENT_TOKEN"',
12364
12509
  '-H "x-hanoman-session: $HANOMAN_SESSION_ID"',
12365
12510
  '${HANOMAN_EVENT_HOST:+-H "host: $HANOMAN_EVENT_HOST"}',
12366
- "--data-binary @- >/dev/null 2>&1; exit 0"
12511
+ "--data-binary @- >/dev/null 2>&1; fi; exit 0"
12367
12512
  ].join(" ");
12368
12513
  guardSettings = (decisionFile, goal, eventHook) => {
12369
12514
  const hooks2 = {};
@@ -12380,6 +12525,8 @@ var init_settings2 = __esm({
12380
12525
  matcher: "AskUserQuestion",
12381
12526
  hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }]
12382
12527
  }];
12528
+ hooks2.SubagentStart = [{ hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }] }];
12529
+ hooks2.SubagentStop = [{ hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }] }];
12383
12530
  }
12384
12531
  if (goal) hooks2.Stop = [{ hooks: [{ type: "prompt", prompt: goal }] }];
12385
12532
  return { hooks: hooks2 };
@@ -12458,6 +12605,10 @@ function codexHookArgs(o) {
12458
12605
  const args = [];
12459
12606
  if (stop.length) args.push("-c", `hooks.Stop=${group(stop)}`);
12460
12607
  if (submit.length) args.push("-c", `hooks.UserPromptSubmit=${group(submit)}`);
12608
+ if (o.eventHook) {
12609
+ args.push("-c", `hooks.SubagentStart=${group([EVENT_HOOK_COMMAND])}`);
12610
+ args.push("-c", `hooks.SubagentStop=${group([EVENT_HOOK_COMMAND])}`);
12611
+ }
12461
12612
  return args;
12462
12613
  }
12463
12614
  function codexGoalScript(o) {
@@ -12554,9 +12705,12 @@ var init_agent_cli = __esm({
12554
12705
  // ../runner/src/custom-agents.ts
12555
12706
  function agentPromptOf(def2, roster) {
12556
12707
  const can = liveMentions(def2, roster);
12708
+ const instructions = def2.timeoutSeconds ? `${def2.instructions}
12709
+
12710
+ Batas waktu Hanoman untuk pekerjaan ini ${def2.timeoutSeconds} detik. Prioritaskan putusan dan bukti sebelum batas itu.` : def2.instructions;
12557
12711
  if (can.length === 0) {
12558
12712
  return [
12559
- def2.instructions,
12713
+ instructions,
12560
12714
  "",
12561
12715
  "---",
12562
12716
  "Kamu TIDAK boleh mendelegasikan ke agen lain. Selesaikan sendiri lalu laporkan hasilnya.",
@@ -12566,7 +12720,7 @@ function agentPromptOf(def2, roster) {
12566
12720
  }
12567
12721
  const list2 = can.map((m) => `@${m}`).join(", ");
12568
12722
  return [
12569
- def2.instructions,
12723
+ instructions,
12570
12724
  "",
12571
12725
  "---",
12572
12726
  `Kamu boleh mendelegasikan HANYA ke: ${list2}. Panggil lewat ${MENTION_TOOL} dengan nama agennya.`,
@@ -12576,44 +12730,37 @@ function agentPromptOf(def2, roster) {
12576
12730
  CODE_STYLE_CLAUSE
12577
12731
  ].join("\n");
12578
12732
  }
12579
- function renderAgentsJson(defs) {
12733
+ function renderAgentsJson(defs, options2 = {}) {
12580
12734
  if (defs.length === 0) return "";
12581
12735
  const out4 = {};
12582
12736
  for (const d of defs) {
12737
+ const resolvedTools = resolveTools({ tools: d.tools, mentions: d.mentions });
12738
+ const readOnly = d.workspacePolicy === "read-only";
12583
12739
  out4[d.name] = {
12584
12740
  description: d.description,
12585
12741
  prompt: agentPromptOf(d, defs),
12586
- tools: resolveTools({ tools: d.tools, mentions: d.mentions }),
12587
- ...d.model ? { model: d.model } : {}
12742
+ tools: readOnly ? resolvedTools.filter((tool) => READ_ONLY_TOOLS.has(tool)) : resolvedTools,
12743
+ ...d.model ? { model: d.model } : {},
12744
+ ...d.effort ? { effort: d.effort } : {},
12745
+ ...typeof d.maxTurns === "number" ? { maxTurns: d.maxTurns } : {},
12746
+ ...d.workspacePolicy === "isolated-worktree" ? { isolation: "worktree" } : {},
12747
+ ...readOnly ? { permissionMode: "plan" } : {},
12748
+ ...readOnly && options2.readOnlyHookCommand ? {
12749
+ hooks: {
12750
+ PreToolUse: [{
12751
+ hooks: [{
12752
+ type: "command",
12753
+ command: options2.readOnlyHookCommand,
12754
+ timeout: 5
12755
+ }]
12756
+ }]
12757
+ }
12758
+ } : {}
12588
12759
  };
12589
12760
  }
12590
12761
  return JSON.stringify(out4);
12591
12762
  }
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) {
12763
+ function agentDelegationClause(defs, runtime = "claude") {
12617
12764
  if (defs.length === 0) return "";
12618
12765
  return [
12619
12766
  "",
@@ -12625,35 +12772,343 @@ function agentDelegationClause(defs) {
12625
12772
  "",
12626
12773
  ...defs.map((d) => `- **${d.name}** \u2014 ${d.description}`),
12627
12774
  "",
12628
- `Panggil lewat tool ${MENTION_TOOL} dengan nama agennya. Mereka tak bisa mendelegasikan lagi,`,
12775
+ runtime === "codex" ? "Panggil target bernama persis lewat `spawn_agent`." : `Panggil lewat tool ${MENTION_TOOL} dengan nama agennya.`,
12776
+ "Mereka tak bisa mendelegasikan lagi,",
12629
12777
  "jadi tak ada rantai panggilan yang perlu kamu jaga. Laporan mereka adalah MASUKAN \u2014 kamu yang",
12630
12778
  "memutuskan, dan kamu yang bertanggung jawab atas hasilnya.",
12631
12779
  ""
12632
12780
  ].join("\n");
12633
12781
  }
12634
- var liveMentions;
12782
+ var liveMentions, READ_ONLY_TOOLS;
12635
12783
  var init_custom_agents = __esm({
12636
12784
  "../runner/src/custom-agents.ts"() {
12637
12785
  "use strict";
12638
12786
  init_src();
12639
12787
  init_code_style();
12640
12788
  liveMentions = (def2, roster) => {
12789
+ if (def2.workspacePolicy === "read-only") return [];
12641
12790
  const names = new Set(roster.map((r) => r.name));
12642
12791
  return def2.mentions.filter((m) => names.has(m) && m !== def2.name);
12643
12792
  };
12793
+ READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "Bash", "WebFetch", "WebSearch"]);
12794
+ }
12795
+ });
12796
+
12797
+ // ../runner/src/agent-readonly.ts
12798
+ import { chmodSync, writeFileSync } from "node:fs";
12799
+ import { join } from "node:path";
12800
+ function denyReadOnly(detail) {
12801
+ return { allowed: false, reason: `Hanoman read-only policy: ${detail}` };
12802
+ }
12803
+ function tokenizeReadOnlyCommand(command) {
12804
+ const tokens = [];
12805
+ let token = "";
12806
+ let quote2 = "";
12807
+ let escaped = false;
12808
+ let active = false;
12809
+ for (let index = 0; index < command.length; index++) {
12810
+ const char = command[index];
12811
+ if (escaped) {
12812
+ token += char;
12813
+ escaped = false;
12814
+ active = true;
12815
+ continue;
12816
+ }
12817
+ if (quote2) {
12818
+ if (char === quote2) quote2 = "";
12819
+ else if (char === "\\" && quote2 === '"') escaped = true;
12820
+ else token += char;
12821
+ active = true;
12822
+ continue;
12823
+ }
12824
+ if (char === "'" || char === '"') {
12825
+ quote2 = char;
12826
+ active = true;
12827
+ } else if (char === "\\") {
12828
+ escaped = true;
12829
+ active = true;
12830
+ } else if (/\s/.test(char)) {
12831
+ if (active) {
12832
+ tokens.push(token);
12833
+ token = "";
12834
+ active = false;
12835
+ }
12836
+ } else {
12837
+ token += char;
12838
+ active = true;
12839
+ }
12840
+ }
12841
+ if (quote2 || escaped) return null;
12842
+ if (active) tokens.push(token);
12843
+ return tokens;
12844
+ }
12845
+ function evaluateReadOnlyPayload(payload, policy, environment) {
12846
+ if (!payload || typeof payload !== "object") return denyReadOnly("payload hook tidak sah");
12847
+ const event = payload;
12848
+ const tool = typeof event.tool_name === "string" ? event.tool_name : typeof event.toolName === "string" ? event.toolName : "";
12849
+ if (!tool) return denyReadOnly("nama tool tidak tersedia");
12850
+ if (policy.directTools.includes(tool)) return { allowed: true };
12851
+ if (policy.deniedTools.includes(tool) || tool.startsWith("mcp__")) {
12852
+ return denyReadOnly(`tool ${tool} dapat mengubah state`);
12853
+ }
12854
+ if (!policy.shellTools.includes(tool)) return denyReadOnly(`tool ${tool} tidak terbukti read-only`);
12855
+ const rawInput = event.tool_input;
12856
+ const input = rawInput && typeof rawInput === "object" ? rawInput : {};
12857
+ const command = typeof input.command === "string" ? input.command : typeof input.cmd === "string" ? input.cmd : "";
12858
+ const trimmed = command.trim();
12859
+ if (!trimmed) return denyReadOnly("perintah shell kosong atau tidak dikenal");
12860
+ if (/\r|\n|;|&&|\|\||\||[<>]|\$|`/.test(trimmed)) {
12861
+ return denyReadOnly("operator shell yang dapat merangkai atau menulis dilarang");
12862
+ }
12863
+ const tokens = tokenizeReadOnlyCommand(trimmed);
12864
+ if (!tokens?.length) return denyReadOnly("perintah shell tidak dapat diparse dengan aman");
12865
+ const first = tokens[0] ?? "";
12866
+ const commandName = first.split("/").pop() ?? "";
12867
+ if (first !== commandName) {
12868
+ return denyReadOnly("executable ber-path tidak diizinkan; gunakan command allowlist dari PATH");
12869
+ }
12870
+ if (commandName === "git") {
12871
+ const subcommand = tokens[1] ?? "";
12872
+ if (!policy.gitCommands.includes(subcommand)) {
12873
+ return denyReadOnly(`git ${subcommand || "<kosong>"} bukan operasi baca yang diizinkan`);
12874
+ }
12875
+ const args = tokens.slice(2);
12876
+ if (args.some((arg) => arg === "--output" || arg.startsWith("--output=") || arg === "--ext-diff" || arg === "--textconv")) {
12877
+ return denyReadOnly("opsi git dapat menulis atau menjalankan helper eksternal");
12878
+ }
12879
+ if (subcommand !== "status" && (!args.includes("--no-ext-diff") || !args.includes("--no-textconv"))) {
12880
+ return denyReadOnly("git diff/show/log wajib menonaktifkan helper eksternal dan textconv");
12881
+ }
12882
+ return { allowed: true };
12883
+ }
12884
+ if (!policy.shellCommands.includes(commandName)) {
12885
+ return denyReadOnly(`perintah ${commandName || "<kosong>"} tidak terbukti read-only`);
12886
+ }
12887
+ if (commandName === "rg" && environment.RIPGREP_CONFIG_PATH?.trim()) {
12888
+ return denyReadOnly("RIPGREP_CONFIG_PATH dapat menyuntikkan preprocessor eksternal");
12889
+ }
12890
+ 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="))) {
12891
+ return denyReadOnly("opsi rg dapat menjalankan helper eksternal");
12892
+ }
12893
+ if (commandName === "sed") {
12894
+ const quiet = tokens[1] === "-n" || tokens[1] === "--quiet" || tokens[1] === "--silent";
12895
+ const printOnly = /^\d+(?:,\d+)?p$/.test(tokens[2] ?? "");
12896
+ const files = tokens.slice(3);
12897
+ if (!quiet || !printOnly || files.length === 0 || files.some((arg) => arg.startsWith("-"))) {
12898
+ return denyReadOnly("hanya sed -n '<baris>[,<baris>]p' <berkas> yang diizinkan");
12899
+ }
12900
+ }
12901
+ return { allowed: true };
12902
+ }
12903
+ function readOnlyHookSource() {
12904
+ return [
12905
+ '"use strict";',
12906
+ `const denyReadOnly = ${denyReadOnly.toString()};`,
12907
+ `const tokenizeReadOnlyCommand = ${tokenizeReadOnlyCommand.toString()};`,
12908
+ `const evaluate = ${evaluateReadOnlyPayload.toString()};`,
12909
+ `const policy = ${JSON.stringify(POLICY)};`,
12910
+ "let input = '';",
12911
+ "process.stdin.setEncoding('utf8');",
12912
+ "process.stdin.on('data', chunk => { input += chunk; });",
12913
+ "process.stdin.on('end', () => {",
12914
+ " let payload;",
12915
+ " try { payload = JSON.parse(input); } catch { payload = null; }",
12916
+ " const decision = evaluate(payload, policy, process.env);",
12917
+ " if (!decision.allowed) { process.stderr.write(decision.reason + '\\n'); process.exitCode = 2; }",
12918
+ "});",
12919
+ "process.stdin.resume();",
12920
+ ""
12921
+ ].join("\n");
12922
+ }
12923
+ function writeReadOnlyHook(dir2) {
12924
+ const path = join(dir2, "custom-agent-readonly.cjs");
12925
+ writeFileSync(path, readOnlyHookSource(), { mode: 384 });
12926
+ chmodSync(path, 384);
12927
+ return { path, command: `node ${shellQuote(path)}` };
12928
+ }
12929
+ var POLICY, shellQuote;
12930
+ var init_agent_readonly = __esm({
12931
+ "../runner/src/agent-readonly.ts"() {
12932
+ "use strict";
12933
+ POLICY = {
12934
+ directTools: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
12935
+ shellTools: ["Bash", "local_shell", "exec_command"],
12936
+ deniedTools: ["Write", "Edit", "Task", "apply_patch", "spawn_agent"],
12937
+ shellCommands: ["rg", "sed", "head", "tail", "wc", "ls"],
12938
+ gitCommands: ["diff", "show", "status", "log"]
12939
+ };
12940
+ shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
12941
+ }
12942
+ });
12943
+
12944
+ // ../runner/src/runtime-profile.ts
12945
+ function resolveHardening(env) {
12946
+ if (env.HANOMAN_HARDENING === "1") return true;
12947
+ return env.HANOMAN_SESSION_SANDBOX === "podman" || filled(env.HANOMAN_PUBLIC_ORIGINS) || filled(env.HANOMAN_TRUST_PROXY);
12948
+ }
12949
+ function resolveDeployment(env) {
12950
+ if (resolveHardening(env)) return "public";
12951
+ return env.HANOMAN_DEPLOYMENT === "public" ? "public" : "local";
12952
+ }
12953
+ var filled;
12954
+ var init_runtime_profile = __esm({
12955
+ "../runner/src/runtime-profile.ts"() {
12956
+ "use strict";
12957
+ filled = (v) => !!v && v.trim() !== "";
12958
+ }
12959
+ });
12960
+
12961
+ // ../runner/src/codex-agent-config.ts
12962
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync2 } from "node:fs";
12963
+ import { join as join2 } from "node:path";
12964
+ function codexNativeAgentsSupported(version) {
12965
+ const parsed = version ? /(\d+)\.(\d+)\.(\d+)/.exec(version)?.[0] : null;
12966
+ return parsed ? cmpVersion(parsed, CODEX_NATIVE_AGENTS_MIN_CLIENT) >= 0 : false;
12967
+ }
12968
+ function codexNativeVersionProbe(env, codexBin3 = env.HANOMAN_CODEX_BIN ?? "codex") {
12969
+ const sandbox = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
12970
+ if (sandbox !== "podman") return { bin: codexBin3, args: ["--version"] };
12971
+ return {
12972
+ bin: env.HANOMAN_PODMAN_BIN ?? "podman",
12973
+ args: [
12974
+ "run",
12975
+ "--rm",
12976
+ "--read-only",
12977
+ "--cap-drop=ALL",
12978
+ "--userns=keep-id",
12979
+ "--network",
12980
+ "none",
12981
+ env.HANOMAN_SESSION_IMAGE ?? "hanoman-agent:latest",
12982
+ "/bin/sh",
12983
+ "-lc",
12984
+ `${shellQuote2(codexBin3)} --version`
12985
+ ]
12986
+ };
12987
+ }
12988
+ function renderCodexAgentToml(def2, roster, options2 = {}) {
12989
+ const lines2 = [
12990
+ `name = ${tomlString(def2.name)}`,
12991
+ `description = ${tomlString(def2.description)}`,
12992
+ `developer_instructions = ${tomlString(agentPromptOf(def2, roster))}`,
12993
+ ...def2.model ? [`model = ${tomlString(def2.model)}`] : [],
12994
+ ...def2.effort ? [`model_reasoning_effort = ${tomlString(def2.effort)}`] : [],
12995
+ ...def2.workspacePolicy === "read-only" ? ['sandbox_mode = "read-only"'] : []
12996
+ ];
12997
+ if (def2.workspacePolicy === "read-only" && options2.readOnlyHookCommand) {
12998
+ lines2.push(
12999
+ "",
13000
+ "[[hooks.PreToolUse]]",
13001
+ "",
13002
+ "[[hooks.PreToolUse.hooks]]",
13003
+ 'type = "command"',
13004
+ `command = ${tomlString(options2.readOnlyHookCommand)}`,
13005
+ "timeout = 5"
13006
+ );
13007
+ }
13008
+ return `${lines2.join("\n")}
13009
+ `;
13010
+ }
13011
+ function materializeCodexAgents(defs, tempDir, options2 = {}) {
13012
+ if (defs.length === 0) {
13013
+ return { args: [], delegationClause: "", configPaths: [], warnings: [], liveDefs: [] };
13014
+ }
13015
+ if ("clientVersion" in options2 && !codexNativeAgentsSupported(options2.clientVersion ?? null)) {
13016
+ const seen2 = options2.clientVersion ?? "tak terdeteksi";
13017
+ return {
13018
+ args: [],
13019
+ delegationClause: "",
13020
+ configPaths: [],
13021
+ liveDefs: [],
13022
+ warnings: defs.map((def2) => ({
13023
+ agentName: def2.name,
13024
+ reason: `Codex ${seen2} tidak mendukung custom agent native; butuh >= ${CODEX_NATIVE_AGENTS_MIN_CLIENT}`
13025
+ }))
13026
+ };
13027
+ }
13028
+ const write2 = options2.writeFile ?? ((path, content) => writeFileSync2(path, content, { mode: 384 }));
13029
+ const chmod3 = options2.chmod ?? chmodSync2;
13030
+ const successful = [];
13031
+ const warnings = [];
13032
+ for (const [index, def2] of defs.entries()) {
13033
+ if (def2.workspacePolicy === "isolated-worktree") {
13034
+ warnings.push({
13035
+ agentName: def2.name,
13036
+ reason: "isolated-worktree belum tersedia untuk subagent Codex"
13037
+ });
13038
+ continue;
13039
+ }
13040
+ const path = join2(tempDir, `${String(index).padStart(2, "0")}-${safeFilename(def2.name)}.toml`);
13041
+ try {
13042
+ write2(path, renderCodexAgentToml(def2, defs, options2));
13043
+ chmod3(path, 384);
13044
+ successful.push({ def: def2, path });
13045
+ } catch (error) {
13046
+ warnings.push({
13047
+ agentName: def2.name,
13048
+ reason: error instanceof Error ? error.message : String(error)
13049
+ });
13050
+ }
13051
+ }
13052
+ if (successful.length === 0) {
13053
+ return { args: [], delegationClause: "", configPaths: [], warnings, liveDefs: [] };
13054
+ }
13055
+ const args = [
13056
+ "-c",
13057
+ "agents.enabled=true",
13058
+ "-c",
13059
+ "agents.max_concurrent_threads_per_session=3"
13060
+ ];
13061
+ for (const { def: def2, path } of successful) {
13062
+ const key = `agents.${tomlKey(def2.name)}`;
13063
+ args.push("-c", `${key}.description=${tomlString(def2.description)}`);
13064
+ args.push("-c", `${key}.config_file=${tomlString(path)}`);
13065
+ }
13066
+ const liveDefs = successful.map((entry) => entry.def);
13067
+ return {
13068
+ args,
13069
+ delegationClause: agentDelegationClause(liveDefs, "codex"),
13070
+ configPaths: successful.map((entry) => entry.path),
13071
+ warnings,
13072
+ liveDefs
13073
+ };
13074
+ }
13075
+ var CODEX_NATIVE_AGENTS_MIN_CLIENT, shellQuote2, tomlString, tomlKey, safeFilename;
13076
+ var init_codex_agent_config = __esm({
13077
+ "../runner/src/codex-agent-config.ts"() {
13078
+ "use strict";
13079
+ init_src();
13080
+ init_custom_agents();
13081
+ init_runtime_profile();
13082
+ CODEX_NATIVE_AGENTS_MIN_CLIENT = "0.151.0";
13083
+ shellQuote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`;
13084
+ tomlString = (value) => JSON.stringify(value);
13085
+ tomlKey = (value) => JSON.stringify(value);
13086
+ safeFilename = (name2) => name2.replace(/[^a-z0-9-]/gi, "-");
13087
+ }
13088
+ });
13089
+
13090
+ // ../runner/src/custom-agent-eval.ts
13091
+ var init_custom_agent_eval = __esm({
13092
+ "../runner/src/custom-agent-eval.ts"() {
13093
+ "use strict";
13094
+ init_src();
13095
+ init_codex_agent_config();
13096
+ init_codex_settings();
13097
+ init_custom_agents();
13098
+ init_settings2();
12644
13099
  }
12645
13100
  });
12646
13101
 
12647
13102
  // ../runner/src/paths.ts
12648
13103
  import { homedir } from "node:os";
12649
- import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve5 } from "node:path";
13104
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve as resolve5 } from "node:path";
12650
13105
  function resolveHome(env = process.env, home3 = homedir()) {
12651
13106
  const v = env.HANOMAN_HOME?.trim();
12652
- return v ? v : join(home3, ".hanoman");
13107
+ return v ? v : join3(home3, ".hanoman");
12653
13108
  }
12654
13109
  function resolveDataDirs(env = process.env, home3 = homedir()) {
12655
13110
  const root = resolveHome(env, home3);
12656
- const dir2 = (key, name2) => env[key]?.trim() || join(root, name2);
13111
+ const dir2 = (key, name2) => env[key]?.trim() || join3(root, name2);
12657
13112
  return {
12658
13113
  home: root,
12659
13114
  transcripts: dir2("HANOMAN_TRANSCRIPT_DIR", "transcripts"),
@@ -12675,7 +13130,7 @@ function resolveDbUrl(env, schemaDir2) {
12675
13130
  return absoluteFileUrl(own, schemaDir2);
12676
13131
  }
12677
13132
  const raw = env.DATABASE_URL?.trim();
12678
- if (!raw || !raw.startsWith("file:")) return `file:${join(resolveHome(env), "hanoman.db")}`;
13133
+ if (!raw || !raw.startsWith("file:")) return `file:${join3(resolveHome(env), "hanoman.db")}`;
12679
13134
  return absoluteFileUrl(raw, schemaDir2);
12680
13135
  }
12681
13136
  function absoluteFileUrl(raw, schemaDir2) {
@@ -12703,7 +13158,7 @@ var init_paths = __esm({
12703
13158
 
12704
13159
  // ../runner/src/skills.ts
12705
13160
  import { homedir as homedir2 } from "node:os";
12706
- import { basename as basename2, dirname as dirname3, join as join2, resolve as resolve6 } from "node:path";
13161
+ import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve6 } from "node:path";
12707
13162
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "node:fs";
12708
13163
  function agentSkillHome(agent, env = process.env, osHome = homedir2()) {
12709
13164
  const own = (agent === "codex" ? env.HANOMAN_CODEX_HOME : env.HANOMAN_CLAUDE_HOME)?.trim();
@@ -12712,10 +13167,10 @@ function agentSkillHome(agent, env = process.env, osHome = homedir2()) {
12712
13167
  const cx = env.CODEX_HOME?.trim();
12713
13168
  if (cx) return cx;
12714
13169
  }
12715
- return join2(osHome, agent === "codex" ? ".codex" : ".claude");
13170
+ return join4(osHome, agent === "codex" ? ".codex" : ".claude");
12716
13171
  }
12717
13172
  function agentsSkillHome(env = process.env, osHome = homedir2()) {
12718
- return env.HANOMAN_AGENTS_HOME?.trim() || join2(osHome, ".agents");
13173
+ return env.HANOMAN_AGENTS_HOME?.trim() || join4(osHome, ".agents");
12719
13174
  }
12720
13175
  function dirsIn(dir2) {
12721
13176
  try {
@@ -12732,7 +13187,7 @@ function readJson(file) {
12732
13187
  }
12733
13188
  }
12734
13189
  function isSkillDir(dir2) {
12735
- return existsSync3(join2(dir2, "SKILL.md"));
13190
+ return existsSync3(join4(dir2, "SKILL.md"));
12736
13191
  }
12737
13192
  function asSkill(dir2, pkg, name2 = basename2(dir2)) {
12738
13193
  return { id: pkg ? `${pkg}:${name2}` : name2, name: name2, pkg, dir: dir2 };
@@ -12740,7 +13195,7 @@ function asSkill(dir2, pkg, name2 = basename2(dir2)) {
12740
13195
  function skillsUnder(dir2, pkg, depth = 2) {
12741
13196
  const out4 = [];
12742
13197
  for (const name2 of dirsIn(dir2)) {
12743
- const sub = join2(dir2, name2);
13198
+ const sub = join4(dir2, name2);
12744
13199
  if (isSkillDir(sub)) out4.push(asSkill(sub, pkg, name2));
12745
13200
  else if (depth > 1) out4.push(...skillsUnder(sub, pkg, depth - 1));
12746
13201
  }
@@ -12748,7 +13203,7 @@ function skillsUnder(dir2, pkg, depth = 2) {
12748
13203
  }
12749
13204
  function manifestSkills(installPath, pkg) {
12750
13205
  for (const marker of [".claude-plugin", ".codex-plugin"]) {
12751
- const j = readJson(join2(installPath, marker, "plugin.json"));
13206
+ const j = readJson(join4(installPath, marker, "plugin.json"));
12752
13207
  const declared = j?.skills;
12753
13208
  if (!Array.isArray(declared)) continue;
12754
13209
  const out4 = [];
@@ -12764,11 +13219,11 @@ function manifestSkills(installPath, pkg) {
12764
13219
  }
12765
13220
  function pluginSkills(installPath, pkg) {
12766
13221
  const declared = manifestSkills(installPath, pkg);
12767
- return declared.length ? declared : skillsUnder(join2(installPath, "skills"), pkg);
13222
+ return declared.length ? declared : skillsUnder(join4(installPath, "skills"), pkg);
12768
13223
  }
12769
13224
  function lockPluginNames(agentsHome) {
12770
13225
  const out4 = /* @__PURE__ */ new Map();
12771
- const j = readJson(join2(agentsHome, ".skill-lock.json"));
13226
+ const j = readJson(join4(agentsHome, ".skill-lock.json"));
12772
13227
  const skills = j?.skills;
12773
13228
  if (!skills || typeof skills !== "object") return out4;
12774
13229
  for (const [name2, entry] of Object.entries(skills)) {
@@ -12782,7 +13237,7 @@ function splitPluginKey(key) {
12782
13237
  return at > 0 ? { pkg: key.slice(0, at), marketplace: key.slice(at + 1) } : { pkg: key, marketplace: "" };
12783
13238
  }
12784
13239
  function manifestRoots(home3) {
12785
- const j = readJson(join2(home3, "plugins", "installed_plugins.json"));
13240
+ const j = readJson(join4(home3, "plugins", "installed_plugins.json"));
12786
13241
  const plugins = j?.plugins;
12787
13242
  if (!plugins || typeof plugins !== "object") return [];
12788
13243
  const out4 = [];
@@ -12796,18 +13251,18 @@ function manifestRoots(home3) {
12796
13251
  return out4;
12797
13252
  }
12798
13253
  function cacheRoots(home3) {
12799
- const cache6 = join2(home3, "plugins", "cache");
13254
+ const cache6 = join4(home3, "plugins", "cache");
12800
13255
  const out4 = [];
12801
13256
  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) });
13257
+ for (const pkg of dirsIn(join4(cache6, marketplace)))
13258
+ for (const version of dirsIn(join4(cache6, marketplace, pkg)))
13259
+ out4.push({ pkg, marketplace, dir: join4(cache6, marketplace, pkg, version) });
12805
13260
  return out4;
12806
13261
  }
12807
13262
  function disabledPlugins(agent, home3) {
12808
13263
  const out4 = /* @__PURE__ */ new Set();
12809
13264
  if (agent === "claude") {
12810
- const en = readJson(join2(home3, "settings.json"))?.enabledPlugins;
13265
+ const en = readJson(join4(home3, "settings.json"))?.enabledPlugins;
12811
13266
  if (en && typeof en === "object") {
12812
13267
  for (const [k, v] of Object.entries(en)) if (v === false) out4.add(k);
12813
13268
  }
@@ -12815,7 +13270,7 @@ function disabledPlugins(agent, home3) {
12815
13270
  }
12816
13271
  let toml;
12817
13272
  try {
12818
- toml = readFileSync2(join2(home3, "config.toml"), "utf8");
13273
+ toml = readFileSync2(join4(home3, "config.toml"), "utf8");
12819
13274
  } catch {
12820
13275
  return out4;
12821
13276
  }
@@ -12851,18 +13306,18 @@ function scanAgentSkills(agent, env = process.env, osHome = homedir2()) {
12851
13306
  skills.push(s2);
12852
13307
  }
12853
13308
  };
12854
- const userDir = join2(home3, "skills");
13309
+ const userDir = join4(home3, "skills");
12855
13310
  add(userDir, skillsUnder(userDir, null));
12856
13311
  for (const r of [...manifestRoots(home3), ...cacheRoots(home3)]) {
12857
13312
  if (disabled.has(`${r.pkg}@${r.marketplace}`)) continue;
12858
13313
  const found = pluginSkills(r.dir, r.pkg);
12859
13314
  if (!found.length) continue;
12860
13315
  packages.add(r.pkg);
12861
- add(join2(r.dir, "skills"), found);
13316
+ add(join4(r.dir, "skills"), found);
12862
13317
  }
12863
13318
  if (agent === "codex") {
12864
13319
  const agentsHome = agentsSkillHome(env, osHome);
12865
- const dir2 = join2(agentsHome, "skills");
13320
+ const dir2 = join4(agentsHome, "skills");
12866
13321
  const names = lockPluginNames(agentsHome);
12867
13322
  const flat = skillsUnder(dir2, null);
12868
13323
  const withPkg = flat.flatMap((s2) => {
@@ -12953,12 +13408,12 @@ var init_telegram_operator = __esm({
12953
13408
  });
12954
13409
 
12955
13410
  // ../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";
13411
+ import { readdirSync as readdirSync2, existsSync as existsSync4, statSync, chmodSync as chmodSync3 } from "node:fs";
13412
+ import { dirname as dirname4, join as join5 } from "node:path";
12958
13413
  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);
13414
+ const dirs = [join5(ptyDir, "build", "Release")];
13415
+ for (const name2 of listDir(join5(ptyDir, "prebuilds"))) dirs.push(join5(ptyDir, "prebuilds", name2));
13416
+ return dirs.map((d) => join5(d, "spawn-helper")).filter(exists);
12962
13417
  }
12963
13418
  function ensureSpawnHelpersExecutable(paths2, ops) {
12964
13419
  const fixed = [];
@@ -12973,10 +13428,10 @@ function ensureSpawnHelpersExecutable(paths2, ops) {
12973
13428
  }
12974
13429
  return fixed;
12975
13430
  }
12976
- function repairSpawnHelper(resolve21, notify) {
13431
+ function repairSpawnHelper(resolve22, notify) {
12977
13432
  let ptyDir;
12978
13433
  try {
12979
- ptyDir = dirname4(resolve21("node-pty/package.json"));
13434
+ ptyDir = dirname4(resolve22("node-pty/package.json"));
12980
13435
  } catch {
12981
13436
  return [];
12982
13437
  }
@@ -12989,15 +13444,15 @@ function repairSpawnHelper(resolve21, notify) {
12989
13444
  }, existsSync4);
12990
13445
  const fixed = ensureSpawnHelpersExecutable(paths2, {
12991
13446
  mode: (p3) => statSync(p3).mode,
12992
- chmod: (p3, m) => chmodSync(p3, m)
13447
+ chmod: (p3, m) => chmodSync3(p3, m)
12993
13448
  });
12994
13449
  if (fixed.length) notify?.("hanoman \xB7 memperbaiki izin `spawn-helper` node-pty (sekali per instalasi)\n");
12995
13450
  return fixed;
12996
13451
  }
12997
- function ensureSpawnHelperOnce(resolve21, notify) {
13452
+ function ensureSpawnHelperOnce(resolve22, notify) {
12998
13453
  if (repaired) return [];
12999
13454
  repaired = true;
13000
- return repairSpawnHelper(resolve21, notify);
13455
+ return repairSpawnHelper(resolve22, notify);
13001
13456
  }
13002
13457
  var repaired;
13003
13458
  var init_spawn_helper = __esm({
@@ -13008,10 +13463,10 @@ var init_spawn_helper = __esm({
13008
13463
  });
13009
13464
 
13010
13465
  // ../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";
13466
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
13467
+ import { join as join6 } from "node:path";
13013
13468
  function configEnvPath(home3) {
13014
- return join4(home3, CONFIG_ENV_FILE);
13469
+ return join6(home3, CONFIG_ENV_FILE);
13015
13470
  }
13016
13471
  function parseConfigEnv(text) {
13017
13472
  const out4 = {};
@@ -13039,8 +13494,8 @@ function readConfigEnv(home3) {
13039
13494
  function writeConfigEnv(home3, values) {
13040
13495
  mkdirSync2(home3, { recursive: true, mode: 448 });
13041
13496
  const path = configEnvPath(home3);
13042
- writeFileSync(path, formatConfigEnv(values), { mode: 384 });
13043
- chmodSync2(path, 384);
13497
+ writeFileSync3(path, formatConfigEnv(values), { mode: 384 });
13498
+ chmodSync4(path, 384);
13044
13499
  }
13045
13500
  var CONFIG_ENV_FILE;
13046
13501
  var init_config_env = __esm({
@@ -13050,23 +13505,6 @@ var init_config_env = __esm({
13050
13505
  }
13051
13506
  });
13052
13507
 
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
13508
  // ../runner/src/sandbox-probe.ts
13071
13509
  import { execFileSync } from "node:child_process";
13072
13510
  import { accessSync, constants } from "node:fs";
@@ -13179,6 +13617,9 @@ var init_src2 = __esm({
13179
13617
  init_codex_settings();
13180
13618
  init_agent_cli();
13181
13619
  init_custom_agents();
13620
+ init_agent_readonly();
13621
+ init_codex_agent_config();
13622
+ init_custom_agent_eval();
13182
13623
  init_verify_scope();
13183
13624
  init_code_style();
13184
13625
  init_paths();
@@ -13465,9 +13906,9 @@ var init_session_id = __esm({
13465
13906
  });
13466
13907
 
13467
13908
  // 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";
13909
+ import { chmodSync as chmodSync5, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
13469
13910
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
13470
- import { dirname as dirname6, join as join5 } from "node:path";
13911
+ import { dirname as dirname6, join as join7 } from "node:path";
13471
13912
  function isEncrypted(value) {
13472
13913
  return value.startsWith(ENC_PREFIX);
13473
13914
  }
@@ -13499,7 +13940,7 @@ function fromEnv(raw) {
13499
13940
  return null;
13500
13941
  }
13501
13942
  function secretKeyPath() {
13502
- return join5(resolveHome(), "secret.key");
13943
+ return join7(resolveHome(), "secret.key");
13503
13944
  }
13504
13945
  function secretKey() {
13505
13946
  if (cached) return cached;
@@ -13519,8 +13960,8 @@ function secretKey() {
13519
13960
  }
13520
13961
  const key = randomBytes(KEY_BYTES);
13521
13962
  mkdirSync4(dirname6(path), { recursive: true });
13522
- writeFileSync2(path, key.toString("base64url"), { mode: 384 });
13523
- chmodSync3(path, 384);
13963
+ writeFileSync4(path, key.toString("base64url"), { mode: 384 });
13964
+ chmodSync5(path, 384);
13524
13965
  cached = key;
13525
13966
  return key;
13526
13967
  }
@@ -13713,7 +14154,7 @@ import { request as httpRequest } from "node:http";
13713
14154
  import { request as httpsRequest } from "node:https";
13714
14155
  import { createGunzip } from "node:zlib";
13715
14156
  async function pinnedRequest(input) {
13716
- return new Promise((resolve21, reject2) => {
14157
+ return new Promise((resolve22, reject2) => {
13717
14158
  const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
13718
14159
  const req = transport({
13719
14160
  protocol: input.url.protocol,
@@ -13735,7 +14176,7 @@ async function pinnedRequest(input) {
13735
14176
  const capTerurai = input.maxDecodedBytes ?? input.maxResponseBytes;
13736
14177
  const chunks = [];
13737
14178
  let kabel = 0, terurai = 0;
13738
- const selesai = () => resolve21({
14179
+ const selesai = () => resolve22({
13739
14180
  status: response.statusCode ?? 0,
13740
14181
  headers: response.headers,
13741
14182
  body: Buffer.concat(chunks)
@@ -13801,7 +14242,7 @@ var init_safe_outbound_request = __esm({
13801
14242
  // src/services/uploads.ts
13802
14243
  import { randomUUID } from "node:crypto";
13803
14244
  import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
13804
- import { join as join6, resolve as resolve8 } from "node:path";
14245
+ import { join as join8, resolve as resolve8 } from "node:path";
13805
14246
  function extFor(mimeType) {
13806
14247
  return EXT[mimeType] ?? ".bin";
13807
14248
  }
@@ -13810,12 +14251,12 @@ function uploadDir() {
13810
14251
  }
13811
14252
  function sessionUploadDir(sessionId2) {
13812
14253
  if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
13813
- return join6(uploadDir(), "terminal", sessionId2);
14254
+ return join8(uploadDir(), "terminal", sessionId2);
13814
14255
  }
13815
14256
  async function saveSessionUpload(sessionId2, buf, mimeType) {
13816
14257
  const dir2 = sessionUploadDir(sessionId2);
13817
14258
  await mkdir(dir2, { recursive: true, mode: 448 });
13818
- const path = join6(dir2, `${randomUUID()}${extFor(mimeType)}`);
14259
+ const path = join8(dir2, `${randomUUID()}${extFor(mimeType)}`);
13819
14260
  await writeFile(path, buf, { mode: 384 });
13820
14261
  return { path, size: buf.length };
13821
14262
  }
@@ -13831,11 +14272,11 @@ async function dropSessionUploads(sessionId2) {
13831
14272
  }
13832
14273
  async function readUpload(storageKey) {
13833
14274
  const safe = storageKey.replace(/[/\\]/g, "");
13834
- return readFile2(join6(uploadDir(), safe));
14275
+ return readFile2(join8(uploadDir(), safe));
13835
14276
  }
13836
14277
  async function readUploadOrFetch(storageKey) {
13837
14278
  const safe = storageKey.replace(/[/\\]/g, "");
13838
- const target2 = join6(uploadDir(), safe);
14279
+ const target2 = join8(uploadDir(), safe);
13839
14280
  try {
13840
14281
  return await readFile2(target2);
13841
14282
  } catch {
@@ -13862,7 +14303,7 @@ async function readUploadOrFetch(storageKey) {
13862
14303
  }
13863
14304
  async function deleteUpload(storageKey) {
13864
14305
  const safe = storageKey.replace(/[/\\]/g, "");
13865
- await unlink(join6(uploadDir(), safe)).catch(() => {
14306
+ await unlink(join8(uploadDir(), safe)).catch(() => {
13866
14307
  });
13867
14308
  }
13868
14309
  var EXT, SESSION_ID;
@@ -14211,11 +14652,15 @@ function sandboxArgv(input) {
14211
14652
  const mounts = ["--volume", `${input.worktree}:/workspace:${input.worktreeMode ?? "rw"}`];
14212
14653
  if (input.phaseFile) mounts.push("--volume", `${input.phaseFile}:${input.phaseFile}:rw`);
14213
14654
  if (input.promptFile) mounts.push("--volume", `${input.promptFile}:${input.promptFile}:ro`);
14655
+ if (input.agentConfigDir)
14656
+ mounts.push("--volume", `${input.agentConfigDir}:${input.agentConfigDir}:ro`);
14657
+ if (input.eventDir)
14658
+ mounts.push("--volume", `${input.eventDir}:${input.eventDir}:rw`);
14214
14659
  if (input.attachmentsDir)
14215
14660
  mounts.push("--volume", `${input.attachmentsDir}:${input.attachmentsDir}:ro`);
14216
14661
  mounts.push("--volume", `${input.credentialDir}:/agent-home:ro`);
14217
14662
  return [
14218
- "podman",
14663
+ input.podmanBin ?? "podman",
14219
14664
  "run",
14220
14665
  "--rm",
14221
14666
  "--read-only",
@@ -14243,6 +14688,7 @@ function sandboxArgv(input) {
14243
14688
  "NO_PROXY=localhost,127.0.0.1,::1",
14244
14689
  "--env",
14245
14690
  "HOME=/agent-home",
14691
+ ...input.eventDir ? ["--env", `HANOMAN_EVENT_DIR=${input.eventDir}`] : [],
14246
14692
  ...mounts,
14247
14693
  input.image,
14248
14694
  "/bin/sh",
@@ -14262,6 +14708,7 @@ function sandboxArgvFromEnv(input) {
14262
14708
  ...input,
14263
14709
  credentialDir,
14264
14710
  proxy,
14711
+ podmanBin: env.HANOMAN_PODMAN_BIN ?? "podman",
14265
14712
  image: env.HANOMAN_SESSION_IMAGE ?? "hanoman-agent:latest",
14266
14713
  network: env.HANOMAN_SESSION_NETWORK ?? "hanoman-egress"
14267
14714
  });
@@ -14280,18 +14727,30 @@ var init_session_sandbox = __esm({
14280
14727
  }
14281
14728
  });
14282
14729
 
14730
+ // src/services/session-event-spool.ts
14731
+ import { tmpdir } from "node:os";
14732
+ import { join as join9 } from "node:path";
14733
+ var sessionEventSpoolRoot, sessionEventDir;
14734
+ var init_session_event_spool = __esm({
14735
+ "src/services/session-event-spool.ts"() {
14736
+ "use strict";
14737
+ sessionEventSpoolRoot = () => join9(tmpdir(), "hanoman-session-events");
14738
+ sessionEventDir = (sessionId2) => join9(sessionEventSpoolRoot(), sessionId2);
14739
+ }
14740
+ });
14741
+
14283
14742
  // src/services/pty.ts
14284
14743
  import { spawn } from "node-pty";
14285
14744
  import { execFile, execFileSync as execFileSync2 } from "node:child_process";
14286
14745
  import { createRequire } from "node:module";
14287
14746
  import { randomUUID as randomUUID2 } from "node:crypto";
14288
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
14747
+ import { chmodSync as chmodSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync5 } from "node:fs";
14289
14748
  import { dirname as dirname7 } from "node:path";
14290
- import { tmpdir } from "node:os";
14749
+ import { tmpdir as tmpdir2 } from "node:os";
14291
14750
  function noTtyPromptEnv() {
14292
14751
  const path = askpassDenyPath();
14293
14752
  mkdirSync5(dirname7(path), { recursive: true, mode: 448 });
14294
- writeFileSync3(path, ASKPASS_DENY, { mode: 448 });
14753
+ writeFileSync5(path, ASKPASS_DENY, { mode: 448 });
14295
14754
  return { SSH_ASKPASS: path, SSH_ASKPASS_REQUIRE: "force", GIT_TERMINAL_PROMPT: "0" };
14296
14755
  }
14297
14756
  function tmux(...args) {
@@ -14310,14 +14769,14 @@ function tmux(...args) {
14310
14769
  }
14311
14770
  }
14312
14771
  function tmuxAsync(...args) {
14313
- return new Promise((resolve21, reject2) => {
14772
+ return new Promise((resolve22, reject2) => {
14314
14773
  execFile(
14315
14774
  "tmux",
14316
14775
  ["-L", socket(), "-f", "/dev/null", ...args],
14317
14776
  { encoding: "utf8" },
14318
14777
  (e, stdout, stderr) => {
14319
14778
  if (!e) {
14320
- resolve21(stdout);
14779
+ resolve22(stdout);
14321
14780
  return;
14322
14781
  }
14323
14782
  const err = e;
@@ -14368,7 +14827,8 @@ function parsePanes(out4) {
14368
14827
  alternate,
14369
14828
  activity,
14370
14829
  eventHook,
14371
- created
14830
+ created,
14831
+ agentRoster
14372
14832
  ] = line.split(" ");
14373
14833
  if (!n2?.startsWith(PREFIX)) return [];
14374
14834
  const exited = dead === "1";
@@ -14398,10 +14858,30 @@ function parsePanes(out4) {
14398
14858
  // SPEC-919 · sesi yang lahir di tmux yang tak menjawab field ini → 0 (epoch).
14399
14859
  startedAt: Number(created) || 0,
14400
14860
  // SPEC-909 · ADR-0146 · sesi yang lahir sebelum pembaruan tak punya opsi ini → false.
14401
- eventHook: eventHook === "1"
14861
+ eventHook: eventHook === "1",
14862
+ agentRoster: parseAgentRoster(agentRoster)
14402
14863
  }];
14403
14864
  });
14404
14865
  }
14866
+ function parseAgentRoster(value) {
14867
+ try {
14868
+ const parsed = JSON.parse(value || "[]");
14869
+ if (!Array.isArray(parsed)) return [];
14870
+ return parsed.flatMap((entry) => {
14871
+ if (!entry || typeof entry !== "object") return [];
14872
+ const row = entry;
14873
+ if (typeof row.name !== "string") return [];
14874
+ return [{
14875
+ name: row.name,
14876
+ ...typeof row.id === "string" ? { id: row.id } : {},
14877
+ ...typeof row.model === "string" ? { model: row.model } : {},
14878
+ ...typeof row.timeoutSeconds === "number" ? { timeoutSeconds: row.timeoutSeconds } : {}
14879
+ }];
14880
+ });
14881
+ } catch {
14882
+ return [];
14883
+ }
14884
+ }
14405
14885
  function sessionEventEnv(sessionId2, env = process.env) {
14406
14886
  const port2 = Number(env.PORT ?? 8787);
14407
14887
  const host2 = controlHost(loadIngressPolicy(env));
@@ -14409,6 +14889,7 @@ function sessionEventEnv(sessionId2, env = process.env) {
14409
14889
  HANOMAN_SESSION_ID: sessionId2,
14410
14890
  HANOMAN_EVENT_URL: `http://127.0.0.1:${port2}/api/session-events`,
14411
14891
  HANOMAN_EVENT_TOKEN: sessionEventToken(sessionId2),
14892
+ HANOMAN_EVENT_DIR: sessionEventDir(sessionId2),
14412
14893
  ...host2 ? { HANOMAN_EVENT_HOST: host2 } : {}
14413
14894
  };
14414
14895
  }
@@ -14418,6 +14899,24 @@ function registerSessionHooks(h) {
14418
14899
  function registerCustomAgentSource(fn) {
14419
14900
  customAgentSource = fn;
14420
14901
  }
14902
+ function registerCodexNativeAgentSupport(fn) {
14903
+ codexNativeAgentSupport = fn;
14904
+ }
14905
+ function collectChangedFiles(cwd, baseSha, run4 = runGitDiff) {
14906
+ try {
14907
+ const files = /* @__PURE__ */ new Set();
14908
+ const add = (args) => {
14909
+ for (const path of run4(cwd, args).split("\0")) if (path) files.add(path);
14910
+ };
14911
+ if (baseSha) add(["diff", "--name-only", "-z", `${baseSha}...HEAD`]);
14912
+ add(["diff", "--name-only", "-z"]);
14913
+ add(["diff", "--cached", "--name-only", "-z"]);
14914
+ add(["ls-files", "--others", "--exclude-standard", "-z"]);
14915
+ return [...files].sort();
14916
+ } catch {
14917
+ return [];
14918
+ }
14919
+ }
14421
14920
  function sessionKind(o, projectId, cwd) {
14422
14921
  if (o.specId) return "spec";
14423
14922
  if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
@@ -14440,15 +14939,62 @@ function createSession(projectId, cwd, opts = {}) {
14440
14939
  const existing = getSession(id);
14441
14940
  if (existing && !existing.exited) return existing;
14442
14941
  if (existing) killSession(id);
14942
+ const eventDir = opts.command ? void 0 : sessionEventDir(id);
14943
+ if (eventDir) {
14944
+ mkdirSync5(eventDir, { recursive: true, mode: 448 });
14945
+ chmodSync6(eventDir, 448);
14946
+ }
14443
14947
  const agentForDefs = opts.agent ?? "claude";
14444
- const customDefs = opts.command ? [] : customAgentsFor(projectId, agentForDefs);
14445
- const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : agentDelegationClause(customDefs);
14948
+ const selectionContext = {
14949
+ projectId,
14950
+ runtime: agentForDefs,
14951
+ flow: opts.flow,
14952
+ cwd,
14953
+ baseSha: opts.env?.HANOMAN_BASE_SHA,
14954
+ prompt: opts.prompt,
14955
+ changedFiles: opts.command ? [] : collectChangedFiles(cwd, opts.env?.HANOMAN_BASE_SHA)
14956
+ };
14957
+ const customDefs = opts.command ? [] : customAgentsFor(selectionContext);
14958
+ let rosterBlock = "";
14959
+ let codexAgentArgs = [];
14960
+ let agentsFile;
14961
+ let agentConfigDir;
14962
+ let liveAgentDefs = [];
14963
+ if (customDefs.length > 0) {
14964
+ const tempDir = agentTempDir(id);
14965
+ agentConfigDir = tempDir;
14966
+ mkdirSync5(tempDir, { recursive: true, mode: 448 });
14967
+ const readOnlyHook = customDefs.some((def2) => def2.workspacePolicy === "read-only") ? writeReadOnlyHook(tempDir) : void 0;
14968
+ if (agentForDefs === "claude") {
14969
+ const json = renderAgentsJson(customDefs, { readOnlyHookCommand: readOnlyHook?.command });
14970
+ if (json) {
14971
+ agentsFile = agentsFilePath(id);
14972
+ writeFileSync5(agentsFile, json, { mode: 384 });
14973
+ rosterBlock = agentDelegationClause(customDefs, "claude");
14974
+ liveAgentDefs = customDefs;
14975
+ }
14976
+ } else {
14977
+ const materialized = materializeCodexAgents(customDefs, tempDir, {
14978
+ readOnlyHookCommand: readOnlyHook?.command,
14979
+ clientVersion: codexNativeAgentSupport().version
14980
+ });
14981
+ codexAgentArgs = materialized.args;
14982
+ rosterBlock = materialized.delegationClause;
14983
+ liveAgentDefs = materialized.liveDefs;
14984
+ for (const warning of materialized.warnings) {
14985
+ process.stderr.write(
14986
+ `hanoman: custom agent ${warning.agentName} tidak dimaterialisasi: ${warning.reason}
14987
+ `
14988
+ );
14989
+ }
14990
+ }
14991
+ }
14446
14992
  let promptArg = "";
14447
14993
  let promptFile;
14448
14994
  if (!opts.command && opts.prompt) {
14449
14995
  promptFile = promptFilePath(id);
14450
14996
  mkdirSync5(dirname7(promptFile), { recursive: true, mode: 448 });
14451
- writeFileSync3(promptFile, opts.prompt + rosterBlock, { mode: 384 });
14997
+ writeFileSync5(promptFile, opts.prompt + rosterBlock, { mode: 384 });
14452
14998
  promptArg = `"$(cat ${sq(promptFile)})"`;
14453
14999
  }
14454
15000
  const agent = opts.agent ?? "claude";
@@ -14460,7 +15006,7 @@ function createSession(projectId, cwd, opts = {}) {
14460
15006
  if (agent === "codex" && opts.goal && opts.flow && opts.specId) {
14461
15007
  goalGate = goalGatePath(id);
14462
15008
  mkdirSync5(dirname7(goalGate), { recursive: true, mode: 448 });
14463
- writeFileSync3(goalGate, codexGoalScript({
15009
+ writeFileSync5(goalGate, codexGoalScript({
14464
15010
  flow: opts.flow,
14465
15011
  specId: opts.specId,
14466
15012
  condition: opts.goal,
@@ -14470,15 +15016,6 @@ function createSession(projectId, cwd, opts = {}) {
14470
15016
  }), { mode: 448 });
14471
15017
  }
14472
15018
  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
15019
  const flags = agentFlags({
14483
15020
  agent,
14484
15021
  model: opts.model,
@@ -14491,7 +15028,8 @@ function createSession(projectId, cwd, opts = {}) {
14491
15028
  eventHook: true
14492
15029
  }).map(sq).join(" ");
14493
15030
  const agentsArg = agentsFile ? `--agents "$(cat ${sq(agentsFile)})"` : "";
14494
- argv = [sq(agentBin(agent)), promptArg, flags, agentsArg].filter(Boolean).join(" ");
15031
+ const nativeAgentArgs = agent === "codex" ? codexAgentArgs.map(sq).join(" ") : "";
15032
+ argv = [sq(agentBin(agent)), promptArg, flags, agentsArg, nativeAgentArgs].filter(Boolean).join(" ");
14495
15033
  }
14496
15034
  const envPairs = [];
14497
15035
  if (!opts.command && agent === "claude") {
@@ -14510,16 +15048,16 @@ function createSession(projectId, cwd, opts = {}) {
14510
15048
  if (opts.attachmentsDir) envPairs.push(`HANOMAN_ATTACHMENTS_DIR=${sq(opts.attachmentsDir)}`);
14511
15049
  for (const [k, v] of Object.entries(opts.env ?? {})) envPairs.push(`${k}=${sq(v)}`);
14512
15050
  let cmd = envPairs.length ? `${envPairs.join(" ")} ${argv}` : argv;
14513
- let sandboxed = false;
14514
15051
  if (!opts.command) {
14515
15052
  const wrapped = sandboxCommand({
14516
15053
  command: cmd,
14517
15054
  worktree: cwd,
14518
15055
  phaseFile: opts.phaseFile,
14519
15056
  promptFile,
15057
+ agentConfigDir,
15058
+ eventDir,
14520
15059
  attachmentsDir: opts.attachmentsDir
14521
15060
  });
14522
- sandboxed = wrapped !== cmd;
14523
15061
  cmd = wrapped;
14524
15062
  }
14525
15063
  if (opts.decisionFile) mkdirSync5(dirname7(opts.decisionFile), { recursive: true });
@@ -14594,9 +15132,18 @@ function createSession(projectId, cwd, opts = {}) {
14594
15132
  if (opts.flow) tmux("set-option", "-t", name(id), "@hanoman_flow", opts.flow);
14595
15133
  if (opts.branch) tmux("set-option", "-t", name(id), "@hanoman_branch", opts.branch);
14596
15134
  tmux("set-option", "-t", name(id), "@hanoman_agent", agent);
15135
+ if (liveAgentDefs.length > 0) {
15136
+ const roster = liveAgentDefs.map((def2) => ({
15137
+ ...def2.id ? { id: def2.id } : {},
15138
+ name: def2.name,
15139
+ ...def2.model ? { model: def2.model } : {},
15140
+ ...def2.timeoutSeconds ? { timeoutSeconds: def2.timeoutSeconds } : {}
15141
+ }));
15142
+ tmux("set-option", "-t", name(id), "@hanoman_agent_roster", JSON.stringify(roster));
15143
+ }
14597
15144
  if (opts.phaseFile) tmux("set-option", "-t", name(id), "@hanoman_phase_file", opts.phaseFile);
14598
15145
  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");
15146
+ if (!opts.command) tmux("set-option", "-t", name(id), "@hanoman_event_hook", "1");
14600
15147
  if (opts.goal && !opts.command) void armGoalInTui(id, opts.goal, { agent }).catch(() => {
14601
15148
  });
14602
15149
  emitBirth({
@@ -14787,10 +15334,10 @@ function startPoll() {
14787
15334
  poll = setInterval(() => {
14788
15335
  if (polling) return;
14789
15336
  polling = true;
14790
- const snapshot2 = [...attached.entries()];
15337
+ const snapshot3 = [...attached.entries()];
14791
15338
  listPanesAsync().then((panes) => {
14792
15339
  const live = new Map(panes.map((p3) => [p3.id, p3]));
14793
- for (const [id, a] of snapshot2) {
15340
+ for (const [id, a] of snapshot3) {
14794
15341
  if (attached.get(id) !== a) continue;
14795
15342
  const p3 = live.get(id);
14796
15343
  if (!p3) end(id, 0);
@@ -14853,6 +15400,14 @@ function killSession(id) {
14853
15400
  const transcript = captureTranscript(id);
14854
15401
  drop(id);
14855
15402
  tmux("kill-session", "-t", name(id));
15403
+ try {
15404
+ rmSync2(agentTempDir(id), { recursive: true, force: true });
15405
+ } catch {
15406
+ }
15407
+ try {
15408
+ rmSync2(sessionEventDir(id), { recursive: true, force: true });
15409
+ } catch {
15410
+ }
14856
15411
  emitDeath({ sessionId: id, exitCode: p3.exited ? p3.code : null, transcript });
14857
15412
  void dropSessionUploads(id).catch(() => {
14858
15413
  });
@@ -14878,7 +15433,7 @@ function spawnPty(...args) {
14878
15433
  );
14879
15434
  }
14880
15435
  }
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;
15436
+ 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
15437
  var init_pty = __esm({
14883
15438
  "src/services/pty.ts"() {
14884
15439
  "use strict";
@@ -14892,6 +15447,8 @@ var init_pty = __esm({
14892
15447
  init_ingress_policy();
14893
15448
  init_session_event_token();
14894
15449
  init_session_sandbox();
15450
+ init_session_event_spool();
15451
+ init_session_event_spool();
14895
15452
  socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
14896
15453
  PREFIX = "hanoman-";
14897
15454
  MAX_SCROLLBACK = 256 * 1024;
@@ -14906,7 +15463,7 @@ var init_pty = __esm({
14906
15463
  };
14907
15464
  clearMarker = (f) => {
14908
15465
  try {
14909
- writeFileSync3(f, "");
15466
+ writeFileSync5(f, "");
14910
15467
  } catch {
14911
15468
  }
14912
15469
  };
@@ -14934,11 +15491,12 @@ var init_pty = __esm({
14934
15491
  rootBypassEnv = (uid = process.getuid?.()) => uid === 0 ? { IS_SANDBOX: "1" } : {};
14935
15492
  frame = (f) => JSON.stringify(f);
14936
15493
  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`;
15494
+ promptFilePath = (id) => `${tmpdir2()}/hanoman-prompts/${id}`;
15495
+ goalGatePath = (id) => `${tmpdir2()}/hanoman-goal-gates/${id}.sh`;
15496
+ goalStatePath = (id) => `${tmpdir2()}/hanoman-goal-gates/${id}.count`;
15497
+ agentTempDir = (id) => `${tmpdir2()}/hanoman-agents/${id}`;
15498
+ agentsFilePath = (id) => `${agentTempDir(id)}/claude.json`;
15499
+ askpassDenyPath = () => `${tmpdir2()}/hanoman-askpass/deny.sh`;
14942
15500
  ASKPASS_DENY = `#!/bin/sh
14943
15501
  echo "hanoman: tak ada manusia di pane ini \u2014 permintaan ketikan ditolak: $1" >&2
14944
15502
  echo "hanoman: buka kuncinya di luar sesi (mis. ssh-add ~/.ssh/id_rsa), lalu ulangi." >&2
@@ -14970,7 +15528,8 @@ exit 1
14970
15528
  // `pty-parse.test.ts` mengunci panjang FMT terhadap destructuring `parsePanes`.
14971
15529
  "#{window_activity}",
14972
15530
  "#{@hanoman_event_hook}",
14973
- "#{session_created}"
15531
+ "#{session_created}",
15532
+ "#{@hanoman_agent_roster}"
14974
15533
  ].join(" ");
14975
15534
  toSessionInfo = ({
14976
15535
  id,
@@ -15028,13 +15587,18 @@ exit 1
15028
15587
  }
15029
15588
  };
15030
15589
  customAgentSource = () => [];
15031
- customAgentsFor = (projectId, agent) => {
15590
+ codexNativeAgentSupport = () => ({ version: "0.151.0", ok: true });
15591
+ customAgentsFor = (context) => {
15032
15592
  try {
15033
- return customAgentSource(projectId, agent);
15593
+ return customAgentSource(context);
15034
15594
  } catch {
15035
15595
  return [];
15036
15596
  }
15037
15597
  };
15598
+ runGitDiff = (cwd, args) => execFileSync2("git", ["-C", cwd, ...args], {
15599
+ encoding: "utf8",
15600
+ stdio: ["ignore", "pipe", "ignore"]
15601
+ });
15038
15602
  sleep = (ms) => new Promise((r) => {
15039
15603
  setTimeout(r, ms);
15040
15604
  });
@@ -15260,6 +15824,11 @@ function validateSyncData(entity, data, options2 = {}) {
15260
15824
  if (!Number.isSafeInteger(value)) throw new Error(`sync tipe invalid: ${entity}.${field}`);
15261
15825
  continue;
15262
15826
  }
15827
+ if (NULLABLE_NUMBER_FIELDS.has(key)) {
15828
+ if (value !== null && !Number.isSafeInteger(value))
15829
+ throw new Error(`sync tipe invalid: ${entity}.${field}`);
15830
+ continue;
15831
+ }
15263
15832
  if (BOOLEAN_FIELDS.has(key)) {
15264
15833
  if (typeof value !== "boolean") throw new Error(`sync tipe invalid: ${entity}.${field}`);
15265
15834
  continue;
@@ -15527,7 +16096,7 @@ async function upsertLocal(entity, id, version, data) {
15527
16096
  function setAcceptedHook(hook) {
15528
16097
  onAccepted = hook;
15529
16098
  }
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;
16099
+ 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
16100
  var init_sync = __esm({
15532
16101
  "src/services/sync.ts"() {
15533
16102
  "use strict";
@@ -15584,7 +16153,24 @@ var init_sync = __esm({
15584
16153
  // SPEC-484 · ADR-0101 · `runtime` ikut: ia menentukan sesi mesin mana yang memakai persona ini,
15585
16154
  // dan kolom yang terlewat di sini mendarat sebagai default palsu (= "warisi") di setiap mesin
15586
16155
  // lain tanpa satu pun error.
15587
- customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "runtime", "enabled", "createdAt", "updatedAt"],
16156
+ customAgent: [
16157
+ "projectId",
16158
+ "name",
16159
+ "description",
16160
+ "instructions",
16161
+ "tools",
16162
+ "model",
16163
+ "mentions",
16164
+ "runtime",
16165
+ "activation",
16166
+ "effort",
16167
+ "workspacePolicy",
16168
+ "maxTurns",
16169
+ "timeoutSeconds",
16170
+ "enabled",
16171
+ "createdAt",
16172
+ "updatedAt"
16173
+ ],
15588
16174
  // SPEC-471 · ADR-0095 · SELURUH kolom bermakna ikut. `status`/`specId` termasuk: keputusan
15589
16175
  // triase adalah bagian keadaan yang harus dilihat sama oleh semua mesin — tanpa itu satu
15590
16176
  // mesin bisa menerima ulang issue yang di mesin lain sudah jadi backlog.
@@ -15663,6 +16249,7 @@ var init_sync = __esm({
15663
16249
  "ticketAttachment:size",
15664
16250
  "githubIssue:number"
15665
16251
  ]);
16252
+ NULLABLE_NUMBER_FIELDS = /* @__PURE__ */ new Set(["customAgent:maxTurns", "customAgent:timeoutSeconds"]);
15666
16253
  FLOAT_FIELDS = /* @__PURE__ */ new Set(["task:order"]);
15667
16254
  BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["vps:hardened", "customAgent:enabled", "member:active"]);
15668
16255
  JSON_FIELDS = /* @__PURE__ */ new Set([
@@ -15697,6 +16284,33 @@ var init_sync = __esm({
15697
16284
  }
15698
16285
  });
15699
16286
 
16287
+ // src/services/sync-notify.ts
16288
+ async function notifySynced(entity, id) {
16289
+ try {
16290
+ if (!isEntity(entity)) return;
16291
+ await consumeTombstoneOnRecreate(entity, id);
16292
+ if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
16293
+ else await publishLocal(entity, id);
16294
+ } catch {
16295
+ }
16296
+ }
16297
+ async function notifyDeleted(entity, id) {
16298
+ try {
16299
+ if (!isEntity(entity)) return;
16300
+ if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
16301
+ else await publishDelete(entity, id);
16302
+ } catch {
16303
+ }
16304
+ }
16305
+ var init_sync_notify = __esm({
16306
+ "src/services/sync-notify.ts"() {
16307
+ "use strict";
16308
+ init_config2();
16309
+ init_outbox();
16310
+ init_sync();
16311
+ }
16312
+ });
16313
+
15700
16314
  // src/services/settings.ts
15701
16315
  async function getSetting() {
15702
16316
  const raw = (await prisma.setting.findUnique({ where: { id: 1 } }))?.data;
@@ -15777,8 +16391,10 @@ var init_settings3 = __esm({
15777
16391
  // SPEC-518 · agen pembuat changelog (opt-in, mati)
15778
16392
  portalChat: PORTAL_CHAT_DEFAULTS,
15779
16393
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
15780
- builtinAgents: {}
16394
+ builtinAgents: {},
15781
16395
  // SPEC-881 · ADR-0136 · sidik jari seed (lokal, tak disync)
16396
+ builtinAgentPolicies: {}
16397
+ // SPEC-950 · marker safety policy sekali-jalan (lokal)
15782
16398
  };
15783
16399
  RETIRED_MODELS = { "claude-opus-4-8": "claude-opus-5" };
15784
16400
  }
@@ -16184,12 +16800,12 @@ var require_common = __commonJS({
16184
16800
  createDebug.skips = [];
16185
16801
  createDebug.formatters = {};
16186
16802
  function selectColor(namespace) {
16187
- let hash2 = 0;
16803
+ let hash3 = 0;
16188
16804
  for (let i = 0; i < namespace.length; i++) {
16189
- hash2 = (hash2 << 5) - hash2 + namespace.charCodeAt(i);
16190
- hash2 |= 0;
16805
+ hash3 = (hash3 << 5) - hash3 + namespace.charCodeAt(i);
16806
+ hash3 |= 0;
16191
16807
  }
16192
- return createDebug.colors[Math.abs(hash2) % createDebug.colors.length];
16808
+ return createDebug.colors[Math.abs(hash3) % createDebug.colors.length];
16193
16809
  }
16194
16810
  createDebug.selectColor = selectColor;
16195
16811
  function createDebug(namespace) {
@@ -17335,7 +17951,7 @@ var init_gateway = __esm({
17335
17951
  const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
17336
17952
  updateTelegramRuntimeStatus({ running: false, readiness: "error", lastError: message });
17337
17953
  if (error instanceof TelegramApiError && error.code === 409) return;
17338
- await new Promise((resolve21) => setTimeout(resolve21, 1e3));
17954
+ await new Promise((resolve22) => setTimeout(resolve22, 1e3));
17339
17955
  }
17340
17956
  }
17341
17957
  }
@@ -17498,9 +18114,9 @@ ${input.text}`;
17498
18114
  }
17499
18115
  const engine = await this.deps.defaults();
17500
18116
  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}`;
18117
+ const hash3 = chatHash(input.chatId);
18118
+ const projectId = `telegram:${hash3}`;
18119
+ const cwd = `${this.deps.home.replace(/\/$/, "")}/telegram/${hash3}`;
17504
18120
  this.deps.ensureDir(cwd);
17505
18121
  if (engine.agent === "codex") this.deps.ensureCodexTrust(cwd);
17506
18122
  const personality = await this.deps.personality(context.personalityAgentId, context.activeProjectId);
@@ -18049,6 +18665,57 @@ var init_bootstrap = __esm({
18049
18665
  }
18050
18666
  });
18051
18667
 
18668
+ // src/services/codex-version.ts
18669
+ import { execFile as execFile14 } from "node:child_process";
18670
+ import { promisify as promisify13 } from "node:util";
18671
+ async function probeCodexVersion(env = process.env, execute = runVersion) {
18672
+ const probe = codexNativeVersionProbe(env, codexBin2());
18673
+ try {
18674
+ return parseCodexVersion((await execute(probe.bin, probe.args)).stdout);
18675
+ } catch {
18676
+ return null;
18677
+ }
18678
+ }
18679
+ function parseCodexVersion(out4) {
18680
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(out4);
18681
+ return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
18682
+ }
18683
+ async function getCodexVersion(now = Date.now()) {
18684
+ if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
18685
+ const version = await probeCodexVersion();
18686
+ cache3 = { at: now, version };
18687
+ return version;
18688
+ }
18689
+ async function codexVersionInfo() {
18690
+ const version = await getCodexVersion();
18691
+ return {
18692
+ version,
18693
+ minRequired: CODEX_MIN_CLIENT,
18694
+ ok: version === null || cmpVersion(version, CODEX_MIN_CLIENT) >= 0
18695
+ };
18696
+ }
18697
+ function _resetCodexVersionCache() {
18698
+ cache3 = null;
18699
+ }
18700
+ var run3, CODEX_MIN_CLIENT, TTL_MS4, cache3, codexBin2, runVersion;
18701
+ var init_codex_version = __esm({
18702
+ "src/services/codex-version.ts"() {
18703
+ "use strict";
18704
+ init_src();
18705
+ init_src2();
18706
+ init_config2();
18707
+ run3 = promisify13(execFile14);
18708
+ CODEX_MIN_CLIENT = "0.144.0";
18709
+ TTL_MS4 = 5 * 6e4;
18710
+ cache3 = null;
18711
+ codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
18712
+ runVersion = async (bin, args) => {
18713
+ const { stdout } = await run3(bin, args, { timeout: 1e4 });
18714
+ return { stdout };
18715
+ };
18716
+ }
18717
+ });
18718
+
18052
18719
  // src/services/presence/snapshot.ts
18053
18720
  function paneToPresence(p3, phase) {
18054
18721
  return {
@@ -20448,7 +21115,7 @@ var require_websocket = __commonJS({
20448
21115
  var http = __require("http");
20449
21116
  var net = __require("net");
20450
21117
  var tls = __require("tls");
20451
- var { randomBytes: randomBytes11, createHash: createHash11 } = __require("crypto");
21118
+ var { randomBytes: randomBytes11, createHash: createHash12 } = __require("crypto");
20452
21119
  var { Duplex, Readable } = __require("stream");
20453
21120
  var { URL: URL2 } = __require("url");
20454
21121
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -21116,8 +21783,8 @@ var require_websocket = __commonJS({
21116
21783
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
21117
21784
  return;
21118
21785
  }
21119
- const digest = createHash11("sha1").update(key + GUID).digest("base64");
21120
- if (res.headers["sec-websocket-accept"] !== digest) {
21786
+ const digest2 = createHash12("sha1").update(key + GUID).digest("base64");
21787
+ if (res.headers["sec-websocket-accept"] !== digest2) {
21121
21788
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
21122
21789
  return;
21123
21790
  }
@@ -21397,7 +22064,7 @@ var require_stream = __commonJS({
21397
22064
  };
21398
22065
  duplex._final = function(callback) {
21399
22066
  if (ws2.readyState === ws2.CONNECTING) {
21400
- ws2.once("open", function open4() {
22067
+ ws2.once("open", function open5() {
21401
22068
  duplex._final(callback);
21402
22069
  });
21403
22070
  return;
@@ -21418,7 +22085,7 @@ var require_stream = __commonJS({
21418
22085
  };
21419
22086
  duplex._write = function(chunk, encoding, callback) {
21420
22087
  if (ws2.readyState === ws2.CONNECTING) {
21421
- ws2.once("open", function open4() {
22088
+ ws2.once("open", function open5() {
21422
22089
  duplex._write(chunk, encoding, callback);
21423
22090
  });
21424
22091
  return;
@@ -21485,7 +22152,7 @@ var require_websocket_server = __commonJS({
21485
22152
  var EventEmitter = __require("events");
21486
22153
  var http = __require("http");
21487
22154
  var { Duplex } = __require("stream");
21488
- var { createHash: createHash11 } = __require("crypto");
22155
+ var { createHash: createHash12 } = __require("crypto");
21489
22156
  var extension2 = require_extension();
21490
22157
  var PerMessageDeflate2 = require_permessage_deflate();
21491
22158
  var subprotocol2 = require_subprotocol();
@@ -21792,12 +22459,12 @@ var require_websocket_server = __commonJS({
21792
22459
  );
21793
22460
  }
21794
22461
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
21795
- const digest = createHash11("sha1").update(key + GUID).digest("base64");
22462
+ const digest2 = createHash12("sha1").update(key + GUID).digest("base64");
21796
22463
  const headers = [
21797
22464
  "HTTP/1.1 101 Switching Protocols",
21798
22465
  "Upgrade: websocket",
21799
22466
  "Connection: Upgrade",
21800
- `Sec-WebSocket-Accept: ${digest}`
22467
+ `Sec-WebSocket-Accept: ${digest2}`
21801
22468
  ];
21802
22469
  const ws2 = new this.options.WebSocket(null, void 0, this.options);
21803
22470
  if (protocols.size) {
@@ -22343,6 +23010,382 @@ var init_sync_client = __esm({
22343
23010
  }
22344
23011
  });
22345
23012
 
23013
+ // src/services/agent-tool-catalog.ts
23014
+ import { readFileSync as readFileSync17 } from "node:fs";
23015
+ import { homedir as homedir10 } from "node:os";
23016
+ import { join as join29 } from "node:path";
23017
+ function mcpServerNames(repoDir) {
23018
+ const names = [];
23019
+ const claudeJson = readJson2(join29(home(), ".claude.json"));
23020
+ names.push(...serversOf(claudeJson));
23021
+ if (repoDir) {
23022
+ const projects = claudeJson?.projects;
23023
+ if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
23024
+ names.push(...serversOf(readJson2(join29(repoDir, ".mcp.json"))));
23025
+ }
23026
+ names.push(...codexServers());
23027
+ return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
23028
+ }
23029
+ function agentToolCatalog(repoDir) {
23030
+ return [ALL_TOOLS_ENTRY, ...BUILTIN_AGENT_TOOLS, ...mcpServerNames(repoDir).map(mcpToolEntry)];
23031
+ }
23032
+ var home, readJson2, serversOf, codexServers, agentToolIds;
23033
+ var init_agent_tool_catalog = __esm({
23034
+ "src/services/agent-tool-catalog.ts"() {
23035
+ "use strict";
23036
+ init_src();
23037
+ home = () => process.env.HOME || homedir10();
23038
+ readJson2 = (path) => {
23039
+ try {
23040
+ return JSON.parse(readFileSync17(path, "utf8"));
23041
+ } catch {
23042
+ return null;
23043
+ }
23044
+ };
23045
+ serversOf = (node) => {
23046
+ const ms = node?.mcpServers;
23047
+ if (!ms || typeof ms !== "object" || Array.isArray(ms)) return [];
23048
+ return Object.keys(ms);
23049
+ };
23050
+ codexServers = () => {
23051
+ let text;
23052
+ try {
23053
+ text = readFileSync17(join29(home(), ".codex", "config.toml"), "utf8");
23054
+ } catch {
23055
+ return [];
23056
+ }
23057
+ const out4 = [];
23058
+ for (const m of text.matchAll(/^\s*\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.[^\]]*)?\]/gm)) {
23059
+ const name2 = m[1] ?? m[2];
23060
+ if (name2) out4.push(name2);
23061
+ }
23062
+ return out4;
23063
+ };
23064
+ agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
23065
+ }
23066
+ });
23067
+
23068
+ // src/services/builtin-agents.ts
23069
+ import { createHash as createHash9 } from "node:crypto";
23070
+ async function seedBuiltinAgents() {
23071
+ try {
23072
+ const setting = await getSetting();
23073
+ const stamps = { ...setting.builtinAgents };
23074
+ const policies = { ...setting.builtinAgentPolicies };
23075
+ let changed = false;
23076
+ for (const a of BUILTIN_AGENTS) {
23077
+ const id = customAgentId(null, a.name);
23078
+ const fp = builtinFingerprint(a);
23079
+ const row = await prisma.customAgent.findUnique({ where: { id } });
23080
+ const qaPolicyPending = a.name === "qa-verifier" && policies[a.name] !== QA_SAFETY_POLICY;
23081
+ if (!row) {
23082
+ if (await findTombstone("customAgent", id)) {
23083
+ if (qaPolicyPending) {
23084
+ policies[a.name] = QA_SAFETY_POLICY;
23085
+ changed = true;
23086
+ }
23087
+ continue;
23088
+ }
23089
+ await prisma.customAgent.create({ data: {
23090
+ id,
23091
+ projectId: null,
23092
+ name: a.name,
23093
+ description: a.description,
23094
+ instructions: a.instructions,
23095
+ tools: [...a.tools],
23096
+ model: null,
23097
+ mentions: [],
23098
+ runtime: null,
23099
+ activation: a.activation,
23100
+ effort: a.effort,
23101
+ workspacePolicy: a.workspacePolicy,
23102
+ maxTurns: a.maxTurns,
23103
+ timeoutSeconds: a.timeoutSeconds,
23104
+ enabled: a.enabledByDefault
23105
+ } });
23106
+ await notifySynced("customAgent", id);
23107
+ stamps[a.name] = fp;
23108
+ changed = true;
23109
+ if (qaPolicyPending) policies[a.name] = QA_SAFETY_POLICY;
23110
+ continue;
23111
+ }
23112
+ const stamped = stamps[a.name];
23113
+ const unedited = Boolean(stamped) && (stamped === rowFingerprint(row) || stamped === legacyRowFingerprint(row));
23114
+ const data = {};
23115
+ if (unedited && stamped !== fp) {
23116
+ Object.assign(data, {
23117
+ description: a.description,
23118
+ instructions: a.instructions,
23119
+ tools: [...a.tools],
23120
+ activation: a.activation,
23121
+ effort: a.effort,
23122
+ workspacePolicy: a.workspacePolicy,
23123
+ maxTurns: a.maxTurns,
23124
+ timeoutSeconds: a.timeoutSeconds
23125
+ });
23126
+ stamps[a.name] = fp;
23127
+ changed = true;
23128
+ }
23129
+ if (qaPolicyPending && unedited) data.enabled = false;
23130
+ if (Object.keys(data).length > 0) {
23131
+ await prisma.customAgent.update({ where: { id }, data });
23132
+ await notifySynced("customAgent", id);
23133
+ }
23134
+ if (qaPolicyPending) {
23135
+ policies[a.name] = QA_SAFETY_POLICY;
23136
+ changed = true;
23137
+ }
23138
+ }
23139
+ if (changed) {
23140
+ const data = { ...setting, builtinAgents: stamps, builtinAgentPolicies: policies };
23141
+ await prisma.setting.upsert({
23142
+ where: { id: 1 },
23143
+ update: { data },
23144
+ create: { id: 1, data }
23145
+ });
23146
+ }
23147
+ } catch {
23148
+ }
23149
+ }
23150
+ var digest, legacyFingerprint, fingerprint, builtinFingerprint, rowFingerprint, legacyRowFingerprint, QA_SAFETY_POLICY;
23151
+ var init_builtin_agents2 = __esm({
23152
+ "src/services/builtin-agents.ts"() {
23153
+ "use strict";
23154
+ init_src();
23155
+ init_db();
23156
+ init_settings3();
23157
+ init_tombstone();
23158
+ init_sync_notify();
23159
+ digest = (parts) => createHash9("sha256").update(parts.join(" ")).digest("hex").slice(0, 16);
23160
+ legacyFingerprint = (name2, description, instructions, tools) => digest([name2, description, instructions, [...tools].join(",")]);
23161
+ fingerprint = (a) => digest([
23162
+ a.name,
23163
+ a.description,
23164
+ a.instructions,
23165
+ (toolsOf(a.tools) ?? []).join(","),
23166
+ activationOf(a.activation),
23167
+ effortOf(a.effort) ?? "",
23168
+ workspacePolicyOf(a.workspacePolicy),
23169
+ String(maxTurnsOf(a.maxTurns) ?? ""),
23170
+ String(timeoutSecondsOf(a.timeoutSeconds) ?? "")
23171
+ ]);
23172
+ builtinFingerprint = (a) => fingerprint(a);
23173
+ rowFingerprint = (r) => fingerprint(r);
23174
+ legacyRowFingerprint = (r) => legacyFingerprint(r.name, r.description, r.instructions, toolsOf(r.tools) ?? []);
23175
+ QA_SAFETY_POLICY = "disable-unedited-v1";
23176
+ }
23177
+ });
23178
+
23179
+ // src/services/custom-agents.ts
23180
+ var custom_agents_exports = {};
23181
+ __export(custom_agents_exports, {
23182
+ agentDefsFor: () => agentDefsFor,
23183
+ collectChangedFiles: () => collectChangedFiles,
23184
+ currentCustomAgentRuntimeSupport: () => currentCustomAgentRuntimeSupport,
23185
+ installCustomAgents: () => installCustomAgents,
23186
+ loadCustomAgents: () => loadCustomAgents,
23187
+ refreshCustomAgentRuntimeSupport: () => refreshCustomAgentRuntimeSupport,
23188
+ selectAgentRows: () => selectAgentRows,
23189
+ toDef: () => toDef,
23190
+ unknownMentions: () => unknownMentions,
23191
+ validateGraph: () => validateGraph
23192
+ });
23193
+ function smartBuiltinSelected(row, context) {
23194
+ switch (row.name) {
23195
+ case "scout":
23196
+ return hasPhase(context, "Plan") || hasPhase(context, "Execute") || hasPhase(context, "Audit") || context.changedFiles.length === 0;
23197
+ case "blast-radius":
23198
+ return hasPhase(context, "Execute") || hasPhase(context, "Audit") || context.changedFiles.length > 0;
23199
+ case "security-reviewer":
23200
+ return (hasPhase(context, "Execute") || hasPhase(context, "Audit")) && touchesExternalInput(context);
23201
+ case "spec-auditor":
23202
+ return hasPhase(context, "Plan") || hasPhase(context, "Execute");
23203
+ case "dep-auditor":
23204
+ return touchesDependency(context.changedFiles);
23205
+ case "root-causer":
23206
+ return hasPhase(context, "Audit");
23207
+ case "qa-verifier":
23208
+ return context.runtime === "claude" && hasPhase(context, "Execute") && workspacePolicyOf(row.workspacePolicy) === "isolated-worktree" && touchesExecutableWork(context.changedFiles);
23209
+ case "edge-case-hunter":
23210
+ return context.runtime === "claude" && hasPhase(context, "Execute") && workspacePolicyOf(row.workspacePolicy) === "isolated-worktree";
23211
+ default:
23212
+ return true;
23213
+ }
23214
+ }
23215
+ function selectAgentRows(rows, context) {
23216
+ return rows.filter((row) => {
23217
+ if (!row.enabled) return false;
23218
+ const runtime = runtimeOf(row.runtime);
23219
+ if (runtime !== null && runtime !== context.runtime) return false;
23220
+ if (workspacePolicyOf(row.workspacePolicy) === "isolated-worktree" && context.runtime !== "claude") return false;
23221
+ if (activationOf(row.activation) === "always") return true;
23222
+ const builtin = row.projectId === null && BUILTIN_AGENT_NAMES.includes(row.name);
23223
+ return builtin ? smartBuiltinSelected(row, context) : true;
23224
+ });
23225
+ }
23226
+ function currentCustomAgentRuntimeSupport() {
23227
+ return { ...codexNativeSupport };
23228
+ }
23229
+ function toDef(r) {
23230
+ return {
23231
+ id: r.id,
23232
+ name: r.name,
23233
+ description: r.description,
23234
+ instructions: r.instructions,
23235
+ tools: toolsOf(r.tools),
23236
+ model: r.model,
23237
+ mentions: mentionsOf(r.mentions),
23238
+ activation: activationOf(r.activation),
23239
+ effort: effortOf(r.effort),
23240
+ workspacePolicy: workspacePolicyOf(r.workspacePolicy),
23241
+ maxTurns: maxTurnsOf(r.maxTurns),
23242
+ timeoutSeconds: timeoutSecondsOf(r.timeoutSeconds)
23243
+ };
23244
+ }
23245
+ function recommendedModel(row, runtime) {
23246
+ if (row.model) return row.model;
23247
+ if (row.projectId !== null) return null;
23248
+ const builtin = BUILTIN_AGENTS.find((agent) => agent.name === row.name);
23249
+ if (!builtin) return null;
23250
+ const model = builtin.models[runtime];
23251
+ if (runtime === "claude" && (model === "haiku" || model === "sonnet")) return model;
23252
+ return modelsForRuntime(runtime).some((entry) => entry.id === model) ? model : null;
23253
+ }
23254
+ function toRuntimeDef(row, runtime) {
23255
+ return { ...toDef(row), model: recommendedModel(row, runtime) };
23256
+ }
23257
+ async function loadCustomAgents() {
23258
+ try {
23259
+ cache4 = await prisma.customAgent.findMany();
23260
+ const projects = await prisma.project.findMany({ select: { id: true, repoDir: true } });
23261
+ const bindings = await prisma.localBinding.findMany({ select: { projectId: true, repoDir: true } });
23262
+ const next = /* @__PURE__ */ new Map();
23263
+ for (const p3 of projects) next.set(p3.id, p3.repoDir ?? null);
23264
+ for (const b of bindings) next.set(b.projectId, b.repoDir ?? null);
23265
+ repoDirCache = next;
23266
+ } catch {
23267
+ cache4 = [];
23268
+ repoDirCache = /* @__PURE__ */ new Map();
23269
+ }
23270
+ }
23271
+ function agentDefsFor(contextOrProjectId, legacyAgent) {
23272
+ const legacy = typeof contextOrProjectId === "string";
23273
+ const context = legacy ? {
23274
+ projectId: contextOrProjectId,
23275
+ runtime: legacyAgent ?? "claude",
23276
+ cwd: "",
23277
+ changedFiles: []
23278
+ } : contextOrProjectId;
23279
+ const { projectId } = context;
23280
+ const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
23281
+ const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
23282
+ const effectiveIds = new Set(effectiveAgents(globals, project).map((agent) => agent.id));
23283
+ const effectiveRows = cache4.filter((row) => effectiveIds.has(row.id));
23284
+ 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);
23285
+ const needsCatalog = eff.some((row) => (toolsOf(row.tools) ?? []).includes(ALL_TOOLS));
23286
+ const catalogIds = needsCatalog ? agentToolIds(repoDirCache.get(projectId) ?? null) : [];
23287
+ return eff.map((row) => {
23288
+ const a = asCustomAgent(row);
23289
+ return {
23290
+ ...toRuntimeDef(row, context.runtime),
23291
+ // Ekspansi terjadi DI SINI, sebelum `resolveTools` di runner: meneruskan `"*"` apa adanya
23292
+ // membuat claude membuangnya senyap (agen tanpa alat), sementara menerjemahkannya jadi `null`
23293
+ // membuat agen mewarisi SELURUH tool termasuk `Task` — lapis 2 anti-loop lenyap tanpa jejak.
23294
+ tools: expandTools(a.tools, catalogIds),
23295
+ mentions: a.mentions ?? []
23296
+ };
23297
+ });
23298
+ }
23299
+ function validateGraph(rows) {
23300
+ const projectScopes = [...new Set(rows.map((r) => r.projectId).filter((p3) => p3 !== null))];
23301
+ const globals = rows.filter((r) => r.projectId === null).map(asCustomAgent);
23302
+ for (const scope of [null, ...projectScopes]) {
23303
+ const project = scope === null ? [] : rows.filter((r) => r.projectId === scope).map(asCustomAgent);
23304
+ const nodes = effectiveAgents(globals, project).map((a) => ({ name: a.name, mentions: a.mentions ?? [] }));
23305
+ const cycle = detectCycle(nodes);
23306
+ if (cycle) return { scope: scope ?? GLOBAL_SCOPE, cycle };
23307
+ }
23308
+ return null;
23309
+ }
23310
+ function unknownMentions(row, all) {
23311
+ const visible = new Set(
23312
+ all.filter((r) => r.projectId === null || row.projectId !== null && r.projectId === row.projectId).map((r) => r.name)
23313
+ );
23314
+ return mentionsOf(row.mentions).filter((m) => !visible.has(m));
23315
+ }
23316
+ async function refreshCustomAgentRuntimeSupport(probe = defaultCodexSupportProbe) {
23317
+ const refresh = codexSupportRefreshTail.then(async () => {
23318
+ const version = await probe();
23319
+ codexNativeSupport = { version, ok: codexNativeAgentsSupported(version) };
23320
+ });
23321
+ codexSupportRefreshTail = refresh.catch(() => {
23322
+ });
23323
+ return refresh;
23324
+ }
23325
+ async function installCustomAgents() {
23326
+ await seedBuiltinAgents();
23327
+ await loadCustomAgents();
23328
+ await refreshCustomAgentRuntimeSupport();
23329
+ registerCodexNativeAgentSupport(() => codexNativeSupport);
23330
+ registerCustomAgentSource((context) => agentDefsFor(context));
23331
+ if (!codexSupportRefreshTimer) {
23332
+ codexSupportRefreshTimer = setInterval(() => {
23333
+ void refreshCustomAgentRuntimeSupport().catch((error) => console.error("custom agent: probe Codex gagal:", error));
23334
+ }, 5 * 6e4);
23335
+ codexSupportRefreshTimer.unref();
23336
+ }
23337
+ }
23338
+ var phasesOf, hasPhase, touchesDependency, touchesExternalInput, touchesExecutableWork, cache4, codexNativeSupport, codexSupportRefreshTimer, codexSupportRefreshTail, repoDirCache, asCustomAgent, defaultCodexSupportProbe;
23339
+ var init_custom_agents2 = __esm({
23340
+ "src/services/custom-agents.ts"() {
23341
+ "use strict";
23342
+ init_db();
23343
+ init_src();
23344
+ init_src2();
23345
+ init_pty();
23346
+ init_agent_tool_catalog();
23347
+ init_builtin_agents2();
23348
+ init_codex_version();
23349
+ phasesOf = (flow) => flow ? PIPELINES[flow] : [];
23350
+ hasPhase = (context, name2) => phasesOf(context.flow).includes(name2);
23351
+ 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));
23352
+ touchesExternalInput = (context) => {
23353
+ const surface = [context.prompt ?? "", ...context.changedFiles].join("\n");
23354
+ return /(?:^|[\W_/.-])(route|routes|handler|auth|oauth|api|cli|config|filesystem|upload|webhook|input)(?:$|[\W_/.-])/i.test(surface);
23355
+ };
23356
+ 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));
23357
+ cache4 = [];
23358
+ codexNativeSupport = { version: null, ok: false };
23359
+ codexSupportRefreshTimer = null;
23360
+ codexSupportRefreshTail = Promise.resolve();
23361
+ repoDirCache = /* @__PURE__ */ new Map();
23362
+ asCustomAgent = (r) => ({
23363
+ id: r.id,
23364
+ projectId: r.projectId,
23365
+ name: r.name,
23366
+ description: r.description,
23367
+ instructions: r.instructions,
23368
+ tools: toolsOf(r.tools),
23369
+ model: r.model,
23370
+ mentions: mentionsOf(r.mentions),
23371
+ runtime: runtimeOf(r.runtime),
23372
+ activation: activationOf(r.activation),
23373
+ effort: effortOf(r.effort),
23374
+ workspacePolicy: workspacePolicyOf(r.workspacePolicy),
23375
+ maxTurns: maxTurnsOf(r.maxTurns),
23376
+ timeoutSeconds: timeoutSecondsOf(r.timeoutSeconds),
23377
+ enabled: r.enabled,
23378
+ createdAt: "",
23379
+ updatedAt: ""
23380
+ // tak dipakai lapis ini
23381
+ });
23382
+ defaultCodexSupportProbe = async () => {
23383
+ _resetCodexVersionCache();
23384
+ return getCodexVersion();
23385
+ };
23386
+ }
23387
+ });
23388
+
22346
23389
  // src/services/config-apply.ts
22347
23390
  var config_apply_exports = {};
22348
23391
  __export(config_apply_exports, {
@@ -22370,6 +23413,10 @@ async function applyConfigSideEffect(key) {
22370
23413
  return;
22371
23414
  }
22372
23415
  if (configEntry(key)?.inheritEnv) mirrorInheritEnv(key);
23416
+ if (key === "HANOMAN_CODEX_BIN") {
23417
+ const { refreshCustomAgentRuntimeSupport: refreshCustomAgentRuntimeSupport2 } = await Promise.resolve().then(() => (init_custom_agents2(), custom_agents_exports));
23418
+ await refreshCustomAgentRuntimeSupport2();
23419
+ }
22373
23420
  }
22374
23421
  async function rotateSyncOrigin(input) {
22375
23422
  const url2 = new URL(input);
@@ -24854,14 +25901,14 @@ var require_stream_consumer = __commonJS({
24854
25901
  "../node_modules/.pnpm/@fastify+multipart@10.1.0/node_modules/@fastify/multipart/lib/stream-consumer.js"(exports, module) {
24855
25902
  "use strict";
24856
25903
  module.exports = function streamToNull(stream) {
24857
- return new Promise((resolve21, reject2) => {
25904
+ return new Promise((resolve22, reject2) => {
24858
25905
  stream.on("data", () => {
24859
25906
  });
24860
25907
  stream.on("close", () => {
24861
- resolve21();
25908
+ resolve22();
24862
25909
  });
24863
25910
  stream.on("end", () => {
24864
- resolve21();
25911
+ resolve22();
24865
25912
  });
24866
25913
  stream.on("error", (error) => {
24867
25914
  reject2(error);
@@ -25310,10 +26357,10 @@ var require_multipart2 = __commonJS({
25310
26357
  }
25311
26358
  };
25312
26359
  const parts = () => {
25313
- return new Promise((resolve21, reject2) => {
26360
+ return new Promise((resolve22, reject2) => {
25314
26361
  handle((val) => {
25315
26362
  if (val instanceof Error) return reject2(val);
25316
- resolve21(val);
26363
+ resolve22(val);
25317
26364
  });
25318
26365
  });
25319
26366
  };
@@ -25494,7 +26541,7 @@ var require_multipart2 = __commonJS({
25494
26541
  parts = this.parts(options3);
25495
26542
  }
25496
26543
  this.savedRequestFiles = [];
25497
- const tmpdir6 = options3?.tmpdir || os.tmpdir();
26544
+ const tmpdir7 = options3?.tmpdir || os.tmpdir();
25498
26545
  this.tmpUploads = [];
25499
26546
  let i = 0;
25500
26547
  for await (const part of parts) {
@@ -25502,7 +26549,7 @@ var require_multipart2 = __commonJS({
25502
26549
  if (!part.file) {
25503
26550
  continue;
25504
26551
  }
25505
- const filepath = path.join(tmpdir6, generateId() + path.extname(part.filename || "file" + i++));
26552
+ const filepath = path.join(tmpdir7, generateId() + path.extname(part.filename || "file" + i++));
25506
26553
  const target2 = createWriteStream2(filepath);
25507
26554
  try {
25508
26555
  this.tmpUploads.push(filepath);
@@ -25679,15 +26726,15 @@ init_stage_machine();
25679
26726
  init_src();
25680
26727
  import { execFile as execFile2 } from "node:child_process";
25681
26728
  import { promisify } from "node:util";
25682
- import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
26729
+ import { existsSync as existsSync6, rmSync as rmSync3 } from "node:fs";
25683
26730
  import { resolve as resolve10 } from "node:path";
25684
26731
 
25685
26732
  // src/services/safe-repo-path.ts
25686
26733
  import { constants as constants2 } from "node:fs";
25687
26734
  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";
26735
+ import { isAbsolute as isAbsolute4, join as join10, relative, resolve as resolve9, sep as sep2 } from "node:path";
25689
26736
  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";
26737
+ 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
26738
  var PathContainmentError = class extends Error {
25692
26739
  code = "PATH_CONTAINMENT";
25693
26740
  };
@@ -25715,7 +26762,7 @@ async function ensureRepoParents(root, rel) {
25715
26762
  const parts = components(rel).slice(0, -1);
25716
26763
  let current = base2;
25717
26764
  for (const part of parts) {
25718
- current = join7(current, part);
26765
+ current = join10(current, part);
25719
26766
  try {
25720
26767
  await mkdir2(current, { mode: 448 });
25721
26768
  } catch (error) {
@@ -25732,7 +26779,7 @@ function ensureRepoParentsSync(root, rel) {
25732
26779
  const parts = components(rel).slice(0, -1);
25733
26780
  let current = base2;
25734
26781
  for (const part of parts) {
25735
- current = join7(current, part);
26782
+ current = join10(current, part);
25736
26783
  try {
25737
26784
  mkdirSync6(current, { mode: 448 });
25738
26785
  } catch (error) {
@@ -25748,7 +26795,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
25748
26795
  const parts = components(rel);
25749
26796
  let current = base2;
25750
26797
  for (let i = 0; i < parts.length; i++) {
25751
- current = join7(current, parts[i]);
26798
+ current = join10(current, parts[i]);
25752
26799
  const stat4 = await lstat(current).catch((error) => {
25753
26800
  if (error.code === "ENOENT" && opts.allowMissingTail) return null;
25754
26801
  if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
@@ -25769,7 +26816,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
25769
26816
  const parts = components(rel);
25770
26817
  let current = base2;
25771
26818
  for (let i = 0; i < parts.length; i++) {
25772
- current = join7(current, parts[i]);
26819
+ current = join10(current, parts[i]);
25773
26820
  try {
25774
26821
  const stat4 = lstatSync(current);
25775
26822
  if (stat4.isSymbolicLink()) denied("symlink");
@@ -25799,10 +26846,10 @@ function writeRepoFileAtomicSync(root, rel, data) {
25799
26846
  const parent = resolve9(path, "..");
25800
26847
  const parentRel = relative(realpathSync2(root), parent);
25801
26848
  if (parentRel) assertSafeRepoPathSync(root, parentRel);
25802
- const temp = join7(parent, `.hanoman-${randomUUID3()}.tmp`);
26849
+ const temp = join10(parent, `.hanoman-${randomUUID3()}.tmp`);
25803
26850
  const fd = openSync(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
25804
26851
  try {
25805
- writeFileSync4(fd, data);
26852
+ writeFileSync6(fd, data);
25806
26853
  } finally {
25807
26854
  closeSync(fd);
25808
26855
  }
@@ -25846,7 +26893,7 @@ async function writeRepoFileAtomic(root, rel, data) {
25846
26893
  await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
25847
26894
  if (error.code !== "EEXIST") throw error;
25848
26895
  });
25849
- const temp = join7(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
26896
+ const temp = join10(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
25850
26897
  const handle = await open2(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
25851
26898
  try {
25852
26899
  await handle.writeFile(data);
@@ -25945,7 +26992,7 @@ function writeDocFile(repoDir, rel, content) {
25945
26992
  function deleteDocFile(repoDir, rel) {
25946
26993
  const abs = docAbsPath(repoDir, rel);
25947
26994
  if (!existsSync6(abs)) return false;
25948
- rmSync2(abs);
26995
+ rmSync3(abs);
25949
26996
  return true;
25950
26997
  }
25951
26998
 
@@ -26003,9 +27050,9 @@ async function toProjectView(p3, sessions, devices2) {
26003
27050
  const specs = await prisma.spec.findMany({ where: { projectId: p3.id } });
26004
27051
  const { coverage } = await scanRepoDocs(await resolveRepoDir(p3.id));
26005
27052
  const binding = await getBinding(p3.id);
26006
- const open4 = specs.filter((s2) => s2.stage !== "done");
27053
+ const open5 = specs.filter((s2) => s2.stage !== "done");
26007
27054
  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";
27055
+ const topStage = open5.length ? open5.map((s2) => s2.stage).sort((a, b) => STAGES.indexOf(b) - STAGES.indexOf(a))[0] : "spec";
26009
27056
  return {
26010
27057
  id: p3.id,
26011
27058
  name: p3.name,
@@ -26018,7 +27065,7 @@ async function toProjectView(p3, sessions, devices2) {
26018
27065
  docStatus: docStatusFor(coverage),
26019
27066
  coverage,
26020
27067
  createdAt: p3.createdAt.toISOString(),
26021
- backlog: open4.length,
27068
+ backlog: open5.length,
26022
27069
  topStage,
26023
27070
  session,
26024
27071
  activity: session.status === "running" ? `running \xB7 ${session.flow ?? "sesi"}` : "idle",
@@ -26037,31 +27084,13 @@ async function toProjectView(p3, sessions, devices2) {
26037
27084
  };
26038
27085
  }
26039
27086
 
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
- }
27087
+ // src/routes/projects.ts
27088
+ init_sync_notify();
26061
27089
 
26062
27090
  // src/services/sync-delete.ts
26063
27091
  init_sync();
26064
27092
  init_tombstone();
27093
+ init_sync_notify();
26065
27094
  init_outbox();
26066
27095
  async function deleteSynced(entity, id, deviceId) {
26067
27096
  const snap = await snapshot(entity, id);
@@ -26305,8 +27334,8 @@ import { existsSync as existsSync9 } from "node:fs";
26305
27334
  // src/services/integrate.ts
26306
27335
  import { execFile as execFile4 } from "node:child_process";
26307
27336
  import { promisify as promisify3 } from "node:util";
26308
- import { rmSync as rmSync3 } from "node:fs";
26309
- import { join as join8 } from "node:path";
27337
+ import { rmSync as rmSync4 } from "node:fs";
27338
+ import { join as join11 } from "node:path";
26310
27339
  var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
26311
27340
  var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
26312
27341
  var exec3 = promisify3(execFile4);
@@ -26342,7 +27371,7 @@ async function resolveTarget(repoDir, target2) {
26342
27371
  async function reclaim(repoDir, wt) {
26343
27372
  await sh(repoDir, ["worktree", "remove", "--force", wt]);
26344
27373
  await sh(repoDir, ["worktree", "prune"]);
26345
- rmSync3(wt, { recursive: true, force: true });
27374
+ rmSync4(wt, { recursive: true, force: true });
26346
27375
  }
26347
27376
  async function integrateBranch(repoDir, src, op, target2) {
26348
27377
  const source = await resolveSource(repoDir, src.branch);
@@ -26350,7 +27379,7 @@ async function integrateBranch(repoDir, src, op, target2) {
26350
27379
  const tgt = await resolveTarget(repoDir, target2);
26351
27380
  if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
26352
27381
  await sh(repoDir, ["fetch", "origin"]);
26353
- const wt = join8(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
27382
+ const wt = join11(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
26354
27383
  await reclaim(repoDir, wt);
26355
27384
  const baseRef = op === "merge" ? tgt.ref : source;
26356
27385
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
@@ -26411,7 +27440,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
26411
27440
  const src = await resolveGraphSource(repoDir, source);
26412
27441
  if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
26413
27442
  await sh(repoDir, ["fetch", "origin"]);
26414
- const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
27443
+ const wt = join11(repoDir, ".worktrees", `merge-${sanitize(current)}`);
26415
27444
  await reclaim(repoDir, wt);
26416
27445
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
26417
27446
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -26449,7 +27478,7 @@ async function replayCurrent(repoDir, source, cmd) {
26449
27478
  const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
26450
27479
  if (!current || current === "HEAD")
26451
27480
  return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
26452
- const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
27481
+ const wt = join11(repoDir, ".worktrees", `merge-${sanitize(current)}`);
26453
27482
  await reclaim(repoDir, wt);
26454
27483
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
26455
27484
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -26497,16 +27526,16 @@ init_db();
26497
27526
  import { execFile as execFile5 } from "node:child_process";
26498
27527
  import { promisify as promisify4 } from "node:util";
26499
27528
  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";
27529
+ import { tmpdir as tmpdir3 } from "node:os";
27530
+ import { join as join12, resolve as resolve11 } from "node:path";
26502
27531
  var exec4 = promisify4(execFile5);
26503
27532
  var GIT3 = { maxBuffer: 1 << 24 };
26504
27533
  var MAX = 256 * 1024;
26505
- var worktreeDir = (repoDir, specId) => join9(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
27534
+ var worktreeDir = (repoDir, specId) => join12(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
26506
27535
  async function withTempIndex(wt, fn) {
26507
27536
  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");
27537
+ const dir2 = await mkdtemp(join12(tmpdir3(), "hanoman-idx-"));
27538
+ const tmp = join12(dir2, "index");
26510
27539
  await copyFile(resolve11(wt, idx), tmp);
26511
27540
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
26512
27541
  try {
@@ -26705,7 +27734,7 @@ function appendSourceHistory(current, entry) {
26705
27734
  // src/services/spec-reset.ts
26706
27735
  import { execFile as execFile8 } from "node:child_process";
26707
27736
  import { existsSync as existsSync8 } from "node:fs";
26708
- import { join as join11 } from "node:path";
27737
+ import { join as join14 } from "node:path";
26709
27738
  import { promisify as promisify7 } from "node:util";
26710
27739
 
26711
27740
  // src/services/stage-artifacts.ts
@@ -26754,7 +27783,7 @@ init_db();
26754
27783
  import { execFile as execFile6 } from "node:child_process";
26755
27784
  import { readdir, rm as rm3 } from "node:fs/promises";
26756
27785
  import { promisify as promisify5 } from "node:util";
26757
- import { join as join10, resolve as resolve12 } from "node:path";
27786
+ import { join as join13, resolve as resolve12 } from "node:path";
26758
27787
  init_notifications2();
26759
27788
  var TICK_MS = 6e4;
26760
27789
  var trashDirOf = (repoDir) => resolve12(repoDir, ".worktrees", ".trash");
@@ -26814,7 +27843,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
26814
27843
  }
26815
27844
  let removed = 0;
26816
27845
  for (const entry of entries3) {
26817
- const path = join10(dir2, entry);
27846
+ const path = join13(dir2, entry);
26818
27847
  const known = pending.get(path);
26819
27848
  const row = known ?? { path, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
26820
27849
  pending.set(path, row);
@@ -27370,7 +28399,7 @@ async function planSpecReset(spec) {
27370
28399
  const repoDir = await resolveRepoDir(spec.projectId);
27371
28400
  if (!repoDir) return EMPTY;
27372
28401
  const sid = sessionIdForSpec(spec.id);
27373
- const wt = join11(repoDir, ".worktrees", sid);
28402
+ const wt = join14(repoDir, ".worktrees", sid);
27374
28403
  const branch = `hanoman/${sid}`;
27375
28404
  const hasBranch = await shaResolvable(repoDir, `refs/heads/${branch}`);
27376
28405
  return {
@@ -27401,6 +28430,7 @@ async function applySpecReset(spec, plan) {
27401
28430
 
27402
28431
  // src/routes/specs.ts
27403
28432
  init_notifications2();
28433
+ init_sync_notify();
27404
28434
 
27405
28435
  // src/services/spec-complete.ts
27406
28436
  init_db();
@@ -27408,6 +28438,7 @@ init_notifications2();
27408
28438
 
27409
28439
  // src/services/session-result.ts
27410
28440
  init_db();
28441
+ init_sync_notify();
27411
28442
  import { randomUUID as randomUUID4 } from "node:crypto";
27412
28443
  var WHITELIST = [
27413
28444
  "projectId",
@@ -27433,6 +28464,7 @@ async function recordSessionResult(input) {
27433
28464
  }
27434
28465
 
27435
28466
  // src/services/spec-complete.ts
28467
+ init_sync_notify();
27436
28468
  async function completeSpecManually(spec, input) {
27437
28469
  const at = input.at ?? /* @__PURE__ */ new Date();
27438
28470
  const manualDone = {
@@ -27468,7 +28500,7 @@ import { extname } from "node:path";
27468
28500
  import { randomUUID as randomUUID5 } from "node:crypto";
27469
28501
  import { spawn as spawn2 } from "node:child_process";
27470
28502
  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";
28503
+ import { basename as basename3, isAbsolute as isAbsolute5, join as join15 } from "node:path";
27472
28504
 
27473
28505
  // ../node_modules/.pnpm/strtok3@10.3.5/node_modules/strtok3/lib/stream/Errors.js
27474
28506
  var defaultMessages = "End-Of-Stream";
@@ -29581,7 +30613,7 @@ function readByobReaderWithSignal(reader, buffer, signal) {
29581
30613
  return reader.read(buffer);
29582
30614
  }
29583
30615
  signal.throwIfAborted();
29584
- return new Promise((resolve21, reject2) => {
30616
+ return new Promise((resolve22, reject2) => {
29585
30617
  const cleanup = () => {
29586
30618
  signal.removeEventListener("abort", onAbort);
29587
30619
  };
@@ -29601,7 +30633,7 @@ function readByobReaderWithSignal(reader, buffer, signal) {
29601
30633
  try {
29602
30634
  const result = await reader.read(buffer);
29603
30635
  cleanup();
29604
- resolve21(result);
30636
+ resolve22(result);
29605
30637
  } catch (error) {
29606
30638
  cleanup();
29607
30639
  reject2(error);
@@ -31360,11 +32392,11 @@ var UploadError = class extends Error {
31360
32392
  }
31361
32393
  };
31362
32394
  function timeout(promise, ms, code) {
31363
- return new Promise((resolve21, reject2) => {
32395
+ return new Promise((resolve22, reject2) => {
31364
32396
  const timer9 = setTimeout(() => reject2(new UploadError(code, "upload operation timed out")), ms);
31365
32397
  promise.then((value) => {
31366
32398
  clearTimeout(timer9);
31367
- resolve21(value);
32399
+ resolve22(value);
31368
32400
  }, (error) => {
31369
32401
  clearTimeout(timer9);
31370
32402
  reject2(error);
@@ -31387,7 +32419,7 @@ function scannerFromEnv(path) {
31387
32419
  return Promise.resolve();
31388
32420
  }
31389
32421
  if (!isAbsolute5(command)) return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner path must be absolute"));
31390
- return new Promise((resolve21, reject2) => {
32422
+ return new Promise((resolve22, reject2) => {
31391
32423
  const child = spawn2(command, [path], { shell: false, stdio: "ignore" });
31392
32424
  const timer9 = setTimeout(() => {
31393
32425
  child.kill("SIGKILL");
@@ -31399,27 +32431,27 @@ function scannerFromEnv(path) {
31399
32431
  });
31400
32432
  child.once("exit", (code) => {
31401
32433
  clearTimeout(timer9);
31402
- if (code === 0) resolve21();
32434
+ if (code === 0) resolve22();
31403
32435
  else reject2(new UploadError("UPLOAD_SCAN", `scanner exit ${code}`));
31404
32436
  });
31405
32437
  });
31406
32438
  }
31407
- function safeFilename(input, extension2) {
32439
+ function safeFilename2(input, extension2) {
31408
32440
  const stem = basename3(input).replace(/\.[^.]*$/, "").replace(/[^a-zA-Z0-9._ -]/g, "_").slice(0, 180).trim();
31409
32441
  return `${stem || "upload"}${extension2}`;
31410
32442
  }
31411
32443
  async function commitToStorage(buffer, extension2, deps, beforePromote) {
31412
32444
  const storageDir = deps.storageDir ?? uploadDir();
31413
- const quarantineDir = join12(storageDir, ".quarantine");
32445
+ const quarantineDir = join15(storageDir, ".quarantine");
31414
32446
  await mkdir3(quarantineDir, { recursive: true, mode: 448 });
31415
32447
  await mkdir3(storageDir, { recursive: true, mode: 448 });
31416
- const quarantine = join12(quarantineDir, `${randomUUID5()}.upload`);
32448
+ const quarantine = join15(quarantineDir, `${randomUUID5()}.upload`);
31417
32449
  const storageKey = `${randomUUID5()}${extension2}`;
31418
32450
  await writeFile2(quarantine, buffer, { mode: 384, flag: "wx" });
31419
32451
  try {
31420
32452
  await timeout((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
31421
32453
  await beforePromote?.();
31422
- await rename2(quarantine, join12(storageDir, storageKey));
32454
+ await rename2(quarantine, join15(storageDir, storageKey));
31423
32455
  } catch (error) {
31424
32456
  await unlink3(quarantine).catch(() => {
31425
32457
  });
@@ -31466,7 +32498,7 @@ async function processUpload(input, deps = {}) {
31466
32498
  });
31467
32499
  return {
31468
32500
  storageKey,
31469
- filename: safeFilename(input.clientName, type.extension),
32501
+ filename: safeFilename2(input.clientName, type.extension),
31470
32502
  mimeType,
31471
32503
  extension: type.extension,
31472
32504
  size: normalized.byteLength,
@@ -31506,7 +32538,7 @@ async function processDocumentUpload(input, deps = {}) {
31506
32538
  const storageKey = await commitToStorage(input.buffer, extension2, deps);
31507
32539
  return {
31508
32540
  storageKey,
31509
- filename: safeFilename(input.clientName, extension2),
32541
+ filename: safeFilename2(input.clientName, extension2),
31510
32542
  mimeType: input.clientMime,
31511
32543
  extension: extension2,
31512
32544
  size: input.buffer.byteLength
@@ -31620,10 +32652,10 @@ async function dropSpecAttachments(specId) {
31620
32652
  // src/services/spec-attachment-dir.ts
31621
32653
  init_db();
31622
32654
  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";
32655
+ import { join as join16 } from "node:path";
31624
32656
  init_session_id();
31625
32657
  init_uploads();
31626
- var specAttachmentsDir = (repoDir, sessionId2) => join13(repoDir, ".worktrees", ".attachments", sessionId2);
32658
+ var specAttachmentsDir = (repoDir, sessionId2) => join16(repoDir, ".worktrees", ".attachments", sessionId2);
31627
32659
  var INDEX = "INDEX.md";
31628
32660
  var humanSize = (n2) => n2 >= 1024 * 1024 ? `${(n2 / 1024 / 1024).toFixed(1)} MB` : n2 >= 1024 ? `${Math.round(n2 / 1024)} KB` : `${n2} B`;
31629
32661
  function uniqueName(taken, filename) {
@@ -31673,19 +32705,19 @@ async function syncSpecAttachmentsDir(specId, projectId) {
31673
32705
  for (const a of rows) {
31674
32706
  let bytes;
31675
32707
  try {
31676
- bytes = await readFile3(join13(uploadDir(), a.storageKey));
32708
+ bytes = await readFile3(join16(uploadDir(), a.storageKey));
31677
32709
  } catch {
31678
32710
  continue;
31679
32711
  }
31680
32712
  const filename = uniqueName(taken, a.filename);
31681
- const path = join13(dir2, filename);
32713
+ const path = join16(dir2, filename);
31682
32714
  await writeFile3(path, bytes, { mode: 384 });
31683
32715
  items.push({ filename, mimeType: a.mimeType, size: a.size, path });
31684
32716
  }
31685
- await writeFile3(join13(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
32717
+ await writeFile3(join16(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
31686
32718
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
31687
32719
  for (const name2 of await readdir3(dir2)) {
31688
- if (!keep.has(name2)) await rm4(join13(dir2, name2), { recursive: true, force: true }).catch(() => {
32720
+ if (!keep.has(name2)) await rm4(join16(dir2, name2), { recursive: true, force: true }).catch(() => {
31689
32721
  });
31690
32722
  }
31691
32723
  return items;
@@ -34242,7 +35274,7 @@ function renderDocPdf(text, name2, meta) {
34242
35274
  });
34243
35275
  const chunks = [];
34244
35276
  doc.on("data", (c) => chunks.push(c));
34245
- const done = new Promise((resolve21) => doc.on("end", () => resolve21(Buffer.concat(chunks))));
35277
+ const done = new Promise((resolve22) => doc.on("end", () => resolve22(Buffer.concat(chunks))));
34246
35278
  doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
34247
35279
  doc.moveDown(0.25);
34248
35280
  doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
@@ -34317,6 +35349,7 @@ init_pty();
34317
35349
  init_session_phases();
34318
35350
  init_stage_machine();
34319
35351
  init_notifications2();
35352
+ init_sync_notify();
34320
35353
 
34321
35354
  // src/services/spec-head.ts
34322
35355
  init_db();
@@ -35227,7 +36260,7 @@ async function deleteBranches(repoDir, names, opts) {
35227
36260
  import { execFile as execFile11 } from "node:child_process";
35228
36261
  import { realpathSync as realpathSync4 } from "node:fs";
35229
36262
  import { stat } from "node:fs/promises";
35230
- import { basename as basename4, join as join14, resolve as resolve15, sep as sep4 } from "node:path";
36263
+ import { basename as basename4, join as join17, resolve as resolve15, sep as sep4 } from "node:path";
35231
36264
  import { promisify as promisify10 } from "node:util";
35232
36265
  var exec10 = promisify10(execFile11);
35233
36266
  var GIT7 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
@@ -35272,7 +36305,7 @@ var real = (p3) => {
35272
36305
  };
35273
36306
  async function bornAt(path) {
35274
36307
  try {
35275
- const st = await stat(join14(path, ".git"));
36308
+ const st = await stat(join17(path, ".git"));
35276
36309
  const ms = st.birthtimeMs > 0 ? st.birthtimeMs : st.mtimeMs;
35277
36310
  return new Date(ms).toISOString();
35278
36311
  } catch {
@@ -35437,7 +36470,7 @@ init_session_id();
35437
36470
  // src/services/repo-fs.ts
35438
36471
  import { lstat as lstat2, rename as rename3, rm as rm5 } from "node:fs/promises";
35439
36472
  import { createWriteStream } from "node:fs";
35440
- import { join as join15 } from "node:path";
36473
+ import { join as join18 } from "node:path";
35441
36474
  import { randomUUID as randomUUID6 } from "node:crypto";
35442
36475
  import { pipeline } from "node:stream/promises";
35443
36476
  var EntryExistsError = class extends Error {
@@ -35498,7 +36531,7 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
35498
36531
  if (current && !opts.overwrite) return { status: "exists" };
35499
36532
  if (current && !current.isFile())
35500
36533
  throw new PathContainmentError("repository path ditolak: target bukan file regular");
35501
- const temp = join15(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
36534
+ const temp = join18(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
35502
36535
  try {
35503
36536
  await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
35504
36537
  if (opts.isTruncated?.()) {
@@ -35516,13 +36549,13 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
35516
36549
  // src/routes/ide.ts
35517
36550
  var activeSessions = (id) => listSessions().filter((s2) => s2.projectId === id && !s2.exited).length;
35518
36551
  async function lockInputs(id) {
35519
- const open4 = await prisma.spec.findMany({
36552
+ const open5 = await prisma.spec.findMany({
35520
36553
  where: { projectId: id, stage: { not: "done" } },
35521
36554
  select: { id: true }
35522
36555
  });
35523
36556
  const sessions = listSessions().filter((s2) => s2.projectId === id && !s2.exited).map((s2) => s2.branch || (s2.specId ? `hanoman/${s2.id}` : "")).filter(Boolean);
35524
36557
  return {
35525
- openSpecBranches: new Set(open4.map((s2) => sourceBranch(s2.id))),
36558
+ openSpecBranches: new Set(open5.map((s2) => sourceBranch(s2.id))),
35526
36559
  sessionBranches: new Set(sessions)
35527
36560
  };
35528
36561
  }
@@ -36014,14 +37047,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
36014
37047
  // src/routes/fs.ts
36015
37048
  import { readdir as readdir4 } from "node:fs/promises";
36016
37049
  import { homedir as homedir4 } from "node:os";
36017
- import { resolve as resolve17, dirname as dirname9, join as join16 } from "node:path";
37050
+ import { resolve as resolve17, dirname as dirname9, join as join19 } from "node:path";
36018
37051
  async function fs_default(app2) {
36019
37052
  app2.get("/fs/browse", async (req, reply) => {
36020
37053
  const q = req.query.path;
36021
37054
  const dir2 = q && q.trim() ? resolve17(q.trim()) : homedir4();
36022
37055
  try {
36023
37056
  const ents = await readdir4(dir2, { withFileTypes: true });
36024
- 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));
37057
+ 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));
36025
37058
  const parent = dirname9(dir2);
36026
37059
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries3 };
36027
37060
  } catch {
@@ -36503,14 +37536,14 @@ function installCommand(m, agent, shell = shellBin()) {
36503
37536
  }
36504
37537
 
36505
37538
  // src/services/terminal-diag.ts
36506
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync4 } from "node:fs";
36507
- import { join as join17 } from "node:path";
37539
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync5 } from "node:fs";
37540
+ import { join as join20 } from "node:path";
36508
37541
  var DIAG_MAX_BYTES = 2 * 1024 * 1024;
36509
37542
  var KINDS = /* @__PURE__ */ new Set(["key", "comp", "data", "ack", "pred"]);
36510
37543
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
36511
37544
  function diagFile(home3, sessionId2) {
36512
37545
  if (!ID.test(sessionId2)) throw new Error(`id sesi tak sah untuk diag: ${sessionId2}`);
36513
- return join17(home3, "diag", `${sessionId2}.jsonl`);
37546
+ return join20(home3, "diag", `${sessionId2}.jsonl`);
36514
37547
  }
36515
37548
  function usable(ev) {
36516
37549
  if (!ev || typeof ev !== "object") return false;
@@ -36521,9 +37554,9 @@ function appendDiag(home3, sessionId2, events) {
36521
37554
  const file = diagFile(home3, sessionId2);
36522
37555
  const rows = events.filter(usable);
36523
37556
  if (!rows.length) return;
36524
- mkdirSync9(join17(home3, "diag"), { recursive: true });
37557
+ mkdirSync9(join20(home3, "diag"), { recursive: true });
36525
37558
  try {
36526
- if (statSync3(file).size > DIAG_MAX_BYTES) rmSync4(file);
37559
+ if (statSync3(file).size > DIAG_MAX_BYTES) rmSync5(file);
36527
37560
  } catch {
36528
37561
  }
36529
37562
  appendFileSync2(file, rows.map((r) => JSON.stringify(r)).join("\n") + "\n");
@@ -37078,9 +38111,9 @@ init_pty();
37078
38111
  init_session_sandbox();
37079
38112
  import { execFile as execFile13 } from "node:child_process";
37080
38113
  import { randomUUID as randomUUID7 } from "node:crypto";
37081
- import { mkdirSync as mkdirSync10, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
37082
- import { tmpdir as tmpdir3 } from "node:os";
37083
- import { join as join18 } from "node:path";
38114
+ import { mkdirSync as mkdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
38115
+ import { tmpdir as tmpdir4 } from "node:os";
38116
+ import { join as join21 } from "node:path";
37084
38117
  var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
37085
38118
  function leadArgv(o) {
37086
38119
  if (o.agent === "codex") {
@@ -37101,22 +38134,22 @@ function leadArgv(o) {
37101
38134
  ];
37102
38135
  }
37103
38136
  var leadEnv = (agent, base2 = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base2 } : { ...base2 };
37104
- var shellQuote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
38137
+ var shellQuote3 = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
37105
38138
  function leadProcess(prompt, o, env = process.env) {
37106
38139
  const file = binFor(o.agent);
37107
38140
  const directArgs = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
37108
38141
  const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
37109
38142
  if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
37110
38143
  } };
37111
- const promptDir = join18(tmpdir3(), "hanoman-prompts");
38144
+ const promptDir = join21(tmpdir4(), "hanoman-prompts");
37112
38145
  mkdirSync10(promptDir, { recursive: true, mode: 448 });
37113
- const promptFile = join18(promptDir, `oneshot-${randomUUID7()}`);
37114
- writeFileSync5(promptFile, prompt, { flag: "wx", mode: 384 });
37115
- const workspace = o.cwd ?? join18(tmpdir3(), `hanoman-oneshot-${randomUUID7()}`);
38146
+ const promptFile = join21(promptDir, `oneshot-${randomUUID7()}`);
38147
+ writeFileSync7(promptFile, prompt, { flag: "wx", mode: 384 });
38148
+ const workspace = o.cwd ?? join21(tmpdir4(), `hanoman-oneshot-${randomUUID7()}`);
37116
38149
  if (!o.cwd) mkdirSync10(workspace, { recursive: false, mode: 448 });
37117
38150
  try {
37118
38151
  const argsWithoutPrompt = directArgs.slice(0, -1);
37119
- const command = [file, ...argsWithoutPrompt].map(shellQuote).join(" ") + ` "$(cat ${shellQuote(promptFile)})"`;
38152
+ const command = [file, ...argsWithoutPrompt].map(shellQuote3).join(" ") + ` "$(cat ${shellQuote3(promptFile)})"`;
37120
38153
  const sandbox = sandboxArgvFromEnv({
37121
38154
  command,
37122
38155
  worktree: workspace,
@@ -37130,13 +38163,13 @@ function leadProcess(prompt, o, env = process.env) {
37130
38163
  args: sandbox.slice(1),
37131
38164
  promptFile,
37132
38165
  cleanup: () => {
37133
- rmSync5(promptFile, { force: true });
37134
- if (!o.cwd) rmSync5(workspace, { recursive: true, force: true });
38166
+ rmSync6(promptFile, { force: true });
38167
+ if (!o.cwd) rmSync6(workspace, { recursive: true, force: true });
37135
38168
  }
37136
38169
  };
37137
38170
  } catch (error) {
37138
- rmSync5(promptFile, { force: true });
37139
- if (!o.cwd) rmSync5(workspace, { recursive: true, force: true });
38171
+ rmSync6(promptFile, { force: true });
38172
+ if (!o.cwd) rmSync6(workspace, { recursive: true, force: true });
37140
38173
  throw error;
37141
38174
  }
37142
38175
  }
@@ -37154,7 +38187,7 @@ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
37154
38187
  }
37155
38188
  function think(prompt, o) {
37156
38189
  const process2 = leadProcess(prompt, o);
37157
- return new Promise((resolve21, reject2) => {
38190
+ return new Promise((resolve22, reject2) => {
37158
38191
  const child = execFile13(process2.file, process2.args, {
37159
38192
  cwd: process2.cwd,
37160
38193
  timeout: o.timeoutMs,
@@ -37168,7 +38201,7 @@ function think(prompt, o) {
37168
38201
  reject2(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
37169
38202
  return;
37170
38203
  }
37171
- resolve21(stdout);
38204
+ resolve22(stdout);
37172
38205
  });
37173
38206
  child.stdin?.end();
37174
38207
  });
@@ -37205,9 +38238,9 @@ function acquire(cap, waitMs) {
37205
38238
  return Promise.resolve();
37206
38239
  }
37207
38240
  const startedAt = Date.now();
37208
- return new Promise((resolve21, reject2) => {
38241
+ return new Promise((resolve22, reject2) => {
37209
38242
  const w = {
37210
- grant: resolve21,
38243
+ grant: resolve22,
37211
38244
  deny: reject2,
37212
38245
  timer: setTimeout(() => {
37213
38246
  const i = queue.indexOf(w);
@@ -38603,9 +39636,9 @@ import { readFileSync as readFileSync10 } from "node:fs";
38603
39636
  // src/services/vps-ssh.ts
38604
39637
  init_config2();
38605
39638
  import { spawn as spawn4 } from "node:child_process";
38606
- import { mkdtempSync, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
38607
- import { tmpdir as tmpdir4 } from "node:os";
38608
- import { dirname as dirname10, join as join19 } from "node:path";
39639
+ import { mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
39640
+ import { tmpdir as tmpdir5 } from "node:os";
39641
+ import { dirname as dirname10, join as join22 } from "node:path";
38609
39642
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
38610
39643
  function consoleArgv(t) {
38611
39644
  return [
@@ -38620,9 +39653,9 @@ function consoleArgv(t) {
38620
39653
  ];
38621
39654
  }
38622
39655
  function askpassScript() {
38623
- const dir2 = mkdtempSync(join19(tmpdir4(), "hanoman-askpass-"));
38624
- const path = join19(dir2, "askpass.sh");
38625
- writeFileSync6(path, `#!/bin/sh
39656
+ const dir2 = mkdtempSync(join22(tmpdir5(), "hanoman-askpass-"));
39657
+ const path = join22(dir2, "askpass.sh");
39658
+ writeFileSync8(path, `#!/bin/sh
38626
39659
  printf '%s' "$HANOMAN_SSH_PASSWORD"
38627
39660
  `, { mode: 448 });
38628
39661
  return path;
@@ -38655,14 +39688,14 @@ function sshExec(t, remoteCmd, opts = {}) {
38655
39688
  SSH_ASKPASS_REQUIRE: "force",
38656
39689
  HANOMAN_SSH_PASSWORD: opts.password
38657
39690
  } : process.env;
38658
- return new Promise((resolve21) => {
39691
+ return new Promise((resolve22) => {
38659
39692
  const p3 = spawn4(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
38660
39693
  let out4 = "";
38661
39694
  const timer9 = setTimeout(() => p3.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
38662
39695
  const done = (r) => {
38663
39696
  clearTimeout(timer9);
38664
- if (askpass) rmSync6(dirname10(askpass), { recursive: true, force: true });
38665
- resolve21(r);
39697
+ if (askpass) rmSync7(dirname10(askpass), { recursive: true, force: true });
39698
+ resolve22(r);
38666
39699
  };
38667
39700
  p3.stdout.on("data", (d) => {
38668
39701
  out4 += d;
@@ -38682,16 +39715,16 @@ function sshExec(t, remoteCmd, opts = {}) {
38682
39715
  // src/services/vps-audit.ts
38683
39716
  init_db();
38684
39717
  import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
38685
- import { join as join21 } from "node:path";
39718
+ import { join as join24 } from "node:path";
38686
39719
  import { fileURLToPath as fileURLToPath3 } from "node:url";
38687
39720
 
38688
39721
  // src/runner/deps.ts
38689
39722
  import { existsSync as existsSync13 } from "node:fs";
38690
- import { dirname as dirname11, join as join20 } from "node:path";
39723
+ import { dirname as dirname11, join as join23 } from "node:path";
38691
39724
  function repoRootFrom(startDir) {
38692
39725
  let dir2 = startDir;
38693
39726
  for (let i = 0; i < 8; i++) {
38694
- if (existsSync13(join20(dir2, "pnpm-workspace.yaml"))) return dir2;
39727
+ if (existsSync13(join23(dir2, "pnpm-workspace.yaml"))) return dir2;
38695
39728
  const parent = dirname11(dir2);
38696
39729
  if (parent === dir2) break;
38697
39730
  dir2 = parent;
@@ -38700,6 +39733,9 @@ function repoRootFrom(startDir) {
38700
39733
  }
38701
39734
  var repoRoot = (startDir = process.cwd()) => repoRootFrom(startDir);
38702
39735
 
39736
+ // src/services/vps-audit.ts
39737
+ init_sync_notify();
39738
+
38703
39739
  // src/vps/scoring.ts
38704
39740
  var pct = (fulfilled, applicable) => applicable === 0 ? 100 : Math.round(fulfilled / applicable * 100);
38705
39741
  function scoreCompliance(probeStatus, states, items = CATALOG) {
@@ -38801,7 +39837,7 @@ var packagedScript = (f) => fileURLToPath3(new URL(`../scripts/vps/${f}`, import
38801
39837
  var moduleDir = () => fileURLToPath3(new URL(".", import.meta.url));
38802
39838
  var scriptPath = (f) => {
38803
39839
  const packed = packagedScript(f);
38804
- return existsSync14(packed) ? packed : join21(repoRoot(moduleDir()), "server", "scripts", "vps", f);
39840
+ return existsSync14(packed) ? packed : join24(repoRoot(moduleDir()), "server", "scripts", "vps", f);
38805
39841
  };
38806
39842
  async function itemStatesOf(vpsId) {
38807
39843
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
@@ -39009,27 +40045,27 @@ async function buildChecklist(vpsId) {
39009
40045
  init_src2();
39010
40046
  init_config2();
39011
40047
  import { execFileSync as execFileSync3 } from "node:child_process";
39012
- import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
40048
+ import { chmodSync as chmodSync7, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
39013
40049
  import { homedir as homedir5 } from "node:os";
39014
- import { join as join22 } from "node:path";
40050
+ import { join as join25 } from "node:path";
39015
40051
  var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? resolveDataDirs().sshKeys;
39016
40052
  var KEY_FILES = ["id_ed25519", "id_ed25519.pub"];
39017
40053
  function adoptLegacyKey(dir2) {
39018
40054
  if (effectiveStr("HANOMAN_SSH_KEY_DIR")) return;
39019
- const legacy = join22(homedir5(), ".hanoman");
40055
+ const legacy = join25(homedir5(), ".hanoman");
39020
40056
  if (legacy === dir2) return;
39021
- if (!KEY_FILES.every((f) => existsSync15(join22(legacy, f)))) return;
40057
+ if (!KEY_FILES.every((f) => existsSync15(join25(legacy, f)))) return;
39022
40058
  mkdirSync11(dir2, { recursive: true, mode: 448 });
39023
40059
  for (const f of KEY_FILES) {
39024
- copyFileSync(join22(legacy, f), join22(dir2, f));
39025
- chmodSync4(join22(dir2, f), 384);
40060
+ copyFileSync(join25(legacy, f), join25(dir2, f));
40061
+ chmodSync7(join25(dir2, f), 384);
39026
40062
  }
39027
- for (const f of KEY_FILES) unlinkSync2(join22(legacy, f));
40063
+ for (const f of KEY_FILES) unlinkSync2(join25(legacy, f));
39028
40064
  console.log(`vps: key SSH dipindah dari ${legacy} ke ${dir2} (SPEC-846 \u2014 satu batas backup)`);
39029
40065
  }
39030
40066
  function ensureHanomanKey() {
39031
40067
  const dir2 = keyDir();
39032
- const privPath = join22(dir2, KEY_FILES[0]);
40068
+ const privPath = join25(dir2, KEY_FILES[0]);
39033
40069
  const pubPath = `${privPath}.pub`;
39034
40070
  if (!existsSync15(privPath)) adoptLegacyKey(dir2);
39035
40071
  if (!existsSync15(privPath)) {
@@ -39058,6 +40094,7 @@ async function bootstrapKey(t, password) {
39058
40094
  // src/routes/vps.ts
39059
40095
  init_pty();
39060
40096
  init_settings3();
40097
+ init_sync_notify();
39061
40098
  function keyMissing(v) {
39062
40099
  return !!v.keyPath && !existsSync16(v.keyPath);
39063
40100
  }
@@ -39341,7 +40378,7 @@ init_config2();
39341
40378
  import { execFileSync as execFileSync4 } from "node:child_process";
39342
40379
  import { readFileSync as readFileSync14 } from "node:fs";
39343
40380
  import { homedir as homedir7 } from "node:os";
39344
- import { join as join23 } from "node:path";
40381
+ import { join as join26 } from "node:path";
39345
40382
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
39346
40383
  var TTL_MS2 = 3e4;
39347
40384
  var lastOk = null;
@@ -39354,7 +40391,7 @@ var LABELS = {
39354
40391
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
39355
40392
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
39356
40393
  function credsFile() {
39357
- return join23(effectiveStr("CLAUDE_CONFIG_DIR") ?? join23(homedir7(), ".claude"), ".credentials.json");
40394
+ return join26(effectiveStr("CLAUDE_CONFIG_DIR") ?? join26(homedir7(), ".claude"), ".credentials.json");
39358
40395
  }
39359
40396
  function readAccessToken() {
39360
40397
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
@@ -39428,14 +40465,14 @@ async function getLimits() {
39428
40465
  // src/services/codex-limits.ts
39429
40466
  import { open as open3, readdir as readdir5, stat as stat2 } from "node:fs/promises";
39430
40467
  import { homedir as homedir8 } from "node:os";
39431
- import { join as join24 } from "node:path";
40468
+ import { join as join27 } from "node:path";
39432
40469
  var TTL_MS3 = 3e4;
39433
40470
  var STALE_AFTER_MS = 12 * 36e5;
39434
40471
  var TAIL_BYTES = 512 * 1024;
39435
40472
  var MAX_FILES = 8;
39436
40473
  var cache2 = null;
39437
40474
  var freshUntil2 = 0;
39438
- var codexSessionsDir = () => join24(process.env.CODEX_HOME ?? join24(homedir8(), ".codex"), "sessions");
40475
+ var codexSessionsDir = () => join27(process.env.CODEX_HOME ?? join27(homedir8(), ".codex"), "sessions");
39439
40476
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
39440
40477
  function windowLabel(minutes) {
39441
40478
  if (minutes === 300) return "Sesi 5 jam";
@@ -39468,7 +40505,7 @@ async function recentRollouts(dir2) {
39468
40505
  return [];
39469
40506
  }
39470
40507
  const stamped = await Promise.all(entries3.map(async (rel) => {
39471
- const full = join24(dir2, rel);
40508
+ const full = join27(dir2, rel);
39472
40509
  try {
39473
40510
  return { full, mtime: (await stat2(full)).mtimeMs };
39474
40511
  } catch {
@@ -39545,41 +40582,8 @@ async function limits(app2) {
39545
40582
  app2.get("/limits/codex", async () => getCodexLimits());
39546
40583
  }
39547
40584
 
39548
- // src/services/codex-version.ts
39549
- init_src();
39550
- init_config2();
39551
- import { execFile as execFile14 } from "node:child_process";
39552
- import { promisify as promisify13 } from "node:util";
39553
- var run3 = promisify13(execFile14);
39554
- var CODEX_MIN_CLIENT = "0.144.0";
39555
- var TTL_MS4 = 5 * 6e4;
39556
- var cache3 = null;
39557
- var codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
39558
- function parseCodexVersion(out4) {
39559
- const m = /(\d+)\.(\d+)\.(\d+)/.exec(out4);
39560
- return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
39561
- }
39562
- async function getCodexVersion(now = Date.now()) {
39563
- if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
39564
- let version = null;
39565
- try {
39566
- const { stdout } = await run3(codexBin2(), ["--version"], { timeout: 5e3 });
39567
- version = parseCodexVersion(stdout);
39568
- } catch {
39569
- }
39570
- cache3 = { at: now, version };
39571
- return version;
39572
- }
39573
- async function codexVersionInfo() {
39574
- const version = await getCodexVersion();
39575
- return {
39576
- version,
39577
- minRequired: CODEX_MIN_CLIENT,
39578
- ok: version === null || cmpVersion(version, CODEX_MIN_CLIENT) >= 0
39579
- };
39580
- }
39581
-
39582
40585
  // src/routes/codex.ts
40586
+ init_codex_version();
39583
40587
  async function codex(app2) {
39584
40588
  app2.get("/codex/version", async () => codexVersionInfo());
39585
40589
  }
@@ -39776,6 +40780,35 @@ async function presenceView(o = {}) {
39776
40780
  return { enabled: rows.length > 0, devices: devices2 };
39777
40781
  }
39778
40782
 
40783
+ // src/services/pending-counts.ts
40784
+ init_src();
40785
+ init_db();
40786
+ var PRD_TTL_MS = 6e4;
40787
+ var prdCache = null;
40788
+ async function prdDraftCount(now, prds) {
40789
+ if (prdCache && now - prdCache.at < PRD_TTL_MS) return prdCache.count;
40790
+ try {
40791
+ const count = (await prds()).filter((p3) => p3.status === "draft").length;
40792
+ prdCache = { at: now, count };
40793
+ } catch {
40794
+ prdCache = { at: now, count: prdCache?.count ?? 0 };
40795
+ }
40796
+ return prdCache.count;
40797
+ }
40798
+ async function pendingCounts(now = Date.now(), prds = listAllPrds) {
40799
+ const [tickets2, issues, backlog, lead, prd] = await Promise.all([
40800
+ prisma.ticket.count({ where: { status: "new" } }),
40801
+ prisma.githubIssue.count({ where: { status: "new" } }),
40802
+ // `startedAt` = kapan sesi PERTAMA lahir (ADR-0090), null = belum pernah dikerjakan. Stage
40803
+ // `done` dikecualikan karena item bisa ditandai selesai manual tanpa sesi (SPEC-804/ADR-0120) —
40804
+ // tanpa gerbang itu ia terhitung selamanya sebagai pekerjaan yang belum diajukan.
40805
+ prisma.spec.count({ where: { startedAt: null, stage: { not: "done" } } }),
40806
+ prisma.leadFlow.count({ where: { status: { in: [...OPEN_LEAD_FLOW_STATUSES] } } }),
40807
+ prdDraftCount(now, prds)
40808
+ ]);
40809
+ return { ...EMPTY_PENDING, triage: tickets2 + issues, backlog, prd, lead };
40810
+ }
40811
+
39779
40812
  // src/services/events.ts
39780
40813
  init_db();
39781
40814
  init_config2();
@@ -40653,7 +41686,14 @@ var GROUPS = [
40653
41686
  // dihitung). 3 dtk: presence berdenyut 30 dtk, jadi kadens lebih rapat hanya menambah build tanpa
40654
41687
  // menambah informasi. `presenceView` menyegarkan sesi mesin ini sendiri di dalamnya — satu
40655
41688
  // `tmux list-panes` asinkron, tak menahan event loop.
40656
- { everyTicks: 3, cookieOnly: true, last: "", build: async () => ({ t: "presence", ...await presenceView() }) }
41689
+ { everyTicks: 3, cookieOnly: true, last: "", build: async () => ({ t: "presence", ...await presenceView() }) },
41690
+ // SPEC-961 · grup GLOBAL ke-11 · angka "butuh pengajuan" untuk badge sidebar. 5 dtk: badge bukan
41691
+ // board — yang dijanjikannya adalah "ada yang menunggu", bukan detik keberapa ia muncul — dan
41692
+ // dedup signature membuat frame lahir hanya saat salah satu angka berubah (jarang). Empat angka,
41693
+ // bukan empat grup: keempatnya berubah dari peristiwa yang sama (item diajukan/diputuskan) dan
41694
+ // dibaca satu komponen yang sama. `cookieOnly` TIDAK dipasang — muatannya jumlah agregat tanpa
41695
+ // id/judul, dan agent token sudah boleh membaca daftar sumbernya lewat capability masing-masing.
41696
+ { everyTicks: 5, last: "", build: async () => ({ t: "pending", counts: await pendingCounts() }) }
40657
41697
  ];
40658
41698
  function broadcast2(msg, cookieOnly = false) {
40659
41699
  const s2 = JSON.stringify(msg);
@@ -41180,7 +42220,7 @@ init_config2();
41180
42220
  init_src2();
41181
42221
  import { randomUUID as randomUUID8 } from "node:crypto";
41182
42222
  import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir6, stat as stat3 } from "node:fs/promises";
41183
- import { join as join25, resolve as resolve19, basename as basename6 } from "node:path";
42223
+ import { join as join28, resolve as resolve19, basename as basename6 } from "node:path";
41184
42224
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
41185
42225
  function transcriptDir() {
41186
42226
  return resolve19(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
@@ -41201,13 +42241,13 @@ async function saveTranscript(text) {
41201
42241
  const dir2 = transcriptDir();
41202
42242
  await mkdir5(dir2, { recursive: true, mode: 448 });
41203
42243
  const key = `${randomUUID8()}.log`;
41204
- await writeFile4(join25(dir2, key), body, { encoding: "utf8", mode: 384 });
42244
+ await writeFile4(join28(dir2, key), body, { encoding: "utf8", mode: 384 });
41205
42245
  return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
41206
42246
  }
41207
42247
  async function readTranscript(key) {
41208
42248
  if (!key) return null;
41209
42249
  try {
41210
- return await readFile4(join25(transcriptDir(), basename6(key)), "utf8");
42250
+ return await readFile4(join28(transcriptDir(), basename6(key)), "utf8");
41211
42251
  } catch {
41212
42252
  return null;
41213
42253
  }
@@ -41215,7 +42255,7 @@ async function readTranscript(key) {
41215
42255
  async function deleteTranscript(key) {
41216
42256
  if (!key) return;
41217
42257
  try {
41218
- await unlink4(join25(transcriptDir(), basename6(key)));
42258
+ await unlink4(join28(transcriptDir(), basename6(key)));
41219
42259
  } catch (e) {
41220
42260
  if (e.code !== "ENOENT") throw e;
41221
42261
  }
@@ -41233,7 +42273,7 @@ async function listTranscripts() {
41233
42273
  for (const name2 of names) {
41234
42274
  if (!name2.endsWith(".log")) continue;
41235
42275
  try {
41236
- rows.push({ key: name2, mtimeMs: (await stat3(join25(dir2, name2))).mtimeMs });
42276
+ rows.push({ key: name2, mtimeMs: (await stat3(join28(dir2, name2))).mtimeMs });
41237
42277
  } catch {
41238
42278
  }
41239
42279
  }
@@ -41287,11 +42327,11 @@ async function beginSession(b) {
41287
42327
  });
41288
42328
  }
41289
42329
  async function finishSession(d) {
41290
- const open4 = await prisma.sessionHistory.findFirst({
42330
+ const open5 = await prisma.sessionHistory.findFirst({
41291
42331
  where: { sessionId: d.sessionId, endedAt: null },
41292
42332
  orderBy: { startedAt: "desc" }
41293
42333
  });
41294
- if (!open4) return;
42334
+ if (!open5) return;
41295
42335
  let t = { key: "", bytes: 0 };
41296
42336
  if (d.transcript) {
41297
42337
  try {
@@ -41301,7 +42341,7 @@ async function finishSession(d) {
41301
42341
  }
41302
42342
  }
41303
42343
  await prisma.sessionHistory.update({
41304
- where: { id: open4.id },
42344
+ where: { id: open5.id },
41305
42345
  data: {
41306
42346
  endedAt: /* @__PURE__ */ new Date(),
41307
42347
  endedReason: CLOSED,
@@ -41416,14 +42456,14 @@ async function reconcileTranscripts(opts = {}) {
41416
42456
  return report;
41417
42457
  }
41418
42458
  async function reconcileHistory(liveSessionIds) {
41419
- const open4 = await prisma.sessionHistory.findMany({
42459
+ const open5 = await prisma.sessionHistory.findMany({
41420
42460
  where: { endedAt: null },
41421
42461
  select: { id: true, sessionId: true, updatedAt: true }
41422
42462
  });
41423
42463
  const live = new Set(liveSessionIds);
41424
42464
  const at = /* @__PURE__ */ new Date();
41425
42465
  let closed = 0;
41426
- for (const r of open4) {
42466
+ for (const r of open5) {
41427
42467
  if (live.has(r.sessionId)) continue;
41428
42468
  await prisma.sessionHistory.update({
41429
42469
  where: { id: r.id },
@@ -41474,7 +42514,285 @@ async function session_history_default(app2) {
41474
42514
  init_src();
41475
42515
  init_session_event_token();
41476
42516
  init_pty();
42517
+
42518
+ // src/services/agent-invocations.ts
42519
+ init_src();
42520
+ init_db();
42521
+ import { createHash as createHash8, randomUUID as randomUUID10 } from "node:crypto";
42522
+ import { execFileSync as execFileSync5 } from "node:child_process";
42523
+ import { homedir as homedir9 } from "node:os";
42524
+ import { readFileSync as readFileSync16, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
42525
+ import { isAbsolute as isAbsolute7, relative as relative3, resolve as resolve20 } from "node:path";
42526
+ var MAX_EXCERPT_BYTES = 4096;
42527
+ var MAX_TRANSCRIPT_BYTES2 = 10 * 1024 * 1024;
42528
+ var ANSI2 = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
42529
+ var snapshotHashes = /* @__PURE__ */ new Map();
42530
+ var keyOf = (x) => `${x.sessionId}\0${x.runtimeInvocationId}`;
42531
+ var hash2 = (value) => createHash8("sha256").update(value).digest("hex");
42532
+ var defaultGitStatus = (cwd) => {
42533
+ try {
42534
+ return execFileSync5("git", ["-C", cwd, "status", "--porcelain=v1", "-z"], {
42535
+ encoding: "utf8",
42536
+ stdio: ["ignore", "pipe", "ignore"]
42537
+ });
42538
+ } catch {
42539
+ return null;
42540
+ }
42541
+ };
42542
+ function snapshot2(cwd, run4 = defaultGitStatus) {
42543
+ try {
42544
+ const value = run4(cwd);
42545
+ return value === null ? null : hash2(value);
42546
+ } catch {
42547
+ return null;
42548
+ }
42549
+ }
42550
+ var stripAnsi = (value) => value.replace(ANSI2, "");
42551
+ var utf8Prefix = (value, maxBytes) => {
42552
+ let out4 = "", bytes = 0;
42553
+ for (const char of value) {
42554
+ const next = Buffer.byteLength(char, "utf8");
42555
+ if (bytes + next > maxBytes) break;
42556
+ out4 += char;
42557
+ bytes += next;
42558
+ }
42559
+ return out4;
42560
+ };
42561
+ var transcriptRoots = () => [
42562
+ resolve20(process.env.CLAUDE_CONFIG_DIR ?? `${homedir9()}/.claude`),
42563
+ resolve20(process.env.CODEX_HOME ?? `${homedir9()}/.codex`)
42564
+ ];
42565
+ var inside = (path, root) => {
42566
+ const rel = relative3(root, path);
42567
+ return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
42568
+ };
42569
+ var EMPTY_USAGE = { inputTokens: null, outputTokens: null, cachedTokens: null };
42570
+ var nonnegativeInt = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
42571
+ function usageFromText(text) {
42572
+ const found = [];
42573
+ const inspect = (value) => {
42574
+ if (!value || typeof value !== "object") return;
42575
+ const record2 = value;
42576
+ const usage2 = record2.usage;
42577
+ if (usage2 && typeof usage2 === "object") {
42578
+ const u = usage2;
42579
+ found.push([
42580
+ nonnegativeInt(u.input_tokens ?? u.inputTokens) ?? -1,
42581
+ nonnegativeInt(u.output_tokens ?? u.outputTokens) ?? -1,
42582
+ nonnegativeInt(u.cached_tokens ?? u.cachedTokens ?? u.cache_read_input_tokens ?? u.cacheReadInputTokens) ?? -1
42583
+ ]);
42584
+ }
42585
+ };
42586
+ for (const line of text.split("\n")) {
42587
+ if (!line.trim()) continue;
42588
+ try {
42589
+ inspect(JSON.parse(line));
42590
+ } catch {
42591
+ }
42592
+ }
42593
+ if (found.length === 0) return EMPTY_USAGE;
42594
+ const max = (index) => {
42595
+ const values = found.map((entry) => entry[index]).filter((n2) => n2 >= 0);
42596
+ return values.length ? Math.max(...values) : null;
42597
+ };
42598
+ return { inputTokens: max(0), outputTokens: max(1), cachedTokens: max(2) };
42599
+ }
42600
+ function transcriptUsage(path, roots = transcriptRoots()) {
42601
+ if (!path) return EMPTY_USAGE;
42602
+ try {
42603
+ const real2 = realpathSync5(path);
42604
+ const safeRoots = roots.map((root) => realpathSync5(root));
42605
+ if (!safeRoots.some((root) => inside(real2, root))) return EMPTY_USAGE;
42606
+ const info = statSync4(real2);
42607
+ if (!info.isFile() || info.size > MAX_TRANSCRIPT_BYTES2) return EMPTY_USAGE;
42608
+ return usageFromText(readFileSync16(real2, "utf8"));
42609
+ } catch {
42610
+ return EMPTY_USAGE;
42611
+ }
42612
+ }
42613
+ async function startAgentInvocation(input, io = {}) {
42614
+ const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
42615
+ const existing = await prisma.agentInvocation.findUnique({
42616
+ where: { sessionId_runtimeInvocationId: {
42617
+ sessionId: input.sessionId,
42618
+ runtimeInvocationId: input.runtimeInvocationId
42619
+ } }
42620
+ });
42621
+ const row = await prisma.agentInvocation.upsert({
42622
+ where: { sessionId_runtimeInvocationId: {
42623
+ sessionId: input.sessionId,
42624
+ runtimeInvocationId: input.runtimeInvocationId
42625
+ } },
42626
+ update: {},
42627
+ create: {
42628
+ id: randomUUID10(),
42629
+ sessionId: input.sessionId,
42630
+ projectId: input.projectId,
42631
+ specId: input.specId ?? null,
42632
+ runtime: input.runtime,
42633
+ runtimeInvocationId: input.runtimeInvocationId,
42634
+ customAgentId: input.customAgentId ?? null,
42635
+ agentName: input.agentName,
42636
+ model: input.model ?? null,
42637
+ status: "running",
42638
+ startedAt
42639
+ }
42640
+ });
42641
+ const before2 = snapshot2(input.cwd, io.gitStatus);
42642
+ if (before2 !== null) snapshotHashes.set(keyOf(input), before2);
42643
+ return { row, duplicate: existing !== null };
42644
+ }
42645
+ async function stopAgentInvocation(input, io = {}) {
42646
+ const unique = { sessionId_runtimeInvocationId: {
42647
+ sessionId: input.sessionId,
42648
+ runtimeInvocationId: input.runtimeInvocationId
42649
+ } };
42650
+ const existing = await prisma.agentInvocation.findUnique({ where: unique });
42651
+ if (existing?.endedAt) return { row: existing, duplicate: true };
42652
+ const endedAt = input.endedAt ?? /* @__PURE__ */ new Date();
42653
+ const startedAt = existing?.startedAt ?? endedAt;
42654
+ const cleanResult = input.result === void 0 ? null : stripAnsi(input.result);
42655
+ const usage2 = transcriptUsage(input.transcriptPath, io.transcriptRoots);
42656
+ const before2 = snapshotHashes.get(keyOf(input));
42657
+ const after = snapshot2(input.cwd, io.gitStatus);
42658
+ snapshotHashes.delete(keyOf(input));
42659
+ const evidence = {
42660
+ // Stop tanpa start lazim setelah restart server di tengah invocation. Waktu start dan status
42661
+ // runtime sudah hilang; simpan baris sintetis yang dapat diaudit tanpa mengarang durasi 0 ms.
42662
+ status: existing ? input.status ?? "completed" : "completed",
42663
+ endedAt,
42664
+ durationMs: existing ? Math.max(0, endedAt.getTime() - startedAt.getTime()) : null,
42665
+ ...usage2,
42666
+ resultExcerpt: cleanResult === null ? null : utf8Prefix(cleanResult, MAX_EXCERPT_BYTES),
42667
+ resultHash: cleanResult === null ? null : hash2(cleanResult),
42668
+ workspaceChanged: before2 !== void 0 && after !== null && before2 !== after
42669
+ };
42670
+ if (existing) {
42671
+ const row2 = await prisma.agentInvocation.update({ where: { id: existing.id }, data: evidence });
42672
+ return { row: row2, duplicate: false };
42673
+ }
42674
+ const row = await prisma.agentInvocation.create({
42675
+ data: {
42676
+ id: randomUUID10(),
42677
+ sessionId: input.sessionId,
42678
+ projectId: input.projectId,
42679
+ specId: input.specId ?? null,
42680
+ runtime: input.runtime,
42681
+ runtimeInvocationId: input.runtimeInvocationId,
42682
+ customAgentId: input.customAgentId ?? null,
42683
+ agentName: input.agentName,
42684
+ model: input.model ?? null,
42685
+ startedAt,
42686
+ ...evidence
42687
+ }
42688
+ });
42689
+ return { row, duplicate: false };
42690
+ }
42691
+ async function reconcileAgentInvocations(liveSessionIds) {
42692
+ const live = new Set(liveSessionIds);
42693
+ const open5 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
42694
+ let changed = 0;
42695
+ const endedAt = /* @__PURE__ */ new Date();
42696
+ for (const row of open5) {
42697
+ if (live.has(row.sessionId)) continue;
42698
+ await prisma.agentInvocation.update({
42699
+ where: { id: row.id },
42700
+ data: {
42701
+ status: "abandoned",
42702
+ endedAt,
42703
+ durationMs: Math.max(0, endedAt.getTime() - row.startedAt.getTime())
42704
+ }
42705
+ });
42706
+ changed++;
42707
+ }
42708
+ return changed;
42709
+ }
42710
+ var agentInvocationView = (row) => ({
42711
+ id: row.id,
42712
+ sessionId: row.sessionId,
42713
+ projectId: row.projectId,
42714
+ specId: row.specId,
42715
+ runtime: row.runtime === "codex" ? "codex" : "claude",
42716
+ customAgentId: row.customAgentId,
42717
+ agentName: row.agentName,
42718
+ model: row.model,
42719
+ status: row.status,
42720
+ startedAt: row.startedAt.toISOString(),
42721
+ endedAt: row.endedAt?.toISOString() ?? null,
42722
+ durationMs: row.durationMs,
42723
+ inputTokens: row.inputTokens,
42724
+ outputTokens: row.outputTokens,
42725
+ cachedTokens: row.cachedTokens,
42726
+ resultExcerpt: row.resultExcerpt,
42727
+ resultHash: row.resultHash,
42728
+ workspaceChanged: row.workspaceChanged,
42729
+ disposition: AGENT_DISPOSITIONS.includes(row.disposition) ? row.disposition : "pending",
42730
+ dispositionNote: row.dispositionNote,
42731
+ evaluatedAt: row.evaluatedAt?.toISOString() ?? null
42732
+ });
42733
+ var median = (values) => {
42734
+ if (values.length === 0) return null;
42735
+ values.sort((a, b) => a - b);
42736
+ const middle = Math.floor(values.length / 2);
42737
+ return values.length % 2 === 1 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
42738
+ };
42739
+ var availableSum = (values) => {
42740
+ const known = values.filter((value) => value !== null);
42741
+ return known.length ? known.reduce((sum, value) => sum + value, 0) : null;
42742
+ };
42743
+ async function agentMetrics(query2) {
42744
+ const where = {
42745
+ ...query2.projectId ? { projectId: query2.projectId } : {},
42746
+ ...query2.from || query2.to ? { startedAt: {
42747
+ ...query2.from ? { gte: query2.from } : {},
42748
+ ...query2.to ? { lte: query2.to } : {}
42749
+ } } : {}
42750
+ };
42751
+ const rows = await prisma.agentInvocation.findMany({ where, orderBy: { startedAt: "desc" } });
42752
+ const groups = /* @__PURE__ */ new Map();
42753
+ for (const row of rows) groups.set(row.agentName, [...groups.get(row.agentName) ?? [], row]);
42754
+ const agents = [...groups.entries()].map(([agentName, invocations]) => {
42755
+ const dispositions = { pending: 0, accepted: 0, partial: 0, rejected: 0, falsePositive: 0 };
42756
+ for (const row of invocations) {
42757
+ if (row.disposition === "accepted") dispositions.accepted++;
42758
+ else if (row.disposition === "partial") dispositions.partial++;
42759
+ else if (row.disposition === "rejected") dispositions.rejected++;
42760
+ else if (row.disposition === "false-positive") dispositions.falsePositive++;
42761
+ else dispositions.pending++;
42762
+ }
42763
+ const evaluated = dispositions.accepted + dispositions.partial + dispositions.rejected + dispositions.falsePositive;
42764
+ return {
42765
+ agentName,
42766
+ invocationCount: invocations.length,
42767
+ medianDurationMs: median(invocations.flatMap((row) => row.durationMs === null ? [] : [row.durationMs])),
42768
+ inputTokens: availableSum(invocations.map((row) => row.inputTokens)),
42769
+ outputTokens: availableSum(invocations.map((row) => row.outputTokens)),
42770
+ cachedTokens: availableSum(invocations.map((row) => row.cachedTokens)),
42771
+ dispositions,
42772
+ operationalPrecision: evaluated ? (dispositions.accepted + dispositions.partial) / evaluated : null,
42773
+ workspaceChanged: invocations.some((row) => row.workspaceChanged)
42774
+ };
42775
+ }).sort((a, b) => a.agentName.localeCompare(b.agentName));
42776
+ return { agents, recent: rows.slice(0, 100).map(agentInvocationView) };
42777
+ }
42778
+ async function updateAgentInvocationDisposition(id, disposition, note) {
42779
+ const exists = await prisma.agentInvocation.findUnique({ where: { id } });
42780
+ if (!exists) return null;
42781
+ const row = await prisma.agentInvocation.update({
42782
+ where: { id },
42783
+ data: {
42784
+ disposition,
42785
+ dispositionNote: note?.trim() || null,
42786
+ evaluatedAt: /* @__PURE__ */ new Date()
42787
+ }
42788
+ });
42789
+ return agentInvocationView(row);
42790
+ }
42791
+
42792
+ // src/routes/session-events.ts
41477
42793
  var bearer = (h) => /^Bearer (.+)$/.exec(h ?? "")?.[1] ?? "";
42794
+ var recordOf = (value) => value && typeof value === "object" ? value : null;
42795
+ var boundedString = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max ? value : void 0;
41478
42796
  async function session_events_default(app2) {
41479
42797
  app2.post("/session-events", async (req, reply) => {
41480
42798
  const sessionId2 = String(req.headers["x-hanoman-session"] ?? "");
@@ -41483,6 +42801,38 @@ async function session_events_default(app2) {
41483
42801
  return reply.code(401).send({ error: "unauthorized" });
41484
42802
  const s2 = await getSessionAsync(sessionId2);
41485
42803
  if (!s2 || s2.exited) return reply.code(404).send({ error: "live session not found" });
42804
+ const body = recordOf(req.body);
42805
+ const lifecycle = body?.hook_event_name;
42806
+ if (body && (lifecycle === "SubagentStart" || lifecycle === "SubagentStop")) {
42807
+ const runtimeInvocationId = boundedString(
42808
+ body.agent_id ?? body.subagent_id ?? body.thread_id,
42809
+ 500
42810
+ );
42811
+ const agentName = boundedString(body.agent_type ?? body.agent_name, 200);
42812
+ const meta = agentName ? (s2.agentRoster ?? []).find((agent) => agent.name === agentName) : void 0;
42813
+ if (!runtimeInvocationId || !meta) return reply.code(202).send({ ignored: true });
42814
+ const identity = {
42815
+ sessionId: sessionId2,
42816
+ projectId: s2.projectId,
42817
+ specId: s2.specId,
42818
+ runtime: s2.agent,
42819
+ runtimeInvocationId,
42820
+ customAgentId: meta.id,
42821
+ agentName: meta.name,
42822
+ model: meta.model,
42823
+ cwd: s2.cwd
42824
+ };
42825
+ const outcome = lifecycle === "SubagentStart" ? await startAgentInvocation(identity) : await stopAgentInvocation({
42826
+ ...identity,
42827
+ status: body.status === "interrupted" ? "interrupted" : "completed",
42828
+ result: boundedString(body.last_assistant_message ?? body.result, 1e6),
42829
+ transcriptPath: boundedString(
42830
+ body.agent_transcript_path ?? body.transcript_path,
42831
+ 4096
42832
+ )
42833
+ });
42834
+ return reply.code(202).send(outcome.duplicate ? { duplicate: true } : { accepted: true });
42835
+ }
41486
42836
  const event = parseHookEvent(req.body);
41487
42837
  if (!event) return reply.code(202).send({ ignored: true });
41488
42838
  const r = await intakeAsk({
@@ -41576,9 +42926,9 @@ init_db();
41576
42926
  init_db();
41577
42927
  init_uploads();
41578
42928
  init_src();
41579
- import { createHash as createHash8, randomBytes as randomBytes6 } from "node:crypto";
42929
+ import { createHash as createHash10, randomBytes as randomBytes6 } from "node:crypto";
41580
42930
  function hashAccessKey(key) {
41581
- return createHash8("sha256").update(key).digest("hex");
42931
+ return createHash10("sha256").update(key).digest("hex");
41582
42932
  }
41583
42933
  function generateAccessKey() {
41584
42934
  const key = "hnm_tkt_" + randomBytes6(24).toString("hex");
@@ -41588,13 +42938,13 @@ function generateShareToken() {
41588
42938
  return "hnm_shr_" + randomBytes6(24).toString("hex");
41589
42939
  }
41590
42940
  async function createTicket(input) {
41591
- const { key, hash: hash2 } = generateAccessKey();
42941
+ const { key, hash: hash3 } = generateAccessKey();
41592
42942
  for (let attempt = 0; attempt < 3; attempt++) {
41593
42943
  const max = await prisma.ticket.aggregate({ where: { projectId: input.projectId }, _max: { number: true } });
41594
42944
  const number = (max._max.number ?? 0) + 1;
41595
42945
  try {
41596
42946
  const ticket = await prisma.ticket.create({
41597
- data: { ...input, number, accessKeyHash: hash2, shareToken: generateShareToken(), status: "new" }
42947
+ data: { ...input, number, accessKeyHash: hash3, shareToken: generateShareToken(), status: "new" }
41598
42948
  });
41599
42949
  return { ticket, key };
41600
42950
  } catch (e) {
@@ -41621,6 +42971,7 @@ async function pruneOldTickets(now = Date.now()) {
41621
42971
  // src/services/ticket-intake.ts
41622
42972
  init_db();
41623
42973
  init_notifications2();
42974
+ init_sync_notify();
41624
42975
  init_uploads();
41625
42976
  var TICKET_UPLOAD = { MAX_FILES: 3 };
41626
42977
  async function parseTicketUpload(req) {
@@ -41784,10 +43135,12 @@ async function help_default(app2) {
41784
43135
 
41785
43136
  // src/routes/tickets.ts
41786
43137
  init_db();
43138
+ init_sync_notify();
41787
43139
  init_uploads();
41788
43140
 
41789
43141
  // src/services/ticket-accept.ts
41790
43142
  init_db();
43143
+ init_sync_notify();
41791
43144
  var attachmentData = (t, atts) => {
41792
43145
  if (atts.length === 0) return "Tanpa lampiran.";
41793
43146
  const list2 = atts.map(
@@ -42702,201 +44055,28 @@ async function changelog_default(app2) {
42702
44055
  // src/routes/custom-agents.ts
42703
44056
  init_src();
42704
44057
  init_db();
42705
-
42706
- // src/services/agent-tool-catalog.ts
42707
- init_src();
42708
- import { readFileSync as readFileSync16 } from "node:fs";
42709
- import { homedir as homedir9 } from "node:os";
42710
- import { join as join26 } from "node:path";
42711
- var home = () => process.env.HOME || homedir9();
42712
- var readJson2 = (path) => {
42713
- try {
42714
- return JSON.parse(readFileSync16(path, "utf8"));
42715
- } catch {
42716
- return null;
42717
- }
42718
- };
42719
- var serversOf = (node) => {
42720
- const ms = node?.mcpServers;
42721
- if (!ms || typeof ms !== "object" || Array.isArray(ms)) return [];
42722
- return Object.keys(ms);
42723
- };
42724
- var codexServers = () => {
42725
- let text;
42726
- try {
42727
- text = readFileSync16(join26(home(), ".codex", "config.toml"), "utf8");
42728
- } catch {
42729
- return [];
42730
- }
42731
- const out4 = [];
42732
- for (const m of text.matchAll(/^\s*\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.[^\]]*)?\]/gm)) {
42733
- const name2 = m[1] ?? m[2];
42734
- if (name2) out4.push(name2);
42735
- }
42736
- return out4;
42737
- };
42738
- function mcpServerNames(repoDir) {
42739
- const names = [];
42740
- const claudeJson = readJson2(join26(home(), ".claude.json"));
42741
- names.push(...serversOf(claudeJson));
42742
- if (repoDir) {
42743
- const projects = claudeJson?.projects;
42744
- if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
42745
- names.push(...serversOf(readJson2(join26(repoDir, ".mcp.json"))));
42746
- }
42747
- names.push(...codexServers());
42748
- return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
42749
- }
42750
- function agentToolCatalog(repoDir) {
42751
- return [ALL_TOOLS_ENTRY, ...BUILTIN_AGENT_TOOLS, ...mcpServerNames(repoDir).map(mcpToolEntry)];
42752
- }
42753
- var agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
42754
-
42755
- // src/routes/custom-agents.ts
42756
- init_settings3();
42757
-
42758
- // src/services/builtin-agents.ts
42759
- init_src();
42760
- init_db();
44058
+ init_sync_notify();
44059
+ init_agent_tool_catalog();
42761
44060
  init_settings3();
42762
- init_tombstone();
42763
- import { createHash as createHash9 } from "node:crypto";
42764
- var fingerprint = (name2, description, instructions, tools) => createHash9("sha256").update([name2, description, instructions, [...tools].join(",")].join(" ")).digest("hex").slice(0, 16);
42765
- var builtinFingerprint = (a) => fingerprint(a.name, a.description, a.instructions, a.tools);
42766
- var rowFingerprint = (r) => fingerprint(r.name, r.description, r.instructions, toolsOf(r.tools) ?? []);
42767
- async function seedBuiltinAgents() {
42768
- try {
42769
- const setting = await getSetting();
42770
- const stamps = { ...setting.builtinAgents };
42771
- let changed = false;
42772
- for (const a of BUILTIN_AGENTS) {
42773
- const id = customAgentId(null, a.name);
42774
- const fp = builtinFingerprint(a);
42775
- const row = await prisma.customAgent.findUnique({ where: { id } });
42776
- if (!row) {
42777
- if (await findTombstone("customAgent", id)) continue;
42778
- await prisma.customAgent.create({ data: {
42779
- id,
42780
- projectId: null,
42781
- name: a.name,
42782
- description: a.description,
42783
- instructions: a.instructions,
42784
- tools: [...a.tools],
42785
- model: null,
42786
- mentions: [],
42787
- runtime: null,
42788
- enabled: a.enabledByDefault
42789
- } });
42790
- await notifySynced("customAgent", id);
42791
- stamps[a.name] = fp;
42792
- changed = true;
42793
- continue;
42794
- }
42795
- const stamped = stamps[a.name];
42796
- if (!stamped || stamped === fp) continue;
42797
- if (stamped !== rowFingerprint(row)) continue;
42798
- await prisma.customAgent.update({ where: { id }, data: {
42799
- description: a.description,
42800
- instructions: a.instructions,
42801
- tools: [...a.tools]
42802
- // `enabled` TIDAK di sini. Sengaja.
42803
- } });
42804
- await notifySynced("customAgent", id);
42805
- stamps[a.name] = fp;
42806
- changed = true;
42807
- }
42808
- if (changed) {
42809
- const data = { ...setting, builtinAgents: stamps };
42810
- await prisma.setting.upsert({
42811
- where: { id: 1 },
42812
- update: { data },
42813
- create: { id: 1, data }
42814
- });
42815
- }
42816
- } catch {
42817
- }
42818
- }
42819
-
42820
- // src/services/custom-agents.ts
42821
- init_db();
42822
- init_src();
42823
- init_pty();
42824
- var cache4 = [];
42825
- var repoDirCache = /* @__PURE__ */ new Map();
42826
- var asCustomAgent = (r) => ({
42827
- id: r.id,
42828
- projectId: r.projectId,
42829
- name: r.name,
42830
- description: r.description,
42831
- instructions: r.instructions,
42832
- tools: toolsOf(r.tools),
42833
- model: r.model,
42834
- mentions: mentionsOf(r.mentions),
42835
- runtime: runtimeOf(r.runtime),
42836
- enabled: r.enabled,
42837
- createdAt: "",
42838
- updatedAt: ""
42839
- // tak dipakai lapis ini
42840
- });
42841
- async function loadCustomAgents() {
42842
- try {
42843
- cache4 = await prisma.customAgent.findMany();
42844
- const projects = await prisma.project.findMany({ select: { id: true, repoDir: true } });
42845
- const bindings = await prisma.localBinding.findMany({ select: { projectId: true, repoDir: true } });
42846
- const next = /* @__PURE__ */ new Map();
42847
- for (const p3 of projects) next.set(p3.id, p3.repoDir ?? null);
42848
- for (const b of bindings) next.set(b.projectId, b.repoDir ?? null);
42849
- repoDirCache = next;
42850
- } catch {
42851
- cache4 = [];
42852
- repoDirCache = /* @__PURE__ */ new Map();
42853
- }
42854
- }
42855
- function agentDefsFor(projectId, agent) {
42856
- const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
42857
- const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
42858
- const eff = effectiveAgents(globals, project).filter((a) => a.runtime === null || a.runtime === agent);
42859
- const needsCatalog = eff.some((a) => (a.tools ?? []).includes(ALL_TOOLS));
42860
- const catalogIds = needsCatalog ? agentToolIds(repoDirCache.get(projectId) ?? null) : [];
42861
- return eff.map((a) => ({
42862
- name: a.name,
42863
- description: a.description,
42864
- instructions: a.instructions,
42865
- // Ekspansi terjadi DI SINI, sebelum `resolveTools` di runner: meneruskan `"*"` apa adanya
42866
- // membuat claude membuangnya senyap (agen tanpa alat), sementara menerjemahkannya jadi `null`
42867
- // membuat agen mewarisi SELURUH tool termasuk `Task` — lapis 2 anti-loop lenyap tanpa jejak.
42868
- tools: expandTools(a.tools, catalogIds),
42869
- model: a.model,
42870
- mentions: a.mentions ?? []
42871
- }));
42872
- }
42873
- function validateGraph(rows) {
42874
- const projectScopes = [...new Set(rows.map((r) => r.projectId).filter((p3) => p3 !== null))];
42875
- const globals = rows.filter((r) => r.projectId === null).map(asCustomAgent);
42876
- for (const scope of [null, ...projectScopes]) {
42877
- const project = scope === null ? [] : rows.filter((r) => r.projectId === scope).map(asCustomAgent);
42878
- const nodes = effectiveAgents(globals, project).map((a) => ({ name: a.name, mentions: a.mentions ?? [] }));
42879
- const cycle = detectCycle(nodes);
42880
- if (cycle) return { scope: scope ?? GLOBAL_SCOPE, cycle };
42881
- }
42882
- return null;
42883
- }
42884
- function unknownMentions(row, all) {
42885
- const visible = new Set(
42886
- all.filter((r) => r.projectId === null || row.projectId !== null && r.projectId === row.projectId).map((r) => r.name)
42887
- );
42888
- return mentionsOf(row.mentions).filter((m) => !visible.has(m));
42889
- }
42890
- async function installCustomAgents() {
42891
- await seedBuiltinAgents();
42892
- await loadCustomAgents();
42893
- registerCustomAgentSource((projectId, agent) => agentDefsFor(projectId, agent));
42894
- }
42895
-
42896
- // src/routes/custom-agents.ts
44061
+ init_builtin_agents2();
44062
+ init_custom_agents2();
42897
44063
  var rowsOf = async () => await prisma.customAgent.findMany();
42898
44064
  var stampsOf = async () => (await getSetting()).builtinAgents;
42899
- var view8 = (r, projectId, stamps = {}) => {
44065
+ var availabilityOf = (r, requestedRuntime) => {
44066
+ const configuredRuntime = runtimeOf(r.runtime);
44067
+ if (requestedRuntime && configuredRuntime && configuredRuntime !== requestedRuntime) {
44068
+ return { available: false, availabilityReason: `hanya tersedia untuk runtime ${configuredRuntime}` };
44069
+ }
44070
+ const effectiveRuntime = requestedRuntime ?? configuredRuntime;
44071
+ if (workspacePolicyOf(r.workspacePolicy) === "isolated-worktree" && effectiveRuntime === "codex") {
44072
+ return {
44073
+ available: false,
44074
+ availabilityReason: "isolated-worktree belum tersedia untuk subagent Codex"
44075
+ };
44076
+ }
44077
+ return { available: true };
44078
+ };
44079
+ var view8 = (r, projectId, stamps = {}, requestedRuntime) => {
42900
44080
  const builtin = r.projectId === null && BUILTIN_AGENT_NAMES.includes(r.name);
42901
44081
  return {
42902
44082
  id: r.id,
@@ -42908,12 +44088,18 @@ var view8 = (r, projectId, stamps = {}) => {
42908
44088
  model: r.model,
42909
44089
  mentions: mentionsOf(r.mentions),
42910
44090
  runtime: runtimeOf(r.runtime),
44091
+ activation: activationOf(r.activation),
44092
+ effort: effortOf(r.effort),
44093
+ workspacePolicy: workspacePolicyOf(r.workspacePolicy),
44094
+ maxTurns: maxTurnsOf(r.maxTurns),
44095
+ timeoutSeconds: timeoutSecondsOf(r.timeoutSeconds),
42911
44096
  enabled: r.enabled,
42912
44097
  builtin,
42913
44098
  // Sidik jari yang tak tercatat (baris menyeberang sync dari mesin lain, seed di sini belum
42914
44099
  // pernah menyentuhnya) dibaca sebagai "disunting" — lebih baik menandai berlebih daripada
42915
44100
  // menjanjikan "asli bawaan" untuk isi yang tak bisa kita buktikan.
42916
44101
  builtinEdited: builtin ? stamps[r.name] !== rowFingerprint(r) : false,
44102
+ ...availabilityOf(r, requestedRuntime),
42917
44103
  ...projectId ? { inherited: r.projectId === null } : {}
42918
44104
  };
42919
44105
  };
@@ -42931,6 +44117,11 @@ function modelProblem(model, runtime) {
42931
44117
  const ok2 = modelsForRuntime(runtime).some((m) => m.id === model);
42932
44118
  return ok2 ? null : { error: "model tak dikenal untuk runtime ini", model, runtime };
42933
44119
  }
44120
+ function effortProblem(effort, runtime, model) {
44121
+ if (!effort) return null;
44122
+ const ok2 = effortsForRuntimeModel(runtime, model).includes(effort);
44123
+ return ok2 ? null : { error: "effort tak didukung runtime/model ini", effort, runtime, model };
44124
+ }
42934
44125
  async function custom_agents_default(app2) {
42935
44126
  app2.get("/custom-agents/catalog", async (req) => {
42936
44127
  const projectId = req.query.projectId;
@@ -42940,8 +44131,13 @@ async function custom_agents_default(app2) {
42940
44131
  runtimes: AGENT_RUNTIMES.map((id) => ({ id, label: AGENT_RUNTIME_LABELS[id] }))
42941
44132
  };
42942
44133
  });
42943
- app2.get("/custom-agents", async (req) => {
42944
- const projectId = req.query.projectId;
44134
+ app2.get("/custom-agents", async (req, reply) => {
44135
+ const query2 = req.query;
44136
+ const projectId = query2.projectId;
44137
+ const requestedRuntime = query2.runtime;
44138
+ if (requestedRuntime && !AGENT_RUNTIMES.includes(requestedRuntime)) {
44139
+ return reply.code(400).send({ error: "runtime harus claude atau codex" });
44140
+ }
42945
44141
  const rows = await prisma.customAgent.findMany({
42946
44142
  where: projectId ? { OR: [{ projectId: null }, { projectId }] } : { projectId: null },
42947
44143
  orderBy: { name: "asc" }
@@ -42950,7 +44146,7 @@ async function custom_agents_default(app2) {
42950
44146
  for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
42951
44147
  for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
42952
44148
  const stamps = await stampsOf();
42953
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view8(r, projectId, stamps));
44149
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view8(r, projectId, stamps, requestedRuntime));
42954
44150
  });
42955
44151
  app2.post("/custom-agents", async (req, reply) => {
42956
44152
  const parsed = zCreateCustomAgent.safeParse(req.body);
@@ -42963,6 +44159,8 @@ async function custom_agents_default(app2) {
42963
44159
  if (tp) return reply.code(400).send(tp);
42964
44160
  const mp = modelProblem(p3.model ?? null, p3.runtime ?? null);
42965
44161
  if (mp) return reply.code(400).send(mp);
44162
+ const ep = effortProblem(p3.effort ?? null, p3.runtime ?? null, p3.model ?? null);
44163
+ if (ep) return reply.code(400).send(ep);
42966
44164
  const id = customAgentId(projectId, p3.name);
42967
44165
  if (await prisma.customAgent.findUnique({ where: { id } }))
42968
44166
  return reply.code(409).send({ error: "nama sudah dipakai di scope ini", id });
@@ -42976,6 +44174,11 @@ async function custom_agents_default(app2) {
42976
44174
  model: p3.model ?? null,
42977
44175
  mentions: p3.mentions ?? [],
42978
44176
  runtime: p3.runtime ?? null,
44177
+ activation: p3.activation ?? "always",
44178
+ effort: p3.effort ?? null,
44179
+ workspacePolicy: p3.workspacePolicy ?? "inherit",
44180
+ maxTurns: p3.maxTurns ?? null,
44181
+ timeoutSeconds: p3.timeoutSeconds ?? null,
42979
44182
  enabled: p3.enabled ?? true
42980
44183
  };
42981
44184
  const all = [...await rowsOf(), candidate];
@@ -42993,6 +44196,11 @@ async function custom_agents_default(app2) {
42993
44196
  model: candidate.model,
42994
44197
  mentions: candidate.mentions,
42995
44198
  runtime: candidate.runtime,
44199
+ activation: candidate.activation,
44200
+ effort: candidate.effort,
44201
+ workspacePolicy: candidate.workspacePolicy,
44202
+ maxTurns: candidate.maxTurns,
44203
+ timeoutSeconds: candidate.timeoutSeconds,
42996
44204
  enabled: candidate.enabled
42997
44205
  } });
42998
44206
  await loadCustomAgents();
@@ -43010,6 +44218,12 @@ async function custom_agents_default(app2) {
43010
44218
  if (!existing) return reply.code(404).send({ error: "not found" });
43011
44219
  const before2 = existing;
43012
44220
  const effRuntime = "runtime" in parsed.data ? parsed.data.runtime ?? null : runtimeOf(before2.runtime);
44221
+ const effWorkspacePolicy = "workspacePolicy" in parsed.data ? parsed.data.workspacePolicy ?? "inherit" : workspacePolicyOf(before2.workspacePolicy);
44222
+ if (("runtime" in parsed.data || "workspacePolicy" in parsed.data) && effWorkspacePolicy === "isolated-worktree" && effRuntime !== "claude") {
44223
+ return reply.code(400).send({
44224
+ error: "isolated-worktree hanya tersedia untuk agen ber-runtime Claude Code"
44225
+ });
44226
+ }
43013
44227
  if (parsed.data.tools !== void 0) {
43014
44228
  const tp = toolsProblem(parsed.data.tools, agentToolIds(await repoDirOf2(before2.projectId)));
43015
44229
  if (tp) return reply.code(400).send(tp);
@@ -43021,12 +44235,25 @@ async function custom_agents_default(app2) {
43021
44235
  );
43022
44236
  if (mp) return reply.code(400).send(mp);
43023
44237
  }
44238
+ if (parsed.data.effort !== void 0 || parsed.data.model !== void 0 || "runtime" in parsed.data) {
44239
+ const ep = effortProblem(
44240
+ parsed.data.effort !== void 0 ? parsed.data.effort : effortOf(before2.effort),
44241
+ effRuntime,
44242
+ parsed.data.model !== void 0 ? parsed.data.model : before2.model
44243
+ );
44244
+ if (ep) return reply.code(400).send(ep);
44245
+ }
43024
44246
  const candidate = {
43025
44247
  ...before2,
43026
44248
  ...parsed.data,
43027
44249
  mentions: parsed.data.mentions ?? mentionsOf(before2.mentions),
43028
44250
  tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before2.tools),
43029
- runtime: effRuntime
44251
+ runtime: effRuntime,
44252
+ activation: parsed.data.activation ?? activationOf(before2.activation),
44253
+ effort: parsed.data.effort !== void 0 ? parsed.data.effort : effortOf(before2.effort),
44254
+ workspacePolicy: effWorkspacePolicy,
44255
+ maxTurns: parsed.data.maxTurns !== void 0 ? parsed.data.maxTurns : maxTurnsOf(before2.maxTurns),
44256
+ timeoutSeconds: parsed.data.timeoutSeconds !== void 0 ? parsed.data.timeoutSeconds : timeoutSecondsOf(before2.timeoutSeconds)
43030
44257
  };
43031
44258
  const all = (await rowsOf()).map((r) => r.id === id ? candidate : r);
43032
44259
  const unknown = unknownMentions(candidate, all);
@@ -43040,6 +44267,11 @@ async function custom_agents_default(app2) {
43040
44267
  model: candidate.model,
43041
44268
  mentions: candidate.mentions,
43042
44269
  runtime: candidate.runtime,
44270
+ activation: candidate.activation,
44271
+ effort: candidate.effort,
44272
+ workspacePolicy: candidate.workspacePolicy,
44273
+ maxTurns: candidate.maxTurns,
44274
+ timeoutSeconds: candidate.timeoutSeconds,
43043
44275
  enabled: candidate.enabled
43044
44276
  } });
43045
44277
  await loadCustomAgents();
@@ -43066,9 +44298,39 @@ async function custom_agents_default(app2) {
43066
44298
  });
43067
44299
  }
43068
44300
 
44301
+ // src/routes/custom-agent-metrics.ts
44302
+ init_zod();
44303
+ var zQuery = external_exports.object({
44304
+ projectId: external_exports.string().min(1).optional(),
44305
+ from: external_exports.string().datetime({ offset: true }).optional(),
44306
+ to: external_exports.string().datetime({ offset: true }).optional()
44307
+ });
44308
+ var zPatch = external_exports.object({
44309
+ disposition: external_exports.enum(["accepted", "partial", "rejected", "false-positive"]),
44310
+ note: external_exports.string().max(500).nullable().optional()
44311
+ }).strict();
44312
+ async function custom_agent_metrics_default(app2) {
44313
+ app2.get("/custom-agents/metrics", async (req, reply) => {
44314
+ const parsed = zQuery.safeParse(req.query);
44315
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message });
44316
+ const from = parsed.data.from ? new Date(parsed.data.from) : void 0;
44317
+ const to = parsed.data.to ? new Date(parsed.data.to) : void 0;
44318
+ if (from && to && from > to) return reply.code(400).send({ error: "from harus sebelum to" });
44319
+ return agentMetrics({ projectId: parsed.data.projectId, from, to });
44320
+ });
44321
+ app2.patch("/custom-agents/invocations/:id", async (req, reply) => {
44322
+ const body = zPatch.safeParse(req.body);
44323
+ if (!body.success) return reply.code(400).send({ error: body.error.issues[0]?.message });
44324
+ const id = String(req.params.id ?? "");
44325
+ const view13 = await updateAgentInvocationDisposition(id, body.data.disposition, body.data.note);
44326
+ return view13 ?? reply.code(404).send({ error: "invocation tidak ditemukan" });
44327
+ });
44328
+ }
44329
+
43069
44330
  // src/routes/members.ts
43070
44331
  init_src();
43071
44332
  init_db();
44333
+ init_sync_notify();
43072
44334
  var view9 = (m) => ({
43073
44335
  id: m.id,
43074
44336
  name: m.name,
@@ -43125,10 +44387,12 @@ async function members_default(app2) {
43125
44387
  // src/routes/tasks.ts
43126
44388
  init_src();
43127
44389
  init_db();
44390
+ init_sync_notify();
43128
44391
 
43129
44392
  // src/services/task-escalate.ts
43130
44393
  init_src();
43131
44394
  init_db();
44395
+ init_sync_notify();
43132
44396
  var day = (d) => d ? d.toISOString().slice(0, 10) : null;
43133
44397
  function contextOf(task, member, backlink) {
43134
44398
  const lines2 = [
@@ -43409,7 +44673,7 @@ function issuesFromRest(raw) {
43409
44673
  }
43410
44674
  var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
43411
44675
  var API2 = "https://api.github.com";
43412
- var defaultRunGh = (args, env) => new Promise((resolve21, reject2) => {
44676
+ var defaultRunGh = (args, env) => new Promise((resolve22, reject2) => {
43413
44677
  execFile16(
43414
44678
  args[0],
43415
44679
  args.slice(1),
@@ -43417,7 +44681,7 @@ var defaultRunGh = (args, env) => new Promise((resolve21, reject2) => {
43417
44681
  (err, stdout, stderr) => {
43418
44682
  const e = err;
43419
44683
  if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject2(e);
43420
- resolve21({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
44684
+ resolve22({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
43421
44685
  }
43422
44686
  );
43423
44687
  });
@@ -43512,6 +44776,7 @@ async function fetchIssues(repo, opts, deps = {}) {
43512
44776
  }
43513
44777
 
43514
44778
  // src/services/github-issues.ts
44779
+ init_sync_notify();
43515
44780
  var issueRowId = (projectId, slug, number) => `${projectId}:${slug}#${number}`;
43516
44781
  async function pullIssues(projectId, opts = {}, deps = {}) {
43517
44782
  const resolved = await resolveGithubRepo(projectId);
@@ -43560,6 +44825,7 @@ async function pullIssues(projectId, opts = {}, deps = {}) {
43560
44825
  // src/services/github-accept.ts
43561
44826
  init_src();
43562
44827
  init_db();
44828
+ init_sync_notify();
43563
44829
  var backlinkOf = (i) => `Dari GitHub issue ${i.repoSlug}#${i.number} (${i.url}).`;
43564
44830
  async function acceptGithubIssue(issue2, opts) {
43565
44831
  if (issue2.specId) {
@@ -43616,6 +44882,7 @@ ${backlink}`;
43616
44882
  }
43617
44883
 
43618
44884
  // src/routes/github-issues.ts
44885
+ init_sync_notify();
43619
44886
  var zPull = external_exports.object({
43620
44887
  state: external_exports.enum(["open", "all"]).optional(),
43621
44888
  limit: external_exports.number().int().min(1).max(1e3).optional()
@@ -44452,10 +45719,10 @@ function sanitizeClientText(text) {
44452
45719
  return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").slice(0, MAX_PESAN);
44453
45720
  }
44454
45721
  function wrapClientMessage(text, nonce) {
44455
- const open4 = `<pesan-klien-${nonce}>`;
45722
+ const open5 = `<pesan-klien-${nonce}>`;
44456
45723
  const close = `</pesan-klien-${nonce}>`;
44457
45724
  const jinak = sanitizeClientText(text).replaceAll("</pesan-klien", "<\u200B/pesan-klien").replaceAll("<pesan-klien", "<\u200Bpesan-klien");
44458
- return `${open4}
45725
+ return `${open5}
44459
45726
  ${jinak}
44460
45727
  ${close}`;
44461
45728
  }
@@ -44554,9 +45821,9 @@ ${baru}`;
44554
45821
  // src/services/portal-chat/workspace.ts
44555
45822
  init_src();
44556
45823
  init_db();
44557
- import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "node:fs";
44558
- import { tmpdir as tmpdir5 } from "node:os";
44559
- import { join as join27 } from "node:path";
45824
+ import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
45825
+ import { tmpdir as tmpdir6 } from "node:os";
45826
+ import { join as join30 } from "node:path";
44560
45827
  var STAGE_LABEL = {
44561
45828
  brainstorming: "Dirumuskan",
44562
45829
  objective: "Dirumuskan",
@@ -44611,7 +45878,7 @@ ${c.body}
44611
45878
  ${baris.join("\n")}`;
44612
45879
  }
44613
45880
  async function buildChatWorkspace(projectId) {
44614
- const dir2 = mkdtempSync2(join27(tmpdir5(), "hanoman-portal-chat-"));
45881
+ const dir2 = mkdtempSync2(join30(tmpdir6(), "hanoman-portal-chat-"));
44615
45882
  try {
44616
45883
  const project = await prisma.project.findUnique({
44617
45884
  where: { id: projectId },
@@ -44632,7 +45899,7 @@ async function buildChatWorkspace(projectId) {
44632
45899
  });
44633
45900
  const files = [];
44634
45901
  const tulis = (rel, isi) => {
44635
- writeFileSync7(join27(dir2, rel), isi, { mode: 384 });
45902
+ writeFileSync9(join30(dir2, rel), isi, { mode: 384 });
44636
45903
  files.push(rel);
44637
45904
  };
44638
45905
  tulis("project.md", renderProjectDoc(project));
@@ -44641,16 +45908,16 @@ async function buildChatWorkspace(projectId) {
44641
45908
  tulis("catatan-rilis.md", renderChangelogDoc(changelogs));
44642
45909
  const prds = await listPrds(projectId);
44643
45910
  if (prds.length) {
44644
- mkdirSync12(join27(dir2, "dokumen"), { mode: 448 });
45911
+ mkdirSync12(join30(dir2, "dokumen"), { mode: 448 });
44645
45912
  for (const prd of prds) {
44646
45913
  const isi = await readPrd(projectId, prd.path);
44647
45914
  if (!isi) continue;
44648
- tulis(join27("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
45915
+ tulis(join30("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
44649
45916
  }
44650
45917
  }
44651
- return { dir: dir2, files, cleanup: () => rmSync7(dir2, { recursive: true, force: true }) };
45918
+ return { dir: dir2, files, cleanup: () => rmSync8(dir2, { recursive: true, force: true }) };
44652
45919
  } catch (error) {
44653
- rmSync7(dir2, { recursive: true, force: true });
45920
+ rmSync8(dir2, { recursive: true, force: true });
44654
45921
  throw error;
44655
45922
  }
44656
45923
  }
@@ -44684,7 +45951,7 @@ function portalChatArgv(o) {
44684
45951
  o.prompt
44685
45952
  ];
44686
45953
  }
44687
- var shellQuote2 = (v) => `'${v.replace(/'/g, `'"'"'`)}'`;
45954
+ var shellQuote4 = (v) => `'${v.replace(/'/g, `'"'"'`)}'`;
44688
45955
  function portalChatProcess(o, env = process.env) {
44689
45956
  const file = effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
44690
45957
  const args = portalChatArgv(o);
@@ -44694,7 +45961,7 @@ function portalChatProcess(o, env = process.env) {
44694
45961
  throw new Error("chat portal menolak jalan: sandbox sesi wajib saat hardening menyala");
44695
45962
  return { file, args, cwd: o.workspace };
44696
45963
  }
44697
- const command = [file, ...args].map(shellQuote2).join(" ");
45964
+ const command = [file, ...args].map(shellQuote4).join(" ");
44698
45965
  const sandbox = sandboxArgvFromEnv({
44699
45966
  command,
44700
45967
  worktree: o.workspace,
@@ -44728,7 +45995,7 @@ function chatFailureReason(err, stdout, stderr, timeoutMs) {
44728
45995
  return `chat portal gagal (${bagaimana}): ${detail}`;
44729
45996
  }
44730
45997
  function runProcess(p3, timeoutMs) {
44731
- return new Promise((resolve21, reject2) => {
45998
+ return new Promise((resolve22, reject2) => {
44732
45999
  const child = execFile17(p3.file, p3.args, {
44733
46000
  cwd: p3.cwd,
44734
46001
  timeout: timeoutMs,
@@ -44740,7 +46007,7 @@ function runProcess(p3, timeoutMs) {
44740
46007
  stdout,
44741
46008
  stderr,
44742
46009
  timeoutMs
44743
- ))) : resolve21(stdout));
46010
+ ))) : resolve22(stdout));
44744
46011
  child.stdin?.end();
44745
46012
  });
44746
46013
  }
@@ -44964,7 +46231,7 @@ init_zod();
44964
46231
  init_db();
44965
46232
  init_settings3();
44966
46233
  var zPrd = external_exports.object({ slug: external_exports.string().regex(/^[a-z0-9]([a-z0-9-]{0,60}[a-z0-9])?$/) });
44967
- var zQuery = external_exports.object({ project: external_exports.string().min(1) });
46234
+ var zQuery2 = external_exports.object({ project: external_exports.string().min(1) });
44968
46235
  var sessionRow = (s2) => ({
44969
46236
  id: s2.id,
44970
46237
  projectId: s2.projectId,
@@ -44992,7 +46259,7 @@ var messageRow = (m) => ({
44992
46259
  });
44993
46260
  async function portal_chat_admin_default(app2) {
44994
46261
  app2.get("/portal-chat/sessions", async (req, reply) => {
44995
- const parsed = zQuery.safeParse(req.query);
46262
+ const parsed = zQuery2.safeParse(req.query);
44996
46263
  if (!parsed.success) return reply.code(400).send({ error: "project wajib" });
44997
46264
  const { page, limit } = req.query;
44998
46265
  const rows = await prisma.portalChatSession.findMany({
@@ -45036,7 +46303,7 @@ async function portal_chat_admin_default(app2) {
45036
46303
  return reply.code(201).send({ path });
45037
46304
  });
45038
46305
  app2.get("/portal-chat/export", async (req, reply) => {
45039
- const parsed = zQuery.safeParse(req.query);
46306
+ const parsed = zQuery2.safeParse(req.query);
45040
46307
  if (!parsed.success) return reply.code(400).send({ error: "project wajib" });
45041
46308
  const { from, to } = req.query;
45042
46309
  const rows = await prisma.portalChatSession.findMany({
@@ -45156,15 +46423,15 @@ init_src();
45156
46423
  init_db();
45157
46424
 
45158
46425
  // src/services/bootstrap.ts
45159
- import { createHash as createHash10, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
46426
+ import { createHash as createHash11, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
45160
46427
  import { chmod, lstat as lstat3, mkdir as mkdir6, readFile as readFile5, unlink as unlink5, writeFile as writeFile5 } from "node:fs/promises";
45161
- import { join as join28 } from "node:path";
46428
+ import { join as join31 } from "node:path";
45162
46429
  var TTL_MS5 = 15 * 6e4;
45163
46430
  var SETUP_TOKEN_FILE = "setup.token";
45164
46431
  var BootstrapError = class extends Error {
45165
46432
  code = "BOOTSTRAP_PROOF";
45166
46433
  };
45167
- var tokenPath = (home3) => join28(home3, SETUP_TOKEN_FILE);
46434
+ var tokenPath = (home3) => join31(home3, SETUP_TOKEN_FILE);
45168
46435
  async function readStored(home3) {
45169
46436
  try {
45170
46437
  const info = await lstat3(tokenPath(home3));
@@ -45209,8 +46476,8 @@ ${new Date(expiresAt).toISOString()}
45209
46476
  }
45210
46477
  async function verifySetupToken(candidate, home3, now = Date.now()) {
45211
46478
  const stored = await readStored(home3);
45212
- const got = createHash10("sha256").update(candidate).digest();
45213
- const want = createHash10("sha256").update(stored.token).digest();
46479
+ const got = createHash11("sha256").update(candidate).digest();
46480
+ const want = createHash11("sha256").update(stored.token).digest();
45214
46481
  if (stored.expiresAt <= now || !timingSafeEqual4(got, want)) throw new BootstrapError("invalid setup proof");
45215
46482
  }
45216
46483
  async function consumeSetupToken(home3) {
@@ -45489,7 +46756,10 @@ function capabilityForRoute(method, path) {
45489
46756
  return rw("settings");
45490
46757
  }
45491
46758
  if (top === "lead") return rw("lead");
45492
- if (top === "custom-agents") return rw("agents");
46759
+ if (top === "custom-agents") {
46760
+ if (seg[1] === "metrics" || seg[1] === "invocations") return "COOKIE_ONLY";
46761
+ return rw("agents");
46762
+ }
45493
46763
  if (top === "telegram") {
45494
46764
  const sub = seg[1] ?? "";
45495
46765
  if (sub === "settings" || sub === "test" || sub === "credentials") return "COOKIE_ONLY";
@@ -45776,6 +47046,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
45776
47046
  await api.register(methods);
45777
47047
  await api.register(lead_default);
45778
47048
  await api.register(custom_agents_default);
47049
+ await api.register(custom_agent_metrics_default);
45779
47050
  await api.register(githubIssues);
45780
47051
  await api.register(telegramRoutes);
45781
47052
  await api.register(webhooks_default);
@@ -45976,6 +47247,7 @@ init_db();
45976
47247
  init_src2();
45977
47248
  init_notifications2();
45978
47249
  init_stage_machine();
47250
+ init_sync_notify();
45979
47251
  init_pty();
45980
47252
  init_session_phases();
45981
47253
  var FAIL_REASON = "sesi berakhir sebelum mencapai done (gagal/limit)";
@@ -46142,6 +47414,7 @@ function registerTriaseSource() {
46142
47414
  }
46143
47415
 
46144
47416
  // src/server.ts
47417
+ init_custom_agents2();
46145
47418
  init_pty();
46146
47419
  init_bootstrap();
46147
47420
 
@@ -46151,7 +47424,7 @@ init_tap();
46151
47424
  // src/services/webhooks/emit.ts
46152
47425
  init_src();
46153
47426
  init_db();
46154
- import { randomUUID as randomUUID10 } from "node:crypto";
47427
+ import { randomUUID as randomUUID11 } from "node:crypto";
46155
47428
  function skipped(def2, row) {
46156
47429
  if (!def2.skipWhen || !row) return false;
46157
47430
  return row[def2.skipWhen.field] === def2.skipWhen.equals;
@@ -46217,7 +47490,7 @@ async function emitWebhook(i) {
46217
47490
  const targets = matchingEndpoints(type, projectId);
46218
47491
  if (!targets.length) return;
46219
47492
  const name2 = projectId ? (await prisma.project.findUnique({ where: { id: projectId }, select: { name: true } }))?.name ?? null : null;
46220
- const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID10().replace(/-/g, "")}`);
47493
+ const env = buildEnvelope(i, name2, (/* @__PURE__ */ new Date()).toISOString(), `evt_${randomUUID11().replace(/-/g, "")}`);
46221
47494
  if (!env) return;
46222
47495
  await enqueueEnvelope(env, targets);
46223
47496
  } catch (e) {
@@ -46530,13 +47803,13 @@ init_src2();
46530
47803
 
46531
47804
  // src/services/secure-home.ts
46532
47805
  import { chmod as chmod2, lstat as lstat4, mkdir as mkdir7 } from "node:fs/promises";
46533
- import { isAbsolute as isAbsolute7, resolve as resolve20 } from "node:path";
47806
+ import { isAbsolute as isAbsolute8, resolve as resolve21 } from "node:path";
46534
47807
  var HomePermissionError = class extends Error {
46535
47808
  code = "HOME_SYMLINK";
46536
47809
  };
46537
47810
  async function assertNoSymlink(path, allowMissing) {
46538
47811
  try {
46539
- const info = await lstat4(resolve20(path));
47812
+ const info = await lstat4(resolve21(path));
46540
47813
  if (info.isSymbolicLink()) throw new HomePermissionError(`symlink ditolak: ${path}`);
46541
47814
  } catch (error) {
46542
47815
  if (error.code === "ENOENT" && allowMissing) return;
@@ -46544,7 +47817,7 @@ async function assertNoSymlink(path, allowMissing) {
46544
47817
  }
46545
47818
  }
46546
47819
  async function secureHanomanHome(opts) {
46547
- if (!isAbsolute7(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
47820
+ if (!isAbsolute8(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
46548
47821
  await assertNoSymlink(opts.home, true);
46549
47822
  await mkdir7(opts.home, { recursive: true, mode: 448 });
46550
47823
  await assertNoSymlink(opts.home, false);
@@ -46734,10 +48007,106 @@ function startRetentionSweep() {
46734
48007
 
46735
48008
  // src/server.ts
46736
48009
  init_uploads();
48010
+
48011
+ // src/services/session-event-relay.ts
48012
+ init_session_event_token();
48013
+ init_session_event_spool();
48014
+ import { constants as constants3 } from "node:fs";
48015
+ import { mkdir as mkdir8, open as open4, readdir as readdir7, rm as rm6 } from "node:fs/promises";
48016
+ import { join as join32 } from "node:path";
48017
+ var MAX_EVENT_BYTES = 1e6;
48018
+ var MAX_FILES_PER_DRAIN = 1e3;
48019
+ var SESSION_ID_RE = /^[a-z0-9_-]+$/;
48020
+ async function drainSessionEventSpool(app2, root = sessionEventSpoolRoot()) {
48021
+ await mkdir8(root, { recursive: true, mode: 448 });
48022
+ let delivered = 0;
48023
+ let examined = 0;
48024
+ let readBuffer;
48025
+ for (const session of await readdir7(root, { withFileTypes: true })) {
48026
+ if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
48027
+ const dir2 = join32(root, session.name);
48028
+ let entries3;
48029
+ try {
48030
+ entries3 = await readdir7(dir2, { withFileTypes: true });
48031
+ } catch {
48032
+ continue;
48033
+ }
48034
+ for (const entry of entries3) {
48035
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
48036
+ if (++examined > MAX_FILES_PER_DRAIN) return delivered;
48037
+ const path = join32(dir2, entry.name);
48038
+ let payload;
48039
+ let handle;
48040
+ try {
48041
+ handle = await open4(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
48042
+ const stat4 = await handle.stat();
48043
+ if (!stat4.isFile() || stat4.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
48044
+ readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
48045
+ const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
48046
+ if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
48047
+ const parsed = JSON.parse(readBuffer.subarray(0, bytesRead).toString("utf8"));
48048
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
48049
+ throw new Error("payload event bukan object");
48050
+ }
48051
+ payload = parsed;
48052
+ } catch {
48053
+ await rm6(path, { force: true }).catch(() => {
48054
+ });
48055
+ continue;
48056
+ } finally {
48057
+ await handle?.close().catch(() => {
48058
+ });
48059
+ }
48060
+ try {
48061
+ const response = await app2.inject({
48062
+ method: "POST",
48063
+ url: "/api/session-events",
48064
+ headers: {
48065
+ authorization: `Bearer ${sessionEventToken(session.name)}`,
48066
+ "x-hanoman-session": session.name
48067
+ },
48068
+ payload
48069
+ });
48070
+ if (response.statusCode === 429 || response.statusCode >= 500) continue;
48071
+ await rm6(path, { force: true }).catch(() => {
48072
+ });
48073
+ if (response.statusCode >= 200 && response.statusCode < 300) delivered++;
48074
+ } catch {
48075
+ }
48076
+ }
48077
+ }
48078
+ return delivered;
48079
+ }
48080
+ function startSessionEventRelay(app2, options2 = {}) {
48081
+ const root = options2.root ?? sessionEventSpoolRoot();
48082
+ let running = false;
48083
+ const tick6 = async () => {
48084
+ if (running) return;
48085
+ running = true;
48086
+ try {
48087
+ await drainSessionEventSpool({ inject: async (request) => app2.inject(request) }, root);
48088
+ } catch (error) {
48089
+ console.error("session event relay gagal:", error);
48090
+ } finally {
48091
+ running = false;
48092
+ }
48093
+ };
48094
+ const timer9 = setInterval(() => {
48095
+ void tick6();
48096
+ }, options2.intervalMs ?? 250);
48097
+ timer9.unref();
48098
+ app2.addHook("onClose", async () => {
48099
+ clearInterval(timer9);
48100
+ });
48101
+ void tick6();
48102
+ }
48103
+
48104
+ // src/server.ts
46737
48105
  var app = buildApp();
46738
48106
  var port = Number(process.env.PORT ?? 8787);
46739
48107
  var host = process.env.HOST ?? "127.0.0.1";
46740
48108
  assertRuntimeBoundary(process.env, { uid: process.getuid?.(), host });
48109
+ startSessionEventRelay(app);
46741
48110
  process.on("unhandledRejection", (err) => console.error("unhandledRejection:", err));
46742
48111
  process.on("uncaughtException", (err) => console.error("uncaughtException:", err));
46743
48112
  async function shutdown(sig) {
@@ -46766,9 +48135,19 @@ var bootstrapReady = secureHanomanHome({
46766
48135
  const proof = await ensureSetupToken(resolveHome());
46767
48136
  console.log(`setup admin memerlukan token di ${proof.path}; kedaluwarsa ${new Date(proof.expiresAt).toISOString()}`);
46768
48137
  });
46769
- bootstrapReady.then(() => app.listen({ port, host })).then(async () => {
46770
- console.log(`hanoman api ${host}:${port}`);
48138
+ bootstrapReady.then(async () => {
48139
+ try {
48140
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
48141
+ const { applyConfigOnBoot: applyConfigOnBoot2 } = await Promise.resolve().then(() => (init_config_apply(), config_apply_exports));
48142
+ await loadConfig2();
48143
+ await applyConfigOnBoot2();
48144
+ } catch (e) {
48145
+ console.error("config runtime gagal dimuat \u2014 memakai env/default:", e);
48146
+ }
46771
48147
  await installCustomAgents();
48148
+ return app.listen({ port, host });
48149
+ }).then(async () => {
48150
+ console.log(`hanoman api ${host}:${port}`);
46772
48151
  await installWebhooks();
46773
48152
  installSessionHistory();
46774
48153
  try {
@@ -46776,17 +48155,12 @@ bootstrapReady.then(() => app.listen({ port, host })).then(async () => {
46776
48155
  void reconcileHistory(liveIds).then((n2) => {
46777
48156
  if (n2) console.log(`riwayat sesi: ${n2} baris berjalan direkonsiliasi`);
46778
48157
  }).catch((e) => console.error("rekonsiliasi riwayat sesi:", e));
48158
+ void reconcileAgentInvocations(liveIds).then((n2) => {
48159
+ if (n2) console.log(`custom agent: ${n2} invocation ditandai abandoned`);
48160
+ }).catch((e) => console.error("rekonsiliasi invocation custom agent:", e));
46779
48161
  } catch (e) {
46780
48162
  console.error("rekonsiliasi riwayat sesi dilewati \u2014 tmux tak terbaca:", e);
46781
48163
  }
46782
- try {
46783
- const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
46784
- const { applyConfigOnBoot: applyConfigOnBoot2 } = await Promise.resolve().then(() => (init_config_apply(), config_apply_exports));
46785
- await loadConfig2();
46786
- await applyConfigOnBoot2();
46787
- } catch (e) {
46788
- console.error("config runtime gagal dimuat \u2014 memakai env/default:", e);
46789
- }
46790
48164
  const boundPort = app.server.address().port;
46791
48165
  await installTelegramGateway(app, { apiBase: `http://127.0.0.1:${boundPort}` });
46792
48166
  startVpsMonitor();