hanoman 0.1.9 → 0.1.11

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
@@ -617,10 +617,10 @@ function isValidJWT(jwt, alg) {
617
617
  if (!jwtRegex.test(jwt))
618
618
  return false;
619
619
  try {
620
- const [header] = jwt.split(".");
621
- if (!header)
620
+ const [header2] = jwt.split(".");
621
+ if (!header2)
622
622
  return false;
623
- const base64 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "=");
623
+ const base64 = header2.replace(/-/g, "+").replace(/_/g, "/").padEnd(header2.length + (4 - header2.length % 4) % 4, "=");
624
624
  const decoded = JSON.parse(atob(base64));
625
625
  if (typeof decoded !== "object" || decoded === null)
626
626
  return false;
@@ -4164,6 +4164,105 @@ var init_enums = __esm({
4164
4164
  }
4165
4165
  });
4166
4166
 
4167
+ // ../shared/src/telegram.ts
4168
+ var zTelegramSettings, TELEGRAM_DEFAULTS, zTelegramReadiness, zTelegramGatewayStatus, zTelegramMemoryRecord, zTelegramChatContext, TELEGRAM_REPLY_KINDS, zTelegramReplyKind, zTelegramConfirmationRequest, zTelegramReplyInput, zTelegramAuditRecord, zTerminalSteerInput;
4169
+ var init_telegram = __esm({
4170
+ "../shared/src/telegram.ts"() {
4171
+ "use strict";
4172
+ init_zod();
4173
+ init_enums();
4174
+ zTelegramSettings = external_exports.object({
4175
+ enabled: external_exports.boolean().default(false),
4176
+ progress: external_exports.boolean().default(true)
4177
+ });
4178
+ TELEGRAM_DEFAULTS = zTelegramSettings.parse({});
4179
+ zTelegramReadiness = external_exports.enum([
4180
+ "disabled",
4181
+ "misconfigured",
4182
+ "ready",
4183
+ "running",
4184
+ "error"
4185
+ ]);
4186
+ zTelegramGatewayStatus = external_exports.object({
4187
+ configured: external_exports.boolean(),
4188
+ enabled: external_exports.boolean(),
4189
+ running: external_exports.boolean(),
4190
+ readiness: zTelegramReadiness,
4191
+ botUsername: external_exports.string().min(1).nullable().default(null),
4192
+ allowlistCount: external_exports.number().int().min(0),
4193
+ agentTokenConfigured: external_exports.boolean(),
4194
+ missingCapabilities: external_exports.array(external_exports.string()).default([]),
4195
+ lastUpdateAt: external_exports.string().datetime().nullable().default(null),
4196
+ lastError: external_exports.string().nullable().default(null)
4197
+ });
4198
+ zTelegramMemoryRecord = external_exports.object({
4199
+ id: external_exports.string().min(1),
4200
+ chatId: external_exports.string().min(1),
4201
+ content: external_exports.string().trim().min(1).max(1e3),
4202
+ createdAt: external_exports.string().datetime(),
4203
+ updatedAt: external_exports.string().datetime()
4204
+ });
4205
+ zTelegramChatContext = external_exports.object({
4206
+ chatId: external_exports.string().min(1),
4207
+ userId: external_exports.string().min(1),
4208
+ sessionId: external_exports.string().nullable(),
4209
+ activeProjectId: external_exports.string().nullable(),
4210
+ activeSessionId: external_exports.string().nullable(),
4211
+ personalityAgentId: external_exports.string().nullable(),
4212
+ summary: external_exports.string().max(4e3).nullable(),
4213
+ agent: zAgent,
4214
+ model: external_exports.string().min(1),
4215
+ effort: external_exports.string().min(1),
4216
+ memories: external_exports.array(zTelegramMemoryRecord)
4217
+ });
4218
+ TELEGRAM_REPLY_KINDS = [
4219
+ "progress",
4220
+ "final",
4221
+ "decision",
4222
+ "failure",
4223
+ "confirmation"
4224
+ ];
4225
+ zTelegramReplyKind = external_exports.enum(TELEGRAM_REPLY_KINDS);
4226
+ zTelegramConfirmationRequest = external_exports.object({
4227
+ description: external_exports.string().trim().min(1).max(500),
4228
+ method: external_exports.enum(["POST", "PATCH", "PUT", "DELETE"]),
4229
+ path: external_exports.string().startsWith("/api/").max(1e3)
4230
+ });
4231
+ zTelegramReplyInput = external_exports.object({
4232
+ chatId: external_exports.string().min(1),
4233
+ updateId: external_exports.number().int().nonnegative(),
4234
+ kind: zTelegramReplyKind,
4235
+ text: external_exports.string().trim().min(1).max(12e3),
4236
+ summary: external_exports.string().trim().min(1).max(4e3).optional(),
4237
+ remember: external_exports.array(external_exports.string().trim().min(1).max(1e3)).max(20).optional(),
4238
+ confirmation: zTelegramConfirmationRequest.optional()
4239
+ }).superRefine((value, ctx) => {
4240
+ if (value.kind === "confirmation" && !value.confirmation) {
4241
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["confirmation"], message: "confirmation required" });
4242
+ }
4243
+ if (value.kind !== "confirmation" && value.confirmation) {
4244
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["confirmation"], message: "confirmation only valid for confirmation replies" });
4245
+ }
4246
+ });
4247
+ zTelegramAuditRecord = external_exports.object({
4248
+ id: external_exports.string().min(1),
4249
+ chatId: external_exports.string().nullable(),
4250
+ userId: external_exports.string().nullable(),
4251
+ updateId: external_exports.number().int().nonnegative().nullable(),
4252
+ action: external_exports.string().min(1),
4253
+ outcome: external_exports.string().min(1),
4254
+ correlationId: external_exports.string().nullable(),
4255
+ method: external_exports.string().nullable(),
4256
+ path: external_exports.string().nullable(),
4257
+ statusCode: external_exports.number().int().nullable(),
4258
+ createdAt: external_exports.string().datetime()
4259
+ });
4260
+ zTerminalSteerInput = external_exports.object({
4261
+ text: external_exports.string().trim().min(1).max(16e3)
4262
+ });
4263
+ }
4264
+ });
4265
+
4167
4266
  // ../shared/src/entities.ts
4168
4267
  function codexModel(id) {
4169
4268
  return CODEX_MODELS.find((m) => m.id === id);
@@ -4188,6 +4287,7 @@ var init_entities = __esm({
4188
4287
  "use strict";
4189
4288
  init_zod();
4190
4289
  init_enums();
4290
+ init_telegram();
4191
4291
  zProject = external_exports.object({
4192
4292
  id: external_exports.string(),
4193
4293
  name: external_exports.string(),
@@ -4376,8 +4476,10 @@ var init_entities = __esm({
4376
4476
  // SPEC-376 · ADR-0080 · scope verifikasi sesi
4377
4477
  conflict: zConflict.default(CONFLICT_DEFAULTS),
4378
4478
  // SPEC-383 · ADR-0081 · default sesi konflik rebase/merge
4379
- lead: zLead.default(LEAD_DEFAULTS)
4479
+ lead: zLead.default(LEAD_DEFAULTS),
4380
4480
  // SPEC-409 · ADR-0091 · hanoman-lead (default mati)
4481
+ telegram: zTelegramSettings.default(TELEGRAM_DEFAULTS)
4482
+ // SPEC-476 · ADR-0096 · gateway Telegram (default mati)
4381
4483
  });
4382
4484
  zNotification = external_exports.object({
4383
4485
  id: external_exports.string(),
@@ -4451,7 +4553,11 @@ var init_agent = __esm({
4451
4553
  // definisi agen mengubah apa yang dilihat SETIAP sesi baru di seluruh workspace, jadi izin
4452
4554
  // baca tak pernah cukup untuk itu (kelas bug SPEC-405).
4453
4555
  "agents:read",
4454
- "agents:write"
4556
+ "agents:write",
4557
+ // SPEC-476 · ADR-0096 · context/memory/reply/audit kanal Telegram. Aksi produk tetap memakai
4558
+ // capability domain produk masing-masing; domain ini tidak memberi akses shell atau sesi.
4559
+ "telegram:read",
4560
+ "telegram:write"
4455
4561
  ];
4456
4562
  zCapability = external_exports.enum(CAPABILITY_IDS);
4457
4563
  zCapabilityInfo = external_exports.object({
@@ -4484,7 +4590,9 @@ var init_agent = __esm({
4484
4590
  { id: "lead:read", domain: "lead", access: "read", label: "Lead \u2014 baca", desc: "Baca jejak keputusan hanoman-lead & statusnya." },
4485
4591
  { id: "lead:write", domain: "lead", access: "write", label: "Lead \u2014 tulis", desc: "Minta putusan ke hanoman-lead (keputusan bisa menggerakkan sesi).", risk: "exec" },
4486
4592
  { id: "agents:read", domain: "agents", access: "read", label: "Custom agent \u2014 baca", desc: "Lihat katalog custom agent global & per project." },
4487
- { id: "agents:write", domain: "agents", access: "write", label: "Custom agent \u2014 tulis", desc: "Buat/ubah/hapus custom agent; definisinya dipakai setiap sesi baru.", risk: "exec" }
4593
+ { id: "agents:write", domain: "agents", access: "write", label: "Custom agent \u2014 tulis", desc: "Buat/ubah/hapus custom agent; definisinya dipakai setiap sesi baru.", risk: "exec" },
4594
+ { id: "telegram:read", domain: "telegram", access: "read", label: "Telegram \u2014 baca", desc: "Baca status gateway, binding, memory, dan audit Telegram." },
4595
+ { id: "telegram:write", domain: "telegram", access: "write", label: "Telegram \u2014 tulis", desc: "Perbarui context/memory dan terbitkan reply sesi operator Telegram." }
4488
4596
  ];
4489
4597
  zAgentTokenView = external_exports.object({
4490
4598
  id: external_exports.string(),
@@ -4606,6 +4714,47 @@ var init_custom_agent = __esm({
4606
4714
  }
4607
4715
  });
4608
4716
 
4717
+ // ../shared/src/github.ts
4718
+ function sourceForLabels(labels) {
4719
+ const hay = labels.map((l) => l.toLowerCase());
4720
+ for (const rule2 of LABEL_RULES)
4721
+ if (hay.some((l) => rule2.needles.some((n) => l.includes(n)))) return rule2.source;
4722
+ return "qa";
4723
+ }
4724
+ var zGithubIssueStatus, zNormalIssue, zGithubIssueView, LABEL_RULES;
4725
+ var init_github = __esm({
4726
+ "../shared/src/github.ts"() {
4727
+ "use strict";
4728
+ init_zod();
4729
+ zGithubIssueStatus = external_exports.enum(["new", "accepted", "rejected"]);
4730
+ zNormalIssue = external_exports.object({
4731
+ number: external_exports.number().int().positive(),
4732
+ title: external_exports.string(),
4733
+ body: external_exports.string(),
4734
+ authorLogin: external_exports.string(),
4735
+ labels: external_exports.array(external_exports.string()),
4736
+ url: external_exports.string(),
4737
+ issueState: external_exports.enum(["open", "closed"]),
4738
+ issueCreatedAt: external_exports.string(),
4739
+ // ISO-8601 apa adanya dari GitHub
4740
+ issueUpdatedAt: external_exports.string()
4741
+ });
4742
+ zGithubIssueView = zNormalIssue.extend({
4743
+ id: external_exports.string(),
4744
+ projectId: external_exports.string(),
4745
+ repoSlug: external_exports.string(),
4746
+ status: zGithubIssueStatus,
4747
+ specId: external_exports.string().nullable(),
4748
+ pulledAt: external_exports.string()
4749
+ });
4750
+ LABEL_RULES = [
4751
+ { needles: ["bug", "defect", "regression"], source: "qa" },
4752
+ { needles: ["question", "docs", "documentation"], source: "audit" },
4753
+ { needles: ["enhancement", "feature", "feat"], source: "brief" }
4754
+ ];
4755
+ }
4756
+ });
4757
+
4609
4758
  // ../shared/src/lead.ts
4610
4759
  function leadActionAllowed(action) {
4611
4760
  return LEAD_ACTIONS.includes(action);
@@ -5167,6 +5316,8 @@ var init_api = __esm({
5167
5316
  fsBrowse: (path) => `${API}/fs/browse${path ? `?path=${encodeURIComponent(path)}` : ""}`,
5168
5317
  terminalSessions: `${API}/terminal/sessions`,
5169
5318
  terminalSession: (id) => `${API}/terminal/sessions/${id}`,
5319
+ terminalSteer: (id) => `${API}/terminal/sessions/${id}/steer`,
5320
+ terminalInterrupt: (id) => `${API}/terminal/sessions/${id}/interrupt`,
5170
5321
  terminalPhases: (id) => `${API}/terminal/sessions/${id}/phases`,
5171
5322
  // SPEC-230 · review + integrate ber-skop sesi (sesi project-level PRD, tanpa Spec).
5172
5323
  sessionReview: (id) => `${API}/terminal/sessions/${id}/review`,
@@ -5221,6 +5372,13 @@ var init_api = __esm({
5221
5372
  // SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
5222
5373
  customAgents: `${API}/custom-agents`,
5223
5374
  customAgent: (id) => `${API}/custom-agents/${encodeURIComponent(id)}`,
5375
+ // SPEC-476 · ADR-0096 · observability/context/reply kanal Telegram.
5376
+ telegramStatus: `${API}/telegram/status`,
5377
+ telegramContext: (chatId) => `${API}/telegram/chats/${encodeURIComponent(chatId)}/context`,
5378
+ telegramMemories: (chatId) => `${API}/telegram/chats/${encodeURIComponent(chatId)}/memories`,
5379
+ telegramMemory: (chatId, id) => `${API}/telegram/chats/${encodeURIComponent(chatId)}/memories/${encodeURIComponent(id)}`,
5380
+ telegramReplies: `${API}/telegram/replies`,
5381
+ telegramAudit: `${API}/telegram/audit`,
5224
5382
  // SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
5225
5383
  syncNow: `${API}/sync/now`,
5226
5384
  // SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
@@ -5243,6 +5401,14 @@ var init_api = __esm({
5243
5401
  ticketUnlink: (id) => `${API}/tickets/${id}/unlink`,
5244
5402
  // SPEC-271 · lepas tautan backlog
5245
5403
  ticketReject: (id) => `${API}/tickets/${id}/reject`,
5404
+ // SPEC-471 · ADR-0095 · tarik & triase issue GitHub (di belakang gate cookie, capability `support`).
5405
+ // hanoman TIDAK PERNAH menulis ke GitHub — tak ada path komentar/close di sini, dan itu disengaja.
5406
+ githubPull: (id) => `${API}/projects/${encodeURIComponent(id)}/github/pull`,
5407
+ githubIssues: (id) => `${API}/projects/${encodeURIComponent(id)}/github/issues`,
5408
+ githubIssuesAccept: `${API}/github-issues/accept`,
5409
+ githubIssueAccept: (id) => `${API}/github-issues/${encodeURIComponent(id)}/accept`,
5410
+ githubIssueReject: (id) => `${API}/github-issues/${encodeURIComponent(id)}/reject`,
5411
+ githubIssueUnlink: (id) => `${API}/github-issues/${encodeURIComponent(id)}/unlink`,
5246
5412
  // SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
5247
5413
  // (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
5248
5414
  download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
@@ -5335,7 +5501,8 @@ var init_session_kind = __esm({
5335
5501
  "vps",
5336
5502
  "shell",
5337
5503
  "worktree",
5338
- "terminal"
5504
+ "terminal",
5505
+ "telegram"
5339
5506
  ];
5340
5507
  zSessionKind = external_exports.enum(SESSION_KINDS);
5341
5508
  }
@@ -5450,6 +5617,28 @@ var init_config_registry = __esm({
5450
5617
  // vps
5451
5618
  { key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", help: "Default ~/.hanoman." },
5452
5619
  { key: "HANOMAN_SSH_BIN", group: "vps", label: "Biner ssh", kind: "path", apply: "new-session", category: "knob", default: "ssh" },
5620
+ // github (SPEC-471 · ADR-0095 · tarik issue → backlog). Dua mode auth, satu jalur kode:
5621
+ // `gh` memakai keyring mesin; bila GITHUB_TOKEN diisi ia diteruskan sebagai GH_TOKEN ke
5622
+ // proses gh DAN dipakai jalur REST (hub VPS yang tak punya keyring).
5623
+ {
5624
+ key: "GITHUB_TOKEN",
5625
+ group: "github",
5626
+ label: "GitHub token",
5627
+ kind: "secret",
5628
+ apply: "live",
5629
+ category: "credential",
5630
+ help: "PAT scope `repo` (atau `public_repo`). Kosong = andalkan `gh auth login` di mesin ini."
5631
+ },
5632
+ {
5633
+ key: "HANOMAN_GH_BIN",
5634
+ group: "github",
5635
+ label: "Biner gh",
5636
+ kind: "path",
5637
+ apply: "live",
5638
+ category: "knob",
5639
+ default: "gh",
5640
+ help: "Absen = tarik issue lewat HTTPS langsung ke api.github.com."
5641
+ },
5453
5642
  // runtime
5454
5643
  { key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", default: "1000", min: 100 },
5455
5644
  { key: "HANOMAN_UPDATE_FETCH", group: "runtime", label: "Deteksi update saat boot", kind: "bool", apply: "live", category: "knob", default: "1" },
@@ -5542,6 +5731,7 @@ var init_src = __esm({
5542
5731
  init_entities();
5543
5732
  init_agent();
5544
5733
  init_custom_agent();
5734
+ init_github();
5545
5735
  init_lead();
5546
5736
  init_dto();
5547
5737
  init_api();
@@ -5551,6 +5741,7 @@ var init_src = __esm({
5551
5741
  init_config();
5552
5742
  init_config_registry();
5553
5743
  init_semver();
5744
+ init_telegram();
5554
5745
  }
5555
5746
  });
5556
5747
 
@@ -6425,6 +6616,70 @@ var init_paths = __esm({
6425
6616
  }
6426
6617
  });
6427
6618
 
6619
+ // ../runner/src/telegram-operator.ts
6620
+ function buildTelegramOperatorPrompt(input) {
6621
+ const personality = input.personality ? [
6622
+ `## Personality aktif: @${input.personality.name}`,
6623
+ input.personality.description,
6624
+ input.personality.instructions
6625
+ ].join("\n\n") : "## Personality aktif\n\nGunakan fondasi custom agent Hanoman bawaan dan gaya operator yang ringkas.";
6626
+ const summary = input.summary?.trim() || "Belum ada ringkasan percakapan tersimpan.";
6627
+ const memories = input.memories?.length ? input.memories.map((memory) => `- ${memory.id}: ${memory.content}`).join("\n") : "- Belum ada curated memory.";
6628
+ const { update: update2 } = input;
6629
+ return [
6630
+ "# Hanoman \xB7 session operator Telegram persisten",
6631
+ "",
6632
+ "Kamu adalah main session operator Hanoman yang persisten untuk satu private chat Telegram.",
6633
+ "Telegram hanya transport. Jangan membuat runtime agent, loop headless, tool bus, shell executor,",
6634
+ "antrean, atau otak/memory kedua. Semua aksi produk harus memakai API/service Hanoman yang sudah ada.",
6635
+ "Ikuti internal/docs sebagai Source of Truth dan capability/pagar server apa adanya.",
6636
+ "",
6637
+ personality,
6638
+ "",
6639
+ "## Context tahan restart",
6640
+ "",
6641
+ `Ringkasan: ${summary}`,
6642
+ "Curated memory:",
6643
+ memories,
6644
+ "",
6645
+ "## Kontrak kanal",
6646
+ "",
6647
+ "- Bahasa natural adalah antarmuka utama; command hanya shortcut yang tetap kamu tangani di sesi ini.",
6648
+ `- Command minimum: ${COMMANDS.join(", ")}.`,
6649
+ `- Panggil API dengan base $HANOMAN_API_BASE, bearer $HANOMAN_TELEGRAM_AGENT_TOKEN, dan header x-hanoman-telegram-update: ${update2.updateId}.`,
6650
+ "- Terbitkan jawaban user-facing secara eksplisit melalui POST $HANOMAN_API_BASE/api/telegram/replies.",
6651
+ "- Kind reply hanya progress|final|decision|failure|confirmation. Jangan mengandalkan layar PTY sebagai balasan.",
6652
+ "- Aksi sulit dibatalkan: terbitkan reply confirmation, tunggu callback approval, baru panggil endpoint aksinya.",
6653
+ "- Jangan pernah kirim internal reasoning, ANSI, echo command, secret, token, env, credential, atau isi file credential.",
6654
+ "- Curated memory dan summary harus kamu kurasi sendiri lewat API context/memory; jangan simpan transcript mentah.",
6655
+ "",
6656
+ "## Input pertama",
6657
+ "",
6658
+ `[Telegram update ${update2.updateId} \xB7 chat ${update2.chatId} \xB7 kind ${update2.kind}]`,
6659
+ update2.text
6660
+ ].join("\n");
6661
+ }
6662
+ var COMMANDS;
6663
+ var init_telegram_operator = __esm({
6664
+ "../runner/src/telegram-operator.ts"() {
6665
+ "use strict";
6666
+ COMMANDS = [
6667
+ "/help",
6668
+ "/status",
6669
+ "/projects",
6670
+ "/project <id>",
6671
+ "/backlog [query]",
6672
+ "/sessions",
6673
+ "/use <session-id>",
6674
+ "/new <brief>",
6675
+ "/stop [session-id]",
6676
+ "/memory [forget <id>|reset]",
6677
+ "/personality [nama|reset]",
6678
+ "/skills"
6679
+ ];
6680
+ }
6681
+ });
6682
+
6428
6683
  // ../runner/src/index.ts
6429
6684
  var init_src2 = __esm({
6430
6685
  "../runner/src/index.ts"() {
@@ -6441,6 +6696,7 @@ var init_src2 = __esm({
6441
6696
  init_custom_agents();
6442
6697
  init_verify_scope();
6443
6698
  init_paths();
6699
+ init_telegram_operator();
6444
6700
  }
6445
6701
  });
6446
6702
 
@@ -6568,6 +6824,15 @@ var init_session_phases = __esm({
6568
6824
  }
6569
6825
  });
6570
6826
 
6827
+ // src/services/session-id.ts
6828
+ var sessionIdForSpec;
6829
+ var init_session_id = __esm({
6830
+ "src/services/session-id.ts"() {
6831
+ "use strict";
6832
+ sessionIdForSpec = (specId) => specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
6833
+ }
6834
+ });
6835
+
6571
6836
  // src/services/tui-dialog.ts
6572
6837
  function readChoiceDialog(paneText2) {
6573
6838
  const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
@@ -6582,7 +6847,7 @@ function readChoiceDialog(paneText2) {
6582
6847
  const found = [];
6583
6848
  for (const line of lines2.slice(0, footer)) {
6584
6849
  const m = ROW.exec(line);
6585
- if (m) found.push({ n: Number(m[1]), label: (m[2] ?? "").trim() });
6850
+ if (m) found.push({ n: Number(m[1]), label: cleanLabel(m[2] ?? "") });
6586
6851
  }
6587
6852
  if (found.length < 2) return null;
6588
6853
  const run2 = [];
@@ -6606,6 +6871,65 @@ function readChoiceDialog(paneText2) {
6606
6871
  options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
6607
6872
  };
6608
6873
  }
6874
+ function readTabs(lines2) {
6875
+ for (let i = lines2.length - 1; i >= 0; i--) {
6876
+ const line = lines2[i] ?? "";
6877
+ if (!/[☐☒]/.test(line)) continue;
6878
+ const tabs = [];
6879
+ for (const tok of line.split(/\s{2,}/)) {
6880
+ const m = TAB_BOX.exec(tok.trim());
6881
+ if (m) tabs.push({ header: (m[2] ?? "").trim(), answered: m[1] === "\u2612" });
6882
+ }
6883
+ if (tabs.length) return { tabs, at: i };
6884
+ }
6885
+ return { tabs: [], at: -1 };
6886
+ }
6887
+ function readTitle(lines2, stripAt, footer) {
6888
+ if (stripAt < 0) return "";
6889
+ for (let i = stripAt + 1; i < footer; i++) {
6890
+ const line = (lines2[i] ?? "").trim();
6891
+ if (!line || RULE.test(line)) continue;
6892
+ return ROW.test(lines2[i] ?? "") ? "" : line;
6893
+ }
6894
+ return "";
6895
+ }
6896
+ function readReviewScreen(paneText2) {
6897
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6898
+ const at = lastIndexOf(lines2, REVIEW_PROMPT);
6899
+ if (at < 0) return null;
6900
+ for (const line of lines2.slice(at)) {
6901
+ const m = SUBMIT_ROW.exec(line);
6902
+ if (m) return { submitRow: Number(m[1]) };
6903
+ }
6904
+ return null;
6905
+ }
6906
+ function readDialogScreen(paneText2) {
6907
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6908
+ const footer = lastIndexOf(lines2, FOOTER);
6909
+ if (lastIndexOf(lines2, REVIEW_PROMPT) > footer) {
6910
+ const review = readReviewScreen(paneText2);
6911
+ if (review) return { kind: "review", submitRow: review.submitRow };
6912
+ }
6913
+ const d = readChoiceDialog(paneText2);
6914
+ if (!d) return null;
6915
+ const { tabs, at } = readTabs(lines2.slice(0, footer));
6916
+ return {
6917
+ kind: "question",
6918
+ rows: d.rows,
6919
+ freeIndex: d.freeIndex,
6920
+ options: d.options,
6921
+ notes: d.freeIndex === null && NOTES_FOOTER.test(lines2[footer] ?? ""),
6922
+ tabs,
6923
+ title: readTitle(lines2, at, footer)
6924
+ };
6925
+ }
6926
+ function dialogKey(paneText2) {
6927
+ const s = readDialogScreen(paneText2);
6928
+ if (!s) return "none";
6929
+ if (s.kind === "review") return "review";
6930
+ const tabs = s.tabs.map((t) => `${t.answered ? "x" : "o"}${t.header}`).join(",");
6931
+ return `q|${tabs}|${s.title || s.options.join("|")}`;
6932
+ }
6609
6933
  function freeTextFilled(paneText2, n) {
6610
6934
  const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n);
6611
6935
  if (!row) return false;
@@ -6623,7 +6947,37 @@ async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
6623
6947
  io.enter();
6624
6948
  return true;
6625
6949
  }
6626
- var FOOTER, ROW, PLACEHOLDER, CHAT_ROW, DIALOG_SETTLE_MS;
6950
+ function notesFilled(paneText2) {
6951
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6952
+ for (let i = lines2.length - 1; i >= 0; i--) {
6953
+ const m = NOTES_LINE.exec(lines2[i] ?? "");
6954
+ if (!m) continue;
6955
+ const v = (m[1] ?? "").trim();
6956
+ return v.length > 0 && !NOTES_PLACEHOLDER.test(v);
6957
+ }
6958
+ return false;
6959
+ }
6960
+ async function answerNotesDialog(io, line, chunkMs) {
6961
+ io.literal("n");
6962
+ await io.sleep(DIALOG_SETTLE_MS);
6963
+ for (const chunk of goalChunks(line)) {
6964
+ io.literal(chunk);
6965
+ await io.sleep(chunkMs);
6966
+ }
6967
+ await io.sleep(DIALOG_SETTLE_MS);
6968
+ if (!notesFilled(io.capture())) return false;
6969
+ io.enter();
6970
+ return true;
6971
+ }
6972
+ async function submitReview(io, submitRow) {
6973
+ io.literal(String(submitRow));
6974
+ for (let i = 0; i < SUBMIT_TRIES; i++) {
6975
+ await io.sleep(DIALOG_SETTLE_MS);
6976
+ if (!readReviewScreen(io.capture())) return true;
6977
+ }
6978
+ return false;
6979
+ }
6980
+ var FOOTER, ROW, PLACEHOLDER, CHAT_ROW, SIDE_PANEL, cleanLabel, REVIEW_PROMPT, SUBMIT_ROW, NOTES_FOOTER, TAB_BOX, lastIndexOf, RULE, DIALOG_SETTLE_MS, NOTES_PLACEHOLDER, NOTES_LINE, SUBMIT_TRIES;
6627
6981
  var init_tui_dialog = __esm({
6628
6982
  "src/services/tui-dialog.ts"() {
6629
6983
  "use strict";
@@ -6632,7 +6986,21 @@ var init_tui_dialog = __esm({
6632
6986
  ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+(\S.*)$/;
6633
6987
  PLACEHOLDER = /^(?:type something\.?|other)$/i;
6634
6988
  CHAT_ROW = /^chat about this$/i;
6989
+ SIDE_PANEL = /\s{2,}[│┃┌┐└┘├┤╭╮╰╯─━].*$/;
6990
+ cleanLabel = (s) => s.replace(SIDE_PANEL, "").trim();
6991
+ REVIEW_PROMPT = /^\s*ready to submit your answers\?\s*$/i;
6992
+ SUBMIT_ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+submit answers\s*$/i;
6993
+ NOTES_FOOTER = /\bn to add notes\b/i;
6994
+ TAB_BOX = /^([☐☒])\s*(.+)$/;
6995
+ lastIndexOf = (lines2, re) => {
6996
+ for (let i = lines2.length - 1; i >= 0; i--) if (re.test(lines2[i] ?? "")) return i;
6997
+ return -1;
6998
+ };
6999
+ RULE = /^[\s─━╌╍_=]*$/;
6635
7000
  DIALOG_SETTLE_MS = 250;
7001
+ NOTES_PLACEHOLDER = /^(?:press n to add notes|add notes on this design(?:…|\.\.\.)?)$/i;
7002
+ NOTES_LINE = /(?:^|\s)Notes:\s*(.*)$/;
7003
+ SUBMIT_TRIES = 8;
6636
7004
  }
6637
7005
  });
6638
7006
 
@@ -6751,6 +7119,7 @@ function registerCustomAgentSource(fn) {
6751
7119
  function sessionKind(o, projectId, cwd) {
6752
7120
  if (o.specId) return "spec";
6753
7121
  if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
7122
+ if (projectId.startsWith("telegram:")) return "telegram";
6754
7123
  if (projectId.startsWith("vps")) return "vps";
6755
7124
  if (o.command) return "shell";
6756
7125
  if (cwd.includes("/.worktrees/")) return "worktree";
@@ -6921,18 +7290,13 @@ async function sendToPane(id, text, chunkMs = 50) {
6921
7290
  const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
6922
7291
  if (!line) return false;
6923
7292
  try {
6924
- const io = {
6925
- capture: () => capturePane(id, DIALOG_CAPTURE_LINES),
6926
- literal: (s) => {
6927
- tmux("send-keys", "-t", name(id), "-l", s);
6928
- },
6929
- enter: () => {
6930
- tmux("send-keys", "-t", name(id), "Enter");
6931
- },
6932
- sleep
6933
- };
6934
- const free = readChoiceDialog(io.capture())?.freeIndex ?? null;
6935
- if (free !== null) return await answerChoiceDialog(io, free, line, chunkMs);
7293
+ const io = dialogIO(id);
7294
+ const screen = readDialogScreen(io.capture());
7295
+ if (screen?.kind === "review") return await submitReview(io, screen.submitRow);
7296
+ if (screen?.kind === "question") {
7297
+ if (screen.freeIndex !== null) return await answerChoiceDialog(io, screen.freeIndex, line, chunkMs);
7298
+ if (screen.notes) return await answerNotesDialog(io, line, chunkMs);
7299
+ }
6936
7300
  for (const chunk of goalChunks(line)) {
6937
7301
  io.literal(chunk);
6938
7302
  await sleep(chunkMs);
@@ -6943,6 +7307,28 @@ async function sendToPane(id, text, chunkMs = 50) {
6943
7307
  return false;
6944
7308
  }
6945
7309
  }
7310
+ function interruptPane(id) {
7311
+ const pane = getSession(id);
7312
+ if (!pane || pane.exited) return false;
7313
+ try {
7314
+ tmux("send-keys", "-t", name(id), "Escape");
7315
+ return true;
7316
+ } catch {
7317
+ return false;
7318
+ }
7319
+ }
7320
+ async function submitPaneDialog(id) {
7321
+ const p = getSession(id);
7322
+ if (!p || p.exited) return false;
7323
+ try {
7324
+ const io = dialogIO(id);
7325
+ const screen = readDialogScreen(io.capture());
7326
+ if (screen?.kind !== "review") return false;
7327
+ return await submitReview(io, screen.submitRow);
7328
+ } catch {
7329
+ return false;
7330
+ }
7331
+ }
6946
7332
  async function armGoalInTui(id, condition, o = {}) {
6947
7333
  const pollMs = o.pollMs ?? 500, readyTries = o.readyTries ?? 20;
6948
7334
  const settleMs = o.settleMs ?? 1200, verifyTries = o.verifyTries ?? 12;
@@ -7117,13 +7503,14 @@ function spawnPty(...args) {
7117
7503
  );
7118
7504
  }
7119
7505
  }
7120
- var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
7506
+ var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
7121
7507
  var init_pty = __esm({
7122
7508
  "src/services/pty.ts"() {
7123
7509
  "use strict";
7124
7510
  init_src2();
7125
7511
  init_src();
7126
7512
  init_session_phases();
7513
+ init_session_id();
7127
7514
  init_tui_dialog();
7128
7515
  init_config2();
7129
7516
  socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
@@ -7157,7 +7544,6 @@ var init_pty = __esm({
7157
7544
  }
7158
7545
  };
7159
7546
  sq = (s) => `'${s.split("'").join("'\\''")}'`;
7160
- sessionIdForSpec = (specId) => specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
7161
7547
  idFor = (specId) => specId ? sessionIdForSpec(specId) : randomUUID().slice(0, 8);
7162
7548
  FMT = [
7163
7549
  "#{session_name}",
@@ -7219,6 +7605,16 @@ var init_pty = __esm({
7219
7605
  return "";
7220
7606
  }
7221
7607
  };
7608
+ dialogIO = (id) => ({
7609
+ capture: () => capturePane(id, DIALOG_CAPTURE_LINES),
7610
+ literal: (s) => {
7611
+ tmux("send-keys", "-t", name(id), "-l", s);
7612
+ },
7613
+ enter: () => {
7614
+ tmux("send-keys", "-t", name(id), "Enter");
7615
+ },
7616
+ sleep
7617
+ });
7222
7618
  DIALOG_CAPTURE_LINES = 60;
7223
7619
  GOAL_ARMED_MARKERS = {
7224
7620
  claude: ["/goal"],
@@ -7321,6 +7717,8 @@ var init_rename_project = __esm({
7321
7717
  var sync_exports = {};
7322
7718
  __export(sync_exports, {
7323
7719
  SYNCED: () => SYNCED,
7720
+ __DATE_FIELDS: () => __DATE_FIELDS,
7721
+ __FIELDS: () => __FIELDS,
7324
7722
  __FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
7325
7723
  applyPush: () => applyPush,
7326
7724
  backfillFeed: () => backfillFeed,
@@ -7459,13 +7857,13 @@ async function upsertLocal(entity, id, version, data) {
7459
7857
  function setAcceptedHook(hook) {
7460
7858
  onAccepted = hook;
7461
7859
  }
7462
- var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
7860
+ var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, __FIELDS, __DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
7463
7861
  var init_sync = __esm({
7464
7862
  "src/services/sync.ts"() {
7465
7863
  "use strict";
7466
7864
  init_db();
7467
7865
  init_rename_project();
7468
- SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent"];
7866
+ SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue"];
7469
7867
  DELEGATE = {
7470
7868
  project: prisma.project,
7471
7869
  spec: prisma.spec,
@@ -7473,7 +7871,8 @@ var init_sync = __esm({
7473
7871
  sessionResult: prisma.sessionResult,
7474
7872
  ticket: prisma.ticket,
7475
7873
  ticketAttachment: prisma.ticketAttachment,
7476
- customAgent: prisma.customAgent
7874
+ customAgent: prisma.customAgent,
7875
+ githubIssue: prisma.githubIssue
7477
7876
  };
7478
7877
  FIELDS = {
7479
7878
  project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
@@ -7493,7 +7892,28 @@ var init_sync = __esm({
7493
7892
  // ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
7494
7893
  // mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
7495
7894
  // `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
7496
- customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"]
7895
+ customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"],
7896
+ // SPEC-471 · ADR-0095 · SELURUH kolom bermakna ikut. `status`/`specId` termasuk: keputusan
7897
+ // triase adalah bagian keadaan yang harus dilihat sama oleh semua mesin — tanpa itu satu
7898
+ // mesin bisa menerima ulang issue yang di mesin lain sudah jadi backlog.
7899
+ githubIssue: [
7900
+ "projectId",
7901
+ "repoSlug",
7902
+ "number",
7903
+ "title",
7904
+ "body",
7905
+ "authorLogin",
7906
+ "labels",
7907
+ "url",
7908
+ "issueState",
7909
+ "status",
7910
+ "specId",
7911
+ "issueCreatedAt",
7912
+ "issueUpdatedAt",
7913
+ "pulledAt",
7914
+ "createdAt",
7915
+ "updatedAt"
7916
+ ]
7497
7917
  };
7498
7918
  DATE_FIELDS = {
7499
7919
  project: ["updatedAt"],
@@ -7502,8 +7922,11 @@ var init_sync = __esm({
7502
7922
  sessionResult: ["createdAt", "updatedAt"],
7503
7923
  ticket: ["createdAt", "updatedAt"],
7504
7924
  ticketAttachment: ["createdAt", "updatedAt"],
7505
- customAgent: ["createdAt", "updatedAt"]
7925
+ customAgent: ["createdAt", "updatedAt"],
7926
+ githubIssue: ["issueCreatedAt", "issueUpdatedAt", "pulledAt", "createdAt", "updatedAt"]
7506
7927
  };
7928
+ __FIELDS = FIELDS;
7929
+ __DATE_FIELDS = DATE_FIELDS;
7507
7930
  __FIELDS_FOR_TEST = FIELDS;
7508
7931
  }
7509
7932
  });
@@ -9670,7 +10093,7 @@ var require_extension = __commonJS({
9670
10093
  if (dest[name2] === void 0) dest[name2] = [elem];
9671
10094
  else dest[name2].push(elem);
9672
10095
  }
9673
- function parse2(header) {
10096
+ function parse3(header2) {
9674
10097
  const offers = /* @__PURE__ */ Object.create(null);
9675
10098
  let params = /* @__PURE__ */ Object.create(null);
9676
10099
  let mustUnescape = false;
@@ -9682,8 +10105,8 @@ var require_extension = __commonJS({
9682
10105
  let code = -1;
9683
10106
  let end2 = -1;
9684
10107
  let i = 0;
9685
- for (; i < header.length; i++) {
9686
- code = header.charCodeAt(i);
10108
+ for (; i < header2.length; i++) {
10109
+ code = header2.charCodeAt(i);
9687
10110
  if (extensionName === void 0) {
9688
10111
  if (end2 === -1 && tokenChars[code] === 1) {
9689
10112
  if (start === -1) start = i;
@@ -9694,7 +10117,7 @@ var require_extension = __commonJS({
9694
10117
  throw new SyntaxError(`Unexpected character at index ${i}`);
9695
10118
  }
9696
10119
  if (end2 === -1) end2 = i;
9697
- const name2 = header.slice(start, end2);
10120
+ const name2 = header2.slice(start, end2);
9698
10121
  if (code === 44) {
9699
10122
  push(offers, name2, params);
9700
10123
  params = /* @__PURE__ */ Object.create(null);
@@ -9715,7 +10138,7 @@ var require_extension = __commonJS({
9715
10138
  throw new SyntaxError(`Unexpected character at index ${i}`);
9716
10139
  }
9717
10140
  if (end2 === -1) end2 = i;
9718
- push(params, header.slice(start, end2), true);
10141
+ push(params, header2.slice(start, end2), true);
9719
10142
  if (code === 44) {
9720
10143
  push(offers, extensionName, params);
9721
10144
  params = /* @__PURE__ */ Object.create(null);
@@ -9723,7 +10146,7 @@ var require_extension = __commonJS({
9723
10146
  }
9724
10147
  start = end2 = -1;
9725
10148
  } else if (code === 61 && start !== -1 && end2 === -1) {
9726
- paramName = header.slice(start, i);
10149
+ paramName = header2.slice(start, i);
9727
10150
  start = end2 = -1;
9728
10151
  } else {
9729
10152
  throw new SyntaxError(`Unexpected character at index ${i}`);
@@ -9747,7 +10170,7 @@ var require_extension = __commonJS({
9747
10170
  } else {
9748
10171
  throw new SyntaxError(`Unexpected character at index ${i}`);
9749
10172
  }
9750
- } else if (code === 34 && header.charCodeAt(i - 1) === 61) {
10173
+ } else if (code === 34 && header2.charCodeAt(i - 1) === 61) {
9751
10174
  inQuotes = true;
9752
10175
  } else if (end2 === -1 && tokenChars[code] === 1) {
9753
10176
  if (start === -1) start = i;
@@ -9758,7 +10181,7 @@ var require_extension = __commonJS({
9758
10181
  throw new SyntaxError(`Unexpected character at index ${i}`);
9759
10182
  }
9760
10183
  if (end2 === -1) end2 = i;
9761
- let value = header.slice(start, end2);
10184
+ let value = header2.slice(start, end2);
9762
10185
  if (mustUnescape) {
9763
10186
  value = value.replace(/\\/g, "");
9764
10187
  mustUnescape = false;
@@ -9780,7 +10203,7 @@ var require_extension = __commonJS({
9780
10203
  throw new SyntaxError("Unexpected end of input");
9781
10204
  }
9782
10205
  if (end2 === -1) end2 = i;
9783
- const token = header.slice(start, end2);
10206
+ const token = header2.slice(start, end2);
9784
10207
  if (extensionName === void 0) {
9785
10208
  push(offers, token, params);
9786
10209
  } else {
@@ -9810,7 +10233,7 @@ var require_extension = __commonJS({
9810
10233
  }).join(", ");
9811
10234
  }).join(", ");
9812
10235
  }
9813
- module.exports = { format, parse: parse2 };
10236
+ module.exports = { format, parse: parse3 };
9814
10237
  }
9815
10238
  });
9816
10239
 
@@ -9823,7 +10246,7 @@ var require_websocket = __commonJS({
9823
10246
  var http = __require("http");
9824
10247
  var net = __require("net");
9825
10248
  var tls = __require("tls");
9826
- var { randomBytes: randomBytes5, createHash: createHash5 } = __require("crypto");
10249
+ var { randomBytes: randomBytes6, createHash: createHash8 } = __require("crypto");
9827
10250
  var { Duplex, Readable } = __require("stream");
9828
10251
  var { URL: URL2 } = __require("url");
9829
10252
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -9844,7 +10267,7 @@ var require_websocket = __commonJS({
9844
10267
  var {
9845
10268
  EventTarget: { addEventListener, removeEventListener }
9846
10269
  } = require_event_target();
9847
- var { format, parse: parse2 } = require_extension();
10270
+ var { format, parse: parse3 } = require_extension();
9848
10271
  var { toBuffer } = require_buffer_util();
9849
10272
  var kAborted = Symbol("kAborted");
9850
10273
  var protocolVersions = [8, 13];
@@ -10361,7 +10784,7 @@ var require_websocket = __commonJS({
10361
10784
  }
10362
10785
  }
10363
10786
  const defaultPort = isSecure ? 443 : 80;
10364
- const key = randomBytes5(16).toString("base64");
10787
+ const key = randomBytes6(16).toString("base64");
10365
10788
  const request = isSecure ? https.request : http.request;
10366
10789
  const protocolSet = /* @__PURE__ */ new Set();
10367
10790
  let perMessageDeflate;
@@ -10491,7 +10914,7 @@ var require_websocket = __commonJS({
10491
10914
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
10492
10915
  return;
10493
10916
  }
10494
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
10917
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
10495
10918
  if (res.headers["sec-websocket-accept"] !== digest) {
10496
10919
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
10497
10920
  return;
@@ -10521,7 +10944,7 @@ var require_websocket = __commonJS({
10521
10944
  }
10522
10945
  let extensions;
10523
10946
  try {
10524
- extensions = parse2(secWebSocketExtensions);
10947
+ extensions = parse3(secWebSocketExtensions);
10525
10948
  } catch (err) {
10526
10949
  const message = "Invalid Sec-WebSocket-Extensions header";
10527
10950
  abortHandshake(websocket2, socket2, message);
@@ -10813,13 +11236,13 @@ var require_subprotocol = __commonJS({
10813
11236
  "../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
10814
11237
  "use strict";
10815
11238
  var { tokenChars } = require_validation();
10816
- function parse2(header) {
11239
+ function parse3(header2) {
10817
11240
  const protocols = /* @__PURE__ */ new Set();
10818
11241
  let start = -1;
10819
11242
  let end2 = -1;
10820
11243
  let i = 0;
10821
- for (i; i < header.length; i++) {
10822
- const code = header.charCodeAt(i);
11244
+ for (i; i < header2.length; i++) {
11245
+ const code = header2.charCodeAt(i);
10823
11246
  if (end2 === -1 && tokenChars[code] === 1) {
10824
11247
  if (start === -1) start = i;
10825
11248
  } else if (i !== 0 && (code === 32 || code === 9)) {
@@ -10829,7 +11252,7 @@ var require_subprotocol = __commonJS({
10829
11252
  throw new SyntaxError(`Unexpected character at index ${i}`);
10830
11253
  }
10831
11254
  if (end2 === -1) end2 = i;
10832
- const protocol2 = header.slice(start, end2);
11255
+ const protocol2 = header2.slice(start, end2);
10833
11256
  if (protocols.has(protocol2)) {
10834
11257
  throw new SyntaxError(`The "${protocol2}" subprotocol is duplicated`);
10835
11258
  }
@@ -10842,14 +11265,14 @@ var require_subprotocol = __commonJS({
10842
11265
  if (start === -1 || end2 !== -1) {
10843
11266
  throw new SyntaxError("Unexpected end of input");
10844
11267
  }
10845
- const protocol = header.slice(start, i);
11268
+ const protocol = header2.slice(start, i);
10846
11269
  if (protocols.has(protocol)) {
10847
11270
  throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
10848
11271
  }
10849
11272
  protocols.add(protocol);
10850
11273
  return protocols;
10851
11274
  }
10852
- module.exports = { parse: parse2 };
11275
+ module.exports = { parse: parse3 };
10853
11276
  }
10854
11277
  });
10855
11278
 
@@ -10860,7 +11283,7 @@ var require_websocket_server = __commonJS({
10860
11283
  var EventEmitter = __require("events");
10861
11284
  var http = __require("http");
10862
11285
  var { Duplex } = __require("stream");
10863
- var { createHash: createHash5 } = __require("crypto");
11286
+ var { createHash: createHash8 } = __require("crypto");
10864
11287
  var extension2 = require_extension();
10865
11288
  var PerMessageDeflate2 = require_permessage_deflate();
10866
11289
  var subprotocol2 = require_subprotocol();
@@ -11167,7 +11590,7 @@ var require_websocket_server = __commonJS({
11167
11590
  );
11168
11591
  }
11169
11592
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
11170
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
11593
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
11171
11594
  const headers = [
11172
11595
  "HTTP/1.1 101 Switching Protocols",
11173
11596
  "Upgrade: websocket",
@@ -11814,13 +12237,13 @@ var require_HeaderParser = __commonJS({
11814
12237
  this._parseHeader();
11815
12238
  }
11816
12239
  this.ss.matches = this.ss.maxMatches;
11817
- const header = this.header;
12240
+ const header2 = this.header;
11818
12241
  this.header = {};
11819
12242
  this.buffer = "";
11820
12243
  this.finished = true;
11821
12244
  this.nread = this.npairs = 0;
11822
12245
  this.maxed = false;
11823
- this.emit("header", header);
12246
+ this.emit("header", header2);
11824
12247
  };
11825
12248
  HeaderParser.prototype._parseHeader = function() {
11826
12249
  if (this.npairs === this.maxHeaderPairs) {
@@ -11899,9 +12322,9 @@ var require_Dicer = __commonJS({
11899
12322
  this._pause = false;
11900
12323
  const self = this;
11901
12324
  this._hparser = new HeaderParser(cfg);
11902
- this._hparser.on("header", function(header) {
12325
+ this._hparser.on("header", function(header2) {
11903
12326
  self._inHeader = false;
11904
- self._part.emit("header", header);
12327
+ self._part.emit("header", header2);
11905
12328
  });
11906
12329
  }
11907
12330
  inherits(Dicer, WritableStream);
@@ -12917,7 +13340,7 @@ var require_multipart = __commonJS({
12917
13340
  field.emit("end");
12918
13341
  field.removeAllListeners("end");
12919
13342
  }
12920
- part.on("header", function(header) {
13343
+ part.on("header", function(header2) {
12921
13344
  let contype;
12922
13345
  let fieldname;
12923
13346
  let parsed;
@@ -12925,8 +13348,8 @@ var require_multipart = __commonJS({
12925
13348
  let encoding;
12926
13349
  let filename;
12927
13350
  let nsize = 0;
12928
- if (header["content-type"]) {
12929
- parsed = parseParams(header["content-type"][0]);
13351
+ if (header2["content-type"]) {
13352
+ parsed = parseParams(header2["content-type"][0]);
12930
13353
  if (parsed[0]) {
12931
13354
  contype = parsed[0].toLowerCase();
12932
13355
  for (i = 0, len = parsed.length; i < len; ++i) {
@@ -12943,8 +13366,8 @@ var require_multipart = __commonJS({
12943
13366
  if (charset === void 0) {
12944
13367
  charset = defCharset;
12945
13368
  }
12946
- if (header["content-disposition"]) {
12947
- parsed = parseParams(header["content-disposition"][0]);
13369
+ if (header2["content-disposition"]) {
13370
+ parsed = parseParams(header2["content-disposition"][0]);
12948
13371
  if (!RE_FIELD.test(parsed[0])) {
12949
13372
  return skipPart(part);
12950
13373
  }
@@ -12961,8 +13384,8 @@ var require_multipart = __commonJS({
12961
13384
  } else {
12962
13385
  return skipPart(part);
12963
13386
  }
12964
- if (header["content-transfer-encoding"]) {
12965
- encoding = header["content-transfer-encoding"][0].toLowerCase();
13387
+ if (header2["content-transfer-encoding"]) {
13388
+ encoding = header2["content-transfer-encoding"][0].toLowerCase();
12966
13389
  } else {
12967
13390
  encoding = "7bit";
12968
13391
  }
@@ -14038,7 +14461,7 @@ var require_stream_wormhole = __commonJS({
14038
14461
  "../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
14039
14462
  "use strict";
14040
14463
  module.exports = (stream, throwError) => {
14041
- return new Promise((resolve14, reject) => {
14464
+ return new Promise((resolve14, reject2) => {
14042
14465
  if (typeof stream.resume !== "function") {
14043
14466
  return resolve14();
14044
14467
  }
@@ -14062,7 +14485,7 @@ var require_stream_wormhole = __commonJS({
14062
14485
  function onError(err) {
14063
14486
  cleanup();
14064
14487
  if (throwError) {
14065
- reject(err);
14488
+ reject2(err);
14066
14489
  } else {
14067
14490
  resolve14();
14068
14491
  }
@@ -14280,7 +14703,7 @@ var require_secure_json_parse = __commonJS({
14280
14703
  }
14281
14704
  return obj;
14282
14705
  }
14283
- function parse2(text, reviver, options2) {
14706
+ function parse3(text, reviver, options2) {
14284
14707
  const stackTraceLimit = Error.stackTraceLimit;
14285
14708
  Error.stackTraceLimit = 0;
14286
14709
  try {
@@ -14300,9 +14723,9 @@ var require_secure_json_parse = __commonJS({
14300
14723
  Error.stackTraceLimit = stackTraceLimit;
14301
14724
  }
14302
14725
  }
14303
- module.exports = parse2;
14304
- module.exports.default = parse2;
14305
- module.exports.parse = parse2;
14726
+ module.exports = parse3;
14727
+ module.exports.default = parse3;
14728
+ module.exports.parse = parse3;
14306
14729
  module.exports.safeParse = safeParse;
14307
14730
  module.exports.scan = filter;
14308
14731
  }
@@ -14499,13 +14922,13 @@ var require_multipart2 = __commonJS({
14499
14922
  }
14500
14923
  };
14501
14924
  const parts = () => {
14502
- return new Promise((resolve14, reject) => {
14925
+ return new Promise((resolve14, reject2) => {
14503
14926
  handle((val) => {
14504
14927
  if (val instanceof Error) {
14505
14928
  if (val.message === "Unexpected end of multipart data") {
14506
14929
  resolve14(null);
14507
14930
  } else {
14508
- reject(val);
14931
+ reject2(val);
14509
14932
  }
14510
14933
  } else {
14511
14934
  resolve14(val);
@@ -15339,8 +15762,10 @@ var DEFAULT_SETTING = {
15339
15762
  // SPEC-376 · ADR-0080 · uji hanya yang berubah
15340
15763
  conflict: CONFLICT_DEFAULTS,
15341
15764
  // SPEC-383 · ADR-0081 · default sesi konflik (opt-in, mati)
15342
- lead: LEAD_DEFAULTS
15765
+ lead: LEAD_DEFAULTS,
15343
15766
  // SPEC-409 · ADR-0091 · hanoman-lead (master switch mati)
15767
+ telegram: TELEGRAM_DEFAULTS
15768
+ // SPEC-476 · ADR-0096 · gateway Telegram opt-in
15344
15769
  };
15345
15770
  async function getSetting() {
15346
15771
  const raw = (await prisma.setting.findUnique({ where: { id: 1 } }))?.data;
@@ -15388,18 +15813,18 @@ function ensureCodexTrust(repoDir, home) {
15388
15813
  canonical = realpathSync2(repoDir);
15389
15814
  } catch {
15390
15815
  }
15391
- const header = `[projects."${canonical}"]`;
15816
+ const header2 = `[projects."${canonical}"]`;
15392
15817
  try {
15393
15818
  let existing = "";
15394
15819
  try {
15395
15820
  existing = readFileSync4(path, "utf8");
15396
15821
  } catch {
15397
15822
  }
15398
- if (existing.includes(header)) return;
15823
+ if (existing.includes(header2)) return;
15399
15824
  mkdirSync5(codexHome(home), { recursive: true });
15400
15825
  const lead = existing === "" || existing.endsWith("\n") ? "" : "\n";
15401
15826
  appendFileSync(path, `${lead}
15402
- ${header}
15827
+ ${header2}
15403
15828
  trust_level = "trusted"
15404
15829
  `);
15405
15830
  } catch {
@@ -15695,6 +16120,7 @@ async function readEscalation(specId, sessions = listSessions()) {
15695
16120
  // src/services/spec-deps.ts
15696
16121
  init_db();
15697
16122
  init_src2();
16123
+ init_session_id();
15698
16124
  var REASON_LABEL = {
15699
16125
  missing: "tak ditemukan",
15700
16126
  unfinished: "belum selesai",
@@ -15707,7 +16133,7 @@ function dependsOnOf(spec) {
15707
16133
  for (const x of v) if (typeof x === "string" && x !== "" && !out3.includes(x)) out3.push(x);
15708
16134
  return out3;
15709
16135
  }
15710
- function blockersFor(spec, deps, isMerged) {
16136
+ function blockersFor(spec, deps, tipOf, isMerged) {
15711
16137
  const ids = dependsOnOf(spec);
15712
16138
  if (ids.length === 0) return [];
15713
16139
  const base = spec.branchFrom ?? "HEAD";
@@ -15722,7 +16148,8 @@ function blockersFor(spec, deps, isMerged) {
15722
16148
  out3.push({ id, reason: "unfinished" });
15723
16149
  continue;
15724
16150
  }
15725
- if (d.headSha && !isMerged(d.headSha, base)) out3.push({ id, reason: "unmerged" });
16151
+ const tip = tipOf(d);
16152
+ if (tip && !isMerged(tip, base)) out3.push({ id, reason: "unmerged" });
15726
16153
  }
15727
16154
  return out3;
15728
16155
  }
@@ -15743,6 +16170,7 @@ function reaches(edges, from, target2) {
15743
16170
  }
15744
16171
  var TTL_MS = 15e3;
15745
16172
  var mergeCache = /* @__PURE__ */ new Map();
16173
+ var tipCache = /* @__PURE__ */ new Map();
15746
16174
  function mergedInto(repoDir, sha, baseRef) {
15747
16175
  const key = `${repoDir} ${sha} ${baseRef}`;
15748
16176
  const hit = mergeCache.get(key);
@@ -15757,7 +16185,24 @@ function mergedInto(repoDir, sha, baseRef) {
15757
16185
  mergeCache.set(key, { at: now, v });
15758
16186
  return v;
15759
16187
  }
16188
+ function workTip(repoDir, dep) {
16189
+ if (dep.headSha) return dep.headSha;
16190
+ const branch = `hanoman/${sessionIdForSpec(dep.id)}`;
16191
+ const key = `${repoDir} ${branch}`;
16192
+ const hit = tipCache.get(key);
16193
+ const now = Date.now();
16194
+ if (hit && now - hit.at < TTL_MS) return hit.v;
16195
+ let v = null;
16196
+ try {
16197
+ v = realGit.revParse(repoDir, branch);
16198
+ } catch {
16199
+ v = null;
16200
+ }
16201
+ tipCache.set(key, { at: now, v });
16202
+ return v;
16203
+ }
15760
16204
  var merger = (repoDir) => (sha, base) => repoDir ? mergedInto(repoDir, sha, base) : false;
16205
+ var tipper = (repoDir) => (d) => repoDir ? workTip(repoDir, d) : d.headSha;
15761
16206
  var depRows = (ids) => prisma.spec.findMany({
15762
16207
  where: { id: { in: ids } },
15763
16208
  select: { id: true, stage: true, headSha: true }
@@ -15766,7 +16211,7 @@ async function blockersForSpec(spec, repoDir) {
15766
16211
  const ids = dependsOnOf(spec);
15767
16212
  if (ids.length === 0) return [];
15768
16213
  const rows = await depRows(ids);
15769
- return blockersFor(spec, new Map(rows.map((r) => [r.id, r])), merger(repoDir));
16214
+ return blockersFor(spec, new Map(rows.map((r) => [r.id, r])), tipper(repoDir), merger(repoDir));
15770
16215
  }
15771
16216
  async function decorateBlocked(specs) {
15772
16217
  const ids = [...new Set(specs.flatMap(dependsOnOf))];
@@ -15782,7 +16227,8 @@ async function decorateBlocked(specs) {
15782
16227
  continue;
15783
16228
  }
15784
16229
  if (!repos.has(s.projectId)) repos.set(s.projectId, await resolveRepoDir(s.projectId));
15785
- out3.push({ ...s, dependsOn: own, blockedBy: blockersFor(s, deps, merger(repos.get(s.projectId))) });
16230
+ const repo = repos.get(s.projectId);
16231
+ out3.push({ ...s, dependsOn: own, blockedBy: blockersFor(s, deps, tipper(repo), merger(repo)) });
15786
16232
  }
15787
16233
  return out3;
15788
16234
  }
@@ -16307,10 +16753,10 @@ var _Tokenizer = class {
16307
16753
  item.align.push(null);
16308
16754
  }
16309
16755
  }
16310
- for (const header of headers) {
16756
+ for (const header2 of headers) {
16311
16757
  item.header.push({
16312
- text: header,
16313
- tokens: this.lexer.inline(header)
16758
+ text: header2,
16759
+ tokens: this.lexer.inline(header2)
16314
16760
  });
16315
16761
  }
16316
16762
  for (const row of rows) {
@@ -17158,10 +17604,10 @@ ${quote}</blockquote>
17158
17604
  return `<p>${text}</p>
17159
17605
  `;
17160
17606
  }
17161
- table(header, body) {
17607
+ table(header2, body) {
17162
17608
  if (body)
17163
17609
  body = `<tbody>${body}</tbody>`;
17164
- return "<table>\n<thead>\n" + header + "</thead>\n" + body + "</table>\n";
17610
+ return "<table>\n<thead>\n" + header2 + "</thead>\n" + body + "</table>\n";
17165
17611
  }
17166
17612
  tablerow(content) {
17167
17613
  return `<tr>
@@ -17312,12 +17758,12 @@ var _Parser = class __Parser {
17312
17758
  }
17313
17759
  case "table": {
17314
17760
  const tableToken = token;
17315
- let header = "";
17761
+ let header2 = "";
17316
17762
  let cell = "";
17317
17763
  for (let j = 0; j < tableToken.header.length; j++) {
17318
17764
  cell += this.renderer.tablecell(this.parseInline(tableToken.header[j].tokens), { header: true, align: tableToken.align[j] });
17319
17765
  }
17320
- header += this.renderer.tablerow(cell);
17766
+ header2 += this.renderer.tablerow(cell);
17321
17767
  let body = "";
17322
17768
  for (let j = 0; j < tableToken.rows.length; j++) {
17323
17769
  const row = tableToken.rows[j];
@@ -17327,7 +17773,7 @@ var _Parser = class __Parser {
17327
17773
  }
17328
17774
  body += this.renderer.tablerow(cell);
17329
17775
  }
17330
- out3 += this.renderer.table(header, body);
17776
+ out3 += this.renderer.table(header2, body);
17331
17777
  continue;
17332
17778
  }
17333
17779
  case "blockquote": {
@@ -18276,6 +18722,24 @@ async function notificationsFeed() {
18276
18722
  return { items, unread };
18277
18723
  }
18278
18724
 
18725
+ // src/services/spec-head.ts
18726
+ init_db();
18727
+ init_src2();
18728
+ var readHead = (wt) => {
18729
+ try {
18730
+ return realGit.headSha(wt);
18731
+ } catch {
18732
+ return null;
18733
+ }
18734
+ };
18735
+ async function recordHeadSha(specId, worktree, read = readHead) {
18736
+ const sha = read(worktree);
18737
+ if (!sha) return null;
18738
+ await prisma.spec.update({ where: { id: specId }, data: { headSha: sha } }).catch(() => {
18739
+ });
18740
+ return sha;
18741
+ }
18742
+
18279
18743
  // src/services/live-specs.ts
18280
18744
  async function liveSpecs(filter = {}) {
18281
18745
  const specs = await prisma.spec.findMany({
@@ -18291,14 +18755,16 @@ async function liveSpecs(filter = {}) {
18291
18755
  if (!entry) return s;
18292
18756
  const next = stageForRun(entry.phases, entry.cwd, s.id);
18293
18757
  if (!next || STAGES.indexOf(next) <= STAGES.indexOf(s.stage)) return s;
18294
- advanced.push({ id: s.id, from: s.stage, stage: next });
18758
+ advanced.push({ id: s.id, from: s.stage, stage: next, cwd: entry.cwd });
18295
18759
  if (next === "done") doneNow.push({ specId: s.id, title: s.title, projectId: s.projectId });
18296
18760
  return { ...s, stage: next };
18297
18761
  });
18298
18762
  if (advanced.length)
18299
18763
  await Promise.all(advanced.map(async (a) => {
18300
18764
  const res = await prisma.spec.updateMany({ where: { id: a.id, stage: a.from }, data: { stage: a.stage } }).catch(() => ({ count: 0 }));
18301
- if (res.count > 0) await notifySynced("spec", a.id);
18765
+ if (res.count === 0) return;
18766
+ if (a.stage === "done") await recordHeadSha(a.id, a.cwd);
18767
+ await notifySynced("spec", a.id);
18302
18768
  }));
18303
18769
  await Promise.all(doneNow.map((d) => recordCompletion(d.specId, d.title, d.projectId)));
18304
18770
  return decorateBlocked(out3);
@@ -20093,6 +20559,18 @@ async function terminal_default(app2) {
20093
20559
  if (!s?.flow || !phases) return reply.code(404).send({ error: "not found" });
20094
20560
  return { flow: s.flow, phases };
20095
20561
  });
20562
+ app2.post("/terminal/sessions/:id/steer", async (req, reply) => {
20563
+ const parsed = zTerminalSteerInput.safeParse(req.body);
20564
+ if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
20565
+ const ok2 = await sendToPane(req.params.id, parsed.data.text);
20566
+ if (!ok2) return reply.code(404).send({ error: "live session not found" });
20567
+ return reply.code(202).send({ accepted: true });
20568
+ });
20569
+ app2.post("/terminal/sessions/:id/interrupt", async (req, reply) => {
20570
+ const ok2 = interruptPane(req.params.id);
20571
+ if (!ok2) return reply.code(404).send({ error: "live session not found" });
20572
+ return reply.code(202).send({ accepted: true });
20573
+ });
20096
20574
  const sessionWorktree = async (id) => {
20097
20575
  const s = getSession(id);
20098
20576
  if (!s) return { ok: false, code: 404, msg: "not found" };
@@ -20156,11 +20634,7 @@ async function terminal_default(app2) {
20156
20634
  if (repoDir) {
20157
20635
  if (s.specId) {
20158
20636
  if (s.flow) await advanceStage(s.specId, repoDir, id, s.flow, s.cwd);
20159
- try {
20160
- const headSha = realGit.headSha(s.cwd);
20161
- await prisma.spec.update({ where: { id: s.specId }, data: { headSha } });
20162
- } catch {
20163
- }
20637
+ await recordHeadSha(s.specId, s.cwd);
20164
20638
  }
20165
20639
  killSession(id);
20166
20640
  if (ownsWorktree(repoDir, s.cwd)) realGit.removeWorktree(repoDir, s.cwd);
@@ -21665,11 +22139,11 @@ function clamp(text) {
21665
22139
  const buf = Buffer.from(text, "utf8");
21666
22140
  if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
21667
22141
  const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
21668
- let tail2 = buf.subarray(cut).toString("utf8");
21669
- const nl = tail2.indexOf("\n");
21670
- if (nl >= 0) tail2 = tail2.slice(nl + 1);
22142
+ let tail3 = buf.subarray(cut).toString("utf8");
22143
+ const nl = tail3.indexOf("\n");
22144
+ if (nl >= 0) tail3 = tail3.slice(nl + 1);
21671
22145
  return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
21672
- ${tail2}`, truncated: true };
22146
+ ${tail3}`, truncated: true };
21673
22147
  }
21674
22148
  async function saveTranscript(text) {
21675
22149
  if (!text.trim()) return { key: "", bytes: 0, truncated: false };
@@ -22629,10 +23103,22 @@ function leadArgv(o) {
22629
23103
  ];
22630
23104
  }
22631
23105
  var leadEnv = (agent, base = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base } : { ...base };
23106
+ var EXPLAIN_MAX = 500;
23107
+ var tail = (s) => {
23108
+ const t = s.trim();
23109
+ return t.length > EXPLAIN_MAX ? `\u2026${t.slice(-EXPLAIN_MAX)}` : t;
23110
+ };
23111
+ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
23112
+ if (err.killed) return `lead ${agent} kehabisan waktu ${timeoutMs} ms`;
23113
+ const fromErr = err.message.startsWith("Command failed:") ? "" : err.message;
23114
+ const detail = [tail(stderr), tail(stdout)].filter(Boolean).join(" \xB7 ") || tail(fromErr) || "tanpa keluaran";
23115
+ const how = err.signal ? `sinyal ${err.signal}` : `exit ${err.code ?? "?"}`;
23116
+ return `lead ${agent} gagal (${how}): ${detail}`;
23117
+ }
22632
23118
  function think(prompt, o) {
22633
23119
  const bin = binFor(o.agent);
22634
23120
  const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
22635
- return new Promise((resolve14, reject) => {
23121
+ return new Promise((resolve14, reject2) => {
22636
23122
  const child = execFile9(bin, args, {
22637
23123
  cwd: o.cwd,
22638
23124
  timeout: o.timeoutMs,
@@ -22642,8 +23128,7 @@ function think(prompt, o) {
22642
23128
  killSignal: "SIGTERM"
22643
23129
  }, (err, stdout, stderr) => {
22644
23130
  if (err) {
22645
- const killed = err.killed;
22646
- reject(new Error(killed ? `lead ${o.agent} kehabisan waktu ${o.timeoutMs} ms` : `lead ${o.agent} gagal: ${(stderr || err.message).trim().slice(0, 500)}`));
23131
+ reject2(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
22647
23132
  return;
22648
23133
  }
22649
23134
  resolve14(stdout);
@@ -22921,12 +23406,13 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
22921
23406
 
22922
23407
  // src/services/lead/detect.ts
22923
23408
  init_pty();
23409
+ init_tui_dialog();
22924
23410
  import { writeFileSync as writeFileSync4 } from "node:fs";
22925
23411
 
22926
23412
  // src/services/lead/pane.ts
22927
23413
  init_tui_dialog();
22928
23414
  var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
22929
- var tail = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
23415
+ var tail2 = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
22930
23416
  var CODEX_FINISHED = [
22931
23417
  /Goal achieved/i,
22932
23418
  /Goal unmet/i,
@@ -22939,11 +23425,11 @@ var ASK_SIGNALS = [
22939
23425
  /\b(pilih|apakah|haruskah|mana yang|opsi|which|should i|do you want|proceed\?)\b/i
22940
23426
  ];
22941
23427
  function readPaneQuestion(text, agent) {
22942
- const lines2 = tail(text, 40);
23428
+ const lines2 = tail2(text, 40);
22943
23429
  const body = lines2.join("\n").trim();
22944
23430
  const choices = readChoiceDialog(text)?.options ?? [];
22945
23431
  if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
22946
- const question = tail(text, 25).join("\n").trim();
23432
+ const question = tail2(text, 25).join("\n").trim();
22947
23433
  if (agent === "codex") {
22948
23434
  const finished = CODEX_FINISHED.find((re) => re.test(body));
22949
23435
  if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
@@ -22956,9 +23442,16 @@ function readPaneQuestion(text, agent) {
22956
23442
  // src/services/lead/detect.ts
22957
23443
  var answers = /* @__PURE__ */ new Map();
22958
23444
  var capped = /* @__PURE__ */ new Set();
23445
+ var failures = /* @__PURE__ */ new Map();
23446
+ var failCapped = /* @__PURE__ */ new Set();
23447
+ var MAX_CHAIN_STEPS = 6;
23448
+ var CHAIN_POLL_MS = 300;
23449
+ var CHAIN_POLL_TRIES = 20;
22959
23450
  function resetSession(sessionId2) {
22960
23451
  answers.delete(sessionId2);
22961
23452
  capped.delete(sessionId2);
23453
+ failures.delete(sessionId2);
23454
+ failCapped.delete(sessionId2);
22962
23455
  }
22963
23456
  var prodDetectDeps = {
22964
23457
  live: () => {
@@ -22994,6 +23487,8 @@ var prodDetectDeps = {
22994
23487
  } catch {
22995
23488
  }
22996
23489
  },
23490
+ submit: (id) => submitPaneDialog(id),
23491
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
22997
23492
  decide,
22998
23493
  decideDeps: prodDecideDeps,
22999
23494
  optIn: leadProjects,
@@ -23025,7 +23520,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
23025
23520
  if ((answers.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
23026
23521
  if (!capped.has(s.id)) {
23027
23522
  capped.add(s.id);
23028
- const row2 = await recordDecision({
23523
+ const row = await recordDecision({
23029
23524
  projectId: s.projectId,
23030
23525
  specId: s.specId,
23031
23526
  sessionId: s.id,
@@ -23040,7 +23535,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
23040
23535
  weighty: true
23041
23536
  });
23042
23537
  await deps.notify(
23043
- row2.id,
23538
+ row.id,
23044
23539
  `Lead berhenti menjawab sesi ${s.id} (batas ${cfg.maxAutoAnswers}\xD7 tercapai)`,
23045
23540
  s.projectId,
23046
23541
  s.specId ?? null,
@@ -23050,16 +23545,79 @@ async function scanAndAnswer(deps = prodDetectDeps) {
23050
23545
  skip("batas jawaban otomatis tercapai");
23051
23546
  continue;
23052
23547
  }
23548
+ if ((failures.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
23549
+ if (!failCapped.has(s.id)) {
23550
+ failCapped.add(s.id);
23551
+ const row = await recordDecision({
23552
+ projectId: s.projectId,
23553
+ specId: s.specId,
23554
+ sessionId: s.id,
23555
+ gate: "detected",
23556
+ kind: "quality",
23557
+ question: `Keputusan untuk sesi ${s.id} gagal disusun ${cfg.maxAutoAnswers}\xD7 berturut-turut.`,
23558
+ answer: "Berhenti mencoba; serahkan ke operator.",
23559
+ reason: "Kegagalan beruntun menandakan sebab yang tak hilang dengan mengulang (kunci/kuota agen, biner tak terpasang) \u2014 mencoba lagi tiap denyut hanya membakar kuota. Alasan percobaan terakhir ada di baris jejak `gagal` tepat di atas ini.",
23560
+ refs: [],
23561
+ confidence: "tinggi",
23562
+ action: "none",
23563
+ weighty: true
23564
+ });
23565
+ await deps.notify(
23566
+ row.id,
23567
+ `Lead berhenti mencoba sesi ${s.id} (${cfg.maxAutoAnswers}\xD7 gagal berturut-turut)`,
23568
+ s.projectId,
23569
+ s.specId ?? null,
23570
+ s.id
23571
+ );
23572
+ }
23573
+ skip("batas kegagalan beruntun tercapai");
23574
+ continue;
23575
+ }
23053
23576
  const agent = deps.agentOf(s.id) ?? "claude";
23054
23577
  const read = readPaneQuestion(deps.pane(s.id), agent);
23055
23578
  if (!read.asking) {
23056
23579
  skip(read.reason);
23057
23580
  continue;
23058
23581
  }
23582
+ const chain = await runChain(s, agent, deps);
23583
+ if (chain.acted && chain.done) {
23584
+ deps.clearMarker(s.decisionFile);
23585
+ answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
23586
+ failures.delete(s.id);
23587
+ out3.answered.push(s.id);
23588
+ continue;
23589
+ }
23590
+ if (chain.failed) failures.set(s.id, (failures.get(s.id) ?? 0) + 1);
23591
+ skip(chain.reason);
23592
+ }
23593
+ return out3;
23594
+ }
23595
+ async function runChain(s, agent, deps) {
23596
+ let acted = false;
23597
+ for (let step = 0; step < MAX_CHAIN_STEPS; step++) {
23598
+ const text = deps.pane(s.id);
23599
+ const screen = readDialogScreen(text);
23600
+ if (screen?.kind === "review") {
23601
+ if (!await deps.submit(s.id))
23602
+ return { acted, done: false, failed: true, reason: "gagal menekan Submit answers" };
23603
+ acted = true;
23604
+ continue;
23605
+ }
23606
+ if (step > 0 && !screen) return { acted, done: true, failed: false, reason: "" };
23607
+ const read = readPaneQuestion(text, agent);
23608
+ if (!read.asking) {
23609
+ return step === 0 ? { acted, done: false, failed: false, reason: read.reason } : { acted, done: true, failed: false, reason: "" };
23610
+ }
23059
23611
  const notes = [`Sesi ini menunggu di terminal; teks di bawah adalah layar terakhirnya. Jawablah sebagai masukan yang bisa langsung diketik ke terminal itu (isi \`reply\`).`];
23060
23612
  if (read.choices.length) {
23061
23613
  notes.push("Layarnya adalah dialog pilihan. `reply` akan dimasukkan sebagai JAWABAN BEBAS ke dialog itu, jadi tulislah jawaban yang berdiri sendiri \u2014 sebut opsi yang kamu pilih beserta alasan singkatnya, bukan nomornya saja.");
23062
23614
  }
23615
+ if (screen?.kind === "question" && screen.tabs.length > 1) {
23616
+ const at = screen.tabs.findIndex((t) => !t.answered);
23617
+ notes.push(
23618
+ `Dialog ini BERANTAI: ${screen.tabs.length} pertanyaan dalam satu tanya (${screen.tabs.map((t) => `${t.answered ? "sudah" : "belum"}: ${t.header}`).join(", ")}). Yang sedang tampil pertanyaan ke-${at + 1}; jawab HANYA pertanyaan itu \u2014 sisanya akan ditanyakan sesudah ini.`
23619
+ );
23620
+ }
23063
23621
  const row = await deps.decide({
23064
23622
  projectId: s.projectId,
23065
23623
  specId: s.specId,
@@ -23070,26 +23628,32 @@ async function scanAndAnswer(deps = prodDetectDeps) {
23070
23628
  options: read.choices.length ? read.choices : void 0,
23071
23629
  notes
23072
23630
  }, deps.decideDeps);
23073
- if (!row || row.status !== "berlaku") {
23074
- skip("lead tak menghasilkan keputusan yang berlaku");
23075
- continue;
23076
- }
23631
+ if (!row) return { acted, done: false, failed: false, reason: "lead tak menghasilkan keputusan yang berlaku" };
23632
+ if (row.status !== "berlaku")
23633
+ return { acted, done: false, failed: true, reason: "lead tak menghasilkan keputusan yang berlaku" };
23077
23634
  const reply = takeReply(row.id) || row.answer;
23078
- const sent = await deps.send(s.id, reply);
23079
- if (!sent) {
23080
- skip("gagal mengetik ke pane");
23081
- continue;
23082
- }
23083
- deps.clearMarker(s.decisionFile);
23084
- answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
23085
- out3.answered.push(s.id);
23635
+ if (!await deps.send(s.id, reply))
23636
+ return { acted, done: false, failed: true, reason: "gagal mengetik ke pane" };
23637
+ acted = true;
23638
+ if (!screen) return { acted, done: true, failed: false, reason: "" };
23639
+ if (!await waitScreenChange(s.id, dialogKey(text), deps))
23640
+ return { acted, done: false, failed: true, reason: "layar dialog tak berubah sesudah dijawab" };
23086
23641
  }
23087
- return out3;
23642
+ return { acted, done: false, failed: true, reason: "batas langkah rantai dialog tercapai" };
23643
+ }
23644
+ async function waitScreenChange(id, before, deps) {
23645
+ for (let i = 0; i < CHAIN_POLL_TRIES; i++) {
23646
+ await deps.sleep(CHAIN_POLL_MS);
23647
+ if (dialogKey(deps.pane(id)) !== before) return true;
23648
+ }
23649
+ return false;
23088
23650
  }
23089
23651
  function sweep(liveIds) {
23090
23652
  const live = new Set(liveIds);
23091
23653
  for (const id of [...answers.keys()]) if (!live.has(id)) answers.delete(id);
23092
23654
  for (const id of [...capped]) if (!live.has(id)) capped.delete(id);
23655
+ for (const id of [...failures.keys()]) if (!live.has(id)) failures.delete(id);
23656
+ for (const id of [...failCapped]) if (!live.has(id)) failCapped.delete(id);
23093
23657
  }
23094
23658
 
23095
23659
  // src/services/lead/pulse.ts
@@ -23715,60 +24279,832 @@ async function custom_agents_default(app2) {
23715
24279
  });
23716
24280
  }
23717
24281
 
23718
- // src/app.ts
23719
- var import_multipart = __toESM(require_multipart2(), 1);
24282
+ // src/routes/github-issues.ts
24283
+ init_zod();
24284
+ init_db();
23720
24285
 
23721
- // src/routes/auth.ts
23722
- init_src();
24286
+ // src/services/github-issues.ts
23723
24287
  init_db();
23724
24288
 
23725
- // src/services/auth.ts
24289
+ // src/services/github-repo.ts
23726
24290
  init_db();
23727
- import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual, createHash as createHash3 } from "node:crypto";
23728
- import { promisify as promisify9 } from "node:util";
23729
- var scrypt = promisify9(scryptCb);
23730
- var COOKIE_NAME = "hn_session";
23731
- var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
23732
- async function hashPassword(pw) {
23733
- const salt = randomBytes3(16);
23734
- const key = await scrypt(pw, salt, 64);
23735
- return `${salt.toString("hex")}:${key.toString("hex")}`;
23736
- }
23737
- async function verifyPassword(pw, stored) {
23738
- const [saltHex, hashHex] = stored.split(":");
23739
- if (!saltHex || !hashHex) return false;
23740
- const key = await scrypt(pw, Buffer.from(saltHex, "hex"), 64);
23741
- const want = Buffer.from(hashHex, "hex");
23742
- return key.length === want.length && timingSafeEqual(key, want);
24291
+ function parse2(url2) {
24292
+ const u = url2.trim();
24293
+ let m = /^git@([^:]+):([^/]+)\/(.+?)(?:\.git)?$/.exec(u);
24294
+ if (!m) m = /^https?:\/\/(?:[^@/]+@)?([^/]+)\/([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(u);
24295
+ if (!m || !m[1] || !m[2] || !m[3]) return null;
24296
+ return { host: m[1], owner: m[2], repo: m[3] };
24297
+ }
24298
+ function githubSlugFromUrl(url2) {
24299
+ const p = parse2(url2);
24300
+ if (!p || !p.host.includes("github.")) return null;
24301
+ return { owner: p.owner, repo: p.repo, slug: `${p.owner}/${p.repo}` };
24302
+ }
24303
+ async function resolveGithubRepo(projectId) {
24304
+ const project = await prisma.project.findUnique({ where: { id: projectId } });
24305
+ if (!project) return { ok: false, kind: "no-project", error: `project "${projectId}" tidak ada` };
24306
+ const candidates = [];
24307
+ if (project.gitRemote) candidates.push(project.gitRemote);
24308
+ const repoDir = await resolveRepoDir(projectId).catch(() => null);
24309
+ if (repoDir) {
24310
+ const origin = (await listRemotes(repoDir)).find((r) => r.name === "origin");
24311
+ if (origin?.fetch) candidates.push(origin.fetch);
24312
+ }
24313
+ if (candidates.length === 0)
24314
+ return {
24315
+ ok: false,
24316
+ kind: "no-remote",
24317
+ error: "project belum punya remote GitHub (isi gitRemote atau tambahkan origin di repo lokalnya)"
24318
+ };
24319
+ for (const url2 of candidates) {
24320
+ const gh = githubSlugFromUrl(url2);
24321
+ if (gh) return { ok: true, repo: gh };
24322
+ }
24323
+ const host2 = parse2(candidates[0])?.host ?? candidates[0];
24324
+ return {
24325
+ ok: false,
24326
+ kind: "not-github",
24327
+ error: `remote project ber-host "${host2}", bukan GitHub \u2014 tarik issue hanya mendukung GitHub`
24328
+ };
23743
24329
  }
23744
- var newSessionToken = () => randomBytes3(32).toString("base64url");
23745
- var sessionId = (token) => createHash3("sha256").update(token).digest("hex");
23746
- async function createSession2(userId) {
23747
- const token = newSessionToken();
23748
- await prisma.session.create({
23749
- data: { id: sessionId(token), userId, expiresAt: new Date(Date.now() + SESSION_TTL_MS) }
23750
- });
23751
- return token;
24330
+
24331
+ // src/services/github-fetch.ts
24332
+ init_config2();
24333
+ import { execFile as execFile10 } from "node:child_process";
24334
+ var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
24335
+ var norm = (s) => s.toLowerCase() === "closed" ? "closed" : "open";
24336
+ function issueFromGh(raw) {
24337
+ return {
24338
+ number: raw.number,
24339
+ title: raw.title,
24340
+ body: raw.body ?? "",
24341
+ authorLogin: raw.author?.login ?? "",
24342
+ labels: labelNames(raw.labels),
24343
+ url: raw.url,
24344
+ issueState: norm(raw.state),
24345
+ issueCreatedAt: raw.createdAt,
24346
+ issueUpdatedAt: raw.updatedAt
24347
+ };
23752
24348
  }
23753
- async function lookupSession(token) {
23754
- const s = await prisma.session.findUnique({ where: { id: sessionId(token) }, include: { user: true } });
23755
- if (!s) return null;
23756
- if (s.expiresAt.getTime() < Date.now()) {
23757
- await prisma.session.delete({ where: { id: s.id } }).catch(() => {
24349
+ function issuesFromRest(raw) {
24350
+ let skippedPullRequests = 0;
24351
+ const issues = [];
24352
+ for (const r of raw) {
24353
+ if (r.pull_request !== void 0) {
24354
+ skippedPullRequests++;
24355
+ continue;
24356
+ }
24357
+ issues.push({
24358
+ number: r.number,
24359
+ title: r.title,
24360
+ body: r.body ?? "",
24361
+ authorLogin: r.user?.login ?? "",
24362
+ labels: labelNames(r.labels),
24363
+ url: r.html_url,
24364
+ issueState: norm(r.state),
24365
+ issueCreatedAt: r.created_at,
24366
+ issueUpdatedAt: r.updated_at
23758
24367
  });
23759
- return null;
23760
24368
  }
23761
- return { id: s.user.id, email: s.user.email, createdAt: s.user.createdAt.toISOString() };
24369
+ return { issues, skippedPullRequests };
23762
24370
  }
23763
- async function deleteSession(token) {
23764
- await prisma.session.deleteMany({ where: { id: sessionId(token) } });
23765
- }
23766
- async function deleteUserSessions(userId, exceptToken) {
23767
- await prisma.session.deleteMany({
23768
- where: { userId, ...exceptToken ? { id: { not: sessionId(exceptToken) } } : {} }
24371
+ var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
24372
+ var API2 = "https://api.github.com";
24373
+ var defaultRunGh = (args, env) => new Promise((resolve14, reject2) => {
24374
+ execFile10(
24375
+ args[0],
24376
+ args.slice(1),
24377
+ { env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
24378
+ (err, stdout, stderr) => {
24379
+ const e = err;
24380
+ if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject2(e);
24381
+ resolve14({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
24382
+ }
24383
+ );
24384
+ });
24385
+ var defaultHttpGet = async (url2, headers) => {
24386
+ const res = await fetch(url2, {
24387
+ headers: { Accept: "application/vnd.github+json", "User-Agent": "hanoman", ...headers }
23769
24388
  });
24389
+ const json = await res.json().catch(() => null);
24390
+ return { status: res.status, json };
24391
+ };
24392
+ function classifyGhStderr(stderr) {
24393
+ const s = stderr.toLowerCase();
24394
+ if (s.includes("gh auth login") || s.includes("bad credentials") || s.includes("http 401")) return "unauth";
24395
+ if (s.includes("disabled issues")) return "issues-disabled";
24396
+ if (s.includes("could not resolve to a repository") || s.includes("http 404")) return "not-found";
24397
+ return "other";
23770
24398
  }
23771
- var fails = /* @__PURE__ */ new Map();
24399
+ async function viaGh(repo, opts, deps) {
24400
+ const bin = deps.ghBin ?? effectiveStr("HANOMAN_GH_BIN") ?? "gh";
24401
+ const args = [
24402
+ bin,
24403
+ "issue",
24404
+ "list",
24405
+ "--repo",
24406
+ repo.slug,
24407
+ "--state",
24408
+ opts.state,
24409
+ "--limit",
24410
+ String(opts.limit),
24411
+ "--json",
24412
+ GH_FIELDS
24413
+ ];
24414
+ const env = { ...process.env };
24415
+ if (deps.token) env.GH_TOKEN = deps.token;
24416
+ let out3;
24417
+ try {
24418
+ out3 = await (deps.runGh ?? defaultRunGh)(args, env);
24419
+ } catch {
24420
+ return { fallback: true, reason: "gh tak terpasang" };
24421
+ }
24422
+ if (out3.code !== 0) {
24423
+ const kind = classifyGhStderr(out3.stderr);
24424
+ if (kind === "unauth") return { fallback: true, reason: "gh tak terautentikasi" };
24425
+ return { ok: false, kind, error: out3.stderr.trim() || `gh keluar dengan kode ${out3.code}` };
24426
+ }
24427
+ let raw;
24428
+ try {
24429
+ raw = JSON.parse(out3.stdout || "[]");
24430
+ } catch {
24431
+ return { ok: false, kind: "other", error: "keluaran gh bukan JSON" };
24432
+ }
24433
+ return { ok: true, issues: raw.map(issueFromGh), via: "gh", skippedPullRequests: 0 };
24434
+ }
24435
+ async function viaRest(repo, opts, deps) {
24436
+ const get = deps.httpGet ?? defaultHttpGet;
24437
+ const headers = {};
24438
+ if (deps.token) headers.Authorization = `Bearer ${deps.token}`;
24439
+ const meta = await get(`${API2}/repos/${repo.slug}`, headers);
24440
+ if (meta.status === 404)
24441
+ return { ok: false, kind: "not-found", error: `repo "${repo.slug}" tak ditemukan atau tak terjangkau` };
24442
+ if (meta.status === 401 || meta.status === 403)
24443
+ return { ok: false, kind: "unauthorized", error: "GitHub menolak kredensial \u2014 isi GITHUB_TOKEN di Settings" };
24444
+ if (meta.status !== 200) return { ok: false, kind: "other", error: `GitHub menjawab HTTP ${meta.status}` };
24445
+ if (meta.json?.has_issues === false)
24446
+ return { ok: false, kind: "issues-disabled", error: `repo "${repo.slug}" mematikan fitur issue` };
24447
+ const issues = [];
24448
+ let skippedPullRequests = 0;
24449
+ for (let page = 1; issues.length < opts.limit && page <= 10; page++) {
24450
+ const per = Math.min(100, opts.limit - issues.length);
24451
+ const res = await get(
24452
+ `${API2}/repos/${repo.slug}/issues?state=${opts.state}&per_page=${per}&page=${page}`,
24453
+ headers
24454
+ );
24455
+ if (res.status === 404) return { ok: false, kind: "not-found", error: `repo "${repo.slug}" tak ditemukan` };
24456
+ if (res.status === 401 || res.status === 403)
24457
+ return { ok: false, kind: "unauthorized", error: "GitHub menolak kredensial" };
24458
+ if (res.status !== 200) return { ok: false, kind: "other", error: `GitHub menjawab HTTP ${res.status}` };
24459
+ const batch = Array.isArray(res.json) ? res.json : [];
24460
+ if (batch.length === 0) break;
24461
+ const n = issuesFromRest(batch);
24462
+ issues.push(...n.issues);
24463
+ skippedPullRequests += n.skippedPullRequests;
24464
+ if (batch.length < per) break;
24465
+ }
24466
+ return { ok: true, issues: issues.slice(0, opts.limit), via: "rest", skippedPullRequests };
24467
+ }
24468
+ async function fetchIssues(repo, opts, deps = {}) {
24469
+ const token = deps.token ?? effectiveStr("GITHUB_TOKEN") ?? void 0;
24470
+ const first = await viaGh(repo, opts, { ...deps, token });
24471
+ if (!("fallback" in first)) return first;
24472
+ return viaRest(repo, opts, { ...deps, token });
24473
+ }
24474
+
24475
+ // src/services/github-issues.ts
24476
+ var issueRowId = (projectId, slug, number) => `${projectId}:${slug}#${number}`;
24477
+ async function pullIssues(projectId, opts = {}, deps = {}) {
24478
+ const resolved = await resolveGithubRepo(projectId);
24479
+ if (!resolved.ok) return { ok: false, kind: resolved.kind, error: resolved.error };
24480
+ const state = opts.state ?? "open";
24481
+ const limit = Math.min(Math.max(opts.limit ?? 200, 1), 1e3);
24482
+ const got = await fetchIssues(resolved.repo, { state, limit }, deps);
24483
+ if (!got.ok) return { ok: false, kind: got.kind, error: got.error };
24484
+ const slug = resolved.repo.slug;
24485
+ const now = /* @__PURE__ */ new Date();
24486
+ let created = 0, updated = 0;
24487
+ for (const i of got.issues) {
24488
+ const id = issueRowId(projectId, slug, i.number);
24489
+ const exists = await prisma.githubIssue.findUnique({ where: { id }, select: { id: true } });
24490
+ const fresh = {
24491
+ title: i.title,
24492
+ body: i.body,
24493
+ authorLogin: i.authorLogin,
24494
+ labels: i.labels,
24495
+ url: i.url,
24496
+ issueState: i.issueState,
24497
+ issueCreatedAt: new Date(i.issueCreatedAt),
24498
+ issueUpdatedAt: new Date(i.issueUpdatedAt),
24499
+ pulledAt: now
24500
+ };
24501
+ await prisma.githubIssue.upsert({
24502
+ where: { id },
24503
+ create: { id, projectId, repoSlug: slug, number: i.number, status: "new", specId: null, ...fresh },
24504
+ update: fresh
24505
+ });
24506
+ if (exists) updated++;
24507
+ else created++;
24508
+ await notifySynced("githubIssue", id);
24509
+ }
24510
+ return {
24511
+ ok: true,
24512
+ repo: slug,
24513
+ pulled: got.issues.length,
24514
+ created,
24515
+ updated,
24516
+ via: got.via,
24517
+ skippedPullRequests: got.skippedPullRequests
24518
+ };
24519
+ }
24520
+
24521
+ // src/services/github-accept.ts
24522
+ init_src();
24523
+ init_db();
24524
+ var backlinkOf = (i) => `Dari GitHub issue ${i.repoSlug}#${i.number} (${i.url}).`;
24525
+ async function acceptGithubIssue(issue2, opts) {
24526
+ if (issue2.specId) {
24527
+ const spec2 = await prisma.spec.findUnique({ where: { id: issue2.specId } });
24528
+ if (spec2) return { spec: spec2, created: false };
24529
+ }
24530
+ const labels = Array.isArray(issue2.labels) ? issue2.labels : [];
24531
+ const source = opts.source ?? sourceForLabels(labels);
24532
+ const priority = opts.priority ?? "sedang";
24533
+ const backlink = backlinkOf(issue2);
24534
+ const detail = `${issue2.body}
24535
+
24536
+ Pelapor: @${issue2.authorLogin}
24537
+ Label: ${labels.length ? labels.join(", ") : "(tanpa label)"}
24538
+ ${backlink}`;
24539
+ const payload = source === "qa" ? {
24540
+ severity: "major",
24541
+ steps: "Reproduksi dari deskripsi issue.",
24542
+ expected: "Perilaku yang diharapkan pelapor issue.",
24543
+ actual: detail,
24544
+ env: ""
24545
+ } : { context: detail, outcome: "", constraints: "", priority };
24546
+ const repoDir = await resolveRepoDir(issue2.projectId).catch(() => null);
24547
+ let spec = null;
24548
+ for (let attempt = 0; attempt < 3 && !spec; attempt++) {
24549
+ const sid = await nextSpecId(repoDir);
24550
+ try {
24551
+ spec = await prisma.spec.create({
24552
+ data: {
24553
+ id: sid,
24554
+ projectId: issue2.projectId,
24555
+ title: issue2.title,
24556
+ source,
24557
+ stage: "brainstorming",
24558
+ priority,
24559
+ author: `GitHub \xB7 ${opts.author}`,
24560
+ objective: `${issue2.title}. ${backlink}`,
24561
+ payload
24562
+ }
24563
+ });
24564
+ } catch (e) {
24565
+ if (e.code === "P2002" && attempt < 2) continue;
24566
+ throw e;
24567
+ }
24568
+ }
24569
+ await prisma.githubIssue.update({
24570
+ where: { id: issue2.id },
24571
+ data: { status: "accepted", specId: spec.id }
24572
+ });
24573
+ await notifySynced("spec", spec.id);
24574
+ await notifySynced("githubIssue", issue2.id);
24575
+ return { spec, created: true };
24576
+ }
24577
+
24578
+ // src/routes/github-issues.ts
24579
+ var zPull = external_exports.object({
24580
+ state: external_exports.enum(["open", "all"]).optional(),
24581
+ limit: external_exports.number().int().min(1).max(1e3).optional()
24582
+ }).default({});
24583
+ var zAccept = external_exports.object({
24584
+ priority: external_exports.enum(["tinggi", "sedang", "rendah"]).optional(),
24585
+ source: external_exports.enum(["qa", "brief", "audit"]).optional()
24586
+ }).default({});
24587
+ var zAcceptMany = external_exports.object({
24588
+ ids: external_exports.array(external_exports.string().min(1)).min(1).max(100),
24589
+ priority: external_exports.enum(["tinggi", "sedang", "rendah"]).optional(),
24590
+ source: external_exports.enum(["qa", "brief", "audit"]).optional()
24591
+ });
24592
+ var STATUS = {
24593
+ "no-project": 404,
24594
+ "not-found": 404,
24595
+ "no-remote": 400,
24596
+ "not-github": 400,
24597
+ "issues-disabled": 400,
24598
+ unauthorized: 401,
24599
+ other: 502
24600
+ };
24601
+ var view7 = (i) => ({
24602
+ id: i.id,
24603
+ projectId: i.projectId,
24604
+ repoSlug: i.repoSlug,
24605
+ number: i.number,
24606
+ title: i.title,
24607
+ body: i.body,
24608
+ authorLogin: i.authorLogin,
24609
+ labels: Array.isArray(i.labels) ? i.labels : [],
24610
+ url: i.url,
24611
+ issueState: i.issueState,
24612
+ status: i.status,
24613
+ specId: i.specId,
24614
+ issueCreatedAt: i.issueCreatedAt.toISOString(),
24615
+ issueUpdatedAt: i.issueUpdatedAt.toISOString(),
24616
+ pulledAt: i.pulledAt.toISOString()
24617
+ });
24618
+ async function githubIssues(app2) {
24619
+ app2.post("/projects/:id/github/pull", async (req, reply) => {
24620
+ const { id } = req.params;
24621
+ const parsed = zPull.safeParse(req.body ?? {});
24622
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24623
+ const r = await pullIssues(id, parsed.data);
24624
+ if (!r.ok) return reply.code(STATUS[r.kind] ?? 400).send({ error: r.error });
24625
+ return reply.code(200).send(r);
24626
+ });
24627
+ app2.get("/projects/:id/github/issues", async (req, reply) => {
24628
+ const { id } = req.params;
24629
+ const { status } = req.query;
24630
+ const project = await prisma.project.findUnique({ where: { id }, select: { id: true } });
24631
+ if (!project) return reply.code(404).send({ error: "not found" });
24632
+ const items = await prisma.githubIssue.findMany({
24633
+ where: { projectId: id, ...status ? { status } : {} },
24634
+ orderBy: [{ number: "desc" }]
24635
+ });
24636
+ return reply.send({ items: items.map(view7) });
24637
+ });
24638
+ app2.post("/github-issues/accept", async (req, reply) => {
24639
+ const parsed = zAcceptMany.safeParse(req.body ?? {});
24640
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24641
+ const { ids, priority, source } = parsed.data;
24642
+ const author = req.user?.email ?? "system";
24643
+ const created = [];
24644
+ const failed = [];
24645
+ for (const id of ids) {
24646
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24647
+ if (!issue2) {
24648
+ failed.push({ id, error: "not found" });
24649
+ continue;
24650
+ }
24651
+ try {
24652
+ created.push((await acceptGithubIssue(issue2, { author, priority, source })).spec);
24653
+ } catch (e) {
24654
+ failed.push({ id, error: e.message });
24655
+ }
24656
+ }
24657
+ return reply.code(201).send({ created, failed });
24658
+ });
24659
+ app2.post("/github-issues/:id/accept", async (req, reply) => {
24660
+ const { id } = req.params;
24661
+ const parsed = zAccept.safeParse(req.body ?? {});
24662
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24663
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24664
+ if (!issue2) return reply.code(404).send({ error: "not found" });
24665
+ const { spec, created } = await acceptGithubIssue(issue2, {
24666
+ author: req.user?.email ?? "system",
24667
+ priority: parsed.data.priority,
24668
+ source: parsed.data.source
24669
+ });
24670
+ return reply.code(created ? 201 : 200).send(created ? { spec } : { spec, alreadyPromoted: true });
24671
+ });
24672
+ app2.post("/github-issues/:id/reject", async (req, reply) => {
24673
+ const { id } = req.params;
24674
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24675
+ if (!issue2) return reply.code(404).send({ error: "not found" });
24676
+ const row = await prisma.githubIssue.update({ where: { id }, data: { status: "rejected" } });
24677
+ await notifySynced("githubIssue", id);
24678
+ return reply.send({ id: row.id, status: row.status });
24679
+ });
24680
+ app2.post("/github-issues/:id/unlink", async (req, reply) => {
24681
+ const { id } = req.params;
24682
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24683
+ if (!issue2) return reply.code(404).send({ error: "not found" });
24684
+ const row = await prisma.githubIssue.update({ where: { id }, data: { status: "new", specId: null } });
24685
+ await notifySynced("githubIssue", id);
24686
+ return reply.send({ id: row.id, status: row.status, specId: row.specId });
24687
+ });
24688
+ }
24689
+
24690
+ // src/routes/telegram.ts
24691
+ init_zod();
24692
+ init_src();
24693
+ init_db();
24694
+ import { randomBytes as randomBytes3 } from "node:crypto";
24695
+
24696
+ // src/services/telegram/runtime.ts
24697
+ var stoppedStatus = () => ({
24698
+ configured: false,
24699
+ enabled: false,
24700
+ running: false,
24701
+ readiness: "disabled",
24702
+ botUsername: null,
24703
+ allowlistCount: 0,
24704
+ agentTokenConfigured: false,
24705
+ missingCapabilities: [],
24706
+ lastUpdateAt: null,
24707
+ lastError: null
24708
+ });
24709
+ var registration = {
24710
+ agentTokenId: null,
24711
+ status: stoppedStatus()
24712
+ };
24713
+ var stopRuntime = null;
24714
+ function setTelegramRuntime(input) {
24715
+ registration = {
24716
+ agentTokenId: input.agentTokenId === void 0 ? registration.agentTokenId : input.agentTokenId,
24717
+ status: input.status ?? registration.status
24718
+ };
24719
+ }
24720
+ function updateTelegramRuntimeStatus(patch) {
24721
+ registration = { ...registration, status: { ...registration.status, ...patch } };
24722
+ }
24723
+ function telegramRuntimeStatus() {
24724
+ return { ...registration.status };
24725
+ }
24726
+ function telegramGatewayAgentTokenId() {
24727
+ return registration.agentTokenId;
24728
+ }
24729
+ function registerTelegramRuntimeStop(stop) {
24730
+ stopRuntime = stop;
24731
+ }
24732
+ async function stopTelegramRuntime() {
24733
+ const stop = stopRuntime;
24734
+ stopRuntime = null;
24735
+ if (stop) await stop();
24736
+ }
24737
+
24738
+ // src/services/telegram/store.ts
24739
+ import { Prisma } from "@prisma/client";
24740
+ var isUnique = (error) => error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002";
24741
+ var TelegramStore = class {
24742
+ constructor(db) {
24743
+ this.db = db;
24744
+ }
24745
+ async recordUpdate(input) {
24746
+ try {
24747
+ await this.db.$transaction(async (tx) => {
24748
+ await tx.telegramUpdate.create({ data: {
24749
+ updateId: input.updateId,
24750
+ chatId: input.chatId ?? null,
24751
+ userId: input.userId ?? null,
24752
+ kind: input.kind,
24753
+ digest: input.digest
24754
+ } });
24755
+ const current = await tx.telegramGatewayState.findUnique({ where: { id: 1 }, select: { offset: true } });
24756
+ const offset = Math.max(current?.offset ?? 0, input.updateId + 1);
24757
+ await tx.telegramGatewayState.upsert({
24758
+ where: { id: 1 },
24759
+ create: { id: 1, offset, lastUpdateAt: /* @__PURE__ */ new Date() },
24760
+ update: { offset, lastUpdateAt: /* @__PURE__ */ new Date() }
24761
+ });
24762
+ });
24763
+ return true;
24764
+ } catch (error) {
24765
+ if (isUnique(error)) return false;
24766
+ throw error;
24767
+ }
24768
+ }
24769
+ async offset() {
24770
+ return (await this.db.telegramGatewayState.findUnique({ where: { id: 1 } }))?.offset ?? 0;
24771
+ }
24772
+ async claimUpdate(updateId) {
24773
+ return (await this.db.telegramUpdate.updateMany({
24774
+ where: { updateId, state: "received" },
24775
+ data: { state: "dispatching", claimedAt: /* @__PURE__ */ new Date() }
24776
+ })).count === 1;
24777
+ }
24778
+ async markDispatched(updateId) {
24779
+ await this.db.telegramUpdate.updateMany({
24780
+ where: { updateId, state: "dispatching" },
24781
+ data: { state: "dispatched", dispatchedAt: /* @__PURE__ */ new Date() }
24782
+ });
24783
+ }
24784
+ async rejectUpdate(updateId, reason) {
24785
+ return (await this.db.telegramUpdate.updateMany({
24786
+ where: { updateId, state: "received" },
24787
+ data: { state: "rejected", rejectReason: reason }
24788
+ })).count === 1;
24789
+ }
24790
+ async recoverUncertainClaims() {
24791
+ return (await this.db.telegramUpdate.updateMany({
24792
+ where: { state: { in: ["received", "dispatching"] } },
24793
+ data: { state: "uncertain" }
24794
+ })).count;
24795
+ }
24796
+ async ensureChat(input) {
24797
+ return this.db.telegramChat.upsert({
24798
+ where: { chatId: input.chatId },
24799
+ create: input,
24800
+ update: { userId: input.userId }
24801
+ });
24802
+ }
24803
+ async chatContext(chatId) {
24804
+ const chat = await this.db.telegramChat.findUnique({ where: { chatId } });
24805
+ if (!chat) return null;
24806
+ const memories = await this.db.telegramMemory.findMany({ where: { chatId }, orderBy: { createdAt: "asc" } });
24807
+ return {
24808
+ chatId: chat.chatId,
24809
+ userId: chat.userId,
24810
+ sessionId: chat.sessionId,
24811
+ activeProjectId: chat.activeProjectId,
24812
+ activeSessionId: chat.activeSessionId,
24813
+ personalityAgentId: chat.personalityAgentId,
24814
+ summary: chat.summary,
24815
+ agent: chat.agent,
24816
+ model: chat.model,
24817
+ effort: chat.effort,
24818
+ memories: memories.map(this.memoryView)
24819
+ };
24820
+ }
24821
+ patchChat(chatId, patch) {
24822
+ return this.db.telegramChat.update({ where: { chatId }, data: patch });
24823
+ }
24824
+ async bindSession(chatId, sessionId2) {
24825
+ await this.db.telegramChat.update({ where: { chatId }, data: { sessionId: sessionId2 } });
24826
+ }
24827
+ async rateLimitExceeded(userId, since, limit) {
24828
+ return await this.db.telegramUpdate.count({ where: { userId, receivedAt: { gte: since } } }) > limit;
24829
+ }
24830
+ async markUpdateUncertain(updateId, reason) {
24831
+ await this.db.telegramUpdate.updateMany({
24832
+ where: { updateId, state: "dispatching" },
24833
+ data: { state: "uncertain", rejectReason: reason }
24834
+ });
24835
+ }
24836
+ async addMemory(chatId, content) {
24837
+ return this.memoryView(await this.db.telegramMemory.create({ data: { chatId, content } }));
24838
+ }
24839
+ async forgetMemory(chatId, id) {
24840
+ return (await this.db.telegramMemory.deleteMany({ where: { id, chatId } })).count === 1;
24841
+ }
24842
+ async resetMemory(chatId) {
24843
+ const chat = await this.db.telegramChat.findUnique({ where: { chatId }, select: { chatId: true } });
24844
+ if (!chat) return false;
24845
+ await this.db.$transaction([
24846
+ this.db.telegramMemory.deleteMany({ where: { chatId } }),
24847
+ this.db.telegramChat.update({ where: { chatId }, data: { summary: null } })
24848
+ ]);
24849
+ return true;
24850
+ }
24851
+ memoryView(row) {
24852
+ return { ...row, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString() };
24853
+ }
24854
+ async enqueueReply(input) {
24855
+ const dedupeKey = `${input.chatId}:${input.updateId}:${input.kind}`;
24856
+ try {
24857
+ return await this.db.$transaction(async (tx) => {
24858
+ const existing = await tx.telegramOutbox.findUnique({ where: { dedupeKey } });
24859
+ if (existing) return existing;
24860
+ let confirmationId = null;
24861
+ if (input.confirmation) {
24862
+ const confirmation = await tx.telegramConfirmation.create({ data: {
24863
+ callbackToken: input.confirmation.callbackToken,
24864
+ chatId: input.chatId,
24865
+ userId: input.confirmation.userId,
24866
+ updateId: input.updateId,
24867
+ description: input.confirmation.description,
24868
+ method: input.confirmation.method,
24869
+ path: input.confirmation.path,
24870
+ expiresAt: input.confirmation.expiresAt
24871
+ } });
24872
+ confirmationId = confirmation.id;
24873
+ }
24874
+ return tx.telegramOutbox.create({ data: {
24875
+ chatId: input.chatId,
24876
+ updateId: input.updateId,
24877
+ kind: input.kind,
24878
+ dedupeKey,
24879
+ text: input.text,
24880
+ confirmationId
24881
+ } });
24882
+ });
24883
+ } catch (error) {
24884
+ if (!isUnique(error)) throw error;
24885
+ return await this.db.telegramOutbox.findUnique({ where: { dedupeKey } });
24886
+ }
24887
+ }
24888
+ async claimNextOutbox() {
24889
+ for (; ; ) {
24890
+ const row = await this.db.telegramOutbox.findFirst({ where: { state: "pending" }, orderBy: { createdAt: "asc" } });
24891
+ if (!row) return null;
24892
+ const claimed = await this.db.telegramOutbox.updateMany({
24893
+ where: { id: row.id, state: "pending" },
24894
+ data: { state: "sending", claimedAt: /* @__PURE__ */ new Date() }
24895
+ });
24896
+ if (claimed.count === 1) return this.db.telegramOutbox.findUnique({ where: { id: row.id } });
24897
+ }
24898
+ }
24899
+ async recoverUncertainOutbox() {
24900
+ return (await this.db.telegramOutbox.updateMany({ where: { state: "sending" }, data: { state: "uncertain" } })).count;
24901
+ }
24902
+ markOutboxSent(id, telegramMessageId) {
24903
+ return this.db.telegramOutbox.updateMany({
24904
+ where: { id, state: "sending" },
24905
+ data: { state: "sent", telegramMessageId, sentAt: /* @__PURE__ */ new Date() }
24906
+ });
24907
+ }
24908
+ markOutboxFailed(id, error) {
24909
+ return this.db.telegramOutbox.updateMany({ where: { id, state: "sending" }, data: { state: "failed", error } });
24910
+ }
24911
+ markOutboxUncertain(id, error) {
24912
+ return this.db.telegramOutbox.updateMany({ where: { id, state: "sending" }, data: { state: "uncertain", error } });
24913
+ }
24914
+ confirmation(id) {
24915
+ return this.db.telegramConfirmation.findUnique({ where: { id } });
24916
+ }
24917
+ async resolveConfirmation(input) {
24918
+ const now = input.now ?? /* @__PURE__ */ new Date();
24919
+ const row = await this.db.telegramConfirmation.findUnique({ where: { callbackToken: input.callbackToken } });
24920
+ if (!row || row.chatId !== input.chatId || row.userId !== input.userId || row.state !== "pending") return "invalid";
24921
+ if (row.expiresAt <= now) {
24922
+ await this.db.telegramConfirmation.updateMany({
24923
+ where: { id: row.id, state: "pending" },
24924
+ data: { state: "expired" }
24925
+ });
24926
+ return "expired";
24927
+ }
24928
+ const state = input.action === "approve" ? "approved" : "denied";
24929
+ const changed = await this.db.telegramConfirmation.updateMany({
24930
+ where: { id: row.id, state: "pending" },
24931
+ data: { state, ...state === "approved" ? { approvedAt: now } : {} }
24932
+ });
24933
+ return changed.count === 1 ? state : "invalid";
24934
+ }
24935
+ async audit(input) {
24936
+ const row = await this.db.telegramAudit.create({ data: {
24937
+ chatId: input.chatId ?? null,
24938
+ userId: input.userId ?? null,
24939
+ updateId: input.updateId ?? null,
24940
+ action: input.action,
24941
+ outcome: input.outcome,
24942
+ correlationId: input.correlationId ?? null,
24943
+ method: input.method ?? null,
24944
+ path: input.path ?? null,
24945
+ statusCode: input.statusCode ?? null
24946
+ } });
24947
+ return { ...row, createdAt: row.createdAt.toISOString() };
24948
+ }
24949
+ async listAudit(input) {
24950
+ const where = {
24951
+ ...input.chatId ? { chatId: input.chatId } : {},
24952
+ ...input.updateId !== void 0 ? { updateId: input.updateId } : {}
24953
+ };
24954
+ const [rows, total] = await this.db.$transaction([
24955
+ this.db.telegramAudit.findMany({ where, orderBy: { createdAt: "desc" }, take: input.take, skip: input.skip }),
24956
+ this.db.telegramAudit.count({ where })
24957
+ ]);
24958
+ return { items: rows.map((row) => ({ ...row, createdAt: row.createdAt.toISOString() })), total };
24959
+ }
24960
+ };
24961
+
24962
+ // src/routes/telegram.ts
24963
+ var store = new TelegramStore(prisma);
24964
+ var zContextPatch = external_exports.object({
24965
+ activeProjectId: external_exports.string().min(1).nullable().optional(),
24966
+ activeSessionId: external_exports.string().min(1).nullable().optional(),
24967
+ personalityAgentId: external_exports.string().min(1).nullable().optional(),
24968
+ summary: external_exports.string().trim().min(1).max(4e3).nullable().optional()
24969
+ }).refine((value) => Object.keys(value).length > 0, "empty patch");
24970
+ var zMemory = external_exports.object({ content: external_exports.string().trim().min(1).max(1e3) });
24971
+ async function telegramRoutes(app2) {
24972
+ app2.get("/telegram/status", async () => telegramRuntimeStatus());
24973
+ app2.get("/telegram/chats/:chatId/context", async (req, reply) => {
24974
+ const context = await store.chatContext(req.params.chatId);
24975
+ return context ?? reply.code(404).send({ error: "chat not found" });
24976
+ });
24977
+ app2.patch("/telegram/chats/:chatId/context", async (req, reply) => {
24978
+ const parsed = zContextPatch.safeParse(req.body);
24979
+ if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
24980
+ const chatId = req.params.chatId;
24981
+ try {
24982
+ await store.patchChat(chatId, parsed.data);
24983
+ } catch {
24984
+ return reply.code(404).send({ error: "chat not found" });
24985
+ }
24986
+ return store.chatContext(chatId);
24987
+ });
24988
+ app2.post("/telegram/chats/:chatId/memories", async (req, reply) => {
24989
+ const parsed = zMemory.safeParse(req.body);
24990
+ if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
24991
+ const chatId = req.params.chatId;
24992
+ if (!await prisma.telegramChat.findUnique({ where: { chatId }, select: { chatId: true } })) {
24993
+ return reply.code(404).send({ error: "chat not found" });
24994
+ }
24995
+ return reply.code(201).send(await store.addMemory(chatId, parsed.data.content));
24996
+ });
24997
+ app2.delete("/telegram/chats/:chatId/memories/:id", async (req, reply) => {
24998
+ const { chatId, id } = req.params;
24999
+ if (!await store.forgetMemory(chatId, id)) return reply.code(404).send({ error: "memory not found" });
25000
+ return reply.code(204).send();
25001
+ });
25002
+ app2.delete("/telegram/chats/:chatId/memories", async (req, reply) => {
25003
+ if (!await store.resetMemory(req.params.chatId)) {
25004
+ return reply.code(404).send({ error: "chat not found" });
25005
+ }
25006
+ return reply.code(204).send();
25007
+ });
25008
+ app2.post("/telegram/replies", async (req, reply) => {
25009
+ const parsed = zTelegramReplyInput.safeParse(req.body);
25010
+ if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
25011
+ const input = parsed.data;
25012
+ const update2 = await prisma.telegramUpdate.findFirst({
25013
+ where: { updateId: input.updateId, chatId: input.chatId, state: { in: ["dispatching", "dispatched"] } }
25014
+ });
25015
+ if (!update2?.userId) return reply.code(409).send({ error: "update/chat correlation invalid" });
25016
+ const outbox = await store.enqueueReply({
25017
+ chatId: input.chatId,
25018
+ updateId: input.updateId,
25019
+ kind: input.kind,
25020
+ text: input.text,
25021
+ ...input.confirmation ? { confirmation: {
25022
+ callbackToken: randomBytes3(12).toString("base64url"),
25023
+ userId: update2.userId,
25024
+ description: input.confirmation.description,
25025
+ method: input.confirmation.method,
25026
+ path: input.confirmation.path,
25027
+ expiresAt: new Date(Date.now() + 10 * 6e4)
25028
+ } } : {}
25029
+ });
25030
+ if (input.summary !== void 0) await store.patchChat(input.chatId, { summary: input.summary });
25031
+ for (const memory of input.remember ?? []) await store.addMemory(input.chatId, memory);
25032
+ await store.audit({
25033
+ chatId: input.chatId,
25034
+ userId: update2.userId,
25035
+ updateId: input.updateId,
25036
+ action: "reply-enqueue",
25037
+ outcome: input.kind,
25038
+ correlationId: `tg:${input.updateId}`
25039
+ });
25040
+ return reply.code(202).send({ id: outbox.id, state: outbox.state });
25041
+ });
25042
+ app2.get("/telegram/audit", async (req, reply) => {
25043
+ const parsed = external_exports.object({
25044
+ chatId: external_exports.string().optional(),
25045
+ updateId: external_exports.coerce.number().int().nonnegative().optional(),
25046
+ take: external_exports.coerce.number().int().min(1).max(100).default(50),
25047
+ skip: external_exports.coerce.number().int().min(0).default(0)
25048
+ }).safeParse(req.query);
25049
+ if (!parsed.success) return reply.code(400).send({ error: "invalid query" });
25050
+ return store.listAudit(parsed.data);
25051
+ });
25052
+ }
25053
+
25054
+ // src/app.ts
25055
+ var import_multipart = __toESM(require_multipart2(), 1);
25056
+
25057
+ // src/routes/auth.ts
25058
+ init_src();
25059
+ init_db();
25060
+
25061
+ // src/services/auth.ts
25062
+ init_db();
25063
+ import { randomBytes as randomBytes4, scrypt as scryptCb, timingSafeEqual, createHash as createHash3 } from "node:crypto";
25064
+ import { promisify as promisify9 } from "node:util";
25065
+ var scrypt = promisify9(scryptCb);
25066
+ var COOKIE_NAME = "hn_session";
25067
+ var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
25068
+ async function hashPassword(pw) {
25069
+ const salt = randomBytes4(16);
25070
+ const key = await scrypt(pw, salt, 64);
25071
+ return `${salt.toString("hex")}:${key.toString("hex")}`;
25072
+ }
25073
+ async function verifyPassword(pw, stored) {
25074
+ const [saltHex, hashHex] = stored.split(":");
25075
+ if (!saltHex || !hashHex) return false;
25076
+ const key = await scrypt(pw, Buffer.from(saltHex, "hex"), 64);
25077
+ const want = Buffer.from(hashHex, "hex");
25078
+ return key.length === want.length && timingSafeEqual(key, want);
25079
+ }
25080
+ var newSessionToken = () => randomBytes4(32).toString("base64url");
25081
+ var sessionId = (token) => createHash3("sha256").update(token).digest("hex");
25082
+ async function createSession2(userId) {
25083
+ const token = newSessionToken();
25084
+ await prisma.session.create({
25085
+ data: { id: sessionId(token), userId, expiresAt: new Date(Date.now() + SESSION_TTL_MS) }
25086
+ });
25087
+ return token;
25088
+ }
25089
+ async function lookupSession(token) {
25090
+ const s = await prisma.session.findUnique({ where: { id: sessionId(token) }, include: { user: true } });
25091
+ if (!s) return null;
25092
+ if (s.expiresAt.getTime() < Date.now()) {
25093
+ await prisma.session.delete({ where: { id: s.id } }).catch(() => {
25094
+ });
25095
+ return null;
25096
+ }
25097
+ return { id: s.user.id, email: s.user.email, createdAt: s.user.createdAt.toISOString() };
25098
+ }
25099
+ async function deleteSession(token) {
25100
+ await prisma.session.deleteMany({ where: { id: sessionId(token) } });
25101
+ }
25102
+ async function deleteUserSessions(userId, exceptToken) {
25103
+ await prisma.session.deleteMany({
25104
+ where: { userId, ...exceptToken ? { id: { not: sessionId(exceptToken) } } : {} }
25105
+ });
25106
+ }
25107
+ var fails = /* @__PURE__ */ new Map();
23772
25108
  function loginThrottle(ip) {
23773
25109
  const f = fails.get(ip);
23774
25110
  return { blocked: !!f && f.until > Date.now() };
@@ -23796,7 +25132,7 @@ function cookieOpts() {
23796
25132
  }
23797
25133
 
23798
25134
  // src/routes/auth.ts
23799
- var view7 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
25135
+ var view8 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
23800
25136
  async function issue(reply, userId) {
23801
25137
  const token = await createSession2(userId);
23802
25138
  reply.setCookie(COOKIE_NAME, token, cookieOpts());
@@ -23814,7 +25150,7 @@ async function auth_default(app2) {
23814
25150
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
23815
25151
  });
23816
25152
  await issue(reply, user.id);
23817
- return { user: view7(user) };
25153
+ return { user: view8(user) };
23818
25154
  });
23819
25155
  app2.post("/auth/login", async (req, reply) => {
23820
25156
  if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
@@ -23827,7 +25163,7 @@ async function auth_default(app2) {
23827
25163
  }
23828
25164
  clearLoginFails(req.ip);
23829
25165
  await issue(reply, user.id);
23830
- return { user: view7(user) };
25166
+ return { user: view8(user) };
23831
25167
  });
23832
25168
  app2.post("/auth/logout", async (req, reply) => {
23833
25169
  const token = req.cookies?.[COOKIE_NAME];
@@ -23835,7 +25171,7 @@ async function auth_default(app2) {
23835
25171
  reply.clearCookie(COOKIE_NAME, { path: "/" });
23836
25172
  return reply.code(204).send();
23837
25173
  });
23838
- app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view7));
25174
+ app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view8));
23839
25175
  app2.post("/auth/users", async (req, reply) => {
23840
25176
  const p = zSignup.safeParse(req.body);
23841
25177
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
@@ -23844,7 +25180,7 @@ async function auth_default(app2) {
23844
25180
  const user = await prisma.user.create({
23845
25181
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
23846
25182
  });
23847
- return view7(user);
25183
+ return view8(user);
23848
25184
  });
23849
25185
  app2.delete("/auth/users/:id", async (req, reply) => {
23850
25186
  if (await prisma.user.count() <= 1) return reply.code(400).send({ error: "tak bisa hapus user terakhir" });
@@ -23864,7 +25200,7 @@ async function auth_default(app2) {
23864
25200
  });
23865
25201
  await deleteUserSessions(user.id);
23866
25202
  await issue(reply, user.id);
23867
- return { user: view7(user) };
25203
+ return { user: view8(user) };
23868
25204
  });
23869
25205
  }
23870
25206
 
@@ -23873,7 +25209,7 @@ init_src();
23873
25209
 
23874
25210
  // src/services/agent-token.ts
23875
25211
  init_db();
23876
- import { randomBytes as randomBytes4, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
25212
+ import { randomBytes as randomBytes5, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
23877
25213
  var hash = (token) => createHash4("sha256").update(token).digest("hex");
23878
25214
  function toAgentTokenView(t) {
23879
25215
  return {
@@ -23889,7 +25225,7 @@ function toAgentTokenView(t) {
23889
25225
  };
23890
25226
  }
23891
25227
  async function issueAgentToken(input) {
23892
- const token = "hnm_agt_" + randomBytes4(24).toString("hex");
25228
+ const token = "hnm_agt_" + randomBytes5(24).toString("hex");
23893
25229
  const row = await prisma.agentToken.create({
23894
25230
  data: {
23895
25231
  name: input.name,
@@ -23943,15 +25279,15 @@ async function agent_tokens_default(app2) {
23943
25279
  app2.post("/agent-tokens", async (req, reply) => {
23944
25280
  const p = zAgentTokenCreate.safeParse(req.body);
23945
25281
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
23946
- const { view: view8, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
23947
- return reply.code(201).send({ ...view8, token });
25282
+ const { view: view9, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
25283
+ return reply.code(201).send({ ...view9, token });
23948
25284
  });
23949
25285
  app2.patch("/agent-tokens/:id", async (req, reply) => {
23950
25286
  const p = zAgentTokenPatch.safeParse(req.body);
23951
25287
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
23952
25288
  const { id } = req.params;
23953
- const view8 = await patchAgentToken(id, p.data);
23954
- return view8 ? reply.send(view8) : reply.code(404).send({ error: "not found" });
25289
+ const view9 = await patchAgentToken(id, p.data);
25290
+ return view9 ? reply.send(view9) : reply.code(404).send({ error: "not found" });
23955
25291
  });
23956
25292
  app2.delete("/agent-tokens/:id", async (req, reply) => {
23957
25293
  const { id } = req.params;
@@ -24000,10 +25336,12 @@ function capabilityForRoute(method, path) {
24000
25336
  if (top === "scheduler") return rw("settings");
24001
25337
  if (top === "lead") return rw("lead");
24002
25338
  if (top === "custom-agents") return rw("agents");
25339
+ if (top === "telegram") return rw("telegram");
24003
25340
  if (top === "settings" || top === "config") return rw("settings");
24004
25341
  if (top === "specs") return rw("backlog");
24005
25342
  if (top === "notifications") return rw("notifications");
24006
25343
  if (top === "tickets") return rw("support");
25344
+ if (top === "github-issues") return rw("support");
24007
25345
  if (top === "vps") return rw("vps");
24008
25346
  if (top === "prds") return rw("docs");
24009
25347
  if (top === "terminal") {
@@ -24013,6 +25351,7 @@ function capabilityForRoute(method, path) {
24013
25351
  if (top === "projects") {
24014
25352
  const sub = seg[2];
24015
25353
  if (sub === "docs" || sub === "prds") return rw("docs");
25354
+ if (sub === "github") return rw("support");
24016
25355
  if (sub && IDE_SUBS.has(sub)) return rw("ide");
24017
25356
  return rw("projects");
24018
25357
  }
@@ -24028,6 +25367,119 @@ function checkAgentCapability(caps, method, path) {
24028
25367
 
24029
25368
  // src/app.ts
24030
25369
  init_pty();
25370
+
25371
+ // src/services/telegram/security.ts
25372
+ init_db();
25373
+ var store2 = new TelegramStore(prisma);
25374
+ function isDestructiveTelegramRequest(method, path, body) {
25375
+ const verb = method.toUpperCase();
25376
+ if (verb === "DELETE") return true;
25377
+ if (verb === "POST") {
25378
+ if (/\/update\/apply$/.test(path)) return true;
25379
+ if (/\/(?:specs|terminal\/sessions)\/[^/]+\/integrate$/.test(path)) return true;
25380
+ if (/\/projects\/[^/]+\/(?:git\/(?:merge|rebase|drop|reset|clean)|branches\/delete)$/.test(path)) return true;
25381
+ if (/\/projects\/[^/]+\/git$/.test(path)) {
25382
+ const data = typeof body === "object" && body !== null ? body : {};
25383
+ return (/* @__PURE__ */ new Set([
25384
+ "merge",
25385
+ "cherry-pick",
25386
+ "revert",
25387
+ "delete-branch",
25388
+ "reset",
25389
+ "delete-tag",
25390
+ "reset-worktree",
25391
+ "clean",
25392
+ "stash-pop",
25393
+ "stash-drop"
25394
+ ])).has(String(data.op ?? ""));
25395
+ }
25396
+ if (/\/vps\/[^/]+\/(?:harden|remediate)$/.test(path)) return true;
25397
+ }
25398
+ if (verb === "PATCH" && /\/specs\/[^/]+$/.test(path)) {
25399
+ const data = typeof body === "object" && body !== null ? body : {};
25400
+ return typeof data.stage === "string" && data.confirmDelete === true;
25401
+ }
25402
+ return false;
25403
+ }
25404
+ var header = (req, name2) => {
25405
+ const value = req.headers[name2];
25406
+ return typeof value === "string" && value ? value : null;
25407
+ };
25408
+ async function reject(req, reply, message, updateId) {
25409
+ const path = req.url.split("?")[0] ?? req.url;
25410
+ await store2.audit({
25411
+ updateId: updateId ?? null,
25412
+ action: "api-guard",
25413
+ outcome: `rejected:${message}`,
25414
+ correlationId: updateId === void 0 ? null : `tg:${updateId}`,
25415
+ method: req.method,
25416
+ path,
25417
+ statusCode: 403
25418
+ }).catch(() => {
25419
+ });
25420
+ await reply.code(403).send({ error: message });
25421
+ }
25422
+ async function guardTelegramGatewayRequest(req, reply) {
25423
+ const gatewayId = telegramGatewayAgentTokenId();
25424
+ if (!gatewayId || req.agent?.id !== gatewayId) return;
25425
+ const correlation = header(req, "x-hanoman-telegram-update");
25426
+ const updateId = correlation && /^\d+$/.test(correlation) ? Number(correlation) : null;
25427
+ if (updateId === null || !Number.isSafeInteger(updateId)) {
25428
+ return reject(req, reply, "telegram correlation required");
25429
+ }
25430
+ const update2 = await prisma.telegramUpdate.findUnique({ where: { updateId } });
25431
+ if (!update2?.chatId || !update2.userId) return reject(req, reply, "telegram correlation invalid", updateId);
25432
+ const path = req.url.split("?")[0] ?? req.url;
25433
+ if (!isDestructiveTelegramRequest(req.method, path, req.body)) return;
25434
+ const confirmationId = header(req, "x-hanoman-telegram-confirmation");
25435
+ if (!confirmationId) return reject(req, reply, "telegram confirmation required", updateId);
25436
+ const consumed = await prisma.telegramConfirmation.updateMany({
25437
+ where: {
25438
+ id: confirmationId,
25439
+ state: "approved",
25440
+ updateId,
25441
+ chatId: update2.chatId,
25442
+ userId: update2.userId,
25443
+ method: req.method,
25444
+ path,
25445
+ expiresAt: { gt: /* @__PURE__ */ new Date() }
25446
+ },
25447
+ data: { state: "used", usedAt: /* @__PURE__ */ new Date() }
25448
+ });
25449
+ if (consumed.count !== 1) return reject(req, reply, "telegram confirmation invalid", updateId);
25450
+ await store2.audit({
25451
+ chatId: update2.chatId,
25452
+ userId: update2.userId,
25453
+ updateId,
25454
+ action: "confirmation-consume",
25455
+ outcome: "accepted",
25456
+ correlationId: `tg:${updateId}`,
25457
+ method: req.method,
25458
+ path
25459
+ }).catch(() => {
25460
+ });
25461
+ }
25462
+ async function auditTelegramGatewayResponse(req, reply) {
25463
+ const gatewayId = telegramGatewayAgentTokenId();
25464
+ if (!gatewayId || req.agent?.id !== gatewayId) return;
25465
+ const value = header(req, "x-hanoman-telegram-update");
25466
+ const updateId = value && /^\d+$/.test(value) ? Number(value) : null;
25467
+ const update2 = updateId === null ? null : await prisma.telegramUpdate.findUnique({ where: { updateId } });
25468
+ await store2.audit({
25469
+ chatId: update2?.chatId ?? null,
25470
+ userId: update2?.userId ?? null,
25471
+ updateId,
25472
+ action: "api-request",
25473
+ outcome: reply.statusCode < 400 ? "accepted" : "rejected",
25474
+ correlationId: updateId === null ? null : `tg:${updateId}`,
25475
+ method: req.method,
25476
+ path: req.url.split("?")[0] ?? req.url,
25477
+ statusCode: reply.statusCode
25478
+ }).catch(() => {
25479
+ });
25480
+ }
25481
+
25482
+ // src/app.ts
24031
25483
  var PUBLIC = /* @__PURE__ */ new Set([
24032
25484
  "GET /api/health",
24033
25485
  "GET /api/auth/status",
@@ -24047,6 +25499,7 @@ function buildApp({ requireAuth = true } = {}) {
24047
25499
  });
24048
25500
  app2.register(websocket);
24049
25501
  app2.addHook("onClose", async () => {
25502
+ await stopTelegramRuntime();
24050
25503
  detachAll();
24051
25504
  });
24052
25505
  app2.register(async (api) => {
@@ -24080,6 +25533,8 @@ function buildApp({ requireAuth = true } = {}) {
24080
25533
  return reply.code(401).send({ error: "unauthorized" });
24081
25534
  });
24082
25535
  }
25536
+ api.addHook("preHandler", guardTelegramGatewayRequest);
25537
+ api.addHook("onResponse", auditTelegramGatewayResponse);
24083
25538
  await api.register(auth_default);
24084
25539
  await api.register(health_default);
24085
25540
  await api.register(projects_default);
@@ -24107,6 +25562,8 @@ function buildApp({ requireAuth = true } = {}) {
24107
25562
  await api.register(codex);
24108
25563
  await api.register(lead_default);
24109
25564
  await api.register(custom_agents_default);
25565
+ await api.register(githubIssues);
25566
+ await api.register(telegramRoutes);
24110
25567
  }, { prefix: "/api" });
24111
25568
  if (process.env.NODE_ENV === "production") {
24112
25569
  const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
@@ -24224,13 +25681,14 @@ async function reconcile(deps) {
24224
25681
  }
24225
25682
  if (stage === "done") {
24226
25683
  await recordCompletion(item.specId, spec.title, spec.projectId);
25684
+ const head = p?.cwd ? await recordHeadSha(item.specId, p.cwd, deps.headSha) : null;
24227
25685
  const existing = await prisma.sessionResult.findFirst({ where: { specId: item.specId, newStage: "done" } });
24228
25686
  if (!existing) {
24229
25687
  await recordSessionResult({
24230
25688
  projectId: spec.projectId,
24231
25689
  specId: item.specId,
24232
25690
  newStage: "done",
24233
- commitSha: p?.cwd ? deps.headSha(p.cwd) : null,
25691
+ commitSha: head,
24234
25692
  branch: `hanoman/${item.sessionId}`,
24235
25693
  status: "done"
24236
25694
  }).catch(() => {
@@ -24376,6 +25834,587 @@ function registerTriaseSource() {
24376
25834
 
24377
25835
  // src/server.ts
24378
25836
  init_pty();
25837
+
25838
+ // src/services/telegram/bootstrap.ts
25839
+ init_src2();
25840
+ init_db();
25841
+ import { mkdirSync as mkdirSync7 } from "node:fs";
25842
+ init_pty();
25843
+
25844
+ // src/services/telegram/client.ts
25845
+ var TelegramApiError = class extends Error {
25846
+ constructor(method, code, message) {
25847
+ super(`Telegram ${method} gagal${code ? ` (${code})` : ""}: ${message}`);
25848
+ this.method = method;
25849
+ this.code = code;
25850
+ this.name = "TelegramApiError";
25851
+ }
25852
+ };
25853
+ var TelegramApiClient = class {
25854
+ constructor(token, transport = (url2, init) => fetch(url2, init), baseUrl = "https://api.telegram.org") {
25855
+ this.token = token;
25856
+ this.transport = transport;
25857
+ this.baseUrl = baseUrl;
25858
+ }
25859
+ safe(message) {
25860
+ return message.split(this.token).join("[REDACTED]").replace(/https:\/\/api\.telegram\.org\/bot[^/\s]+/g, "https://api.telegram.org/bot[REDACTED]");
25861
+ }
25862
+ async call(method, body, signal) {
25863
+ let response;
25864
+ try {
25865
+ response = await this.transport(`${this.baseUrl}/bot${this.token}/${method}`, {
25866
+ method: "POST",
25867
+ headers: { "content-type": "application/json" },
25868
+ body: JSON.stringify(body),
25869
+ signal
25870
+ });
25871
+ } catch (error) {
25872
+ throw new TelegramApiError(method, void 0, this.safe(error.message || "network error"));
25873
+ }
25874
+ if (!response.ok) throw new TelegramApiError(method, response.status, `HTTP ${response.status}`);
25875
+ let envelope;
25876
+ try {
25877
+ envelope = await response.json();
25878
+ } catch {
25879
+ throw new TelegramApiError(method, response.status, "respons bukan JSON");
25880
+ }
25881
+ if (!envelope.ok || envelope.result === void 0) {
25882
+ throw new TelegramApiError(method, envelope.error_code, this.safe(envelope.description ?? "respons tidak valid"));
25883
+ }
25884
+ return envelope.result;
25885
+ }
25886
+ getMe() {
25887
+ return this.call("getMe", {});
25888
+ }
25889
+ async getUpdates(input) {
25890
+ if (!Number.isInteger(input.offset) || input.offset < 0) throw new Error("offset harus integer non-negatif");
25891
+ if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new Error("limit harus 1..100");
25892
+ if (!Number.isInteger(input.timeout) || input.timeout < 0 || input.timeout > 50) throw new Error("timeout harus 0..50");
25893
+ return this.call("getUpdates", {
25894
+ offset: input.offset,
25895
+ limit: input.limit,
25896
+ timeout: input.timeout,
25897
+ allowed_updates: ["message", "callback_query"]
25898
+ }, input.signal);
25899
+ }
25900
+ sendMessage(input) {
25901
+ return this.call("sendMessage", {
25902
+ chat_id: input.chatId,
25903
+ text: input.text,
25904
+ ...input.replyMarkup ? { reply_markup: input.replyMarkup } : {}
25905
+ });
25906
+ }
25907
+ editMessageText(input) {
25908
+ return this.call("editMessageText", {
25909
+ chat_id: input.chatId,
25910
+ message_id: input.messageId,
25911
+ text: input.text
25912
+ });
25913
+ }
25914
+ answerCallbackQuery(input) {
25915
+ return this.call("answerCallbackQuery", {
25916
+ callback_query_id: input.callbackQueryId,
25917
+ ...input.text ? { text: input.text } : {}
25918
+ });
25919
+ }
25920
+ };
25921
+
25922
+ // src/services/telegram/gateway.ts
25923
+ import { createHash as createHash6 } from "node:crypto";
25924
+
25925
+ // src/services/telegram/protocol.ts
25926
+ import { createHash as createHash5 } from "node:crypto";
25927
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
25928
+ function normalizeTelegramCommand(text) {
25929
+ const clean = text.trim();
25930
+ const match = clean.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
25931
+ if (!match) return clean;
25932
+ const args = match[2]?.trim().replace(/\s+/g, " ");
25933
+ return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
25934
+ }
25935
+ function parseConfirmationCallback(data) {
25936
+ if (data.length > 64) return null;
25937
+ const match = data.match(/^tgcf:([A-Za-z0-9_-]{1,40}):(approve|deny)$/);
25938
+ return match ? { token: match[1], action: match[2] } : null;
25939
+ }
25940
+ function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
25941
+ const root = record(update2);
25942
+ const updateId = root && Number.isInteger(root.update_id) && Number(root.update_id) >= 0 ? Number(root.update_id) : null;
25943
+ if (!root || updateId === null) return { ok: false, updateId: null, reason: "malformed" };
25944
+ const callback = record(root.callback_query);
25945
+ const message = callback ? record(callback.message) : record(root.message);
25946
+ const from = callback ? record(callback.from) : record(message?.from);
25947
+ const chat = record(message?.chat);
25948
+ const chatId = typeof chat?.id === "number" && Number.isSafeInteger(chat.id) ? String(chat.id) : void 0;
25949
+ const userId = typeof from?.id === "number" && Number.isSafeInteger(from.id) ? String(from.id) : void 0;
25950
+ if (!message || !from || !chat || !chatId || !userId) return { ok: false, updateId, reason: "malformed", chatId, userId };
25951
+ if (chat.type !== "private") return { ok: false, updateId, reason: "group", chatId, userId };
25952
+ if (from.is_bot === true) return { ok: false, updateId, reason: "sender-bot", chatId, userId };
25953
+ if (!allowlist.has(userId)) return { ok: false, updateId, reason: "not-allowed", chatId, userId };
25954
+ const messageId = typeof message.message_id === "number" && Number.isInteger(message.message_id) ? message.message_id : null;
25955
+ if (messageId === null) return { ok: false, updateId, reason: "malformed", chatId, userId };
25956
+ if (callback) {
25957
+ const callbackQueryId = typeof callback.id === "string" ? callback.id : "";
25958
+ const data = typeof callback.data === "string" ? callback.data : "";
25959
+ const parsed = parseConfirmationCallback(data);
25960
+ if (!callbackQueryId || !parsed) return { ok: false, updateId, reason: "invalid-callback", chatId, userId };
25961
+ return {
25962
+ ok: true,
25963
+ input: {
25964
+ updateId,
25965
+ chatId,
25966
+ userId,
25967
+ messageId,
25968
+ kind: "callback",
25969
+ text: data,
25970
+ callbackQueryId,
25971
+ callbackToken: parsed.token,
25972
+ callbackAction: parsed.action
25973
+ }
25974
+ };
25975
+ }
25976
+ if (typeof message.text !== "string" || !message.text.trim()) {
25977
+ return { ok: false, updateId, reason: "non-text", chatId, userId };
25978
+ }
25979
+ const text = normalizeTelegramCommand(message.text);
25980
+ if (text.length > maxText) return { ok: false, updateId, reason: "too-long", chatId, userId };
25981
+ return {
25982
+ ok: true,
25983
+ input: {
25984
+ updateId,
25985
+ chatId,
25986
+ userId,
25987
+ messageId,
25988
+ kind: text.startsWith("/") ? "command" : "text",
25989
+ text
25990
+ }
25991
+ };
25992
+ }
25993
+ function inboundDigest(input) {
25994
+ return createHash5("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
25995
+ }
25996
+ var ANSI = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
25997
+ var CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
25998
+ var CREDENTIAL_PATTERNS = [
25999
+ /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g,
26000
+ /\bhnm_(?:agent_)?[A-Za-z0-9_-]{16,}\b/g,
26001
+ /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g
26002
+ ];
26003
+ function sanitizeTelegramOutput(text, exactSecrets = []) {
26004
+ let clean = text.replace(ANSI, "").replace(CONTROLS, "");
26005
+ for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
26006
+ clean = clean.split(secret).join("[REDACTED]");
26007
+ }
26008
+ for (const pattern of CREDENTIAL_PATTERNS) clean = clean.replace(pattern, "[REDACTED]");
26009
+ return clean.trim();
26010
+ }
26011
+ function splitTelegramText(text, max = 4096, exactSecrets = []) {
26012
+ if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
26013
+ const clean = sanitizeTelegramOutput(text, exactSecrets);
26014
+ if (!clean) return [];
26015
+ const chunks = [];
26016
+ let rest = clean;
26017
+ while (rest.length > max) {
26018
+ let cut = max;
26019
+ const before = rest.charCodeAt(cut - 1);
26020
+ const after = rest.charCodeAt(cut);
26021
+ if (before >= 55296 && before <= 56319 && after >= 56320 && after <= 57343) cut--;
26022
+ const window = rest.slice(0, cut);
26023
+ const newline2 = window.lastIndexOf("\n");
26024
+ const space = window.lastIndexOf(" ");
26025
+ const boundary = Math.max(newline2, space);
26026
+ if (boundary >= Math.floor(max / 2)) cut = boundary + 1;
26027
+ chunks.push(rest.slice(0, cut));
26028
+ rest = rest.slice(cut);
26029
+ }
26030
+ if (rest) chunks.push(rest);
26031
+ return chunks;
26032
+ }
26033
+
26034
+ // src/services/telegram/gateway.ts
26035
+ var digestRaw = (update2) => createHash6("sha256").update(JSON.stringify(update2)).digest("hex");
26036
+ var TelegramGateway = class {
26037
+ constructor(deps) {
26038
+ this.deps = deps;
26039
+ }
26040
+ abort = null;
26041
+ loopPromise = null;
26042
+ async processUpdates(updates) {
26043
+ for (const raw of updates) await this.processUpdate(raw);
26044
+ }
26045
+ async processUpdate(raw) {
26046
+ const parsed = parseTelegramUpdate(raw, this.deps.allowedUserIds);
26047
+ const root = typeof raw === "object" && raw !== null ? raw : {};
26048
+ const rawUpdateId = Number.isInteger(root.update_id) ? Number(root.update_id) : null;
26049
+ if (rawUpdateId === null || rawUpdateId < 0) return;
26050
+ const meta = parsed.ok ? {
26051
+ updateId: parsed.input.updateId,
26052
+ chatId: parsed.input.chatId,
26053
+ userId: parsed.input.userId,
26054
+ kind: parsed.input.kind,
26055
+ digest: inboundDigest(parsed.input)
26056
+ } : {
26057
+ updateId: rawUpdateId,
26058
+ chatId: parsed.chatId ?? null,
26059
+ userId: parsed.userId ?? null,
26060
+ kind: "rejected",
26061
+ digest: digestRaw(raw)
26062
+ };
26063
+ if (!await this.deps.store.recordUpdate(meta)) return;
26064
+ if (!parsed.ok) {
26065
+ await this.deps.store.rejectUpdate(rawUpdateId, parsed.reason);
26066
+ await this.deps.store.audit({
26067
+ chatId: parsed.chatId ?? null,
26068
+ userId: parsed.userId ?? null,
26069
+ updateId: rawUpdateId,
26070
+ action: "inbound",
26071
+ outcome: `rejected:${parsed.reason}`,
26072
+ correlationId: `tg:${rawUpdateId}`
26073
+ });
26074
+ return;
26075
+ }
26076
+ const input = parsed.input;
26077
+ if (await this.deps.store.rateLimitExceeded(
26078
+ input.userId,
26079
+ new Date(Date.now() - this.deps.rateLimit.windowMs),
26080
+ this.deps.rateLimit.limit
26081
+ )) {
26082
+ await this.deps.store.rejectUpdate(input.updateId, "rate-limit");
26083
+ await this.deps.store.audit({
26084
+ chatId: input.chatId,
26085
+ userId: input.userId,
26086
+ updateId: input.updateId,
26087
+ action: "inbound",
26088
+ outcome: "rejected:rate-limit",
26089
+ correlationId: `tg:${input.updateId}`
26090
+ });
26091
+ return;
26092
+ }
26093
+ if (!await this.deps.store.claimUpdate(input.updateId)) return;
26094
+ try {
26095
+ if (input.kind === "callback" && input.callbackToken && input.callbackAction && input.callbackQueryId) {
26096
+ const outcome = await this.deps.store.resolveConfirmation({
26097
+ callbackToken: input.callbackToken,
26098
+ chatId: input.chatId,
26099
+ userId: input.userId,
26100
+ action: input.callbackAction
26101
+ });
26102
+ const answer = outcome === "approved" ? "Disetujui" : outcome === "denied" ? "Dibatalkan" : outcome === "expired" ? "Konfirmasi kedaluwarsa" : "Konfirmasi tidak valid";
26103
+ await this.deps.client.answerCallbackQuery({ callbackQueryId: input.callbackQueryId, text: answer });
26104
+ if (outcome !== "approved" && outcome !== "denied") {
26105
+ await this.deps.store.markDispatched(input.updateId);
26106
+ return;
26107
+ }
26108
+ input.text = `[confirmation ${outcome}] ${input.callbackToken}`;
26109
+ }
26110
+ const target2 = await this.deps.dispatcher.dispatch(input);
26111
+ await this.deps.store.markDispatched(input.updateId);
26112
+ await this.deps.store.audit({
26113
+ chatId: input.chatId,
26114
+ userId: input.userId,
26115
+ updateId: input.updateId,
26116
+ action: "dispatch",
26117
+ outcome: target2.created ? "session-created" : "session-reused",
26118
+ correlationId: `tg:${input.updateId}`
26119
+ });
26120
+ updateTelegramRuntimeStatus({ lastUpdateAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null });
26121
+ } catch (error) {
26122
+ await this.deps.store.markUpdateUncertain(input.updateId, "dispatch-failed");
26123
+ await this.deps.store.audit({
26124
+ chatId: input.chatId,
26125
+ userId: input.userId,
26126
+ updateId: input.updateId,
26127
+ action: "dispatch",
26128
+ outcome: "uncertain",
26129
+ correlationId: `tg:${input.updateId}`
26130
+ });
26131
+ throw error;
26132
+ }
26133
+ }
26134
+ async flushOutbox() {
26135
+ for (; ; ) {
26136
+ const row = await this.deps.store.claimNextOutbox();
26137
+ if (!row) return;
26138
+ try {
26139
+ const text = sanitizeTelegramOutput(row.text, this.deps.exactSecrets);
26140
+ const chunks = splitTelegramText(text);
26141
+ if (!chunks.length) {
26142
+ await this.deps.store.markOutboxFailed(row.id, "empty-after-sanitize");
26143
+ continue;
26144
+ }
26145
+ const confirmation = row.confirmationId ? await this.deps.store.confirmation(row.confirmationId) : null;
26146
+ let messageId = 0;
26147
+ for (let index = 0; index < chunks.length; index++) {
26148
+ const isLast = index === chunks.length - 1;
26149
+ const replyMarkup = isLast && confirmation ? {
26150
+ inline_keyboard: [[
26151
+ { text: "Lanjutkan", callback_data: `tgcf:${confirmation.callbackToken}:approve` },
26152
+ { text: "Batalkan", callback_data: `tgcf:${confirmation.callbackToken}:deny` }
26153
+ ]]
26154
+ } : void 0;
26155
+ const message = await this.deps.client.sendMessage({ chatId: row.chatId, text: chunks[index], replyMarkup });
26156
+ messageId = message.message_id;
26157
+ }
26158
+ await this.deps.store.markOutboxSent(row.id, messageId);
26159
+ } catch (error) {
26160
+ const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
26161
+ if (error instanceof TelegramApiError && error.code !== void 0) {
26162
+ await this.deps.store.markOutboxFailed(row.id, message);
26163
+ } else {
26164
+ await this.deps.store.markOutboxUncertain(row.id, message || "network-outcome-unknown");
26165
+ }
26166
+ }
26167
+ }
26168
+ }
26169
+ async start() {
26170
+ if (this.loopPromise) return;
26171
+ await this.deps.store.recoverUncertainClaims();
26172
+ await this.deps.store.recoverUncertainOutbox();
26173
+ this.abort = new AbortController();
26174
+ updateTelegramRuntimeStatus({ running: true, readiness: "running", lastError: null });
26175
+ this.loopPromise = this.loop(this.abort.signal).finally(() => {
26176
+ this.loopPromise = null;
26177
+ this.abort = null;
26178
+ updateTelegramRuntimeStatus({ running: false });
26179
+ });
26180
+ }
26181
+ async loop(signal) {
26182
+ while (!signal.aborted) {
26183
+ try {
26184
+ const updates = await this.deps.client.getUpdates({
26185
+ offset: await this.deps.store.offset(),
26186
+ limit: 100,
26187
+ timeout: this.deps.pollTimeout ?? 25,
26188
+ signal
26189
+ });
26190
+ await this.processUpdates(updates);
26191
+ await this.flushOutbox();
26192
+ } catch (error) {
26193
+ if (signal.aborted) return;
26194
+ const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
26195
+ updateTelegramRuntimeStatus({ running: false, readiness: "error", lastError: message });
26196
+ if (error instanceof TelegramApiError && error.code === 409) return;
26197
+ await new Promise((resolve14) => setTimeout(resolve14, 1e3));
26198
+ }
26199
+ }
26200
+ }
26201
+ async stop() {
26202
+ this.abort?.abort();
26203
+ await this.loopPromise;
26204
+ }
26205
+ };
26206
+
26207
+ // src/services/telegram/session.ts
26208
+ init_src2();
26209
+ import { createHash as createHash7 } from "node:crypto";
26210
+ var chatHash = (chatId) => createHash7("sha256").update(chatId).digest("hex").slice(0, 16);
26211
+ var telegramOperatorSessionId = (chatId) => `telegram-${chatHash(chatId)}`;
26212
+ var formatTelegramTurn = (input) => `[Telegram update ${input.updateId} \xB7 chat ${input.chatId} \xB7 kind ${input.kind}]
26213
+ ${input.text}`;
26214
+ var TelegramSessionCoordinator = class {
26215
+ constructor(deps) {
26216
+ this.deps = deps;
26217
+ }
26218
+ async dispatch(input) {
26219
+ let context = await this.deps.store.chatContext(input.chatId);
26220
+ if (!context) {
26221
+ const defaults = await this.deps.defaults();
26222
+ await this.deps.store.ensureChat({
26223
+ chatId: input.chatId,
26224
+ userId: input.userId,
26225
+ agent: defaults.agent,
26226
+ model: defaults.model,
26227
+ effort: defaults.effort
26228
+ });
26229
+ context = await this.deps.store.chatContext(input.chatId);
26230
+ }
26231
+ if (!context) throw new Error("gagal membuat binding chat Telegram");
26232
+ const sessionId2 = telegramOperatorSessionId(input.chatId);
26233
+ const live = this.deps.port.getSession(sessionId2);
26234
+ if (live && !live.exited) {
26235
+ if (!await this.deps.port.sendToPane(sessionId2, formatTelegramTurn(input))) {
26236
+ throw new Error("pane operator tidak menerima steer");
26237
+ }
26238
+ if (context.sessionId !== sessionId2) await this.deps.store.bindSession(input.chatId, sessionId2);
26239
+ return { sessionId: sessionId2, created: false };
26240
+ }
26241
+ const hash2 = chatHash(input.chatId);
26242
+ const projectId = `telegram:${hash2}`;
26243
+ const cwd = `${this.deps.home.replace(/\/$/, "")}/telegram/${hash2}`;
26244
+ this.deps.ensureDir(cwd);
26245
+ if (context.agent === "codex") this.deps.ensureCodexTrust(cwd);
26246
+ const personality = await this.deps.personality(context.personalityAgentId, context.activeProjectId);
26247
+ const prompt = buildTelegramOperatorPrompt({
26248
+ update: input,
26249
+ personality,
26250
+ summary: context.summary,
26251
+ memories: context.memories
26252
+ });
26253
+ const born = this.deps.port.createSession(projectId, cwd, {
26254
+ id: sessionId2,
26255
+ prompt,
26256
+ agent: context.agent,
26257
+ model: context.model,
26258
+ effort: context.effort,
26259
+ env: {
26260
+ HANOMAN_API_BASE: this.deps.apiBase,
26261
+ HANOMAN_TELEGRAM_AGENT_TOKEN: this.deps.agentToken,
26262
+ HANOMAN_TELEGRAM_CHAT_ID: input.chatId
26263
+ }
26264
+ });
26265
+ if (born.id !== sessionId2 || born.exited) throw new Error("pane operator gagal lahir");
26266
+ await this.deps.store.bindSession(input.chatId, sessionId2);
26267
+ return { sessionId: sessionId2, created: true };
26268
+ }
26269
+ };
26270
+
26271
+ // src/services/telegram/bootstrap.ts
26272
+ var TELEGRAM_REQUIRED_CAPABILITIES = [
26273
+ "projects:read",
26274
+ "projects:write",
26275
+ "backlog:read",
26276
+ "backlog:write",
26277
+ "sessions:read",
26278
+ "sessions:write",
26279
+ "docs:read",
26280
+ "docs:write",
26281
+ "ide:read",
26282
+ "ide:write",
26283
+ "vps:read",
26284
+ "vps:write",
26285
+ "settings:read",
26286
+ "settings:write",
26287
+ "support:read",
26288
+ "support:write",
26289
+ "notifications:read",
26290
+ "notifications:write",
26291
+ "lead:read",
26292
+ "lead:write",
26293
+ "agents:read",
26294
+ "telegram:read",
26295
+ "telegram:write"
26296
+ ];
26297
+ function parseTelegramAllowedUserIds(raw) {
26298
+ const ids = raw.split(/[\s,]+/).filter(Boolean);
26299
+ if (!ids.length) throw new Error("Telegram allowlist wajib berisi numeric user id");
26300
+ if (ids.some((id) => !/^\d+$/.test(id))) throw new Error("Telegram allowlist hanya menerima numeric user id");
26301
+ return new Set(ids);
26302
+ }
26303
+ var configuredFrom = (env) => Boolean(env.HANOMAN_TELEGRAM_BOT_TOKEN?.trim() && env.HANOMAN_TELEGRAM_ALLOWED_USER_IDS?.trim() && env.HANOMAN_TELEGRAM_AGENT_TOKEN?.trim());
26304
+ async function productionFactory(input) {
26305
+ const client = new TelegramApiClient(input.botToken);
26306
+ const me = await client.getMe();
26307
+ const store3 = new TelegramStore(prisma);
26308
+ const coordinator = new TelegramSessionCoordinator({
26309
+ store: store3,
26310
+ port: { getSession, createSession, sendToPane },
26311
+ defaults: sessionAgentDefaults,
26312
+ personality: async (id, projectId) => {
26313
+ if (!id) return null;
26314
+ const row = await prisma.customAgent.findUnique({ where: { id } });
26315
+ if (!row?.enabled || row.projectId !== null && row.projectId !== projectId) return null;
26316
+ return { name: row.name, description: row.description, instructions: row.instructions };
26317
+ },
26318
+ ensureCodexTrust,
26319
+ home: resolveHome(),
26320
+ apiBase: input.apiBase,
26321
+ agentToken: input.agentToken,
26322
+ ensureDir: (path) => mkdirSync7(path, { recursive: true })
26323
+ });
26324
+ const gateway = new TelegramGateway({
26325
+ client,
26326
+ store: store3,
26327
+ dispatcher: coordinator,
26328
+ allowedUserIds: input.allowedUserIds,
26329
+ rateLimit: { limit: 20, windowMs: 6e4 },
26330
+ exactSecrets: [input.botToken, input.agentToken],
26331
+ progress: input.progress
26332
+ });
26333
+ return { gateway, botUsername: me.username ?? null };
26334
+ }
26335
+ async function installTelegramGateway(_app, options2) {
26336
+ const env = options2.env ?? process.env;
26337
+ const setting = await (options2.getSetting ?? getSetting)();
26338
+ const configured = configuredFrom(env);
26339
+ const base = {
26340
+ configured,
26341
+ enabled: setting.telegram.enabled,
26342
+ running: false,
26343
+ botUsername: null,
26344
+ allowlistCount: 0,
26345
+ agentTokenConfigured: Boolean(env.HANOMAN_TELEGRAM_AGENT_TOKEN?.trim()),
26346
+ missingCapabilities: [],
26347
+ lastUpdateAt: null,
26348
+ lastError: null
26349
+ };
26350
+ if (!setting.telegram.enabled) {
26351
+ setTelegramRuntime({ status: { ...base, readiness: "disabled" } });
26352
+ return;
26353
+ }
26354
+ if (!configured) {
26355
+ setTelegramRuntime({ status: { ...base, readiness: "misconfigured", lastError: "environment Telegram belum lengkap" } });
26356
+ return;
26357
+ }
26358
+ let allowedUserIds;
26359
+ try {
26360
+ allowedUserIds = parseTelegramAllowedUserIds(env.HANOMAN_TELEGRAM_ALLOWED_USER_IDS);
26361
+ } catch (error) {
26362
+ setTelegramRuntime({ status: { ...base, readiness: "misconfigured", lastError: error.message } });
26363
+ return;
26364
+ }
26365
+ const verify = options2.verifyAgentToken ?? verifyAgentToken;
26366
+ const agent = setting.agentAccessEnabled ? await verify(env.HANOMAN_TELEGRAM_AGENT_TOKEN) : null;
26367
+ const missing = agent ? TELEGRAM_REQUIRED_CAPABILITIES.filter((capability) => !agent.capabilities.includes(capability)) : [...TELEGRAM_REQUIRED_CAPABILITIES];
26368
+ if (!agent || missing.length) {
26369
+ setTelegramRuntime({
26370
+ agentTokenId: agent?.id ?? null,
26371
+ status: {
26372
+ ...base,
26373
+ allowlistCount: allowedUserIds.size,
26374
+ missingCapabilities: missing,
26375
+ readiness: "misconfigured",
26376
+ lastError: agent ? "capability AgentToken Telegram belum lengkap" : "AgentToken Telegram tidak valid atau akses agent mati"
26377
+ }
26378
+ });
26379
+ return;
26380
+ }
26381
+ try {
26382
+ const built = await (options2.factory ?? productionFactory)({
26383
+ apiBase: options2.apiBase,
26384
+ botToken: env.HANOMAN_TELEGRAM_BOT_TOKEN,
26385
+ agentToken: env.HANOMAN_TELEGRAM_AGENT_TOKEN,
26386
+ allowedUserIds,
26387
+ progress: setting.telegram.progress
26388
+ });
26389
+ await built.gateway.start();
26390
+ setTelegramRuntime({
26391
+ agentTokenId: agent.id,
26392
+ status: {
26393
+ ...base,
26394
+ configured: true,
26395
+ enabled: true,
26396
+ running: true,
26397
+ readiness: "running",
26398
+ botUsername: built.botUsername,
26399
+ allowlistCount: allowedUserIds.size,
26400
+ missingCapabilities: []
26401
+ }
26402
+ });
26403
+ registerTelegramRuntimeStop(() => built.gateway.stop());
26404
+ } catch {
26405
+ setTelegramRuntime({
26406
+ agentTokenId: agent.id,
26407
+ status: {
26408
+ ...base,
26409
+ allowlistCount: allowedUserIds.size,
26410
+ readiness: "error",
26411
+ lastError: "gagal memverifikasi atau memulai Telegram Bot API"
26412
+ }
26413
+ });
26414
+ }
26415
+ }
26416
+
26417
+ // src/server.ts
24379
26418
  var app = buildApp();
24380
26419
  var port = Number(process.env.PORT ?? 8787);
24381
26420
  var host = process.env.HOST ?? "127.0.0.1";
@@ -24404,6 +26443,8 @@ app.listen({ port, host }).then(async () => {
24404
26443
  } catch (e) {
24405
26444
  console.error("rekonsiliasi riwayat sesi dilewati \u2014 tmux tak terbaca:", e);
24406
26445
  }
26446
+ const boundPort = app.server.address().port;
26447
+ await installTelegramGateway(app, { apiBase: `http://127.0.0.1:${boundPort}` });
24407
26448
  startVpsMonitor();
24408
26449
  registerBacklogSource();
24409
26450
  registerTriaseSource();