hanoman 0.1.28 → 0.1.30

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
@@ -6091,6 +6091,9 @@ var init_api = __esm({
6091
6091
  // SPEC-233 · drop commit isolasi
6092
6092
  fsBrowse: (path) => `${API}/fs/browse${path ? `?path=${encodeURIComponent(path)}` : ""}`,
6093
6093
  terminalSessions: `${API}/terminal/sessions`,
6094
+ // SPEC-742 · ADR-0116 · pembersihan worktree yang masih tertunda sesudah sesi ditutup. Di bawah
6095
+ // prefix /terminal supaya ikut capability `sessions` yang sudah ada (cermin sessionHistory).
6096
+ terminalCleanups: `${API}/terminal/cleanups`,
6094
6097
  terminalSession: (id) => `${API}/terminal/sessions/${id}`,
6095
6098
  terminalSteer: (id) => `${API}/terminal/sessions/${id}/steer`,
6096
6099
  terminalInterrupt: (id) => `${API}/terminal/sessions/${id}/interrupt`,
@@ -8235,17 +8238,30 @@ ${lines2.join("\n")}` : "";
8235
8238
 
8236
8239
  // ../runner/src/git.ts
8237
8240
  import { spawnSync } from "node:child_process";
8238
- import { rmSync, mkdirSync, realpathSync } from "node:fs";
8239
- import { isAbsolute, resolve as resolve4 } from "node:path";
8241
+ import { rmSync, mkdirSync, realpathSync, existsSync as existsSync2, renameSync } from "node:fs";
8242
+ import { basename, isAbsolute, resolve as resolve4, sep } from "node:path";
8243
+ function trashWorktree(repo, path) {
8244
+ const target2 = isAbsolute(path) ? resolve4(path) : resolve4(repo, path);
8245
+ if (target2 === resolve4(repo)) {
8246
+ throw new Error(`trashWorktree menolak memindahkan repo itu sendiri: ${target2}`);
8247
+ }
8248
+ const trash = resolve4(repo, ".worktrees", ".trash");
8249
+ if (target2 === trash || target2.startsWith(trash + sep) || !existsSync2(target2)) return null;
8250
+ mkdirSync(trash, { recursive: true });
8251
+ const dest = resolve4(trash, `${basename(target2)}.${Date.now().toString(36)}-${(trashSeq++).toString(36)}`);
8252
+ renameSync(target2, dest);
8253
+ return dest;
8254
+ }
8240
8255
  function git(cwd, args) {
8241
8256
  const r = spawnSync("git", args, { cwd, encoding: "utf8" });
8242
8257
  if (r.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${r.stderr || r.stdout || r.error?.message || "gagal spawn"}`);
8243
8258
  return r.stdout;
8244
8259
  }
8245
- var tryGit, resolveCommit, samePath, realGit;
8260
+ var trashSeq, tryGit, resolveCommit, samePath, realGit;
8246
8261
  var init_git = __esm({
8247
8262
  "../runner/src/git.ts"() {
8248
8263
  "use strict";
8264
+ trashSeq = 0;
8249
8265
  tryGit = (cwd, args) => {
8250
8266
  spawnSync("git", args, { cwd, encoding: "utf8" });
8251
8267
  };
@@ -8269,9 +8285,13 @@ var init_git = __esm({
8269
8285
  // menolak meng-checkout branch yang sudah dipakai). Agen sendiri yang mem-push HEAD ke
8270
8286
  // branchTo saat pekerjaannya selesai (SPEC-162).
8271
8287
  addWorktree: (repo, path, branchFrom) => {
8272
- tryGit(repo, ["worktree", "remove", "--force", path]);
8288
+ try {
8289
+ trashWorktree(repo, path);
8290
+ } catch {
8291
+ tryGit(repo, ["worktree", "remove", "--force", path]);
8292
+ rmSync(isAbsolute(path) ? path : resolve4(repo, path), { recursive: true, force: true });
8293
+ }
8273
8294
  tryGit(repo, ["worktree", "prune"]);
8274
- rmSync(isAbsolute(path) ? path : resolve4(repo, path), { recursive: true, force: true });
8275
8295
  const base2 = resolveCommit(repo, branchFrom);
8276
8296
  git(repo, ["worktree", "add", "--detach", path, base2]);
8277
8297
  return base2;
@@ -8289,6 +8309,7 @@ var init_git = __esm({
8289
8309
  tryGit(repo, ["worktree", "prune"]);
8290
8310
  rmSync(target2, { recursive: true, force: true });
8291
8311
  },
8312
+ trashWorktree,
8292
8313
  // Dibaca di worktree sesi (bukan repo utama) tepat sebelum removeWorktree: HEAD-nya =
8293
8314
  // ujung range diff review sesudah item selesai (SPEC-176, ADR-0030).
8294
8315
  headSha: (worktree) => git(worktree, ["rev-parse", "HEAD"]).trim(),
@@ -8657,8 +8678,8 @@ var init_paths = __esm({
8657
8678
 
8658
8679
  // ../runner/src/skills.ts
8659
8680
  import { homedir as homedir2 } from "node:os";
8660
- import { basename, dirname as dirname3, join as join2, resolve as resolve6 } from "node:path";
8661
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
8681
+ import { basename as basename2, dirname as dirname3, join as join2, resolve as resolve6 } from "node:path";
8682
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "node:fs";
8662
8683
  function agentSkillHome(agent, env = process.env, osHome = homedir2()) {
8663
8684
  const own = (agent === "codex" ? env.HANOMAN_CODEX_HOME : env.HANOMAN_CLAUDE_HOME)?.trim();
8664
8685
  if (own) return own;
@@ -8686,9 +8707,9 @@ function readJson(file) {
8686
8707
  }
8687
8708
  }
8688
8709
  function isSkillDir(dir2) {
8689
- return existsSync2(join2(dir2, "SKILL.md"));
8710
+ return existsSync3(join2(dir2, "SKILL.md"));
8690
8711
  }
8691
- function asSkill(dir2, pkg, name2 = basename(dir2)) {
8712
+ function asSkill(dir2, pkg, name2 = basename2(dir2)) {
8692
8713
  return { id: pkg ? `${pkg}:${name2}` : name2, name: name2, pkg, dir: dir2 };
8693
8714
  }
8694
8715
  function skillsUnder(dir2, pkg, depth = 2) {
@@ -8709,7 +8730,7 @@ function manifestSkills(installPath, pkg) {
8709
8730
  for (const rel of declared) {
8710
8731
  if (typeof rel !== "string" || !rel) continue;
8711
8732
  const p = resolve6(installPath, rel);
8712
- const dir2 = basename(p) === "SKILL.md" ? dirname3(p) : p;
8733
+ const dir2 = basename2(p) === "SKILL.md" ? dirname3(p) : p;
8713
8734
  if (isSkillDir(dir2)) out3.push(asSkill(dir2, pkg));
8714
8735
  }
8715
8736
  if (out3.length) return out3;
@@ -9429,7 +9450,7 @@ var init_tui_dialog = __esm({
9429
9450
  });
9430
9451
 
9431
9452
  // src/services/secret-box.ts
9432
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
9453
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
9433
9454
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
9434
9455
  import { dirname as dirname5, join as join3 } from "node:path";
9435
9456
  function isEncrypted(value) {
@@ -9475,7 +9496,7 @@ function secretKey() {
9475
9496
  return key2;
9476
9497
  }
9477
9498
  const path = secretKeyPath();
9478
- if (existsSync3(path)) {
9499
+ if (existsSync4(path)) {
9479
9500
  const key2 = Buffer.from(readFileSync4(path, "utf8").trim(), "base64url");
9480
9501
  if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path} rusak \u2014 panjangnya bukan 32 byte`);
9481
9502
  cached = key2;
@@ -11227,7 +11248,7 @@ var init_gateway = __esm({
11227
11248
  const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
11228
11249
  updateTelegramRuntimeStatus({ running: false, readiness: "error", lastError: message });
11229
11250
  if (error instanceof TelegramApiError && error.code === 409) return;
11230
- await new Promise((resolve16) => setTimeout(resolve16, 1e3));
11251
+ await new Promise((resolve17) => setTimeout(resolve17, 1e3));
11231
11252
  }
11232
11253
  }
11233
11254
  }
@@ -11590,21 +11611,21 @@ var init_store = __esm({
11590
11611
  * di 4 detik selamanya.
11591
11612
  */
11592
11613
  async chatsAwaitingReply(since) {
11593
- const pending = await this.db.telegramUpdate.findMany({
11614
+ const pending2 = await this.db.telegramUpdate.findMany({
11594
11615
  where: { state: "dispatched", chatId: { not: null }, dispatchedAt: { gte: since } },
11595
11616
  select: { updateId: true, chatId: true },
11596
11617
  orderBy: { updateId: "asc" }
11597
11618
  });
11598
- if (!pending.length) return [];
11619
+ if (!pending2.length) return [];
11599
11620
  const answered = new Set((await this.db.telegramOutbox.findMany({
11600
11621
  where: {
11601
- updateId: { in: pending.map((row) => row.updateId) },
11622
+ updateId: { in: pending2.map((row) => row.updateId) },
11602
11623
  kind: { in: [...TELEGRAM_FINAL_REPLY_KINDS] }
11603
11624
  },
11604
11625
  select: { updateId: true }
11605
11626
  })).map((row) => row.updateId));
11606
11627
  const chats = [];
11607
- for (const row of pending) {
11628
+ for (const row of pending2) {
11608
11629
  if (answered.has(row.updateId) || !row.chatId || chats.includes(row.chatId)) continue;
11609
11630
  chats.push(row.chatId);
11610
11631
  }
@@ -15731,7 +15752,7 @@ async function syncOnce(transport) {
15731
15752
  let pulled = 0, pushed = 0, conflicts = 0;
15732
15753
  const cursor = await getCursor();
15733
15754
  const outbox = await listOutbox();
15734
- const pending = new Set(outbox.map((o) => `${o.entity}:${o.recordId}`));
15755
+ const pending2 = new Set(outbox.map((o) => `${o.entity}:${o.recordId}`));
15735
15756
  const conflicted = /* @__PURE__ */ new Set();
15736
15757
  const markConflict = async (entity, recordId, local, server) => {
15737
15758
  await recordConflict(entity, recordId, local, server);
@@ -15746,7 +15767,7 @@ async function syncOnce(transport) {
15746
15767
  const deferred = [];
15747
15768
  for (const rec of records) {
15748
15769
  if (!isEntity(rec.entity)) continue;
15749
- if (pending.has(`${rec.entity}:${rec.recordId}`)) {
15770
+ if (pending2.has(`${rec.entity}:${rec.recordId}`)) {
15750
15771
  const local = await snapshot(rec.entity, rec.recordId);
15751
15772
  if (local && JSON.stringify(local.data) !== JSON.stringify(rec.data)) {
15752
15773
  await markConflict(
@@ -15926,16 +15947,16 @@ async function startSyncClient(base2, token, tickMs2) {
15926
15947
  await tick6();
15927
15948
  void connectWs();
15928
15949
  const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
15929
- timer2 = setInterval(() => {
15950
+ timer3 = setInterval(() => {
15930
15951
  void tick6();
15931
15952
  }, ms);
15932
- timer2.unref?.();
15953
+ timer3.unref?.();
15933
15954
  }
15934
15955
  function stopSyncClient() {
15935
15956
  started = false;
15936
- if (timer2) {
15937
- clearInterval(timer2);
15938
- timer2 = void 0;
15957
+ if (timer3) {
15958
+ clearInterval(timer3);
15959
+ timer3 = void 0;
15939
15960
  }
15940
15961
  try {
15941
15962
  ws?.close();
@@ -15949,7 +15970,7 @@ async function applySyncConfig() {
15949
15970
  const token = effectiveStr("SYNC_DEVICE_TOKEN");
15950
15971
  if (base2 && token) await startSyncClient(base2, token, effectiveInt("SYNC_TICK_MS"));
15951
15972
  }
15952
- var feedHole, FULL_PULL_MAX_PAGES, timer2, ws, started;
15973
+ var feedHole, FULL_PULL_MAX_PAGES, timer3, ws, started;
15953
15974
  var init_sync_client = __esm({
15954
15975
  "src/services/sync-client.ts"() {
15955
15976
  "use strict";
@@ -17237,7 +17258,7 @@ var require_parseParams = __commonJS({
17237
17258
  var require_basename = __commonJS({
17238
17259
  "../node_modules/.pnpm/@fastify+busboy@3.2.0/node_modules/@fastify/busboy/lib/utils/basename.js"(exports, module) {
17239
17260
  "use strict";
17240
- module.exports = function basename4(path) {
17261
+ module.exports = function basename5(path) {
17241
17262
  if (typeof path !== "string") {
17242
17263
  return "";
17243
17264
  }
@@ -17264,7 +17285,7 @@ var require_multipart = __commonJS({
17264
17285
  var Dicer = require_Dicer();
17265
17286
  var parseParams = require_parseParams();
17266
17287
  var decodeText = require_decodeText();
17267
- var basename4 = require_basename();
17288
+ var basename5 = require_basename();
17268
17289
  var getLimit = require_getLimit();
17269
17290
  var RE_BOUNDARY = /^boundary$/i;
17270
17291
  var RE_FIELD = /^form-data$/i;
@@ -17381,7 +17402,7 @@ var require_multipart = __commonJS({
17381
17402
  } else if (RE_FILENAME.test(parsed[i][0])) {
17382
17403
  filename = parsed[i][1];
17383
17404
  if (!preservePath) {
17384
- filename = basename4(filename);
17405
+ filename = basename5(filename);
17385
17406
  }
17386
17407
  }
17387
17408
  }
@@ -18465,17 +18486,17 @@ var require_stream_wormhole = __commonJS({
18465
18486
  "../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
18466
18487
  "use strict";
18467
18488
  module.exports = (stream, throwError) => {
18468
- return new Promise((resolve16, reject2) => {
18489
+ return new Promise((resolve17, reject2) => {
18469
18490
  if (typeof stream.resume !== "function") {
18470
- return resolve16();
18491
+ return resolve17();
18471
18492
  }
18472
18493
  stream.unpipe && stream.unpipe();
18473
18494
  stream.resume();
18474
18495
  if (stream._readableState && stream._readableState.ended) {
18475
- return resolve16();
18496
+ return resolve17();
18476
18497
  }
18477
18498
  if (!stream.readable || stream.destroyed) {
18478
- return resolve16();
18499
+ return resolve17();
18479
18500
  }
18480
18501
  function cleanup() {
18481
18502
  stream.removeListener("end", onEnd);
@@ -18484,14 +18505,14 @@ var require_stream_wormhole = __commonJS({
18484
18505
  }
18485
18506
  function onEnd() {
18486
18507
  cleanup();
18487
- resolve16();
18508
+ resolve17();
18488
18509
  }
18489
18510
  function onError(err) {
18490
18511
  cleanup();
18491
18512
  if (throwError) {
18492
18513
  reject2(err);
18493
18514
  } else {
18494
- resolve16();
18515
+ resolve17();
18495
18516
  }
18496
18517
  }
18497
18518
  stream.on("end", onEnd);
@@ -18926,16 +18947,16 @@ var require_multipart2 = __commonJS({
18926
18947
  }
18927
18948
  };
18928
18949
  const parts = () => {
18929
- return new Promise((resolve16, reject2) => {
18950
+ return new Promise((resolve17, reject2) => {
18930
18951
  handle((val) => {
18931
18952
  if (val instanceof Error) {
18932
18953
  if (val.message === "Unexpected end of multipart data") {
18933
- resolve16(null);
18954
+ resolve17(null);
18934
18955
  } else {
18935
18956
  reject2(val);
18936
18957
  }
18937
18958
  } else {
18938
- resolve16(val);
18959
+ resolve17(val);
18939
18960
  }
18940
18961
  });
18941
18962
  });
@@ -19202,7 +19223,7 @@ import websocket from "@fastify/websocket";
19202
19223
  import cookie from "@fastify/cookie";
19203
19224
  import { fileURLToPath as fileURLToPath4 } from "node:url";
19204
19225
  import { dirname as dirname14 } from "node:path";
19205
- import { existsSync as existsSync14 } from "node:fs";
19226
+ import { existsSync as existsSync15 } from "node:fs";
19206
19227
 
19207
19228
  // src/web-dir.ts
19208
19229
  import { resolve } from "node:path";
@@ -19264,8 +19285,8 @@ init_stage_machine();
19264
19285
  init_src();
19265
19286
  import { execFile } from "node:child_process";
19266
19287
  import { promisify } from "node:util";
19267
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, rmSync as rmSync2, mkdirSync as mkdirSync5 } from "node:fs";
19268
- import { resolve as resolve8, dirname as dirname7, sep } from "node:path";
19288
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, rmSync as rmSync2, mkdirSync as mkdirSync5 } from "node:fs";
19289
+ import { resolve as resolve8, dirname as dirname7, sep as sep2 } from "node:path";
19269
19290
  var exec = promisify(execFile);
19270
19291
  async function listRepoDocs(repoDir) {
19271
19292
  try {
@@ -19289,12 +19310,12 @@ function docsDirOf(repoDir) {
19289
19310
  }
19290
19311
  function resolveIndex(repoDir, docsDir) {
19291
19312
  const rel = `${docsDir}/README.md`;
19292
- return existsSync4(resolve8(repoDir, rel)) ? rel : "";
19313
+ return existsSync5(resolve8(repoDir, rel)) ? rel : "";
19293
19314
  }
19294
19315
  var catOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ".";
19295
19316
  var nameOf = (rel) => rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
19296
19317
  async function scanRepoDocs(repoDir) {
19297
- if (!repoDir || !existsSync4(repoDir)) return { coverage: 0, tree: [] };
19318
+ if (!repoDir || !existsSync5(repoDir)) return { coverage: 0, tree: [] };
19298
19319
  const files = await listRepoDocs(repoDir);
19299
19320
  const docsDir = docsDirOf(repoDir);
19300
19321
  const index = resolveIndex(repoDir, docsDir);
@@ -19324,7 +19345,7 @@ function docAbsPath(repoDir, rel) {
19324
19345
  if (!rel.endsWith(".md")) throw new Error("hanya file .md yang diizinkan");
19325
19346
  if (rel.split("/").includes(".git")) throw new Error("tidak boleh menyentuh .git");
19326
19347
  const abs = resolve8(repoDir, rel);
19327
- if (abs !== repoDir && !abs.startsWith(repoDir + sep)) throw new Error("path keluar dari repo");
19348
+ if (abs !== repoDir && !abs.startsWith(repoDir + sep2)) throw new Error("path keluar dari repo");
19328
19349
  return abs;
19329
19350
  }
19330
19351
  function readDocFile(repoDir, rel) {
@@ -19341,7 +19362,7 @@ function writeDocFile(repoDir, rel, content) {
19341
19362
  }
19342
19363
  function deleteDocFile(repoDir, rel) {
19343
19364
  const abs = docAbsPath(repoDir, rel);
19344
- if (!existsSync4(abs)) return false;
19365
+ if (!existsSync5(abs)) return false;
19345
19366
  rmSync2(abs);
19346
19367
  return true;
19347
19368
  }
@@ -19628,7 +19649,7 @@ async function projects_default(app2) {
19628
19649
  // src/routes/specs.ts
19629
19650
  init_src();
19630
19651
  init_src2();
19631
- import { existsSync as existsSync5 } from "node:fs";
19652
+ import { existsSync as existsSync6 } from "node:fs";
19632
19653
 
19633
19654
  // src/services/integrate.ts
19634
19655
  import { execFile as execFile3 } from "node:child_process";
@@ -20072,6 +20093,18 @@ async function recordAutoMerge(specId, projectId, title) {
20072
20093
  }).catch(() => {
20073
20094
  });
20074
20095
  }
20096
+ async function recordCleanupFailure(sessionId2, projectId, entry, reason) {
20097
+ await prisma.notification.create({
20098
+ data: {
20099
+ type: "cleanup",
20100
+ key: `cleanup:${entry}`,
20101
+ sessionId: sessionId2,
20102
+ projectId,
20103
+ title: `Worktree sesi ${sessionId2} gagal dibersihkan \u2014 ${reason}`
20104
+ }
20105
+ }).catch(() => {
20106
+ });
20107
+ }
20075
20108
  async function recordSourceChange(specId, projectId, title, from, to, seq) {
20076
20109
  const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
20077
20110
  await prisma.notification.create({
@@ -22724,7 +22757,7 @@ function renderDocPdf(text, name2, meta) {
22724
22757
  });
22725
22758
  const chunks = [];
22726
22759
  doc.on("data", (c) => chunks.push(c));
22727
- const done = new Promise((resolve16) => doc.on("end", () => resolve16(Buffer.concat(chunks))));
22760
+ const done = new Promise((resolve17) => doc.on("end", () => resolve17(Buffer.concat(chunks))));
22728
22761
  doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
22729
22762
  doc.moveDown(0.25);
22730
22763
  doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
@@ -23106,7 +23139,7 @@ ${item.context}` : item.context;
23106
23139
  });
23107
23140
  const specWithProject = (id) => prisma.spec.findUnique({ where: { id }, include: { project: true } });
23108
23141
  const resolveReview = async (repoDir, spec) => {
23109
- if (existsSync5(worktreeDir(repoDir, spec.id))) return { wt: true };
23142
+ if (existsSync6(worktreeDir(repoDir, spec.id))) return { wt: true };
23110
23143
  if (spec.baseSha && spec.headSha && await shaResolvable(repoDir, spec.baseSha) && await shaResolvable(repoDir, spec.headSha))
23111
23144
  return { wt: false, base: spec.baseSha, head: spec.headSha };
23112
23145
  const r = await specCommitRange(repoDir, spec.id);
@@ -23341,13 +23374,13 @@ async function docs_default(app2) {
23341
23374
 
23342
23375
  // src/routes/ide.ts
23343
23376
  init_src2();
23344
- import { basename as basename2 } from "node:path";
23377
+ import { basename as basename3 } from "node:path";
23345
23378
  import { spawn as spawn2 } from "node:child_process";
23346
23379
 
23347
23380
  // src/services/git-remotes.ts
23348
23381
  import { execFile as execFile5 } from "node:child_process";
23349
23382
  import { promisify as promisify5 } from "node:util";
23350
- import { existsSync as existsSync6 } from "node:fs";
23383
+ import { existsSync as existsSync7 } from "node:fs";
23351
23384
  var exec5 = promisify5(execFile5);
23352
23385
  var GIT4 = { maxBuffer: 1 << 24, encoding: "utf8" };
23353
23386
  function parseRemote(url2) {
@@ -23366,7 +23399,7 @@ function prUrl(remoteUrl, branch, base2 = "main") {
23366
23399
  return null;
23367
23400
  }
23368
23401
  async function listRemotes(repoDir) {
23369
- if (!repoDir || !existsSync6(repoDir)) return [];
23402
+ if (!repoDir || !existsSync7(repoDir)) return [];
23370
23403
  try {
23371
23404
  const { stdout } = await exec5("git", ["remote", "-v"], { cwd: repoDir, ...GIT4 });
23372
23405
  const map = /* @__PURE__ */ new Map();
@@ -23408,8 +23441,8 @@ import { promisify as promisify7 } from "node:util";
23408
23441
  import { execFile as execFile6 } from "node:child_process";
23409
23442
  import { promisify as promisify6 } from "node:util";
23410
23443
  import { readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
23411
- import { existsSync as existsSync7 } from "node:fs";
23412
- import { resolve as resolve10, sep as sep2, dirname as dirname9 } from "node:path";
23444
+ import { existsSync as existsSync8 } from "node:fs";
23445
+ import { resolve as resolve10, sep as sep3, dirname as dirname9 } from "node:path";
23413
23446
  var exec6 = promisify6(execFile6);
23414
23447
  var GIT5 = { maxBuffer: 1 << 24 };
23415
23448
  var MAX2 = 256 * 1024;
@@ -23417,11 +23450,11 @@ var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
23417
23450
  function repoAbsPath(repoDir, rel) {
23418
23451
  if (rel.split(/[\\/]/).includes(".git")) throw new Error("tidak boleh menyentuh .git");
23419
23452
  const abs = resolve10(repoDir, rel);
23420
- if (abs !== repoDir && !abs.startsWith(repoDir + sep2)) throw new Error("path keluar dari repo");
23453
+ if (abs !== repoDir && !abs.startsWith(repoDir + sep3)) throw new Error("path keluar dari repo");
23421
23454
  return abs;
23422
23455
  }
23423
23456
  async function listRepoTree(repoDir, ref = "") {
23424
- if (!repoDir || !existsSync7(repoDir)) return [];
23457
+ if (!repoDir || !existsSync8(repoDir)) return [];
23425
23458
  try {
23426
23459
  const { stdout } = ref ? await exec6("git", ["ls-tree", "-r", "--name-only", "-z", ref], { cwd: repoDir, ...GIT5 }) : await exec6("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { cwd: repoDir, ...GIT5 });
23427
23460
  return [...new Set(splitZ2(stdout))].sort();
@@ -23447,7 +23480,7 @@ async function currentBranch(repoDir) {
23447
23480
  }
23448
23481
  async function repoStatus(repoDir) {
23449
23482
  const empty = { branch: "", ahead: 0, behind: 0, staged: [], unstaged: [], untracked: [], clean: true };
23450
- if (!repoDir || !existsSync7(repoDir)) return empty;
23483
+ if (!repoDir || !existsSync8(repoDir)) return empty;
23451
23484
  try {
23452
23485
  const { stdout } = await exec6("git", ["status", "--porcelain=v1", "-z", "-b"], { cwd: repoDir, ...GIT5 });
23453
23486
  const parts = stdout.split("\0");
@@ -23480,7 +23513,7 @@ async function repoStatus(repoDir) {
23480
23513
  }
23481
23514
  }
23482
23515
  async function listStashes(repoDir) {
23483
- if (!repoDir || !existsSync7(repoDir)) return [];
23516
+ if (!repoDir || !existsSync8(repoDir)) return [];
23484
23517
  try {
23485
23518
  const { stdout } = await exec6("git", ["stash", "list", `--format=%gd${US}%s${US}%aI`], { cwd: repoDir, ...GIT5 });
23486
23519
  return stdout.split("\n").filter(Boolean).map((line) => {
@@ -23492,7 +23525,7 @@ async function listStashes(repoDir) {
23492
23525
  }
23493
23526
  }
23494
23527
  async function listGraph(repoDir, limit = 200, opts = {}) {
23495
- if (!repoDir || !existsSync7(repoDir)) return { commits: [], current: "", total: 0 };
23528
+ if (!repoDir || !existsSync8(repoDir)) return { commits: [], current: "", total: 0 };
23496
23529
  try {
23497
23530
  const fmt = ["%H", "%P", "%an", "%aI", "%s", "%D"].join(US);
23498
23531
  const refArgs = opts.branches?.length ? ["--end-of-options", ...opts.branches] : [
@@ -23562,7 +23595,7 @@ async function changedOf(repoDir, sha) {
23562
23595
  }
23563
23596
  async function compareCommits(repoDir, from, to) {
23564
23597
  const empty = { from, to, changed: [] };
23565
- if (!repoDir || !existsSync7(repoDir)) return empty;
23598
+ if (!repoDir || !existsSync8(repoDir)) return empty;
23566
23599
  try {
23567
23600
  const [num, name2] = await Promise.all([
23568
23601
  exec6("git", ["diff", "--numstat", "-z", "--no-renames", "--end-of-options", from, to], { cwd: repoDir, ...GIT5 }),
@@ -23574,7 +23607,7 @@ async function compareCommits(repoDir, from, to) {
23574
23607
  }
23575
23608
  }
23576
23609
  async function searchCommits(repoDir, q, by = "all") {
23577
- if (!repoDir || !existsSync7(repoDir) || !q) return [];
23610
+ if (!repoDir || !existsSync8(repoDir) || !q) return [];
23578
23611
  const run2 = async (extra) => {
23579
23612
  try {
23580
23613
  return (await exec6("git", ["log", "--all", "--format=%H", ...extra], { cwd: repoDir, ...GIT5 })).stdout.split("\n").filter(Boolean);
@@ -23823,7 +23856,7 @@ async function runGitOp(repoDir, op) {
23823
23856
  }
23824
23857
  }
23825
23858
  async function workingStatus(repoDir) {
23826
- if (!repoDir || !existsSync7(repoDir)) return { branch: "", staged: [], unstaged: [] };
23859
+ if (!repoDir || !existsSync8(repoDir)) return { branch: "", staged: [], unstaged: [] };
23827
23860
  const [branch, staged, unstaged] = await Promise.all([
23828
23861
  currentBranch(repoDir),
23829
23862
  changedFiles(repoDir, ["--cached"]),
@@ -23832,7 +23865,7 @@ async function workingStatus(repoDir) {
23832
23865
  return { branch, staged, unstaged };
23833
23866
  }
23834
23867
  async function workingFileDiff(repoDir, path, staged) {
23835
- if (!repoDir || !existsSync7(repoDir)) return null;
23868
+ if (!repoDir || !existsSync8(repoDir)) return null;
23836
23869
  repoAbsPath(repoDir, path);
23837
23870
  const changed = staged ? await changedFiles(repoDir, ["--cached"]) : await withTempIndex(repoDir, (env) => changedFiles(repoDir, [], env));
23838
23871
  const cf = changed.find((c) => c.path === path);
@@ -24151,7 +24184,7 @@ async function ide_default(app2) {
24151
24184
  const fmt = q.format === "tar" ? "tar" : "zip";
24152
24185
  const child = spawn2("git", ["archive", `--format=${fmt}`, "--end-of-options", ref], { cwd: repoDir });
24153
24186
  reply.header("content-type", fmt === "zip" ? "application/zip" : "application/x-tar");
24154
- reply.header("content-disposition", `attachment; filename="${basename2(repoDir)}-${ref.replace(/[^\w.-]/g, "_")}.${fmt}"`);
24187
+ reply.header("content-disposition", `attachment; filename="${basename3(repoDir)}-${ref.replace(/[^\w.-]/g, "_")}.${fmt}"`);
24155
24188
  return reply.send(child.stdout);
24156
24189
  });
24157
24190
  app2.get("/projects/:id/commit/:sha", async (req, reply) => {
@@ -24303,7 +24336,7 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
24303
24336
  CODE_STYLE_CLAUSE,
24304
24337
  `${verb} via git graph project ${id}.`
24305
24338
  ].join("\n\n");
24306
- const s2 = createSession(id, r.worktree, { id: basename2(r.worktree), model, effort, agent, prompt });
24339
+ const s2 = createSession(id, r.worktree, { id: basename3(r.worktree), model, effort, agent, prompt });
24307
24340
  return { status: "conflict", sessionId: s2.id };
24308
24341
  }
24309
24342
 
@@ -24331,7 +24364,7 @@ init_db();
24331
24364
  init_src();
24332
24365
  init_src2();
24333
24366
  init_session_phases();
24334
- import { existsSync as existsSync8 } from "node:fs";
24367
+ import { existsSync as existsSync9 } from "node:fs";
24335
24368
  init_settings2();
24336
24369
  init_codex_trust();
24337
24370
 
@@ -24476,12 +24509,117 @@ function installCommand(m, agent, shell = shellBin()) {
24476
24509
  }
24477
24510
 
24478
24511
  // src/services/session-worktree.ts
24479
- import { resolve as resolve12, sep as sep3 } from "node:path";
24512
+ import { resolve as resolve12, sep as sep4 } from "node:path";
24480
24513
  function ownsWorktree(repoDir, cwd) {
24481
24514
  const base2 = resolve12(repoDir);
24482
24515
  const dir2 = resolve12(cwd);
24483
24516
  const container = resolve12(base2, ".worktrees");
24484
- return dir2 !== base2 && dir2 !== container && dir2.startsWith(container + sep3);
24517
+ return dir2 !== base2 && dir2 !== container && dir2.startsWith(container + sep4);
24518
+ }
24519
+
24520
+ // src/services/worktree-reaper.ts
24521
+ init_src2();
24522
+ init_db();
24523
+ import { execFile as execFile8 } from "node:child_process";
24524
+ import { readdir as readdir2, rm as rm2 } from "node:fs/promises";
24525
+ import { promisify as promisify8 } from "node:util";
24526
+ import { join as join7, resolve as resolve13 } from "node:path";
24527
+ var TICK_MS = 6e4;
24528
+ var trashDirOf = (repoDir) => resolve13(repoDir, ".worktrees", ".trash");
24529
+ var exec8 = promisify8(execFile8);
24530
+ var prodReaperDeps = {
24531
+ rm: (path) => rm2(path, { recursive: true, force: true }),
24532
+ prune: async (repoDir) => {
24533
+ try {
24534
+ await exec8("git", ["worktree", "prune"], { cwd: repoDir, timeout: 3e4 });
24535
+ } catch {
24536
+ }
24537
+ },
24538
+ repos: async () => {
24539
+ const projects = await prisma.project.findMany({ select: { id: true } });
24540
+ const seen = /* @__PURE__ */ new Set();
24541
+ const out3 = [];
24542
+ for (const p of projects) {
24543
+ const repoDir = await resolveRepoDir(p.id);
24544
+ if (!repoDir || seen.has(repoDir)) continue;
24545
+ seen.add(repoDir);
24546
+ out3.push({ projectId: p.id, repoDir });
24547
+ }
24548
+ return out3;
24549
+ }
24550
+ };
24551
+ var pending = /* @__PURE__ */ new Map();
24552
+ var sessionIdOf = (entry) => entry.split(".")[0] ?? entry;
24553
+ function releaseWorktree(repoDir, cwd, projectId, deps = prodReaperDeps) {
24554
+ let path;
24555
+ try {
24556
+ path = realGit.trashWorktree(repoDir, cwd);
24557
+ } catch {
24558
+ realGit.removeWorktree(repoDir, cwd);
24559
+ return null;
24560
+ }
24561
+ if (!path) return null;
24562
+ const entry = path.slice(trashDirOf(repoDir).length + 1);
24563
+ pending.set(path, { path, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() });
24564
+ reapSoon(repoDir, projectId, deps);
24565
+ return entry;
24566
+ }
24567
+ var listCleanups = () => [...pending.values()].sort((a, b) => a.since - b.since).map((p) => ({
24568
+ sessionId: p.sessionId,
24569
+ projectId: p.projectId,
24570
+ entry: p.entry,
24571
+ since: new Date(p.since).toISOString(),
24572
+ state: p.error ? "failed" : "closing",
24573
+ ...p.error ? { error: p.error } : {}
24574
+ }));
24575
+ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
24576
+ const dir2 = trashDirOf(repoDir);
24577
+ let entries;
24578
+ try {
24579
+ entries = await readdir2(dir2);
24580
+ } catch {
24581
+ return 0;
24582
+ }
24583
+ let removed = 0;
24584
+ for (const entry of entries) {
24585
+ const path = join7(dir2, entry);
24586
+ const known = pending.get(path);
24587
+ const row = known ?? { path, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
24588
+ pending.set(path, row);
24589
+ try {
24590
+ await deps.rm(path);
24591
+ pending.delete(path);
24592
+ removed++;
24593
+ } catch (e) {
24594
+ row.error = e.message;
24595
+ await recordCleanupFailure(row.sessionId, row.projectId, entry, row.error);
24596
+ }
24597
+ }
24598
+ if (removed) await deps.prune(repoDir);
24599
+ return removed;
24600
+ }
24601
+ async function sweepAll(deps = prodReaperDeps) {
24602
+ let removed = 0;
24603
+ for (const { projectId, repoDir } of await deps.repos()) {
24604
+ removed += await sweepRepo(repoDir, projectId, deps).catch(() => 0);
24605
+ }
24606
+ return removed;
24607
+ }
24608
+ var inFlight = /* @__PURE__ */ new Set();
24609
+ function reapSoon(repoDir, projectId, deps = prodReaperDeps) {
24610
+ if (inFlight.has(repoDir)) return;
24611
+ inFlight.add(repoDir);
24612
+ void sweepRepo(repoDir, projectId, deps).catch((e) => console.error("sapuan worktree:", e)).finally(() => inFlight.delete(repoDir));
24613
+ }
24614
+ var timer;
24615
+ function startWorktreeReaper(deps = prodReaperDeps) {
24616
+ if (timer) return;
24617
+ void sweepAll(deps).catch((e) => console.error("sapuan worktree saat boot:", e));
24618
+ timer = setInterval(() => {
24619
+ void sweepAll(deps).catch(() => {
24620
+ });
24621
+ }, TICK_MS);
24622
+ timer.unref();
24485
24623
  }
24486
24624
 
24487
24625
  // src/services/session-result.ts
@@ -24760,7 +24898,7 @@ async function terminal_default(app2) {
24760
24898
  if (!s2) return { ok: false, code: 404, msg: "not found" };
24761
24899
  const repoDir = await resolveRepoDir(s2.projectId);
24762
24900
  if (!repoDir) return { ok: false, code: 409, msg: "project belum punya repoDir" };
24763
- if (!s2.cwd.includes("/.worktrees/") || !existsSync8(s2.cwd))
24901
+ if (!s2.cwd.includes("/.worktrees/") || !existsSync9(s2.cwd))
24764
24902
  return { ok: false, code: 409, msg: "belum ada worktree untuk di-review \u2014 jalankan/lanjutkan sesi dulu" };
24765
24903
  return { ok: true, id: s2.id, repoDir };
24766
24904
  };
@@ -24811,6 +24949,7 @@ async function terminal_default(app2) {
24811
24949
  });
24812
24950
  return { status: "conflict", sessionId: cs.id };
24813
24951
  });
24952
+ app2.get("/terminal/cleanups", async () => ({ items: listCleanups() }));
24814
24953
  app2.delete("/terminal/sessions/:id", async (req, reply) => {
24815
24954
  const { id } = req.params;
24816
24955
  const s2 = getSession(id);
@@ -24823,12 +24962,12 @@ async function terminal_default(app2) {
24823
24962
  await recordHeadSha(s2.specId, s2.cwd);
24824
24963
  }
24825
24964
  killSession(id);
24826
- if (ownsWorktree(repoDir, s2.cwd)) realGit.removeWorktree(repoDir, s2.cwd);
24827
- return reply.code(204).send();
24965
+ const cleanup = ownsWorktree(repoDir, s2.cwd) ? releaseWorktree(repoDir, s2.cwd, s2.projectId) : null;
24966
+ return reply.code(202).send({ cleanup });
24828
24967
  }
24829
24968
  }
24830
24969
  killSession(id);
24831
- return reply.code(204).send();
24970
+ return reply.code(202).send({ cleanup: null });
24832
24971
  });
24833
24972
  app2.get("/terminal/sessions/:id/ws", { websocket: true }, (socket2, req) => {
24834
24973
  const { id } = req.params;
@@ -24852,7 +24991,7 @@ async function terminal_default(app2) {
24852
24991
  // src/routes/vps.ts
24853
24992
  init_db();
24854
24993
  init_src();
24855
- import { readFileSync as readFileSync10, existsSync as existsSync11 } from "node:fs";
24994
+ import { readFileSync as readFileSync10, existsSync as existsSync12 } from "node:fs";
24856
24995
  import { homedir as homedir6 } from "node:os";
24857
24996
 
24858
24997
  // src/vps/catalog/catalog.data.ts
@@ -24942,7 +25081,7 @@ init_config2();
24942
25081
  import { spawn as spawn3 } from "node:child_process";
24943
25082
  import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
24944
25083
  import { tmpdir as tmpdir3 } from "node:os";
24945
- import { dirname as dirname11, join as join7 } from "node:path";
25084
+ import { dirname as dirname11, join as join8 } from "node:path";
24946
25085
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
24947
25086
  function consoleArgv(t) {
24948
25087
  return [
@@ -24957,8 +25096,8 @@ function consoleArgv(t) {
24957
25096
  ];
24958
25097
  }
24959
25098
  function askpassScript() {
24960
- const dir2 = mkdtempSync(join7(tmpdir3(), "hanoman-askpass-"));
24961
- const path = join7(dir2, "askpass.sh");
25099
+ const dir2 = mkdtempSync(join8(tmpdir3(), "hanoman-askpass-"));
25100
+ const path = join8(dir2, "askpass.sh");
24962
25101
  writeFileSync4(path, `#!/bin/sh
24963
25102
  printf '%s' "$HANOMAN_SSH_PASSWORD"
24964
25103
  `, { mode: 448 });
@@ -24992,14 +25131,14 @@ function sshExec(t, remoteCmd, opts = {}) {
24992
25131
  SSH_ASKPASS_REQUIRE: "force",
24993
25132
  HANOMAN_SSH_PASSWORD: opts.password
24994
25133
  } : process.env;
24995
- return new Promise((resolve16) => {
25134
+ return new Promise((resolve17) => {
24996
25135
  const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
24997
25136
  let out3 = "";
24998
- const timer7 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
25137
+ const timer8 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
24999
25138
  const done = (r) => {
25000
- clearTimeout(timer7);
25139
+ clearTimeout(timer8);
25001
25140
  if (askpass) rmSync4(dirname11(askpass), { recursive: true, force: true });
25002
- resolve16(r);
25141
+ resolve17(r);
25003
25142
  };
25004
25143
  p.stdout.on("data", (d) => {
25005
25144
  out3 += d;
@@ -25019,15 +25158,15 @@ function sshExec(t, remoteCmd, opts = {}) {
25019
25158
  // src/services/vps-audit.ts
25020
25159
  init_db();
25021
25160
  import { readFileSync as readFileSync7 } from "node:fs";
25022
- import { join as join9 } from "node:path";
25161
+ import { join as join10 } from "node:path";
25023
25162
 
25024
25163
  // src/runner/deps.ts
25025
- import { existsSync as existsSync9 } from "node:fs";
25026
- import { dirname as dirname12, join as join8 } from "node:path";
25164
+ import { existsSync as existsSync10 } from "node:fs";
25165
+ import { dirname as dirname12, join as join9 } from "node:path";
25027
25166
  function repoRootFrom(startDir) {
25028
25167
  let dir2 = startDir;
25029
25168
  for (let i = 0; i < 8; i++) {
25030
- if (existsSync9(join8(dir2, "pnpm-workspace.yaml"))) return dir2;
25169
+ if (existsSync10(join9(dir2, "pnpm-workspace.yaml"))) return dir2;
25031
25170
  const parent = dirname12(dir2);
25032
25171
  if (parent === dir2) break;
25033
25172
  dir2 = parent;
@@ -25132,7 +25271,7 @@ function parseHealth(out3) {
25132
25271
  return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
25133
25272
  }
25134
25273
  var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
25135
- var scriptPath = (f) => join9(repoRoot(), "server", "scripts", "vps", f);
25274
+ var scriptPath = (f) => join10(repoRoot(), "server", "scripts", "vps", f);
25136
25275
  async function itemStatesOf(vpsId) {
25137
25276
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
25138
25277
  return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
@@ -25279,15 +25418,15 @@ async function buildChecklist(vpsId) {
25279
25418
  // src/services/vps-key.ts
25280
25419
  init_config2();
25281
25420
  import { execFileSync as execFileSync2 } from "node:child_process";
25282
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
25421
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
25283
25422
  import { homedir as homedir5 } from "node:os";
25284
- import { join as join10 } from "node:path";
25285
- var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join10(homedir5(), ".hanoman");
25423
+ import { join as join11 } from "node:path";
25424
+ var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join11(homedir5(), ".hanoman");
25286
25425
  function ensureHanomanKey() {
25287
25426
  const dir2 = keyDir();
25288
- const privPath = join10(dir2, "id_ed25519");
25427
+ const privPath = join11(dir2, "id_ed25519");
25289
25428
  const pubPath = `${privPath}.pub`;
25290
- if (!existsSync10(privPath)) {
25429
+ if (!existsSync11(privPath)) {
25291
25430
  mkdirSync8(dir2, { recursive: true, mode: 448 });
25292
25431
  execFileSync2(
25293
25432
  "ssh-keygen",
@@ -25314,7 +25453,7 @@ async function bootstrapKey(t, password) {
25314
25453
  init_pty();
25315
25454
  init_settings2();
25316
25455
  function keyMissing(v) {
25317
- return !!v.keyPath && !existsSync11(v.keyPath);
25456
+ return !!v.keyPath && !existsSync12(v.keyPath);
25318
25457
  }
25319
25458
  async function vps_default(app2) {
25320
25459
  app2.get("/vps", async () => prisma.vps.findMany({ orderBy: { createdAt: "asc" } }));
@@ -25526,7 +25665,7 @@ init_config2();
25526
25665
  import { execFileSync as execFileSync3 } from "node:child_process";
25527
25666
  import { readFileSync as readFileSync11 } from "node:fs";
25528
25667
  import { homedir as homedir7 } from "node:os";
25529
- import { join as join11 } from "node:path";
25668
+ import { join as join12 } from "node:path";
25530
25669
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
25531
25670
  var TTL_MS2 = 3e4;
25532
25671
  var lastOk = null;
@@ -25539,7 +25678,7 @@ var LABELS = {
25539
25678
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
25540
25679
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
25541
25680
  function credsFile() {
25542
- return join11(effectiveStr("CLAUDE_CONFIG_DIR") ?? join11(homedir7(), ".claude"), ".credentials.json");
25681
+ return join12(effectiveStr("CLAUDE_CONFIG_DIR") ?? join12(homedir7(), ".claude"), ".credentials.json");
25543
25682
  }
25544
25683
  function readAccessToken() {
25545
25684
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
@@ -25611,16 +25750,16 @@ async function getLimits() {
25611
25750
  }
25612
25751
 
25613
25752
  // src/services/codex-limits.ts
25614
- import { open as open2, readdir as readdir2, stat } from "node:fs/promises";
25753
+ import { open as open2, readdir as readdir3, stat } from "node:fs/promises";
25615
25754
  import { homedir as homedir8 } from "node:os";
25616
- import { join as join12 } from "node:path";
25755
+ import { join as join13 } from "node:path";
25617
25756
  var TTL_MS3 = 3e4;
25618
25757
  var STALE_AFTER_MS = 12 * 36e5;
25619
25758
  var TAIL_BYTES = 512 * 1024;
25620
25759
  var MAX_FILES = 8;
25621
25760
  var cache2 = null;
25622
25761
  var freshUntil2 = 0;
25623
- var codexSessionsDir = () => join12(process.env.CODEX_HOME ?? join12(homedir8(), ".codex"), "sessions");
25762
+ var codexSessionsDir = () => join13(process.env.CODEX_HOME ?? join13(homedir8(), ".codex"), "sessions");
25624
25763
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
25625
25764
  function windowLabel(minutes) {
25626
25765
  if (minutes === 300) return "Sesi 5 jam";
@@ -25648,12 +25787,12 @@ function toWindow(slot, raw, reached) {
25648
25787
  async function recentRollouts(dir2) {
25649
25788
  let entries;
25650
25789
  try {
25651
- entries = (await readdir2(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
25790
+ entries = (await readdir3(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
25652
25791
  } catch {
25653
25792
  return [];
25654
25793
  }
25655
25794
  const stamped = await Promise.all(entries.map(async (rel) => {
25656
- const full = join12(dir2, rel);
25795
+ const full = join13(dir2, rel);
25657
25796
  try {
25658
25797
  return { full, mtime: (await stat(full)).mtimeMs };
25659
25798
  } catch {
@@ -25733,9 +25872,9 @@ async function limits(app2) {
25733
25872
  // src/services/codex-version.ts
25734
25873
  init_src();
25735
25874
  init_config2();
25736
- import { execFile as execFile8 } from "node:child_process";
25737
- import { promisify as promisify8 } from "node:util";
25738
- var run = promisify8(execFile8);
25875
+ import { execFile as execFile9 } from "node:child_process";
25876
+ import { promisify as promisify9 } from "node:util";
25877
+ var run = promisify9(execFile9);
25739
25878
  var CODEX_MIN_CLIENT = "0.144.0";
25740
25879
  var TTL_MS4 = 5 * 6e4;
25741
25880
  var cache3 = null;
@@ -25781,7 +25920,7 @@ init_src();
25781
25920
  init_src();
25782
25921
  init_config2();
25783
25922
  import { readFileSync as readFileSync12 } from "node:fs";
25784
- import { resolve as resolve13, dirname as dirname13 } from "node:path";
25923
+ import { resolve as resolve14, dirname as dirname13 } from "node:path";
25785
25924
  import { fileURLToPath as fileURLToPath3 } from "node:url";
25786
25925
  var UPDATE_COMMAND = "npm i -g hanoman@latest";
25787
25926
  function composeUpdate(x) {
@@ -25807,7 +25946,7 @@ var lastLatest = null;
25807
25946
  var lastStatus = "unavailable";
25808
25947
  function runningVersion() {
25809
25948
  const here = dirname13(fileURLToPath3(import.meta.url));
25810
- for (const p of [resolve13(here, "build-info.json"), resolve13(here, "../package.json")]) {
25949
+ for (const p of [resolve14(here, "build-info.json"), resolve14(here, "../package.json")]) {
25811
25950
  try {
25812
25951
  const v = JSON.parse(readFileSync12(p, "utf8")).version;
25813
25952
  if (typeof v === "string" && v) return v;
@@ -25927,6 +26066,10 @@ var GROUPS = [
25927
26066
  }) },
25928
26067
  { everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
25929
26068
  { everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
26069
+ // SPEC-742 · ADR-0116 · pembersihan worktree yang masih jalan. `listCleanups()` membaca peta di
26070
+ // memori, bukan disk — nol I/O per tick, dan dedup signature membuat frame lahir hanya saat
26071
+ // daftarnya berubah (nyaris selalu kosong).
26072
+ { everyTicks: 3, last: "", build: async () => ({ t: "cleanups", cleanups: listCleanups() }) },
25930
26073
  // ponytail: cermin GET /vps (orderBy createdAt asc). Query sepele — tak diekstrak.
25931
26074
  { everyTicks: 15, last: "", build: async () => ({ t: "vps", vps: await prisma.vps.findMany({ orderBy: { createdAt: "asc" } }) }) },
25932
26075
  { everyTicks: 30, last: "", build: async () => ({ t: "limits", limits: await getLimits() }) },
@@ -25950,7 +26093,7 @@ function broadcast2(msg) {
25950
26093
  }
25951
26094
  var tick = 0;
25952
26095
  var busy = false;
25953
- var timer;
26096
+ var timer2;
25954
26097
  async function __tick() {
25955
26098
  if (busy) return;
25956
26099
  busy = true;
@@ -25974,16 +26117,16 @@ async function __tick() {
25974
26117
  }
25975
26118
  }
25976
26119
  function startLoop() {
25977
- if (timer) return;
25978
- timer = setInterval(() => {
26120
+ if (timer2) return;
26121
+ timer2 = setInterval(() => {
25979
26122
  void __tick();
25980
26123
  }, tickMs());
25981
- timer.unref();
26124
+ timer2.unref();
25982
26125
  }
25983
26126
  function stopLoop() {
25984
- if (timer) {
25985
- clearInterval(timer);
25986
- timer = void 0;
26127
+ if (timer2) {
26128
+ clearInterval(timer2);
26129
+ timer2 = void 0;
25987
26130
  }
25988
26131
  tick = 0;
25989
26132
  for (const g of GROUPS) g.last = "";
@@ -26157,7 +26300,7 @@ init_conflicts();
26157
26300
  init_config2();
26158
26301
  import { randomUUID as randomUUID3 } from "node:crypto";
26159
26302
  import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile4, unlink } from "node:fs/promises";
26160
- import { join as join13, resolve as resolve14 } from "node:path";
26303
+ import { join as join14, resolve as resolve15 } from "node:path";
26161
26304
  var EXT = {
26162
26305
  "image/png": ".png",
26163
26306
  "image/jpeg": ".jpg",
@@ -26167,22 +26310,22 @@ function extFor(mimeType) {
26167
26310
  return EXT[mimeType] ?? ".bin";
26168
26311
  }
26169
26312
  function uploadDir() {
26170
- return resolve14(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join13(process.cwd(), "data", "uploads"));
26313
+ return resolve15(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join14(process.cwd(), "data", "uploads"));
26171
26314
  }
26172
26315
  async function saveUpload(buf, mimeType) {
26173
26316
  const dir2 = uploadDir();
26174
26317
  await mkdir2(dir2, { recursive: true });
26175
26318
  const storageKey = `${randomUUID3()}${extFor(mimeType)}`;
26176
- await writeFile2(join13(dir2, storageKey), buf);
26319
+ await writeFile2(join14(dir2, storageKey), buf);
26177
26320
  return { storageKey, size: buf.length };
26178
26321
  }
26179
26322
  async function readUpload(storageKey) {
26180
26323
  const safe = storageKey.replace(/[/\\]/g, "");
26181
- return readFile4(join13(uploadDir(), safe));
26324
+ return readFile4(join14(uploadDir(), safe));
26182
26325
  }
26183
26326
  async function readUploadOrFetch(storageKey) {
26184
26327
  const safe = storageKey.replace(/[/\\]/g, "");
26185
- const target2 = join13(uploadDir(), safe);
26328
+ const target2 = join14(uploadDir(), safe);
26186
26329
  try {
26187
26330
  return await readFile4(target2);
26188
26331
  } catch {
@@ -26201,7 +26344,7 @@ async function readUploadOrFetch(storageKey) {
26201
26344
  }
26202
26345
  async function deleteUpload(storageKey) {
26203
26346
  const safe = storageKey.replace(/[/\\]/g, "");
26204
- await unlink(join13(uploadDir(), safe)).catch(() => {
26347
+ await unlink(join14(uploadDir(), safe)).catch(() => {
26205
26348
  });
26206
26349
  }
26207
26350
 
@@ -26332,10 +26475,10 @@ import { randomUUID as randomUUID5 } from "node:crypto";
26332
26475
  init_config2();
26333
26476
  import { randomUUID as randomUUID4 } from "node:crypto";
26334
26477
  import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile5, unlink as unlink2 } from "node:fs/promises";
26335
- import { join as join14, resolve as resolve15, basename as basename3 } from "node:path";
26478
+ import { join as join15, resolve as resolve16, basename as basename4 } from "node:path";
26336
26479
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
26337
26480
  function transcriptDir() {
26338
- return resolve15(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join14(process.cwd(), "data", "transcripts"));
26481
+ return resolve16(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join15(process.cwd(), "data", "transcripts"));
26339
26482
  }
26340
26483
  function clamp(text) {
26341
26484
  const buf = Buffer.from(text, "utf8");
@@ -26353,20 +26496,20 @@ async function saveTranscript(text) {
26353
26496
  const dir2 = transcriptDir();
26354
26497
  await mkdir3(dir2, { recursive: true });
26355
26498
  const key = `${randomUUID4()}.log`;
26356
- await writeFile3(join14(dir2, key), body, "utf8");
26499
+ await writeFile3(join15(dir2, key), body, "utf8");
26357
26500
  return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
26358
26501
  }
26359
26502
  async function readTranscript(key) {
26360
26503
  if (!key) return null;
26361
26504
  try {
26362
- return await readFile5(join14(transcriptDir(), basename3(key)), "utf8");
26505
+ return await readFile5(join15(transcriptDir(), basename4(key)), "utf8");
26363
26506
  } catch {
26364
26507
  return null;
26365
26508
  }
26366
26509
  }
26367
26510
  async function deleteTranscript(key) {
26368
26511
  if (!key) return;
26369
- await unlink2(join14(transcriptDir(), basename3(key))).catch(() => {
26512
+ await unlink2(join15(transcriptDir(), basename4(key))).catch(() => {
26370
26513
  });
26371
26514
  }
26372
26515
 
@@ -26789,7 +26932,7 @@ init_db();
26789
26932
 
26790
26933
  // src/services/ticket-accept.ts
26791
26934
  init_db();
26792
- import { join as join15 } from "node:path";
26935
+ import { join as join16 } from "node:path";
26793
26936
  async function materializeAttachments(atts) {
26794
26937
  const ready = /* @__PURE__ */ new Set();
26795
26938
  for (const a of atts) {
@@ -26804,7 +26947,7 @@ async function materializeAttachments(atts) {
26804
26947
  var attachmentInstruction = (t, atts, ready) => {
26805
26948
  if (atts.length === 0) return "Tanpa lampiran.";
26806
26949
  const list2 = atts.map(
26807
- (a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${join15(uploadDir(), a.storageKey)}` : `- ${a.filename} (${a.mimeType}) \u2192 BELUM TERUNDUH, ambil via GET /api/tickets/${t.id}/attachments/${a.id}`
26950
+ (a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${join16(uploadDir(), a.storageKey)}` : `- ${a.filename} (${a.mimeType}) \u2192 BELUM TERUNDUH, ambil via GET /api/tickets/${t.id}/attachments/${a.id}`
26808
26951
  ).join("\n");
26809
26952
  return `LAMPIRAN (${atts.length}) dari pelapor \u2014 biasanya screenshot yang menunjukkan masalah. PERIKSA setiap lampiran untuk memahami konteks keluhan sebelum bekerja; jangan berasumsi dari teks saja. Berkas ada di direktori upload server (baca langsung dengan tool Read):
26810
26953
  ${list2}
@@ -27377,8 +27520,8 @@ async function scheduler_default(app2) {
27377
27520
  const cfg = await getScheduler();
27378
27521
  if (!cfg.enabled) return reply.code(409).send({ error: "scheduler sedang berhenti \u2014 aktifkan dulu di panel Kendali" });
27379
27522
  if (cfg.paused) return reply.code(409).send({ error: "scheduler sedang dijeda \u2014 lanjutkan dulu di panel Kendali" });
27380
- const pending = await prisma.schedulerCronRun.findFirst({ where: { cronId: id, status: "queued" } });
27381
- if (pending) return reply.code(409).send({ error: "masih ada run yang menunggu dijalankan", runId: pending.id });
27523
+ const pending2 = await prisma.schedulerCronRun.findFirst({ where: { cronId: id, status: "queued" } });
27524
+ if (pending2) return reply.code(409).send({ error: "masih ada run yang menunggu dijalankan", runId: pending2.id });
27382
27525
  try {
27383
27526
  const run2 = await prisma.schedulerCronRun.create({
27384
27527
  data: { cronId: id, projectId: cron.projectId, dueAt: /* @__PURE__ */ new Date(), manual: true }
@@ -27530,7 +27673,7 @@ function leadPrompt(q, c) {
27530
27673
 
27531
27674
  // src/services/lead/verdict.ts
27532
27675
  init_src();
27533
- import { existsSync as existsSync12 } from "node:fs";
27676
+ import { existsSync as existsSync13 } from "node:fs";
27534
27677
  import { isAbsolute as isAbsolute3, resolve as resolvePath, relative } from "node:path";
27535
27678
  function extractJsonBlock(raw) {
27536
27679
  const re = /```json\s*\n([\s\S]*?)```/g;
@@ -27568,7 +27711,7 @@ function keepExistingRefs(refs2, repoDir) {
27568
27711
  const full = resolvePath(repoDir, ref);
27569
27712
  const rel = relative(repoDir, full);
27570
27713
  if (rel.startsWith("..")) continue;
27571
- if (existsSync12(full)) out3.push(ref);
27714
+ if (existsSync13(full)) out3.push(ref);
27572
27715
  }
27573
27716
  return [...new Set(out3)];
27574
27717
  }
@@ -27718,7 +27861,7 @@ function toDecisionView(r) {
27718
27861
  // src/services/lead/brain.ts
27719
27862
  init_config2();
27720
27863
  init_pty();
27721
- import { execFile as execFile9 } from "node:child_process";
27864
+ import { execFile as execFile10 } from "node:child_process";
27722
27865
  var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
27723
27866
  function leadArgv(o) {
27724
27867
  if (o.agent === "codex") {
@@ -27754,8 +27897,8 @@ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
27754
27897
  function think(prompt, o) {
27755
27898
  const bin = binFor(o.agent);
27756
27899
  const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
27757
- return new Promise((resolve16, reject2) => {
27758
- const child = execFile9(bin, args, {
27900
+ return new Promise((resolve17, reject2) => {
27901
+ const child = execFile10(bin, args, {
27759
27902
  cwd: o.cwd,
27760
27903
  timeout: o.timeoutMs,
27761
27904
  maxBuffer: 16 * 1024 * 1024,
@@ -27767,7 +27910,7 @@ function think(prompt, o) {
27767
27910
  reject2(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
27768
27911
  return;
27769
27912
  }
27770
- resolve16(stdout);
27913
+ resolve17(stdout);
27771
27914
  });
27772
27915
  child.stdin?.end();
27773
27916
  });
@@ -27782,31 +27925,31 @@ var LeadBusyError = class extends Error {
27782
27925
  this.name = "LeadBusyError";
27783
27926
  }
27784
27927
  };
27785
- var inFlight = 0;
27928
+ var inFlight2 = 0;
27786
27929
  var capacity = 1;
27787
27930
  var queue = [];
27788
27931
  function leadGateStats() {
27789
- return { inFlight, queued: queue.length };
27932
+ return { inFlight: inFlight2, queued: queue.length };
27790
27933
  }
27791
27934
  function pump() {
27792
- while (queue.length && inFlight < capacity) {
27935
+ while (queue.length && inFlight2 < capacity) {
27793
27936
  const w = queue.shift();
27794
27937
  clearTimeout(w.timer);
27795
- inFlight++;
27938
+ inFlight2++;
27796
27939
  w.grant();
27797
27940
  }
27798
27941
  }
27799
27942
  function acquire(cap, waitMs) {
27800
27943
  capacity = Math.max(1, cap);
27801
27944
  pump();
27802
- if (!queue.length && inFlight < capacity) {
27803
- inFlight++;
27945
+ if (!queue.length && inFlight2 < capacity) {
27946
+ inFlight2++;
27804
27947
  return Promise.resolve();
27805
27948
  }
27806
27949
  const startedAt = Date.now();
27807
- return new Promise((resolve16, reject2) => {
27950
+ return new Promise((resolve17, reject2) => {
27808
27951
  const w = {
27809
- grant: resolve16,
27952
+ grant: resolve17,
27810
27953
  deny: reject2,
27811
27954
  timer: setTimeout(() => {
27812
27955
  const i = queue.indexOf(w);
@@ -27822,7 +27965,7 @@ async function runGated(o, fn) {
27822
27965
  try {
27823
27966
  return await fn();
27824
27967
  } finally {
27825
- inFlight--;
27968
+ inFlight2--;
27826
27969
  pump();
27827
27970
  }
27828
27971
  }
@@ -28873,27 +29016,27 @@ async function orderProject(projectId, deps) {
28873
29016
  where: { specId: { in: ready.map((r) => r.id) } },
28874
29017
  select: { specId: true }
28875
29018
  })).map((q) => q.specId));
28876
- const pending = ready.filter((r) => !already.has(r.id));
28877
- if (pending.length < 2) return 0;
28878
- const sig = pending.map((r) => r.id).join(",");
29019
+ const pending2 = ready.filter((r) => !already.has(r.id));
29020
+ if (pending2.length < 2) return 0;
29021
+ const sig = pending2.map((r) => r.id).join(",");
28879
29022
  if (sig === lastReadySig.get(projectId)) return 0;
28880
29023
  lastReadySig.set(projectId, sig);
28881
29024
  const row = await decideOrSkip(deps, {
28882
29025
  projectId,
28883
29026
  gate: "pulse",
28884
29027
  kind: "order",
28885
- question: `Ada ${pending.length} backlog siap dikerjakan. Urutkan mana yang lebih dulu berdasarkan isi pekerjaannya, lalu tuliskan urutan id-nya (dipisah koma) di \`decision\`.`,
28886
- options: pending.map((r) => `${r.id} \xB7 [${r.priority}] ${r.title}`),
28887
- notes: pending.map((r) => `${r.id}: ${r.objective.slice(0, 200)}`)
29028
+ question: `Ada ${pending2.length} backlog siap dikerjakan. Urutkan mana yang lebih dulu berdasarkan isi pekerjaannya, lalu tuliskan urutan id-nya (dipisah koma) di \`decision\`.`,
29029
+ options: pending2.map((r) => `${r.id} \xB7 [${r.priority}] ${r.title}`),
29030
+ notes: pending2.map((r) => `${r.id}: ${r.objective.slice(0, 200)}`)
28888
29031
  });
28889
29032
  if (!row || row.status !== "berlaku") return 0;
28890
- const byId2 = new Map(pending.map((r) => [r.id.toLowerCase(), r]));
29033
+ const byId2 = new Map(pending2.map((r) => [r.id.toLowerCase(), r]));
28891
29034
  const named = [];
28892
29035
  for (const tok of row.answer.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean)) {
28893
29036
  const hit = byId2.get(tok);
28894
29037
  if (hit && !named.includes(hit)) named.push(hit);
28895
29038
  }
28896
- const ordered = [...named, ...pending.filter((r) => !named.includes(r))];
29039
+ const ordered = [...named, ...pending2.filter((r) => !named.includes(r))];
28897
29040
  for (const r of ordered) {
28898
29041
  await deps.enqueue({ specId: r.id, projectId: r.projectId, source: "lead", priority: r.priority });
28899
29042
  }
@@ -28902,12 +29045,12 @@ async function orderProject(projectId, deps) {
28902
29045
  }
28903
29046
 
28904
29047
  // src/services/lead/engine.ts
28905
- var TICK_MS = 5e3;
29048
+ var TICK_MS2 = 5e3;
28906
29049
  var busyDetect = false;
28907
29050
  var busyPulse = false;
28908
29051
  var lastPulseAt = 0;
28909
29052
  var pulseEndedAt = 0;
28910
- var timer3;
29053
+ var timer4;
28911
29054
  function lastPulse() {
28912
29055
  return lastPulseAt;
28913
29056
  }
@@ -28960,9 +29103,9 @@ async function tick2(now, deps = {}) {
28960
29103
  await Promise.all(jobs);
28961
29104
  }
28962
29105
  function startLead(deps = {}) {
28963
- if (timer3) return;
28964
- timer3 = setInterval(() => void tick2(Date.now(), deps), TICK_MS);
28965
- timer3.unref();
29106
+ if (timer4) return;
29107
+ timer4 = setInterval(() => void tick2(Date.now(), deps), TICK_MS2);
29108
+ timer4.unref();
28966
29109
  void tick2(Date.now(), deps);
28967
29110
  }
28968
29111
 
@@ -29164,9 +29307,9 @@ init_db();
29164
29307
 
29165
29308
  // src/services/changelog/collect.ts
29166
29309
  init_db();
29167
- import { execFile as execFile10 } from "node:child_process";
29168
- import { promisify as promisify9 } from "node:util";
29169
- import { existsSync as existsSync13 } from "node:fs";
29310
+ import { execFile as execFile11 } from "node:child_process";
29311
+ import { promisify as promisify10 } from "node:util";
29312
+ import { existsSync as existsSync14 } from "node:fs";
29170
29313
 
29171
29314
  // src/services/changelog/scrub.ts
29172
29315
  var MERGE = /^merge\s+(branch|pull request|remote-tracking branch|tag)\b/i;
@@ -29227,32 +29370,32 @@ async function collectBacklog(projectId, from, to) {
29227
29370
  notes.push(`${stampless} item selesai tanpa stempel waktu (selesai sebelum stempel ini ada) dan tak ikut dihitung.`);
29228
29371
  return { ok: true, input: { mode: "backlog", title: `${from} \u2013 ${to}`, items, notes } };
29229
29372
  }
29230
- var exec8 = promisify9(execFile10);
29373
+ var exec9 = promisify10(execFile11);
29231
29374
  var GIT7 = { maxBuffer: 1 << 24, encoding: "utf8" };
29232
29375
  var US2 = "";
29233
29376
  var RS = "";
29234
29377
  var NO_REPO = "project ini belum ditautkan ke repo di mesin ini";
29235
29378
  var NO_TAG = "repo project ini belum punya tag rilis";
29236
- var usable = (repoDir) => !!repoDir && existsSync13(repoDir);
29379
+ var usable = (repoDir) => !!repoDir && existsSync14(repoDir);
29237
29380
  async function listTags(repoDir) {
29238
29381
  if (!usable(repoDir)) return { tags: [], head: null, reason: NO_REPO };
29239
29382
  try {
29240
- const { stdout } = await exec8("git", ["tag", "--list", "--sort=-creatordate"], { cwd: repoDir, ...GIT7 });
29383
+ const { stdout } = await exec9("git", ["tag", "--list", "--sort=-creatordate"], { cwd: repoDir, ...GIT7 });
29241
29384
  const tags = stdout.split("\n").map((s2) => s2.trim()).filter(Boolean);
29242
- const head = await exec8("git", ["rev-parse", "--short", "HEAD"], { cwd: repoDir, ...GIT7 }).then((r) => r.stdout.trim()).catch(() => null);
29385
+ const head = await exec9("git", ["rev-parse", "--short", "HEAD"], { cwd: repoDir, ...GIT7 }).then((r) => r.stdout.trim()).catch(() => null);
29243
29386
  return { tags, head, reason: tags.length ? null : NO_TAG };
29244
29387
  } catch {
29245
29388
  return { tags: [], head: null, reason: NO_TAG };
29246
29389
  }
29247
29390
  }
29248
29391
  async function previousTag(repoDir, tag2) {
29249
- return exec8("git", ["describe", "--tags", "--abbrev=0", "--end-of-options", `${tag2}^`], { cwd: repoDir, ...GIT7 }).then((r) => r.stdout.trim() || null).catch(() => null);
29392
+ return exec9("git", ["describe", "--tags", "--abbrev=0", "--end-of-options", `${tag2}^`], { cwd: repoDir, ...GIT7 }).then((r) => r.stdout.trim() || null).catch(() => null);
29250
29393
  }
29251
29394
  async function revExists(repoDir, rev) {
29252
- return exec8("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${rev}^{commit}`], { cwd: repoDir, ...GIT7 }).then(() => true).catch(() => false);
29395
+ return exec9("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${rev}^{commit}`], { cwd: repoDir, ...GIT7 }).then(() => true).catch(() => false);
29253
29396
  }
29254
29397
  async function logRange(repoDir, range) {
29255
- const { stdout } = await exec8(
29398
+ const { stdout } = await exec9(
29256
29399
  "git",
29257
29400
  ["log", "--no-merges", `--pretty=format:%s${US2}%b${RS}`, ...range],
29258
29401
  { cwd: repoDir, ...GIT7 }
@@ -29496,7 +29639,7 @@ init_db();
29496
29639
  init_src();
29497
29640
  import { readFileSync as readFileSync13 } from "node:fs";
29498
29641
  import { homedir as homedir9 } from "node:os";
29499
- import { join as join16 } from "node:path";
29642
+ import { join as join17 } from "node:path";
29500
29643
  var home = () => process.env.HOME || homedir9();
29501
29644
  var readJson2 = (path) => {
29502
29645
  try {
@@ -29513,7 +29656,7 @@ var serversOf = (node) => {
29513
29656
  var codexServers = () => {
29514
29657
  let text;
29515
29658
  try {
29516
- text = readFileSync13(join16(home(), ".codex", "config.toml"), "utf8");
29659
+ text = readFileSync13(join17(home(), ".codex", "config.toml"), "utf8");
29517
29660
  } catch {
29518
29661
  return [];
29519
29662
  }
@@ -29526,12 +29669,12 @@ var codexServers = () => {
29526
29669
  };
29527
29670
  function mcpServerNames(repoDir) {
29528
29671
  const names = [];
29529
- const claudeJson = readJson2(join16(home(), ".claude.json"));
29672
+ const claudeJson = readJson2(join17(home(), ".claude.json"));
29530
29673
  names.push(...serversOf(claudeJson));
29531
29674
  if (repoDir) {
29532
29675
  const projects = claudeJson?.projects;
29533
29676
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
29534
- names.push(...serversOf(readJson2(join16(repoDir, ".mcp.json"))));
29677
+ names.push(...serversOf(readJson2(join17(repoDir, ".mcp.json"))));
29535
29678
  }
29536
29679
  names.push(...codexServers());
29537
29680
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -29830,7 +29973,7 @@ async function resolveGithubRepo(projectId) {
29830
29973
 
29831
29974
  // src/services/github-fetch.ts
29832
29975
  init_config2();
29833
- import { execFile as execFile11 } from "node:child_process";
29976
+ import { execFile as execFile12 } from "node:child_process";
29834
29977
  var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
29835
29978
  var norm2 = (s2) => s2.toLowerCase() === "closed" ? "closed" : "open";
29836
29979
  function issueFromGh(raw) {
@@ -29870,15 +30013,15 @@ function issuesFromRest(raw) {
29870
30013
  }
29871
30014
  var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
29872
30015
  var API2 = "https://api.github.com";
29873
- var defaultRunGh = (args, env) => new Promise((resolve16, reject2) => {
29874
- execFile11(
30016
+ var defaultRunGh = (args, env) => new Promise((resolve17, reject2) => {
30017
+ execFile12(
29875
30018
  args[0],
29876
30019
  args.slice(1),
29877
30020
  { env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
29878
30021
  (err, stdout, stderr) => {
29879
30022
  const e = err;
29880
30023
  if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject2(e);
29881
- resolve16({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
30024
+ resolve17({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
29882
30025
  }
29883
30026
  );
29884
30027
  });
@@ -30295,7 +30438,7 @@ async function testTelegramConnection(deps = {}) {
30295
30438
  }
30296
30439
  const timeoutMs = deps.timeoutMs ?? 1e4;
30297
30440
  const controller = new AbortController();
30298
- const timer7 = setTimeout(() => controller.abort(), timeoutMs);
30441
+ const timer8 = setTimeout(() => controller.abort(), timeoutMs);
30299
30442
  const send = deps.transport ?? ((url2, init) => fetch(url2, init));
30300
30443
  const transport = (url2, init) => send(url2, { ...init, signal: controller.signal });
30301
30444
  const client = new TelegramApiClient(botToken, transport);
@@ -30309,7 +30452,7 @@ async function testTelegramConnection(deps = {}) {
30309
30452
  const message = controller.signal.aborted ? `Timeout ${timeoutMs} ms \u2014 Telegram tidak menjawab.` : raw;
30310
30453
  return { ok: false, inbound, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
30311
30454
  } finally {
30312
- clearTimeout(timer7);
30455
+ clearTimeout(timer8);
30313
30456
  }
30314
30457
  }
30315
30458
  function resolveTestChatId() {
@@ -30464,7 +30607,7 @@ function matchingEndpoints(eventType, projectId) {
30464
30607
  var newSecret = () => randomBytes6(32).toString("base64url");
30465
30608
  var encryptEndpointSecret = (plain) => encryptSecret(plain);
30466
30609
  var secretOf = (row) => decryptSecret(row.secret);
30467
- function endpointView(r, pending, plainSecret) {
30610
+ function endpointView(r, pending2, plainSecret) {
30468
30611
  const n2 = normalize(r);
30469
30612
  const plain = plainSecret ?? secretOf(r);
30470
30613
  return {
@@ -30485,7 +30628,7 @@ function endpointView(r, pending, plainSecret) {
30485
30628
  lastSuccessAt: n2.lastSuccessAt?.toISOString() ?? null,
30486
30629
  lastFailureAt: n2.lastFailureAt?.toISOString() ?? null,
30487
30630
  failureStreak: n2.failureStreak,
30488
- pending,
30631
+ pending: pending2,
30489
30632
  createdAt: n2.createdAt.toISOString(),
30490
30633
  updatedAt: n2.updatedAt.toISOString(),
30491
30634
  ...plainSecret ? { secret: plainSecret } : {}
@@ -30615,7 +30758,7 @@ async function sendOnce(o, deps = {}) {
30615
30758
  nowSec: o.nowSec ?? Math.floor(Date.now() / 1e3)
30616
30759
  });
30617
30760
  const ac = new AbortController();
30618
- const timer7 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
30761
+ const timer8 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
30619
30762
  try {
30620
30763
  const f = deps.fetcher ?? ((u, init) => fetch(u, init));
30621
30764
  const res = await f(parsed.url.toString(), { method: "POST", headers, body: o.body, signal: ac.signal });
@@ -30629,7 +30772,7 @@ async function sendOnce(o, deps = {}) {
30629
30772
  const msg = e.name === "AbortError" ? `timeout ${WEBHOOK_TIMEOUT_MS} ms` : e.message;
30630
30773
  return fail2(msg);
30631
30774
  } finally {
30632
- clearTimeout(timer7);
30775
+ clearTimeout(timer8);
30633
30776
  }
30634
30777
  }
30635
30778
 
@@ -30953,8 +31096,8 @@ init_db();
30953
31096
  // src/services/auth.ts
30954
31097
  init_db();
30955
31098
  import { randomBytes as randomBytes7, scrypt as scryptCb, timingSafeEqual as timingSafeEqual2, createHash as createHash7 } from "node:crypto";
30956
- import { promisify as promisify10 } from "node:util";
30957
- var scrypt = promisify10(scryptCb);
31099
+ import { promisify as promisify11 } from "node:util";
31100
+ var scrypt = promisify11(scryptCb);
30958
31101
  var COOKIE_NAME = "hn_session";
30959
31102
  var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
30960
31103
  async function hashPassword(pw) {
@@ -31417,7 +31560,7 @@ var PUBLIC = /* @__PURE__ */ new Set([
31417
31560
  "GET /api/agent-integration.md"
31418
31561
  ]);
31419
31562
  function buildApp({ requireAuth = true, agentDocFile } = {}) {
31420
- const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname14(fileURLToPath4(import.meta.url)), process.env, existsSync14);
31563
+ const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname14(fileURLToPath4(import.meta.url)), process.env, existsSync15);
31421
31564
  const app2 = Fastify({ logger: false, trustProxy: true });
31422
31565
  app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
31423
31566
  if (!body) return done(null, void 0);
@@ -31508,7 +31651,7 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
31508
31651
  await api.register(client_accounts_default);
31509
31652
  }, { prefix: "/api" });
31510
31653
  if (process.env.NODE_ENV === "production") {
31511
- const dist = pickWebDir(dirname14(fileURLToPath4(import.meta.url)), process.env, existsSync14);
31654
+ const dist = pickWebDir(dirname14(fileURLToPath4(import.meta.url)), process.env, existsSync15);
31512
31655
  if (dist) {
31513
31656
  app2.register(fastifyStatic, { root: dist });
31514
31657
  app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));
@@ -31826,12 +31969,12 @@ var prodDeps = {
31826
31969
  launchCron: async (cron) => (await startCronSession(cron)).id
31827
31970
  })
31828
31971
  };
31829
- var TICK_MS2 = 1e4;
31830
- var timer4;
31972
+ var TICK_MS3 = 1e4;
31973
+ var timer5;
31831
31974
  function startScheduler(deps = prodDeps) {
31832
- if (timer4) return;
31833
- timer4 = setInterval(() => void tick3(Date.now(), deps), TICK_MS2);
31834
- timer4.unref();
31975
+ if (timer5) return;
31976
+ timer5 = setInterval(() => void tick3(Date.now(), deps), TICK_MS3);
31977
+ timer5.unref();
31835
31978
  void tick3(Date.now(), deps);
31836
31979
  }
31837
31980
 
@@ -31928,10 +32071,10 @@ function buildEnvelope(i, projectName, nowIso2, eventId) {
31928
32071
  }
31929
32072
  async function enqueueEnvelope(env, endpoints) {
31930
32073
  for (const e of endpoints) {
31931
- const pending = await prisma.webhookDelivery.count({
32074
+ const pending2 = await prisma.webhookDelivery.count({
31932
32075
  where: { endpointId: e.id, status: { in: ["pending", "sending"] } }
31933
32076
  });
31934
- const full = pending >= WEBHOOK_QUEUE_CAP;
32077
+ const full = pending2 >= WEBHOOK_QUEUE_CAP;
31935
32078
  await prisma.webhookDelivery.create({
31936
32079
  data: {
31937
32080
  endpointId: e.id,
@@ -31979,7 +32122,7 @@ async function installWebhooks() {
31979
32122
  // src/services/webhooks/engine.ts
31980
32123
  init_src();
31981
32124
  init_db();
31982
- var TICK_MS3 = 2e3;
32125
+ var TICK_MS4 = 2e3;
31983
32126
  var MAX_IN_FLIGHT = 4;
31984
32127
  var PRUNE_EVERY_TICKS = 30;
31985
32128
  function backoffAt(from, attempt) {
@@ -32122,7 +32265,7 @@ async function pruneHistory() {
32122
32265
  }
32123
32266
  return removed;
32124
32267
  }
32125
- var timer5;
32268
+ var timer6;
32126
32269
  var ticks = 0;
32127
32270
  var busy2 = false;
32128
32271
  async function tick4() {
@@ -32138,9 +32281,9 @@ async function tick4() {
32138
32281
  }
32139
32282
  }
32140
32283
  function startWebhookEngine() {
32141
- if (timer5) return;
32142
- timer5 = setInterval(() => void tick4(), TICK_MS3);
32143
- timer5.unref();
32284
+ if (timer6) return;
32285
+ timer6 = setInterval(() => void tick4(), TICK_MS4);
32286
+ timer6.unref();
32144
32287
  void resetStuckDeliveries().then((n2) => {
32145
32288
  if (n2) console.log(`webhook: ${n2} pengiriman tertinggal dikembalikan ke antrean`);
32146
32289
  }).catch((e) => console.error("webhook reset:", e));
@@ -32149,16 +32292,16 @@ function startWebhookEngine() {
32149
32292
  // src/services/auto-merge.ts
32150
32293
  init_src();
32151
32294
  init_db();
32152
- import { execFile as execFile12 } from "node:child_process";
32153
- import { promisify as promisify11 } from "node:util";
32295
+ import { execFile as execFile13 } from "node:child_process";
32296
+ import { promisify as promisify12 } from "node:util";
32154
32297
  var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
32155
32298
  var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
32156
- var TICK_MS4 = 6e4;
32157
- var exec9 = promisify11(execFile12);
32299
+ var TICK_MS5 = 6e4;
32300
+ var exec10 = promisify12(execFile13);
32158
32301
  var GIT8 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
32159
32302
  var gitOut = async (cwd, args) => {
32160
32303
  try {
32161
- return (await exec9("git", args, { cwd, ...GIT8 })).stdout.trim();
32304
+ return (await exec10("git", args, { cwd, ...GIT8 })).stdout.trim();
32162
32305
  } catch {
32163
32306
  return null;
32164
32307
  }
@@ -32241,7 +32384,7 @@ async function settleOne(c, deps, now) {
32241
32384
  }
32242
32385
  return report(`Auto-merge ${spec.id} \u2192 ${target2} GAGAL: ${res.error}`);
32243
32386
  }
32244
- var timer6;
32387
+ var timer7;
32245
32388
  var busy3 = false;
32246
32389
  async function tick5() {
32247
32390
  if (busy3) return;
@@ -32255,9 +32398,9 @@ async function tick5() {
32255
32398
  }
32256
32399
  }
32257
32400
  function startAutoMerge() {
32258
- if (timer6) return;
32259
- timer6 = setInterval(() => void tick5(), TICK_MS4);
32260
- timer6.unref();
32401
+ if (timer7) return;
32402
+ timer7 = setInterval(() => void tick5(), TICK_MS5);
32403
+ timer7.unref();
32261
32404
  }
32262
32405
 
32263
32406
  // src/server.ts
@@ -32307,6 +32450,7 @@ app.listen({ port, host }).then(async () => {
32307
32450
  startLead();
32308
32451
  startWebhookEngine();
32309
32452
  startAutoMerge();
32453
+ startWorktreeReaper();
32310
32454
  }).catch((err) => {
32311
32455
  console.error("listen gagal:", err);
32312
32456
  process.exit(1);