hanoman 0.1.8 → 0.1.10

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
@@ -4446,7 +4446,12 @@ var init_agent = __esm({
4446
4446
  // baris jejak & bisa menggerakkan sesi), dan SPEC-405 sudah membuktikan apa yang terjadi saat
4447
4447
  // endpoint tulis menumpang prefix status yang dipetakan tanpa melihat method (AC-5).
4448
4448
  "lead:read",
4449
- "lead:write"
4449
+ "lead:write",
4450
+ // SPEC-450 · ADR-0094 · custom agent. Domain TERSENDIRI, dipetakan MENURUT METHOD: menulis
4451
+ // definisi agen mengubah apa yang dilihat SETIAP sesi baru di seluruh workspace, jadi izin
4452
+ // baca tak pernah cukup untuk itu (kelas bug SPEC-405).
4453
+ "agents:read",
4454
+ "agents:write"
4450
4455
  ];
4451
4456
  zCapability = external_exports.enum(CAPABILITY_IDS);
4452
4457
  zCapabilityInfo = external_exports.object({
@@ -4477,7 +4482,9 @@ var init_agent = __esm({
4477
4482
  { id: "notifications:read", domain: "notifications", access: "read", label: "Notifikasi \u2014 baca", desc: "Lihat notifikasi." },
4478
4483
  { id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." },
4479
4484
  { id: "lead:read", domain: "lead", access: "read", label: "Lead \u2014 baca", desc: "Baca jejak keputusan hanoman-lead & statusnya." },
4480
- { id: "lead:write", domain: "lead", access: "write", label: "Lead \u2014 tulis", desc: "Minta putusan ke hanoman-lead (keputusan bisa menggerakkan sesi).", risk: "exec" }
4485
+ { id: "lead:write", domain: "lead", access: "write", label: "Lead \u2014 tulis", desc: "Minta putusan ke hanoman-lead (keputusan bisa menggerakkan sesi).", risk: "exec" },
4486
+ { 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" }
4481
4488
  ];
4482
4489
  zAgentTokenView = external_exports.object({
4483
4490
  id: external_exports.string(),
@@ -4502,6 +4509,144 @@ var init_agent = __esm({
4502
4509
  }
4503
4510
  });
4504
4511
 
4512
+ // ../shared/src/custom-agent.ts
4513
+ function mentionsOf(v) {
4514
+ if (!Array.isArray(v)) return [];
4515
+ const out3 = [];
4516
+ for (const x of v) if (typeof x === "string" && x && !out3.includes(x)) out3.push(x);
4517
+ return out3;
4518
+ }
4519
+ function toolsOf(v) {
4520
+ if (!Array.isArray(v)) return null;
4521
+ const out3 = [];
4522
+ for (const x of v) if (typeof x === "string" && x && !out3.includes(x)) out3.push(x);
4523
+ return out3;
4524
+ }
4525
+ function resolveTools(a) {
4526
+ const base = a.tools ?? [...DEFAULT_AGENT_TOOLS];
4527
+ const canMention = (a.mentions ?? []).length > 0;
4528
+ const out3 = base.filter((t) => t !== MENTION_TOOL);
4529
+ if (canMention) out3.push(MENTION_TOOL);
4530
+ return out3;
4531
+ }
4532
+ function detectCycle(nodes) {
4533
+ const edges = new Map(nodes.map((n) => [n.name, n.mentions]));
4534
+ const state = /* @__PURE__ */ new Map();
4535
+ const stack = [];
4536
+ const walk = (name2) => {
4537
+ if (state.get(name2) === 1) return [...stack.slice(stack.indexOf(name2)), name2];
4538
+ if (state.get(name2) === 2) return null;
4539
+ state.set(name2, 1);
4540
+ stack.push(name2);
4541
+ for (const next of edges.get(name2) ?? []) {
4542
+ if (!edges.has(next)) continue;
4543
+ const found = walk(next);
4544
+ if (found) return found;
4545
+ }
4546
+ stack.pop();
4547
+ state.set(name2, 2);
4548
+ return null;
4549
+ };
4550
+ for (const n of nodes) {
4551
+ const found = walk(n.name);
4552
+ if (found) return found;
4553
+ }
4554
+ return null;
4555
+ }
4556
+ function effectiveAgents(globals, project) {
4557
+ const byName = /* @__PURE__ */ new Map();
4558
+ for (const a of globals) byName.set(a.name, a);
4559
+ for (const a of project) byName.set(a.name, a);
4560
+ return [...byName.values()].filter((a) => a.enabled).sort((x, y) => x.name.localeCompare(y.name));
4561
+ }
4562
+ var AGENT_NAME_RE, DEFAULT_AGENT_TOOLS, MENTION_TOOL, MENTION_MAX_HOPS, GLOBAL_SCOPE, zCustomAgent, zCreateCustomAgent, zUpdateCustomAgent, customAgentId;
4563
+ var init_custom_agent = __esm({
4564
+ "../shared/src/custom-agent.ts"() {
4565
+ "use strict";
4566
+ init_zod();
4567
+ AGENT_NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
4568
+ DEFAULT_AGENT_TOOLS = [
4569
+ "Read",
4570
+ "Write",
4571
+ "Edit",
4572
+ "Bash",
4573
+ "Glob",
4574
+ "Grep",
4575
+ "WebFetch",
4576
+ "WebSearch"
4577
+ ];
4578
+ MENTION_TOOL = "Task";
4579
+ MENTION_MAX_HOPS = 3;
4580
+ GLOBAL_SCOPE = "global";
4581
+ zCustomAgent = external_exports.object({
4582
+ id: external_exports.string(),
4583
+ projectId: external_exports.string().nullable(),
4584
+ name: external_exports.string().regex(AGENT_NAME_RE),
4585
+ description: external_exports.string(),
4586
+ instructions: external_exports.string(),
4587
+ tools: external_exports.array(external_exports.string()).nullable(),
4588
+ model: external_exports.string().nullable(),
4589
+ mentions: external_exports.array(external_exports.string()).nullable(),
4590
+ enabled: external_exports.boolean(),
4591
+ createdAt: external_exports.string(),
4592
+ updatedAt: external_exports.string()
4593
+ });
4594
+ zCreateCustomAgent = external_exports.object({
4595
+ projectId: external_exports.string().nullable().optional(),
4596
+ name: external_exports.string().regex(AGENT_NAME_RE),
4597
+ description: external_exports.string().trim().min(1).max(500),
4598
+ instructions: external_exports.string().trim().min(1).max(2e4),
4599
+ tools: external_exports.array(external_exports.string()).nullable().optional(),
4600
+ model: external_exports.string().nullable().optional(),
4601
+ mentions: external_exports.array(external_exports.string()).nullable().optional(),
4602
+ enabled: external_exports.boolean().optional()
4603
+ });
4604
+ zUpdateCustomAgent = zCreateCustomAgent.omit({ name: true, projectId: true }).partial();
4605
+ customAgentId = (projectId, name2) => `${projectId ?? GLOBAL_SCOPE}:${name2}`;
4606
+ }
4607
+ });
4608
+
4609
+ // ../shared/src/github.ts
4610
+ function sourceForLabels(labels) {
4611
+ const hay = labels.map((l) => l.toLowerCase());
4612
+ for (const rule2 of LABEL_RULES)
4613
+ if (hay.some((l) => rule2.needles.some((n) => l.includes(n)))) return rule2.source;
4614
+ return "qa";
4615
+ }
4616
+ var zGithubIssueStatus, zNormalIssue, zGithubIssueView, LABEL_RULES;
4617
+ var init_github = __esm({
4618
+ "../shared/src/github.ts"() {
4619
+ "use strict";
4620
+ init_zod();
4621
+ zGithubIssueStatus = external_exports.enum(["new", "accepted", "rejected"]);
4622
+ zNormalIssue = external_exports.object({
4623
+ number: external_exports.number().int().positive(),
4624
+ title: external_exports.string(),
4625
+ body: external_exports.string(),
4626
+ authorLogin: external_exports.string(),
4627
+ labels: external_exports.array(external_exports.string()),
4628
+ url: external_exports.string(),
4629
+ issueState: external_exports.enum(["open", "closed"]),
4630
+ issueCreatedAt: external_exports.string(),
4631
+ // ISO-8601 apa adanya dari GitHub
4632
+ issueUpdatedAt: external_exports.string()
4633
+ });
4634
+ zGithubIssueView = zNormalIssue.extend({
4635
+ id: external_exports.string(),
4636
+ projectId: external_exports.string(),
4637
+ repoSlug: external_exports.string(),
4638
+ status: zGithubIssueStatus,
4639
+ specId: external_exports.string().nullable(),
4640
+ pulledAt: external_exports.string()
4641
+ });
4642
+ LABEL_RULES = [
4643
+ { needles: ["bug", "defect", "regression"], source: "qa" },
4644
+ { needles: ["question", "docs", "documentation"], source: "audit" },
4645
+ { needles: ["enhancement", "feature", "feat"], source: "brief" }
4646
+ ];
4647
+ }
4648
+ });
4649
+
4505
4650
  // ../shared/src/lead.ts
4506
4651
  function leadActionAllowed(action) {
4507
4652
  return LEAD_ACTIONS.includes(action);
@@ -5114,6 +5259,9 @@ var init_api = __esm({
5114
5259
  leadDecisions: `${API}/lead/decisions`,
5115
5260
  leadDecisionOverride: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/override`,
5116
5261
  leadDecisionCancel: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/cancel`,
5262
+ // SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
5263
+ customAgents: `${API}/custom-agents`,
5264
+ customAgent: (id) => `${API}/custom-agents/${encodeURIComponent(id)}`,
5117
5265
  // SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
5118
5266
  syncNow: `${API}/sync/now`,
5119
5267
  // SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
@@ -5136,6 +5284,14 @@ var init_api = __esm({
5136
5284
  ticketUnlink: (id) => `${API}/tickets/${id}/unlink`,
5137
5285
  // SPEC-271 · lepas tautan backlog
5138
5286
  ticketReject: (id) => `${API}/tickets/${id}/reject`,
5287
+ // SPEC-471 · ADR-0095 · tarik & triase issue GitHub (di belakang gate cookie, capability `support`).
5288
+ // hanoman TIDAK PERNAH menulis ke GitHub — tak ada path komentar/close di sini, dan itu disengaja.
5289
+ githubPull: (id) => `${API}/projects/${encodeURIComponent(id)}/github/pull`,
5290
+ githubIssues: (id) => `${API}/projects/${encodeURIComponent(id)}/github/issues`,
5291
+ githubIssuesAccept: `${API}/github-issues/accept`,
5292
+ githubIssueAccept: (id) => `${API}/github-issues/${encodeURIComponent(id)}/accept`,
5293
+ githubIssueReject: (id) => `${API}/github-issues/${encodeURIComponent(id)}/reject`,
5294
+ githubIssueUnlink: (id) => `${API}/github-issues/${encodeURIComponent(id)}/unlink`,
5139
5295
  // SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
5140
5296
  // (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
5141
5297
  download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
@@ -5343,6 +5499,28 @@ var init_config_registry = __esm({
5343
5499
  // vps
5344
5500
  { key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", help: "Default ~/.hanoman." },
5345
5501
  { key: "HANOMAN_SSH_BIN", group: "vps", label: "Biner ssh", kind: "path", apply: "new-session", category: "knob", default: "ssh" },
5502
+ // github (SPEC-471 · ADR-0095 · tarik issue → backlog). Dua mode auth, satu jalur kode:
5503
+ // `gh` memakai keyring mesin; bila GITHUB_TOKEN diisi ia diteruskan sebagai GH_TOKEN ke
5504
+ // proses gh DAN dipakai jalur REST (hub VPS yang tak punya keyring).
5505
+ {
5506
+ key: "GITHUB_TOKEN",
5507
+ group: "github",
5508
+ label: "GitHub token",
5509
+ kind: "secret",
5510
+ apply: "live",
5511
+ category: "credential",
5512
+ help: "PAT scope `repo` (atau `public_repo`). Kosong = andalkan `gh auth login` di mesin ini."
5513
+ },
5514
+ {
5515
+ key: "HANOMAN_GH_BIN",
5516
+ group: "github",
5517
+ label: "Biner gh",
5518
+ kind: "path",
5519
+ apply: "live",
5520
+ category: "knob",
5521
+ default: "gh",
5522
+ help: "Absen = tarik issue lewat HTTPS langsung ke api.github.com."
5523
+ },
5346
5524
  // runtime
5347
5525
  { key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", default: "1000", min: 100 },
5348
5526
  { key: "HANOMAN_UPDATE_FETCH", group: "runtime", label: "Deteksi update saat boot", kind: "bool", apply: "live", category: "knob", default: "1" },
@@ -5434,6 +5612,8 @@ var init_src = __esm({
5434
5612
  init_enums();
5435
5613
  init_entities();
5436
5614
  init_agent();
5615
+ init_custom_agent();
5616
+ init_github();
5437
5617
  init_lead();
5438
5618
  init_dto();
5439
5619
  init_api();
@@ -6202,6 +6382,74 @@ var init_agent_cli = __esm({
6202
6382
  }
6203
6383
  });
6204
6384
 
6385
+ // ../runner/src/custom-agents.ts
6386
+ function agentPromptOf(def2, roster) {
6387
+ const can = liveMentions(def2, roster);
6388
+ if (can.length === 0) {
6389
+ return `${def2.instructions}
6390
+
6391
+ ---
6392
+ Kamu TIDAK boleh mendelegasikan ke agen lain. Selesaikan sendiri lalu laporkan hasilnya.`;
6393
+ }
6394
+ const list2 = can.map((m) => `@${m}`).join(", ");
6395
+ return [
6396
+ def2.instructions,
6397
+ "",
6398
+ "---",
6399
+ `Kamu boleh mendelegasikan HANYA ke: ${list2}. Panggil lewat ${MENTION_TOOL} dengan nama agennya.`,
6400
+ `Anggaran rantai delegasi seluruh sesi ini ${MENTION_MAX_HOPS} hop. Bila kamu sudah berada di hop ke-${MENTION_MAX_HOPS}, JANGAN mendelegasikan lagi \u2014 selesaikan sendiri lalu laporkan.`,
6401
+ "Sebutkan hop keberapa kamu berada saat mendelegasikan, dan jangan pernah memanggil agen yang sudah ada di rantai yang membawamu ke sini."
6402
+ ].join("\n");
6403
+ }
6404
+ function renderAgentsJson(defs) {
6405
+ if (defs.length === 0) return "";
6406
+ const out3 = {};
6407
+ for (const d of defs) {
6408
+ out3[d.name] = {
6409
+ description: d.description,
6410
+ prompt: agentPromptOf(d, defs),
6411
+ tools: resolveTools({ tools: d.tools, mentions: d.mentions }),
6412
+ ...d.model ? { model: d.model } : {}
6413
+ };
6414
+ }
6415
+ return JSON.stringify(out3);
6416
+ }
6417
+ function agentRosterBlock(defs) {
6418
+ if (defs.length === 0) return "";
6419
+ const lines2 = [
6420
+ "",
6421
+ "## Custom agent hanoman",
6422
+ "",
6423
+ "Peran berikut tersedia untuk sesi ini. Saat sebuah tugas cocok dengan salah satunya, ADOPSI",
6424
+ "perannya (baca instruksinya, kerjakan dengan sudut pandang itu) lalu kembali ke peranmu sendiri.",
6425
+ "Jangan melahirkan proses agen baru.",
6426
+ ""
6427
+ ];
6428
+ for (const d of defs) {
6429
+ const can = liveMentions(d, defs);
6430
+ lines2.push(`### @${d.name} \u2014 ${d.description}`);
6431
+ lines2.push("");
6432
+ lines2.push(d.instructions);
6433
+ lines2.push("");
6434
+ lines2.push(
6435
+ can.length ? `Boleh berkonsultasi ke: ${can.map((m) => `@${m}`).join(", ")} (maks ${MENTION_MAX_HOPS} hop berantai).` : "Tidak boleh berkonsultasi ke peran lain."
6436
+ );
6437
+ lines2.push("");
6438
+ }
6439
+ return lines2.join("\n");
6440
+ }
6441
+ var liveMentions;
6442
+ var init_custom_agents = __esm({
6443
+ "../runner/src/custom-agents.ts"() {
6444
+ "use strict";
6445
+ init_src();
6446
+ liveMentions = (def2, roster) => {
6447
+ const names = new Set(roster.map((r) => r.name));
6448
+ return def2.mentions.filter((m) => names.has(m) && m !== def2.name);
6449
+ };
6450
+ }
6451
+ });
6452
+
6205
6453
  // ../runner/src/paths.ts
6206
6454
  import { homedir } from "node:os";
6207
6455
  import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve4 } from "node:path";
@@ -6262,6 +6510,7 @@ var init_src2 = __esm({
6262
6510
  init_goal_spec();
6263
6511
  init_codex_settings();
6264
6512
  init_agent_cli();
6513
+ init_custom_agents();
6265
6514
  init_verify_scope();
6266
6515
  init_paths();
6267
6516
  }
@@ -6391,6 +6640,177 @@ var init_session_phases = __esm({
6391
6640
  }
6392
6641
  });
6393
6642
 
6643
+ // src/services/tui-dialog.ts
6644
+ function readChoiceDialog(paneText2) {
6645
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6646
+ let footer = -1;
6647
+ for (let i = lines2.length - 1; i >= 0; i--) {
6648
+ if (FOOTER.test(lines2[i] ?? "")) {
6649
+ footer = i;
6650
+ break;
6651
+ }
6652
+ }
6653
+ if (footer < 0) return null;
6654
+ const found = [];
6655
+ for (const line of lines2.slice(0, footer)) {
6656
+ const m = ROW.exec(line);
6657
+ if (m) found.push({ n: Number(m[1]), label: cleanLabel(m[2] ?? "") });
6658
+ }
6659
+ if (found.length < 2) return null;
6660
+ const run2 = [];
6661
+ let expected = found[found.length - 1].n;
6662
+ for (let i = found.length - 1; i >= 0 && expected >= 1; i--) {
6663
+ const row = found[i];
6664
+ if (row.n !== expected) break;
6665
+ run2.unshift(row);
6666
+ expected -= 1;
6667
+ }
6668
+ if (run2.length < 2 || run2[0].n !== 1) return null;
6669
+ const rows = run2.map((r) => ({
6670
+ n: r.n,
6671
+ label: r.label,
6672
+ free: PLACEHOLDER.test(r.label),
6673
+ chat: CHAT_ROW.test(r.label)
6674
+ }));
6675
+ return {
6676
+ rows,
6677
+ freeIndex: rows.find((r) => r.free)?.n ?? null,
6678
+ options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
6679
+ };
6680
+ }
6681
+ function readTabs(lines2) {
6682
+ for (let i = lines2.length - 1; i >= 0; i--) {
6683
+ const line = lines2[i] ?? "";
6684
+ if (!/[☐☒]/.test(line)) continue;
6685
+ const tabs = [];
6686
+ for (const tok of line.split(/\s{2,}/)) {
6687
+ const m = TAB_BOX.exec(tok.trim());
6688
+ if (m) tabs.push({ header: (m[2] ?? "").trim(), answered: m[1] === "\u2612" });
6689
+ }
6690
+ if (tabs.length) return { tabs, at: i };
6691
+ }
6692
+ return { tabs: [], at: -1 };
6693
+ }
6694
+ function readTitle(lines2, stripAt, footer) {
6695
+ if (stripAt < 0) return "";
6696
+ for (let i = stripAt + 1; i < footer; i++) {
6697
+ const line = (lines2[i] ?? "").trim();
6698
+ if (!line || RULE.test(line)) continue;
6699
+ return ROW.test(lines2[i] ?? "") ? "" : line;
6700
+ }
6701
+ return "";
6702
+ }
6703
+ function readReviewScreen(paneText2) {
6704
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6705
+ const at = lastIndexOf(lines2, REVIEW_PROMPT);
6706
+ if (at < 0) return null;
6707
+ for (const line of lines2.slice(at)) {
6708
+ const m = SUBMIT_ROW.exec(line);
6709
+ if (m) return { submitRow: Number(m[1]) };
6710
+ }
6711
+ return null;
6712
+ }
6713
+ function readDialogScreen(paneText2) {
6714
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6715
+ const footer = lastIndexOf(lines2, FOOTER);
6716
+ if (lastIndexOf(lines2, REVIEW_PROMPT) > footer) {
6717
+ const review = readReviewScreen(paneText2);
6718
+ if (review) return { kind: "review", submitRow: review.submitRow };
6719
+ }
6720
+ const d = readChoiceDialog(paneText2);
6721
+ if (!d) return null;
6722
+ const { tabs, at } = readTabs(lines2.slice(0, footer));
6723
+ return {
6724
+ kind: "question",
6725
+ rows: d.rows,
6726
+ freeIndex: d.freeIndex,
6727
+ options: d.options,
6728
+ notes: d.freeIndex === null && NOTES_FOOTER.test(lines2[footer] ?? ""),
6729
+ tabs,
6730
+ title: readTitle(lines2, at, footer)
6731
+ };
6732
+ }
6733
+ function dialogKey(paneText2) {
6734
+ const s = readDialogScreen(paneText2);
6735
+ if (!s) return "none";
6736
+ if (s.kind === "review") return "review";
6737
+ const tabs = s.tabs.map((t) => `${t.answered ? "x" : "o"}${t.header}`).join(",");
6738
+ return `q|${tabs}|${s.title || s.options.join("|")}`;
6739
+ }
6740
+ function freeTextFilled(paneText2, n) {
6741
+ const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n);
6742
+ if (!row) return false;
6743
+ return row.label.length > 0 && !PLACEHOLDER.test(row.label);
6744
+ }
6745
+ async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
6746
+ io.literal(String(freeIndex));
6747
+ await io.sleep(DIALOG_SETTLE_MS);
6748
+ for (const chunk of goalChunks(line)) {
6749
+ io.literal(chunk);
6750
+ await io.sleep(chunkMs);
6751
+ }
6752
+ await io.sleep(DIALOG_SETTLE_MS);
6753
+ if (!freeTextFilled(io.capture(), freeIndex)) return false;
6754
+ io.enter();
6755
+ return true;
6756
+ }
6757
+ function notesFilled(paneText2) {
6758
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6759
+ for (let i = lines2.length - 1; i >= 0; i--) {
6760
+ const m = NOTES_LINE.exec(lines2[i] ?? "");
6761
+ if (!m) continue;
6762
+ const v = (m[1] ?? "").trim();
6763
+ return v.length > 0 && !NOTES_PLACEHOLDER.test(v);
6764
+ }
6765
+ return false;
6766
+ }
6767
+ async function answerNotesDialog(io, line, chunkMs) {
6768
+ io.literal("n");
6769
+ await io.sleep(DIALOG_SETTLE_MS);
6770
+ for (const chunk of goalChunks(line)) {
6771
+ io.literal(chunk);
6772
+ await io.sleep(chunkMs);
6773
+ }
6774
+ await io.sleep(DIALOG_SETTLE_MS);
6775
+ if (!notesFilled(io.capture())) return false;
6776
+ io.enter();
6777
+ return true;
6778
+ }
6779
+ async function submitReview(io, submitRow) {
6780
+ io.literal(String(submitRow));
6781
+ for (let i = 0; i < SUBMIT_TRIES; i++) {
6782
+ await io.sleep(DIALOG_SETTLE_MS);
6783
+ if (!readReviewScreen(io.capture())) return true;
6784
+ }
6785
+ return false;
6786
+ }
6787
+ 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;
6788
+ var init_tui_dialog = __esm({
6789
+ "src/services/tui-dialog.ts"() {
6790
+ "use strict";
6791
+ init_src2();
6792
+ FOOTER = /enter to (?:select|confirm)\b/i;
6793
+ ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+(\S.*)$/;
6794
+ PLACEHOLDER = /^(?:type something\.?|other)$/i;
6795
+ CHAT_ROW = /^chat about this$/i;
6796
+ SIDE_PANEL = /\s{2,}[│┃┌┐└┘├┤╭╮╰╯─━].*$/;
6797
+ cleanLabel = (s) => s.replace(SIDE_PANEL, "").trim();
6798
+ REVIEW_PROMPT = /^\s*ready to submit your answers\?\s*$/i;
6799
+ SUBMIT_ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+submit answers\s*$/i;
6800
+ NOTES_FOOTER = /\bn to add notes\b/i;
6801
+ TAB_BOX = /^([☐☒])\s*(.+)$/;
6802
+ lastIndexOf = (lines2, re) => {
6803
+ for (let i = lines2.length - 1; i >= 0; i--) if (re.test(lines2[i] ?? "")) return i;
6804
+ return -1;
6805
+ };
6806
+ RULE = /^[\s─━╌╍_=]*$/;
6807
+ DIALOG_SETTLE_MS = 250;
6808
+ NOTES_PLACEHOLDER = /^(?:press n to add notes|add notes on this design(?:…|\.\.\.)?)$/i;
6809
+ NOTES_LINE = /(?:^|\s)Notes:\s*(.*)$/;
6810
+ SUBMIT_TRIES = 8;
6811
+ }
6812
+ });
6813
+
6394
6814
  // src/config.ts
6395
6815
  var config_exports = {};
6396
6816
  __export(config_exports, {
@@ -6500,6 +6920,9 @@ function listPanes() {
6500
6920
  function registerSessionHooks(h) {
6501
6921
  hooks = h;
6502
6922
  }
6923
+ function registerCustomAgentSource(fn) {
6924
+ customAgentSource = fn;
6925
+ }
6503
6926
  function sessionKind(o, projectId, cwd) {
6504
6927
  if (o.specId) return "spec";
6505
6928
  if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
@@ -6521,11 +6944,14 @@ function createSession(projectId, cwd, opts = {}) {
6521
6944
  const existing = getSession(id);
6522
6945
  if (existing && !existing.exited) return existing;
6523
6946
  if (existing) killSession(id);
6947
+ const agentForDefs = opts.agent ?? "claude";
6948
+ const customDefs = opts.command ? [] : customAgentsFor(projectId);
6949
+ const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : "";
6524
6950
  let promptArg = "";
6525
6951
  if (!opts.command && opts.prompt) {
6526
6952
  const promptFile = promptFilePath(id);
6527
6953
  mkdirSync3(dirname4(promptFile), { recursive: true });
6528
- writeFileSync(promptFile, opts.prompt);
6954
+ writeFileSync(promptFile, opts.prompt + rosterBlock);
6529
6955
  promptArg = `"$(cat ${sq(promptFile)})"`;
6530
6956
  }
6531
6957
  const agent = opts.agent ?? "claude";
@@ -6547,6 +6973,15 @@ function createSession(projectId, cwd, opts = {}) {
6547
6973
  }), { mode: 493 });
6548
6974
  }
6549
6975
  const effort = agent === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort;
6976
+ let agentsFile;
6977
+ if (agent === "claude") {
6978
+ const json = renderAgentsJson(customDefs);
6979
+ if (json) {
6980
+ agentsFile = agentsFilePath(id);
6981
+ mkdirSync3(dirname4(agentsFile), { recursive: true });
6982
+ writeFileSync(agentsFile, json);
6983
+ }
6984
+ }
6550
6985
  const flags = agentFlags({
6551
6986
  agent,
6552
6987
  model: opts.model,
@@ -6555,7 +6990,8 @@ function createSession(projectId, cwd, opts = {}) {
6555
6990
  goal: opts.goal,
6556
6991
  goalGate
6557
6992
  }).map(sq).join(" ");
6558
- argv = [sq(agentBin(agent)), promptArg, flags].filter(Boolean).join(" ");
6993
+ const agentsArg = agentsFile ? `--agents "$(cat ${sq(agentsFile)})"` : "";
6994
+ argv = [sq(agentBin(agent)), promptArg, flags, agentsArg].filter(Boolean).join(" ");
6559
6995
  }
6560
6996
  const envPairs = [];
6561
6997
  if (!opts.command && agent === "claude") {
@@ -6660,16 +7096,35 @@ async function sendToPane(id, text, chunkMs = 50) {
6660
7096
  const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
6661
7097
  if (!line) return false;
6662
7098
  try {
7099
+ const io = dialogIO(id);
7100
+ const screen = readDialogScreen(io.capture());
7101
+ if (screen?.kind === "review") return await submitReview(io, screen.submitRow);
7102
+ if (screen?.kind === "question") {
7103
+ if (screen.freeIndex !== null) return await answerChoiceDialog(io, screen.freeIndex, line, chunkMs);
7104
+ if (screen.notes) return await answerNotesDialog(io, line, chunkMs);
7105
+ }
6663
7106
  for (const chunk of goalChunks(line)) {
6664
- tmux("send-keys", "-t", name(id), "-l", chunk);
7107
+ io.literal(chunk);
6665
7108
  await sleep(chunkMs);
6666
7109
  }
6667
- tmux("send-keys", "-t", name(id), "Enter");
7110
+ io.enter();
6668
7111
  return true;
6669
7112
  } catch {
6670
7113
  return false;
6671
7114
  }
6672
7115
  }
7116
+ async function submitPaneDialog(id) {
7117
+ const p = getSession(id);
7118
+ if (!p || p.exited) return false;
7119
+ try {
7120
+ const io = dialogIO(id);
7121
+ const screen = readDialogScreen(io.capture());
7122
+ if (screen?.kind !== "review") return false;
7123
+ return await submitReview(io, screen.submitRow);
7124
+ } catch {
7125
+ return false;
7126
+ }
7127
+ }
6673
7128
  async function armGoalInTui(id, condition, o = {}) {
6674
7129
  const pollMs = o.pollMs ?? 500, readyTries = o.readyTries ?? 20;
6675
7130
  const settleMs = o.settleMs ?? 1200, verifyTries = o.verifyTries ?? 12;
@@ -6758,7 +7213,7 @@ function end(id, code) {
6758
7213
  function pollPhases(p, a) {
6759
7214
  if (!p.flow || !p.phaseFile) return;
6760
7215
  const phases = readPhases(p.phaseFile, p.flow);
6761
- const complete = sessionComplete(phases, p.cwd, p.specId);
7216
+ const complete = paneComplete(p);
6762
7217
  const json = phaseKey(phases, complete);
6763
7218
  if (json === a.lastPhases) return;
6764
7219
  a.lastPhases = json;
@@ -6805,7 +7260,7 @@ function attach(id, c) {
6805
7260
  if (a.scrollback) c.send(frame({ t: "data", d: a.scrollback }));
6806
7261
  if (p.flow && p.phaseFile) {
6807
7262
  const phases = readPhases(p.phaseFile, p.flow);
6808
- const complete = sessionComplete(phases, p.cwd, p.specId);
7263
+ const complete = paneComplete(p);
6809
7264
  a.lastPhases = phaseKey(phases, complete);
6810
7265
  c.send(frame({ t: "phase", phases, complete }));
6811
7266
  }
@@ -6844,13 +7299,14 @@ function spawnPty(...args) {
6844
7299
  );
6845
7300
  }
6846
7301
  }
6847
- var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, sleep, paneText, GOAL_ARMED_MARKERS, goalArmed, phaseKey, poll, detach;
7302
+ 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, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
6848
7303
  var init_pty = __esm({
6849
7304
  "src/services/pty.ts"() {
6850
7305
  "use strict";
6851
7306
  init_src2();
6852
7307
  init_src();
6853
7308
  init_session_phases();
7309
+ init_tui_dialog();
6854
7310
  init_config2();
6855
7311
  socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
6856
7312
  PREFIX = "hanoman-";
@@ -6874,6 +7330,7 @@ var init_pty = __esm({
6874
7330
  promptFilePath = (id) => `${tmpdir()}/hanoman-prompts/${id}`;
6875
7331
  goalGatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.sh`;
6876
7332
  goalStatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.count`;
7333
+ agentsFilePath = (id) => `${tmpdir()}/hanoman-agents/${id}.json`;
6877
7334
  NO_SERVER = /no server running|error connecting to/i;
6878
7335
  TmuxError = class extends Error {
6879
7336
  constructor(message, noServer) {
@@ -6926,7 +7383,17 @@ var init_pty = __esm({
6926
7383
  } catch {
6927
7384
  }
6928
7385
  };
6929
- sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7386
+ customAgentSource = () => [];
7387
+ customAgentsFor = (projectId) => {
7388
+ try {
7389
+ return customAgentSource(projectId);
7390
+ } catch {
7391
+ return [];
7392
+ }
7393
+ };
7394
+ sleep = (ms) => new Promise((r) => {
7395
+ setTimeout(r, ms);
7396
+ });
6930
7397
  paneText = (id) => {
6931
7398
  try {
6932
7399
  return tmux("capture-pane", "-p", "-t", name(id));
@@ -6934,6 +7401,17 @@ var init_pty = __esm({
6934
7401
  return "";
6935
7402
  }
6936
7403
  };
7404
+ dialogIO = (id) => ({
7405
+ capture: () => capturePane(id, DIALOG_CAPTURE_LINES),
7406
+ literal: (s) => {
7407
+ tmux("send-keys", "-t", name(id), "-l", s);
7408
+ },
7409
+ enter: () => {
7410
+ tmux("send-keys", "-t", name(id), "Enter");
7411
+ },
7412
+ sleep
7413
+ });
7414
+ DIALOG_CAPTURE_LINES = 60;
6937
7415
  GOAL_ARMED_MARKERS = {
6938
7416
  claude: ["/goal"],
6939
7417
  codex: ["Goal active", "Pursuing goal", "Goal achieved"]
@@ -6943,6 +7421,11 @@ var init_pty = __esm({
6943
7421
  return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
6944
7422
  };
6945
7423
  phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
7424
+ paneComplete = (p) => !!p.flow && !!p.phaseFile && sessionComplete(readPhases(p.phaseFile, p.flow), p.cwd, p.specId);
7425
+ sessionFinished = (id) => {
7426
+ const p = getSession(id);
7427
+ return !!p && paneComplete(p);
7428
+ };
6946
7429
  detach = (id, c) => {
6947
7430
  attached.get(id)?.clients.delete(c);
6948
7431
  };
@@ -7030,6 +7513,8 @@ var init_rename_project = __esm({
7030
7513
  var sync_exports = {};
7031
7514
  __export(sync_exports, {
7032
7515
  SYNCED: () => SYNCED,
7516
+ __DATE_FIELDS: () => __DATE_FIELDS,
7517
+ __FIELDS: () => __FIELDS,
7033
7518
  __FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
7034
7519
  applyPush: () => applyPush,
7035
7520
  backfillFeed: () => backfillFeed,
@@ -7168,20 +7653,22 @@ async function upsertLocal(entity, id, version, data) {
7168
7653
  function setAcceptedHook(hook) {
7169
7654
  onAccepted = hook;
7170
7655
  }
7171
- var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
7656
+ var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, __FIELDS, __DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
7172
7657
  var init_sync = __esm({
7173
7658
  "src/services/sync.ts"() {
7174
7659
  "use strict";
7175
7660
  init_db();
7176
7661
  init_rename_project();
7177
- SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment"];
7662
+ SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue"];
7178
7663
  DELEGATE = {
7179
7664
  project: prisma.project,
7180
7665
  spec: prisma.spec,
7181
7666
  vps: prisma.vps,
7182
7667
  sessionResult: prisma.sessionResult,
7183
7668
  ticket: prisma.ticket,
7184
- ticketAttachment: prisma.ticketAttachment
7669
+ ticketAttachment: prisma.ticketAttachment,
7670
+ customAgent: prisma.customAgent,
7671
+ githubIssue: prisma.githubIssue
7185
7672
  };
7186
7673
  FIELDS = {
7187
7674
  project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
@@ -7196,7 +7683,33 @@ var init_sync = __esm({
7196
7683
  // (kolom required @unique tanpa default); kunci plaintext tak pernah menyeberang.
7197
7684
  ticket: ["projectId", "number", "category", "title", "detail", "reporterEmail", "status", "accessKeyHash", "specId", "createdAt", "updatedAt"],
7198
7685
  // SPEC-272 · ADR-0068 · metadata lampiran (byte tak disync; ditarik lazy dari hub saat dibuka).
7199
- ticketAttachment: ["ticketId", "projectId", "filename", "mimeType", "size", "storageKey", "createdAt", "updatedAt"]
7686
+ ticketAttachment: ["ticketId", "projectId", "filename", "mimeType", "size", "storageKey", "createdAt", "updatedAt"],
7687
+ // SPEC-450 · ADR-0094 · SELURUH kolom bermakna ikut menyeberang. `enabled` & `mentions` wajib
7688
+ // ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
7689
+ // mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
7690
+ // `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
7691
+ customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"],
7692
+ // SPEC-471 · ADR-0095 · SELURUH kolom bermakna ikut. `status`/`specId` termasuk: keputusan
7693
+ // triase adalah bagian keadaan yang harus dilihat sama oleh semua mesin — tanpa itu satu
7694
+ // mesin bisa menerima ulang issue yang di mesin lain sudah jadi backlog.
7695
+ githubIssue: [
7696
+ "projectId",
7697
+ "repoSlug",
7698
+ "number",
7699
+ "title",
7700
+ "body",
7701
+ "authorLogin",
7702
+ "labels",
7703
+ "url",
7704
+ "issueState",
7705
+ "status",
7706
+ "specId",
7707
+ "issueCreatedAt",
7708
+ "issueUpdatedAt",
7709
+ "pulledAt",
7710
+ "createdAt",
7711
+ "updatedAt"
7712
+ ]
7200
7713
  };
7201
7714
  DATE_FIELDS = {
7202
7715
  project: ["updatedAt"],
@@ -7204,8 +7717,12 @@ var init_sync = __esm({
7204
7717
  vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
7205
7718
  sessionResult: ["createdAt", "updatedAt"],
7206
7719
  ticket: ["createdAt", "updatedAt"],
7207
- ticketAttachment: ["createdAt", "updatedAt"]
7720
+ ticketAttachment: ["createdAt", "updatedAt"],
7721
+ customAgent: ["createdAt", "updatedAt"],
7722
+ githubIssue: ["issueCreatedAt", "issueUpdatedAt", "pulledAt", "createdAt", "updatedAt"]
7208
7723
  };
7724
+ __FIELDS = FIELDS;
7725
+ __DATE_FIELDS = DATE_FIELDS;
7209
7726
  __FIELDS_FOR_TEST = FIELDS;
7210
7727
  }
7211
7728
  });
@@ -9372,7 +9889,7 @@ var require_extension = __commonJS({
9372
9889
  if (dest[name2] === void 0) dest[name2] = [elem];
9373
9890
  else dest[name2].push(elem);
9374
9891
  }
9375
- function parse2(header) {
9892
+ function parse3(header) {
9376
9893
  const offers = /* @__PURE__ */ Object.create(null);
9377
9894
  let params = /* @__PURE__ */ Object.create(null);
9378
9895
  let mustUnescape = false;
@@ -9512,7 +10029,7 @@ var require_extension = __commonJS({
9512
10029
  }).join(", ");
9513
10030
  }).join(", ");
9514
10031
  }
9515
- module.exports = { format, parse: parse2 };
10032
+ module.exports = { format, parse: parse3 };
9516
10033
  }
9517
10034
  });
9518
10035
 
@@ -9546,7 +10063,7 @@ var require_websocket = __commonJS({
9546
10063
  var {
9547
10064
  EventTarget: { addEventListener, removeEventListener }
9548
10065
  } = require_event_target();
9549
- var { format, parse: parse2 } = require_extension();
10066
+ var { format, parse: parse3 } = require_extension();
9550
10067
  var { toBuffer } = require_buffer_util();
9551
10068
  var kAborted = Symbol("kAborted");
9552
10069
  var protocolVersions = [8, 13];
@@ -10223,7 +10740,7 @@ var require_websocket = __commonJS({
10223
10740
  }
10224
10741
  let extensions;
10225
10742
  try {
10226
- extensions = parse2(secWebSocketExtensions);
10743
+ extensions = parse3(secWebSocketExtensions);
10227
10744
  } catch (err) {
10228
10745
  const message = "Invalid Sec-WebSocket-Extensions header";
10229
10746
  abortHandshake(websocket2, socket2, message);
@@ -10515,7 +11032,7 @@ var require_subprotocol = __commonJS({
10515
11032
  "../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
10516
11033
  "use strict";
10517
11034
  var { tokenChars } = require_validation();
10518
- function parse2(header) {
11035
+ function parse3(header) {
10519
11036
  const protocols = /* @__PURE__ */ new Set();
10520
11037
  let start = -1;
10521
11038
  let end2 = -1;
@@ -10551,7 +11068,7 @@ var require_subprotocol = __commonJS({
10551
11068
  protocols.add(protocol);
10552
11069
  return protocols;
10553
11070
  }
10554
- module.exports = { parse: parse2 };
11071
+ module.exports = { parse: parse3 };
10555
11072
  }
10556
11073
  });
10557
11074
 
@@ -13982,7 +14499,7 @@ var require_secure_json_parse = __commonJS({
13982
14499
  }
13983
14500
  return obj;
13984
14501
  }
13985
- function parse2(text, reviver, options2) {
14502
+ function parse3(text, reviver, options2) {
13986
14503
  const stackTraceLimit = Error.stackTraceLimit;
13987
14504
  Error.stackTraceLimit = 0;
13988
14505
  try {
@@ -14002,9 +14519,9 @@ var require_secure_json_parse = __commonJS({
14002
14519
  Error.stackTraceLimit = stackTraceLimit;
14003
14520
  }
14004
14521
  }
14005
- module.exports = parse2;
14006
- module.exports.default = parse2;
14007
- module.exports.parse = parse2;
14522
+ module.exports = parse3;
14523
+ module.exports.default = parse3;
14524
+ module.exports.parse = parse3;
14008
14525
  module.exports.safeParse = safeParse;
14009
14526
  module.exports.scan = filter;
14010
14527
  }
@@ -21367,11 +21884,11 @@ function clamp(text) {
21367
21884
  const buf = Buffer.from(text, "utf8");
21368
21885
  if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
21369
21886
  const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
21370
- let tail2 = buf.subarray(cut).toString("utf8");
21371
- const nl = tail2.indexOf("\n");
21372
- if (nl >= 0) tail2 = tail2.slice(nl + 1);
21887
+ let tail3 = buf.subarray(cut).toString("utf8");
21888
+ const nl = tail3.indexOf("\n");
21889
+ if (nl >= 0) tail3 = tail3.slice(nl + 1);
21373
21890
  return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
21374
- ${tail2}`, truncated: true };
21891
+ ${tail3}`, truncated: true };
21375
21892
  }
21376
21893
  async function saveTranscript(text) {
21377
21894
  if (!text.trim()) return { key: "", bytes: 0, truncated: false };
@@ -22331,6 +22848,18 @@ function leadArgv(o) {
22331
22848
  ];
22332
22849
  }
22333
22850
  var leadEnv = (agent, base = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base } : { ...base };
22851
+ var EXPLAIN_MAX = 500;
22852
+ var tail = (s) => {
22853
+ const t = s.trim();
22854
+ return t.length > EXPLAIN_MAX ? `\u2026${t.slice(-EXPLAIN_MAX)}` : t;
22855
+ };
22856
+ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
22857
+ if (err.killed) return `lead ${agent} kehabisan waktu ${timeoutMs} ms`;
22858
+ const fromErr = err.message.startsWith("Command failed:") ? "" : err.message;
22859
+ const detail = [tail(stderr), tail(stdout)].filter(Boolean).join(" \xB7 ") || tail(fromErr) || "tanpa keluaran";
22860
+ const how = err.signal ? `sinyal ${err.signal}` : `exit ${err.code ?? "?"}`;
22861
+ return `lead ${agent} gagal (${how}): ${detail}`;
22862
+ }
22334
22863
  function think(prompt, o) {
22335
22864
  const bin = binFor(o.agent);
22336
22865
  const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
@@ -22344,8 +22873,7 @@ function think(prompt, o) {
22344
22873
  killSignal: "SIGTERM"
22345
22874
  }, (err, stdout, stderr) => {
22346
22875
  if (err) {
22347
- const killed = err.killed;
22348
- reject(new Error(killed ? `lead ${o.agent} kehabisan waktu ${o.timeoutMs} ms` : `lead ${o.agent} gagal: ${(stderr || err.message).trim().slice(0, 500)}`));
22876
+ reject(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
22349
22877
  return;
22350
22878
  }
22351
22879
  resolve14(stdout);
@@ -22573,12 +23101,11 @@ async function integrateMain(row, deps) {
22573
23101
  if (!repoDir) return { ok: false, detail: `project ${row.projectId} belum di-bind ke checkout lokal` };
22574
23102
  const spec = await prisma.spec.findUnique({ where: { id: row.specId } });
22575
23103
  if (!spec) return { ok: false, detail: `backlog ${row.specId} tak ada` };
23104
+ const sid = sessionIdForSpec(spec.id);
23105
+ const done = deps.planDone(worktreeDir(repoDir, spec.id), spec.id);
22576
23106
  const evidence = [];
22577
23107
  if (cfg.requireGreenBeforeIntegrate) {
22578
- const wt = worktreeDir(repoDir, spec.id);
22579
- const done = deps.planDone(wt, spec.id);
22580
23108
  evidence.push(done ? "plan tak menyisakan `- [ ]`" : "plan MASIH menyisakan `- [ ]`");
22581
- const sid = sessionIdForSpec(spec.id);
22582
23109
  const pane = deps.sessionExists(sid);
22583
23110
  evidence.push(pane ? `pane ${sid} masih ada` : `pane ${sid} sudah tak ada`);
22584
23111
  if (!done) {
@@ -22597,6 +23124,9 @@ async function integrateMain(row, deps) {
22597
23124
  }
22598
23125
  const res = await deps.integrate(repoDir, spec.id, "merge", "local:main");
22599
23126
  evidence.push(`integrate \u2192 ${res.status}`);
23127
+ if (res.status === "clean" && done && deps.sessionExists(sid)) {
23128
+ evidence.push(deps.killSession(sid) ? `pane ${sid} dilepas (worktree utuh)` : `pane ${sid} gagal dilepas`);
23129
+ }
22600
23130
  await recordEvidence(row, evidence, res.status === "clean" ? "integrasi bersih" : `integrasi tak bersih: ${res.status}`);
22601
23131
  if (res.status !== "clean") {
22602
23132
  await deps.notify(
@@ -22621,10 +23151,13 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
22621
23151
 
22622
23152
  // src/services/lead/detect.ts
22623
23153
  init_pty();
23154
+ init_tui_dialog();
23155
+ import { writeFileSync as writeFileSync4 } from "node:fs";
22624
23156
 
22625
23157
  // src/services/lead/pane.ts
23158
+ init_tui_dialog();
22626
23159
  var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
22627
- var tail = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
23160
+ var tail2 = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
22628
23161
  var CODEX_FINISHED = [
22629
23162
  /Goal achieved/i,
22630
23163
  /Goal unmet/i,
@@ -22637,25 +23170,33 @@ var ASK_SIGNALS = [
22637
23170
  /\b(pilih|apakah|haruskah|mana yang|opsi|which|should i|do you want|proceed\?)\b/i
22638
23171
  ];
22639
23172
  function readPaneQuestion(text, agent) {
22640
- const lines2 = tail(text, 40);
23173
+ const lines2 = tail2(text, 40);
22641
23174
  const body = lines2.join("\n").trim();
22642
- if (!body) return { asking: false, question: "", reason: "layar kosong" };
22643
- const question = tail(text, 25).join("\n").trim();
23175
+ const choices = readChoiceDialog(text)?.options ?? [];
23176
+ if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
23177
+ const question = tail2(text, 25).join("\n").trim();
22644
23178
  if (agent === "codex") {
22645
23179
  const finished = CODEX_FINISHED.find((re) => re.test(body));
22646
- if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)" };
23180
+ if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
22647
23181
  if (!ASK_SIGNALS.some((re) => re.test(body)))
22648
- return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex" };
23182
+ return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex", choices };
22649
23183
  }
22650
- return { asking: true, question, reason: "" };
23184
+ return { asking: true, question, reason: "", choices };
22651
23185
  }
22652
23186
 
22653
23187
  // src/services/lead/detect.ts
22654
23188
  var answers = /* @__PURE__ */ new Map();
22655
23189
  var capped = /* @__PURE__ */ new Set();
23190
+ var failures = /* @__PURE__ */ new Map();
23191
+ var failCapped = /* @__PURE__ */ new Set();
23192
+ var MAX_CHAIN_STEPS = 6;
23193
+ var CHAIN_POLL_MS = 300;
23194
+ var CHAIN_POLL_TRIES = 20;
22656
23195
  function resetSession(sessionId2) {
22657
23196
  answers.delete(sessionId2);
22658
23197
  capped.delete(sessionId2);
23198
+ failures.delete(sessionId2);
23199
+ failCapped.delete(sessionId2);
22659
23200
  }
22660
23201
  var prodDetectDeps = {
22661
23202
  live: () => {
@@ -22685,6 +23226,14 @@ var prodDetectDeps = {
22685
23226
  }
22686
23227
  },
22687
23228
  send: (id, text) => sendToPane(id, text),
23229
+ clearMarker: (file) => {
23230
+ try {
23231
+ writeFileSync4(file, "");
23232
+ } catch {
23233
+ }
23234
+ },
23235
+ submit: (id) => submitPaneDialog(id),
23236
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
22688
23237
  decide,
22689
23238
  decideDeps: prodDecideDeps,
22690
23239
  optIn: leadProjects,
@@ -22716,7 +23265,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
22716
23265
  if ((answers.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
22717
23266
  if (!capped.has(s.id)) {
22718
23267
  capped.add(s.id);
22719
- const row2 = await recordDecision({
23268
+ const row = await recordDecision({
22720
23269
  projectId: s.projectId,
22721
23270
  specId: s.specId,
22722
23271
  sessionId: s.id,
@@ -22731,7 +23280,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
22731
23280
  weighty: true
22732
23281
  });
22733
23282
  await deps.notify(
22734
- row2.id,
23283
+ row.id,
22735
23284
  `Lead berhenti menjawab sesi ${s.id} (batas ${cfg.maxAutoAnswers}\xD7 tercapai)`,
22736
23285
  s.projectId,
22737
23286
  s.specId ?? null,
@@ -22741,12 +23290,79 @@ async function scanAndAnswer(deps = prodDetectDeps) {
22741
23290
  skip("batas jawaban otomatis tercapai");
22742
23291
  continue;
22743
23292
  }
23293
+ if ((failures.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
23294
+ if (!failCapped.has(s.id)) {
23295
+ failCapped.add(s.id);
23296
+ const row = await recordDecision({
23297
+ projectId: s.projectId,
23298
+ specId: s.specId,
23299
+ sessionId: s.id,
23300
+ gate: "detected",
23301
+ kind: "quality",
23302
+ question: `Keputusan untuk sesi ${s.id} gagal disusun ${cfg.maxAutoAnswers}\xD7 berturut-turut.`,
23303
+ answer: "Berhenti mencoba; serahkan ke operator.",
23304
+ 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.",
23305
+ refs: [],
23306
+ confidence: "tinggi",
23307
+ action: "none",
23308
+ weighty: true
23309
+ });
23310
+ await deps.notify(
23311
+ row.id,
23312
+ `Lead berhenti mencoba sesi ${s.id} (${cfg.maxAutoAnswers}\xD7 gagal berturut-turut)`,
23313
+ s.projectId,
23314
+ s.specId ?? null,
23315
+ s.id
23316
+ );
23317
+ }
23318
+ skip("batas kegagalan beruntun tercapai");
23319
+ continue;
23320
+ }
22744
23321
  const agent = deps.agentOf(s.id) ?? "claude";
22745
23322
  const read = readPaneQuestion(deps.pane(s.id), agent);
22746
23323
  if (!read.asking) {
22747
23324
  skip(read.reason);
22748
23325
  continue;
22749
23326
  }
23327
+ const chain = await runChain(s, agent, deps);
23328
+ if (chain.acted && chain.done) {
23329
+ deps.clearMarker(s.decisionFile);
23330
+ answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
23331
+ failures.delete(s.id);
23332
+ out3.answered.push(s.id);
23333
+ continue;
23334
+ }
23335
+ if (chain.failed) failures.set(s.id, (failures.get(s.id) ?? 0) + 1);
23336
+ skip(chain.reason);
23337
+ }
23338
+ return out3;
23339
+ }
23340
+ async function runChain(s, agent, deps) {
23341
+ let acted = false;
23342
+ for (let step = 0; step < MAX_CHAIN_STEPS; step++) {
23343
+ const text = deps.pane(s.id);
23344
+ const screen = readDialogScreen(text);
23345
+ if (screen?.kind === "review") {
23346
+ if (!await deps.submit(s.id))
23347
+ return { acted, done: false, failed: true, reason: "gagal menekan Submit answers" };
23348
+ acted = true;
23349
+ continue;
23350
+ }
23351
+ if (step > 0 && !screen) return { acted, done: true, failed: false, reason: "" };
23352
+ const read = readPaneQuestion(text, agent);
23353
+ if (!read.asking) {
23354
+ return step === 0 ? { acted, done: false, failed: false, reason: read.reason } : { acted, done: true, failed: false, reason: "" };
23355
+ }
23356
+ 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\`).`];
23357
+ if (read.choices.length) {
23358
+ 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.");
23359
+ }
23360
+ if (screen?.kind === "question" && screen.tabs.length > 1) {
23361
+ const at = screen.tabs.findIndex((t) => !t.answered);
23362
+ notes.push(
23363
+ `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.`
23364
+ );
23365
+ }
22750
23366
  const row = await deps.decide({
22751
23367
  projectId: s.projectId,
22752
23368
  specId: s.specId,
@@ -22754,27 +23370,35 @@ async function scanAndAnswer(deps = prodDetectDeps) {
22754
23370
  gate: "detected",
22755
23371
  kind: "answer",
22756
23372
  question: read.question,
22757
- notes: [`Sesi ini menunggu di terminal; teks di bawah adalah layar terakhirnya. Jawablah sebagai masukan yang bisa langsung diketik ke terminal itu (isi \`reply\`).`]
23373
+ options: read.choices.length ? read.choices : void 0,
23374
+ notes
22758
23375
  }, deps.decideDeps);
22759
- if (!row || row.status !== "berlaku") {
22760
- skip("lead tak menghasilkan keputusan yang berlaku");
22761
- continue;
22762
- }
23376
+ if (!row) return { acted, done: false, failed: false, reason: "lead tak menghasilkan keputusan yang berlaku" };
23377
+ if (row.status !== "berlaku")
23378
+ return { acted, done: false, failed: true, reason: "lead tak menghasilkan keputusan yang berlaku" };
22763
23379
  const reply = takeReply(row.id) || row.answer;
22764
- const sent = await deps.send(s.id, reply);
22765
- if (!sent) {
22766
- skip("gagal mengetik ke pane");
22767
- continue;
22768
- }
22769
- answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
22770
- out3.answered.push(s.id);
23380
+ if (!await deps.send(s.id, reply))
23381
+ return { acted, done: false, failed: true, reason: "gagal mengetik ke pane" };
23382
+ acted = true;
23383
+ if (!screen) return { acted, done: true, failed: false, reason: "" };
23384
+ if (!await waitScreenChange(s.id, dialogKey(text), deps))
23385
+ return { acted, done: false, failed: true, reason: "layar dialog tak berubah sesudah dijawab" };
22771
23386
  }
22772
- return out3;
23387
+ return { acted, done: false, failed: true, reason: "batas langkah rantai dialog tercapai" };
23388
+ }
23389
+ async function waitScreenChange(id, before, deps) {
23390
+ for (let i = 0; i < CHAIN_POLL_TRIES; i++) {
23391
+ await deps.sleep(CHAIN_POLL_MS);
23392
+ if (dialogKey(deps.pane(id)) !== before) return true;
23393
+ }
23394
+ return false;
22773
23395
  }
22774
23396
  function sweep(liveIds) {
22775
23397
  const live = new Set(liveIds);
22776
23398
  for (const id of [...answers.keys()]) if (!live.has(id)) answers.delete(id);
22777
23399
  for (const id of [...capped]) if (!live.has(id)) capped.delete(id);
23400
+ for (const id of [...failures.keys()]) if (!live.has(id)) failures.delete(id);
23401
+ for (const id of [...failCapped]) if (!live.has(id)) failCapped.delete(id);
22778
23402
  }
22779
23403
 
22780
23404
  // src/services/lead/pulse.ts
@@ -22818,6 +23442,13 @@ var prodPulseDeps = {
22818
23442
  }
22819
23443
  },
22820
23444
  planDone: planComplete,
23445
+ finished: (id) => {
23446
+ try {
23447
+ return sessionFinished(id);
23448
+ } catch {
23449
+ return false;
23450
+ }
23451
+ },
22821
23452
  decide,
22822
23453
  decideDeps: prodDecideDeps,
22823
23454
  apply: applyAction,
@@ -22838,6 +23469,10 @@ async function pulse(deps = prodPulseDeps) {
22838
23469
  res.quality = await followUpFinished(cfg, optIn, deps);
22839
23470
  } catch {
22840
23471
  }
23472
+ try {
23473
+ res.quality += await followUpComplete(optIn, deps);
23474
+ } catch {
23475
+ }
22841
23476
  try {
22842
23477
  res.collisions = await detectCollisions(optIn, deps);
22843
23478
  } catch {
@@ -22890,6 +23525,49 @@ async function followUpFinished(cfg, optIn, deps) {
22890
23525
  }
22891
23526
  return n;
22892
23527
  }
23528
+ async function followUpComplete(optIn, deps) {
23529
+ let n = 0;
23530
+ const opt = new Set(optIn);
23531
+ for (const s of deps.sessions()) {
23532
+ if (!s.specId || !opt.has(s.projectId)) continue;
23533
+ if ((s.exitCode ?? 0) !== 0) continue;
23534
+ if (!deps.finished(s.id)) continue;
23535
+ const mark = `Backlog ${s.specId} sudah selesai di sesi ${s.id}`;
23536
+ const seen = await prisma.leadDecision.findFirst({
23537
+ where: { sessionId: s.id, gate: "pulse", question: { startsWith: mark } }
23538
+ });
23539
+ if (seen) continue;
23540
+ const row = await deps.decide({
23541
+ projectId: s.projectId,
23542
+ specId: s.specId,
23543
+ sessionId: s.id,
23544
+ gate: "pulse",
23545
+ kind: "quality",
23546
+ question: `${mark}: seluruh fasenya tercatat dan plan-nya tak menyisakan kotak \`- [ ]\`. Selama sesinya belum dilepas, ia memegang satu slot concurrency scheduler sehingga backlog lain tak bisa mulai. Integrasikan hasilnya ke main, hentikan sesinya saja, atau biarkan?`,
23547
+ options: [
23548
+ "integrate-main \u2014 merge branch sesi ini ke main; panenya ikut dilepas, worktree tetap utuh",
23549
+ "stop-session \u2014 lepas panenya tanpa mengintegrasikan (worktree tetap utuh, ADR-0084 masih bisa melanjutkan)",
23550
+ "none \u2014 biarkan sesinya berdiri, sertakan alasan kenapa slot itu layak ditahan"
23551
+ ],
23552
+ notes: [
23553
+ `Worktree sesi: ${s.cwd}`,
23554
+ // Rebase sengaja tak ditawarkan: `LEAD_ACTIONS` adalah konstanta tertutup (AC-31), dan
23555
+ // merge adalah yang PALING MUDAH DIBATALKAN dari keduanya — kriteria yang diperintahkan
23556
+ // prompt lead sendiri. Rebase tetap tindakan operator lewat POST /specs/:id/integrate.
23557
+ "Rebase tidak tersedia untukmu; bila hasilnya menuntut rebase, pilih `none` dan katakan begitu."
23558
+ ]
23559
+ }, deps.decideDeps);
23560
+ if (!row) continue;
23561
+ n++;
23562
+ if (row.status === "berlaku" && row.action !== "none") {
23563
+ try {
23564
+ await deps.apply(row);
23565
+ } catch {
23566
+ }
23567
+ }
23568
+ }
23569
+ return n;
23570
+ }
22893
23571
  async function detectCollisions(optIn, deps) {
22894
23572
  const opt = new Set(optIn);
22895
23573
  const live = deps.sessions().filter((s) => !s.exited && s.specId && opt.has(s.projectId));
@@ -23162,6 +23840,598 @@ async function lead_default(app2) {
23162
23840
  });
23163
23841
  }
23164
23842
 
23843
+ // src/routes/custom-agents.ts
23844
+ init_src();
23845
+ init_db();
23846
+
23847
+ // src/services/custom-agents.ts
23848
+ init_db();
23849
+ init_src();
23850
+ init_pty();
23851
+ var cache4 = [];
23852
+ var asCustomAgent = (r) => ({
23853
+ id: r.id,
23854
+ projectId: r.projectId,
23855
+ name: r.name,
23856
+ description: r.description,
23857
+ instructions: r.instructions,
23858
+ tools: toolsOf(r.tools),
23859
+ model: r.model,
23860
+ mentions: mentionsOf(r.mentions),
23861
+ enabled: r.enabled,
23862
+ createdAt: "",
23863
+ updatedAt: ""
23864
+ // tak dipakai lapis ini
23865
+ });
23866
+ async function loadCustomAgents() {
23867
+ try {
23868
+ cache4 = await prisma.customAgent.findMany();
23869
+ } catch {
23870
+ cache4 = [];
23871
+ }
23872
+ }
23873
+ function agentDefsFor(projectId) {
23874
+ const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
23875
+ const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
23876
+ return effectiveAgents(globals, project).map((a) => ({
23877
+ name: a.name,
23878
+ description: a.description,
23879
+ instructions: a.instructions,
23880
+ tools: a.tools,
23881
+ model: a.model,
23882
+ mentions: a.mentions ?? []
23883
+ }));
23884
+ }
23885
+ function validateGraph(rows) {
23886
+ const projectScopes = [...new Set(rows.map((r) => r.projectId).filter((p) => p !== null))];
23887
+ const globals = rows.filter((r) => r.projectId === null).map(asCustomAgent);
23888
+ for (const scope of [null, ...projectScopes]) {
23889
+ const project = scope === null ? [] : rows.filter((r) => r.projectId === scope).map(asCustomAgent);
23890
+ const nodes = effectiveAgents(globals, project).map((a) => ({ name: a.name, mentions: a.mentions ?? [] }));
23891
+ const cycle = detectCycle(nodes);
23892
+ if (cycle) return { scope: scope ?? GLOBAL_SCOPE, cycle };
23893
+ }
23894
+ return null;
23895
+ }
23896
+ function unknownMentions(row, all) {
23897
+ const visible = new Set(
23898
+ all.filter((r) => r.projectId === null || row.projectId !== null && r.projectId === row.projectId).map((r) => r.name)
23899
+ );
23900
+ return mentionsOf(row.mentions).filter((m) => !visible.has(m));
23901
+ }
23902
+ async function installCustomAgents() {
23903
+ await loadCustomAgents();
23904
+ registerCustomAgentSource((projectId) => agentDefsFor(projectId));
23905
+ }
23906
+
23907
+ // src/routes/custom-agents.ts
23908
+ var rowsOf = async () => await prisma.customAgent.findMany();
23909
+ var view6 = (r, projectId) => ({
23910
+ id: r.id,
23911
+ projectId: r.projectId,
23912
+ name: r.name,
23913
+ description: r.description,
23914
+ instructions: r.instructions,
23915
+ tools: toolsOf(r.tools),
23916
+ model: r.model,
23917
+ mentions: mentionsOf(r.mentions),
23918
+ enabled: r.enabled,
23919
+ ...projectId ? { inherited: r.projectId === null } : {}
23920
+ });
23921
+ async function custom_agents_default(app2) {
23922
+ app2.get("/custom-agents", async (req) => {
23923
+ const projectId = req.query.projectId;
23924
+ const rows = await prisma.customAgent.findMany({
23925
+ where: projectId ? { OR: [{ projectId: null }, { projectId }] } : { projectId: null },
23926
+ orderBy: { name: "asc" }
23927
+ });
23928
+ const byName = /* @__PURE__ */ new Map();
23929
+ for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
23930
+ for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
23931
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view6(r, projectId));
23932
+ });
23933
+ app2.post("/custom-agents", async (req, reply) => {
23934
+ const parsed = zCreateCustomAgent.safeParse(req.body);
23935
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
23936
+ const p = parsed.data;
23937
+ const projectId = p.projectId ?? null;
23938
+ if (projectId && !await prisma.project.findUnique({ where: { id: projectId } }))
23939
+ return reply.code(400).send({ error: "project tak ditemukan", projectId });
23940
+ const id = customAgentId(projectId, p.name);
23941
+ if (await prisma.customAgent.findUnique({ where: { id } }))
23942
+ return reply.code(409).send({ error: "nama sudah dipakai di scope ini", id });
23943
+ const candidate = {
23944
+ id,
23945
+ projectId,
23946
+ name: p.name,
23947
+ description: p.description,
23948
+ instructions: p.instructions,
23949
+ tools: p.tools ?? null,
23950
+ model: p.model ?? null,
23951
+ mentions: p.mentions ?? [],
23952
+ enabled: p.enabled ?? true
23953
+ };
23954
+ const all = [...await rowsOf(), candidate];
23955
+ const unknown = unknownMentions(candidate, all);
23956
+ if (unknown.length) return reply.code(400).send({ error: "mention tak dikenal", unknown });
23957
+ const cycle = validateGraph(all);
23958
+ if (cycle) return reply.code(409).send({ error: "mention membentuk siklus", ...cycle });
23959
+ const row = await prisma.customAgent.create({ data: {
23960
+ id,
23961
+ projectId,
23962
+ name: p.name,
23963
+ description: p.description,
23964
+ instructions: p.instructions,
23965
+ tools: candidate.tools,
23966
+ model: candidate.model,
23967
+ mentions: candidate.mentions,
23968
+ enabled: candidate.enabled
23969
+ } });
23970
+ await loadCustomAgents();
23971
+ await notifySynced("customAgent", id);
23972
+ return reply.code(201).send(view6(row));
23973
+ });
23974
+ app2.patch("/custom-agents/:id", async (req, reply) => {
23975
+ const { id } = req.params;
23976
+ const body = req.body ?? {};
23977
+ if ("name" in body || "projectId" in body)
23978
+ return reply.code(400).send({ error: "name & projectId tak bisa diubah \u2014 hapus lalu buat baru" });
23979
+ const parsed = zUpdateCustomAgent.safeParse(body);
23980
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
23981
+ const existing = await prisma.customAgent.findUnique({ where: { id } });
23982
+ if (!existing) return reply.code(404).send({ error: "not found" });
23983
+ const before = existing;
23984
+ const candidate = {
23985
+ ...before,
23986
+ ...parsed.data,
23987
+ mentions: parsed.data.mentions ?? mentionsOf(before.mentions),
23988
+ tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before.tools)
23989
+ };
23990
+ const all = (await rowsOf()).map((r) => r.id === id ? candidate : r);
23991
+ const unknown = unknownMentions(candidate, all);
23992
+ if (unknown.length) return reply.code(400).send({ error: "mention tak dikenal", unknown });
23993
+ const cycle = validateGraph(all);
23994
+ if (cycle) return reply.code(409).send({ error: "mention membentuk siklus", ...cycle });
23995
+ const row = await prisma.customAgent.update({ where: { id }, data: {
23996
+ description: candidate.description,
23997
+ instructions: candidate.instructions,
23998
+ tools: candidate.tools,
23999
+ model: candidate.model,
24000
+ mentions: candidate.mentions,
24001
+ enabled: candidate.enabled
24002
+ } });
24003
+ await loadCustomAgents();
24004
+ await notifySynced("customAgent", id);
24005
+ return view6(row);
24006
+ });
24007
+ app2.delete("/custom-agents/:id", async (req, reply) => {
24008
+ const { id } = req.params;
24009
+ const existing = await prisma.customAgent.findUnique({ where: { id } });
24010
+ if (!existing) return reply.code(404).send({ error: "not found" });
24011
+ await prisma.customAgent.delete({ where: { id } });
24012
+ const name2 = existing.name;
24013
+ for (const r of await rowsOf()) {
24014
+ const m = mentionsOf(r.mentions);
24015
+ if (!m.includes(name2)) continue;
24016
+ await prisma.customAgent.update({
24017
+ where: { id: r.id },
24018
+ data: { mentions: m.filter((x) => x !== name2) }
24019
+ });
24020
+ await notifySynced("customAgent", r.id);
24021
+ }
24022
+ await loadCustomAgents();
24023
+ return reply.code(204).send();
24024
+ });
24025
+ }
24026
+
24027
+ // src/routes/github-issues.ts
24028
+ init_zod();
24029
+ init_db();
24030
+
24031
+ // src/services/github-issues.ts
24032
+ init_db();
24033
+
24034
+ // src/services/github-repo.ts
24035
+ init_db();
24036
+ function parse2(url2) {
24037
+ const u = url2.trim();
24038
+ let m = /^git@([^:]+):([^/]+)\/(.+?)(?:\.git)?$/.exec(u);
24039
+ if (!m) m = /^https?:\/\/(?:[^@/]+@)?([^/]+)\/([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(u);
24040
+ if (!m || !m[1] || !m[2] || !m[3]) return null;
24041
+ return { host: m[1], owner: m[2], repo: m[3] };
24042
+ }
24043
+ function githubSlugFromUrl(url2) {
24044
+ const p = parse2(url2);
24045
+ if (!p || !p.host.includes("github.")) return null;
24046
+ return { owner: p.owner, repo: p.repo, slug: `${p.owner}/${p.repo}` };
24047
+ }
24048
+ async function resolveGithubRepo(projectId) {
24049
+ const project = await prisma.project.findUnique({ where: { id: projectId } });
24050
+ if (!project) return { ok: false, kind: "no-project", error: `project "${projectId}" tidak ada` };
24051
+ const candidates = [];
24052
+ if (project.gitRemote) candidates.push(project.gitRemote);
24053
+ const repoDir = await resolveRepoDir(projectId).catch(() => null);
24054
+ if (repoDir) {
24055
+ const origin = (await listRemotes(repoDir)).find((r) => r.name === "origin");
24056
+ if (origin?.fetch) candidates.push(origin.fetch);
24057
+ }
24058
+ if (candidates.length === 0)
24059
+ return {
24060
+ ok: false,
24061
+ kind: "no-remote",
24062
+ error: "project belum punya remote GitHub (isi gitRemote atau tambahkan origin di repo lokalnya)"
24063
+ };
24064
+ for (const url2 of candidates) {
24065
+ const gh = githubSlugFromUrl(url2);
24066
+ if (gh) return { ok: true, repo: gh };
24067
+ }
24068
+ const host2 = parse2(candidates[0])?.host ?? candidates[0];
24069
+ return {
24070
+ ok: false,
24071
+ kind: "not-github",
24072
+ error: `remote project ber-host "${host2}", bukan GitHub \u2014 tarik issue hanya mendukung GitHub`
24073
+ };
24074
+ }
24075
+
24076
+ // src/services/github-fetch.ts
24077
+ init_config2();
24078
+ import { execFile as execFile10 } from "node:child_process";
24079
+ var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
24080
+ var norm = (s) => s.toLowerCase() === "closed" ? "closed" : "open";
24081
+ function issueFromGh(raw) {
24082
+ return {
24083
+ number: raw.number,
24084
+ title: raw.title,
24085
+ body: raw.body ?? "",
24086
+ authorLogin: raw.author?.login ?? "",
24087
+ labels: labelNames(raw.labels),
24088
+ url: raw.url,
24089
+ issueState: norm(raw.state),
24090
+ issueCreatedAt: raw.createdAt,
24091
+ issueUpdatedAt: raw.updatedAt
24092
+ };
24093
+ }
24094
+ function issuesFromRest(raw) {
24095
+ let skippedPullRequests = 0;
24096
+ const issues = [];
24097
+ for (const r of raw) {
24098
+ if (r.pull_request !== void 0) {
24099
+ skippedPullRequests++;
24100
+ continue;
24101
+ }
24102
+ issues.push({
24103
+ number: r.number,
24104
+ title: r.title,
24105
+ body: r.body ?? "",
24106
+ authorLogin: r.user?.login ?? "",
24107
+ labels: labelNames(r.labels),
24108
+ url: r.html_url,
24109
+ issueState: norm(r.state),
24110
+ issueCreatedAt: r.created_at,
24111
+ issueUpdatedAt: r.updated_at
24112
+ });
24113
+ }
24114
+ return { issues, skippedPullRequests };
24115
+ }
24116
+ var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
24117
+ var API2 = "https://api.github.com";
24118
+ var defaultRunGh = (args, env) => new Promise((resolve14, reject) => {
24119
+ execFile10(
24120
+ args[0],
24121
+ args.slice(1),
24122
+ { env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
24123
+ (err, stdout, stderr) => {
24124
+ const e = err;
24125
+ if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject(e);
24126
+ resolve14({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
24127
+ }
24128
+ );
24129
+ });
24130
+ var defaultHttpGet = async (url2, headers) => {
24131
+ const res = await fetch(url2, {
24132
+ headers: { Accept: "application/vnd.github+json", "User-Agent": "hanoman", ...headers }
24133
+ });
24134
+ const json = await res.json().catch(() => null);
24135
+ return { status: res.status, json };
24136
+ };
24137
+ function classifyGhStderr(stderr) {
24138
+ const s = stderr.toLowerCase();
24139
+ if (s.includes("gh auth login") || s.includes("bad credentials") || s.includes("http 401")) return "unauth";
24140
+ if (s.includes("disabled issues")) return "issues-disabled";
24141
+ if (s.includes("could not resolve to a repository") || s.includes("http 404")) return "not-found";
24142
+ return "other";
24143
+ }
24144
+ async function viaGh(repo, opts, deps) {
24145
+ const bin = deps.ghBin ?? effectiveStr("HANOMAN_GH_BIN") ?? "gh";
24146
+ const args = [
24147
+ bin,
24148
+ "issue",
24149
+ "list",
24150
+ "--repo",
24151
+ repo.slug,
24152
+ "--state",
24153
+ opts.state,
24154
+ "--limit",
24155
+ String(opts.limit),
24156
+ "--json",
24157
+ GH_FIELDS
24158
+ ];
24159
+ const env = { ...process.env };
24160
+ if (deps.token) env.GH_TOKEN = deps.token;
24161
+ let out3;
24162
+ try {
24163
+ out3 = await (deps.runGh ?? defaultRunGh)(args, env);
24164
+ } catch {
24165
+ return { fallback: true, reason: "gh tak terpasang" };
24166
+ }
24167
+ if (out3.code !== 0) {
24168
+ const kind = classifyGhStderr(out3.stderr);
24169
+ if (kind === "unauth") return { fallback: true, reason: "gh tak terautentikasi" };
24170
+ return { ok: false, kind, error: out3.stderr.trim() || `gh keluar dengan kode ${out3.code}` };
24171
+ }
24172
+ let raw;
24173
+ try {
24174
+ raw = JSON.parse(out3.stdout || "[]");
24175
+ } catch {
24176
+ return { ok: false, kind: "other", error: "keluaran gh bukan JSON" };
24177
+ }
24178
+ return { ok: true, issues: raw.map(issueFromGh), via: "gh", skippedPullRequests: 0 };
24179
+ }
24180
+ async function viaRest(repo, opts, deps) {
24181
+ const get = deps.httpGet ?? defaultHttpGet;
24182
+ const headers = {};
24183
+ if (deps.token) headers.Authorization = `Bearer ${deps.token}`;
24184
+ const meta = await get(`${API2}/repos/${repo.slug}`, headers);
24185
+ if (meta.status === 404)
24186
+ return { ok: false, kind: "not-found", error: `repo "${repo.slug}" tak ditemukan atau tak terjangkau` };
24187
+ if (meta.status === 401 || meta.status === 403)
24188
+ return { ok: false, kind: "unauthorized", error: "GitHub menolak kredensial \u2014 isi GITHUB_TOKEN di Settings" };
24189
+ if (meta.status !== 200) return { ok: false, kind: "other", error: `GitHub menjawab HTTP ${meta.status}` };
24190
+ if (meta.json?.has_issues === false)
24191
+ return { ok: false, kind: "issues-disabled", error: `repo "${repo.slug}" mematikan fitur issue` };
24192
+ const issues = [];
24193
+ let skippedPullRequests = 0;
24194
+ for (let page = 1; issues.length < opts.limit && page <= 10; page++) {
24195
+ const per = Math.min(100, opts.limit - issues.length);
24196
+ const res = await get(
24197
+ `${API2}/repos/${repo.slug}/issues?state=${opts.state}&per_page=${per}&page=${page}`,
24198
+ headers
24199
+ );
24200
+ if (res.status === 404) return { ok: false, kind: "not-found", error: `repo "${repo.slug}" tak ditemukan` };
24201
+ if (res.status === 401 || res.status === 403)
24202
+ return { ok: false, kind: "unauthorized", error: "GitHub menolak kredensial" };
24203
+ if (res.status !== 200) return { ok: false, kind: "other", error: `GitHub menjawab HTTP ${res.status}` };
24204
+ const batch = Array.isArray(res.json) ? res.json : [];
24205
+ if (batch.length === 0) break;
24206
+ const n = issuesFromRest(batch);
24207
+ issues.push(...n.issues);
24208
+ skippedPullRequests += n.skippedPullRequests;
24209
+ if (batch.length < per) break;
24210
+ }
24211
+ return { ok: true, issues: issues.slice(0, opts.limit), via: "rest", skippedPullRequests };
24212
+ }
24213
+ async function fetchIssues(repo, opts, deps = {}) {
24214
+ const token = deps.token ?? effectiveStr("GITHUB_TOKEN") ?? void 0;
24215
+ const first = await viaGh(repo, opts, { ...deps, token });
24216
+ if (!("fallback" in first)) return first;
24217
+ return viaRest(repo, opts, { ...deps, token });
24218
+ }
24219
+
24220
+ // src/services/github-issues.ts
24221
+ var issueRowId = (projectId, slug, number) => `${projectId}:${slug}#${number}`;
24222
+ async function pullIssues(projectId, opts = {}, deps = {}) {
24223
+ const resolved = await resolveGithubRepo(projectId);
24224
+ if (!resolved.ok) return { ok: false, kind: resolved.kind, error: resolved.error };
24225
+ const state = opts.state ?? "open";
24226
+ const limit = Math.min(Math.max(opts.limit ?? 200, 1), 1e3);
24227
+ const got = await fetchIssues(resolved.repo, { state, limit }, deps);
24228
+ if (!got.ok) return { ok: false, kind: got.kind, error: got.error };
24229
+ const slug = resolved.repo.slug;
24230
+ const now = /* @__PURE__ */ new Date();
24231
+ let created = 0, updated = 0;
24232
+ for (const i of got.issues) {
24233
+ const id = issueRowId(projectId, slug, i.number);
24234
+ const exists = await prisma.githubIssue.findUnique({ where: { id }, select: { id: true } });
24235
+ const fresh = {
24236
+ title: i.title,
24237
+ body: i.body,
24238
+ authorLogin: i.authorLogin,
24239
+ labels: i.labels,
24240
+ url: i.url,
24241
+ issueState: i.issueState,
24242
+ issueCreatedAt: new Date(i.issueCreatedAt),
24243
+ issueUpdatedAt: new Date(i.issueUpdatedAt),
24244
+ pulledAt: now
24245
+ };
24246
+ await prisma.githubIssue.upsert({
24247
+ where: { id },
24248
+ create: { id, projectId, repoSlug: slug, number: i.number, status: "new", specId: null, ...fresh },
24249
+ update: fresh
24250
+ });
24251
+ if (exists) updated++;
24252
+ else created++;
24253
+ await notifySynced("githubIssue", id);
24254
+ }
24255
+ return {
24256
+ ok: true,
24257
+ repo: slug,
24258
+ pulled: got.issues.length,
24259
+ created,
24260
+ updated,
24261
+ via: got.via,
24262
+ skippedPullRequests: got.skippedPullRequests
24263
+ };
24264
+ }
24265
+
24266
+ // src/services/github-accept.ts
24267
+ init_src();
24268
+ init_db();
24269
+ var backlinkOf = (i) => `Dari GitHub issue ${i.repoSlug}#${i.number} (${i.url}).`;
24270
+ async function acceptGithubIssue(issue2, opts) {
24271
+ if (issue2.specId) {
24272
+ const spec2 = await prisma.spec.findUnique({ where: { id: issue2.specId } });
24273
+ if (spec2) return { spec: spec2, created: false };
24274
+ }
24275
+ const labels = Array.isArray(issue2.labels) ? issue2.labels : [];
24276
+ const source = opts.source ?? sourceForLabels(labels);
24277
+ const priority = opts.priority ?? "sedang";
24278
+ const backlink = backlinkOf(issue2);
24279
+ const detail = `${issue2.body}
24280
+
24281
+ Pelapor: @${issue2.authorLogin}
24282
+ Label: ${labels.length ? labels.join(", ") : "(tanpa label)"}
24283
+ ${backlink}`;
24284
+ const payload = source === "qa" ? {
24285
+ severity: "major",
24286
+ steps: "Reproduksi dari deskripsi issue.",
24287
+ expected: "Perilaku yang diharapkan pelapor issue.",
24288
+ actual: detail,
24289
+ env: ""
24290
+ } : { context: detail, outcome: "", constraints: "", priority };
24291
+ const repoDir = await resolveRepoDir(issue2.projectId).catch(() => null);
24292
+ let spec = null;
24293
+ for (let attempt = 0; attempt < 3 && !spec; attempt++) {
24294
+ const sid = await nextSpecId(repoDir);
24295
+ try {
24296
+ spec = await prisma.spec.create({
24297
+ data: {
24298
+ id: sid,
24299
+ projectId: issue2.projectId,
24300
+ title: issue2.title,
24301
+ source,
24302
+ stage: "brainstorming",
24303
+ priority,
24304
+ author: `GitHub \xB7 ${opts.author}`,
24305
+ objective: `${issue2.title}. ${backlink}`,
24306
+ payload
24307
+ }
24308
+ });
24309
+ } catch (e) {
24310
+ if (e.code === "P2002" && attempt < 2) continue;
24311
+ throw e;
24312
+ }
24313
+ }
24314
+ await prisma.githubIssue.update({
24315
+ where: { id: issue2.id },
24316
+ data: { status: "accepted", specId: spec.id }
24317
+ });
24318
+ await notifySynced("spec", spec.id);
24319
+ await notifySynced("githubIssue", issue2.id);
24320
+ return { spec, created: true };
24321
+ }
24322
+
24323
+ // src/routes/github-issues.ts
24324
+ var zPull = external_exports.object({
24325
+ state: external_exports.enum(["open", "all"]).optional(),
24326
+ limit: external_exports.number().int().min(1).max(1e3).optional()
24327
+ }).default({});
24328
+ var zAccept = external_exports.object({
24329
+ priority: external_exports.enum(["tinggi", "sedang", "rendah"]).optional(),
24330
+ source: external_exports.enum(["qa", "brief", "audit"]).optional()
24331
+ }).default({});
24332
+ var zAcceptMany = external_exports.object({
24333
+ ids: external_exports.array(external_exports.string().min(1)).min(1).max(100),
24334
+ priority: external_exports.enum(["tinggi", "sedang", "rendah"]).optional(),
24335
+ source: external_exports.enum(["qa", "brief", "audit"]).optional()
24336
+ });
24337
+ var STATUS = {
24338
+ "no-project": 404,
24339
+ "not-found": 404,
24340
+ "no-remote": 400,
24341
+ "not-github": 400,
24342
+ "issues-disabled": 400,
24343
+ unauthorized: 401,
24344
+ other: 502
24345
+ };
24346
+ var view7 = (i) => ({
24347
+ id: i.id,
24348
+ projectId: i.projectId,
24349
+ repoSlug: i.repoSlug,
24350
+ number: i.number,
24351
+ title: i.title,
24352
+ body: i.body,
24353
+ authorLogin: i.authorLogin,
24354
+ labels: Array.isArray(i.labels) ? i.labels : [],
24355
+ url: i.url,
24356
+ issueState: i.issueState,
24357
+ status: i.status,
24358
+ specId: i.specId,
24359
+ issueCreatedAt: i.issueCreatedAt.toISOString(),
24360
+ issueUpdatedAt: i.issueUpdatedAt.toISOString(),
24361
+ pulledAt: i.pulledAt.toISOString()
24362
+ });
24363
+ async function githubIssues(app2) {
24364
+ app2.post("/projects/:id/github/pull", async (req, reply) => {
24365
+ const { id } = req.params;
24366
+ const parsed = zPull.safeParse(req.body ?? {});
24367
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24368
+ const r = await pullIssues(id, parsed.data);
24369
+ if (!r.ok) return reply.code(STATUS[r.kind] ?? 400).send({ error: r.error });
24370
+ return reply.code(200).send(r);
24371
+ });
24372
+ app2.get("/projects/:id/github/issues", async (req, reply) => {
24373
+ const { id } = req.params;
24374
+ const { status } = req.query;
24375
+ const project = await prisma.project.findUnique({ where: { id }, select: { id: true } });
24376
+ if (!project) return reply.code(404).send({ error: "not found" });
24377
+ const items = await prisma.githubIssue.findMany({
24378
+ where: { projectId: id, ...status ? { status } : {} },
24379
+ orderBy: [{ number: "desc" }]
24380
+ });
24381
+ return reply.send({ items: items.map(view7) });
24382
+ });
24383
+ app2.post("/github-issues/accept", async (req, reply) => {
24384
+ const parsed = zAcceptMany.safeParse(req.body ?? {});
24385
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24386
+ const { ids, priority, source } = parsed.data;
24387
+ const author = req.user?.email ?? "system";
24388
+ const created = [];
24389
+ const failed = [];
24390
+ for (const id of ids) {
24391
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24392
+ if (!issue2) {
24393
+ failed.push({ id, error: "not found" });
24394
+ continue;
24395
+ }
24396
+ try {
24397
+ created.push((await acceptGithubIssue(issue2, { author, priority, source })).spec);
24398
+ } catch (e) {
24399
+ failed.push({ id, error: e.message });
24400
+ }
24401
+ }
24402
+ return reply.code(201).send({ created, failed });
24403
+ });
24404
+ app2.post("/github-issues/:id/accept", async (req, reply) => {
24405
+ const { id } = req.params;
24406
+ const parsed = zAccept.safeParse(req.body ?? {});
24407
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24408
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24409
+ if (!issue2) return reply.code(404).send({ error: "not found" });
24410
+ const { spec, created } = await acceptGithubIssue(issue2, {
24411
+ author: req.user?.email ?? "system",
24412
+ priority: parsed.data.priority,
24413
+ source: parsed.data.source
24414
+ });
24415
+ return reply.code(created ? 201 : 200).send(created ? { spec } : { spec, alreadyPromoted: true });
24416
+ });
24417
+ app2.post("/github-issues/:id/reject", async (req, reply) => {
24418
+ const { id } = req.params;
24419
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24420
+ if (!issue2) return reply.code(404).send({ error: "not found" });
24421
+ const row = await prisma.githubIssue.update({ where: { id }, data: { status: "rejected" } });
24422
+ await notifySynced("githubIssue", id);
24423
+ return reply.send({ id: row.id, status: row.status });
24424
+ });
24425
+ app2.post("/github-issues/:id/unlink", async (req, reply) => {
24426
+ const { id } = req.params;
24427
+ const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
24428
+ if (!issue2) return reply.code(404).send({ error: "not found" });
24429
+ const row = await prisma.githubIssue.update({ where: { id }, data: { status: "new", specId: null } });
24430
+ await notifySynced("githubIssue", id);
24431
+ return reply.send({ id: row.id, status: row.status, specId: row.specId });
24432
+ });
24433
+ }
24434
+
23165
24435
  // src/app.ts
23166
24436
  var import_multipart = __toESM(require_multipart2(), 1);
23167
24437
 
@@ -23243,7 +24513,7 @@ function cookieOpts() {
23243
24513
  }
23244
24514
 
23245
24515
  // src/routes/auth.ts
23246
- var view6 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
24516
+ var view8 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
23247
24517
  async function issue(reply, userId) {
23248
24518
  const token = await createSession2(userId);
23249
24519
  reply.setCookie(COOKIE_NAME, token, cookieOpts());
@@ -23261,7 +24531,7 @@ async function auth_default(app2) {
23261
24531
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
23262
24532
  });
23263
24533
  await issue(reply, user.id);
23264
- return { user: view6(user) };
24534
+ return { user: view8(user) };
23265
24535
  });
23266
24536
  app2.post("/auth/login", async (req, reply) => {
23267
24537
  if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
@@ -23274,7 +24544,7 @@ async function auth_default(app2) {
23274
24544
  }
23275
24545
  clearLoginFails(req.ip);
23276
24546
  await issue(reply, user.id);
23277
- return { user: view6(user) };
24547
+ return { user: view8(user) };
23278
24548
  });
23279
24549
  app2.post("/auth/logout", async (req, reply) => {
23280
24550
  const token = req.cookies?.[COOKIE_NAME];
@@ -23282,7 +24552,7 @@ async function auth_default(app2) {
23282
24552
  reply.clearCookie(COOKIE_NAME, { path: "/" });
23283
24553
  return reply.code(204).send();
23284
24554
  });
23285
- app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view6));
24555
+ app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view8));
23286
24556
  app2.post("/auth/users", async (req, reply) => {
23287
24557
  const p = zSignup.safeParse(req.body);
23288
24558
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
@@ -23291,7 +24561,7 @@ async function auth_default(app2) {
23291
24561
  const user = await prisma.user.create({
23292
24562
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
23293
24563
  });
23294
- return view6(user);
24564
+ return view8(user);
23295
24565
  });
23296
24566
  app2.delete("/auth/users/:id", async (req, reply) => {
23297
24567
  if (await prisma.user.count() <= 1) return reply.code(400).send({ error: "tak bisa hapus user terakhir" });
@@ -23311,7 +24581,7 @@ async function auth_default(app2) {
23311
24581
  });
23312
24582
  await deleteUserSessions(user.id);
23313
24583
  await issue(reply, user.id);
23314
- return { user: view6(user) };
24584
+ return { user: view8(user) };
23315
24585
  });
23316
24586
  }
23317
24587
 
@@ -23390,15 +24660,15 @@ async function agent_tokens_default(app2) {
23390
24660
  app2.post("/agent-tokens", async (req, reply) => {
23391
24661
  const p = zAgentTokenCreate.safeParse(req.body);
23392
24662
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
23393
- const { view: view7, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
23394
- return reply.code(201).send({ ...view7, token });
24663
+ const { view: view9, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
24664
+ return reply.code(201).send({ ...view9, token });
23395
24665
  });
23396
24666
  app2.patch("/agent-tokens/:id", async (req, reply) => {
23397
24667
  const p = zAgentTokenPatch.safeParse(req.body);
23398
24668
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
23399
24669
  const { id } = req.params;
23400
- const view7 = await patchAgentToken(id, p.data);
23401
- return view7 ? reply.send(view7) : reply.code(404).send({ error: "not found" });
24670
+ const view9 = await patchAgentToken(id, p.data);
24671
+ return view9 ? reply.send(view9) : reply.code(404).send({ error: "not found" });
23402
24672
  });
23403
24673
  app2.delete("/agent-tokens/:id", async (req, reply) => {
23404
24674
  const { id } = req.params;
@@ -23446,10 +24716,12 @@ function capabilityForRoute(method, path) {
23446
24716
  return read ? "GLOBAL_READ" : "COOKIE_ONLY";
23447
24717
  if (top === "scheduler") return rw("settings");
23448
24718
  if (top === "lead") return rw("lead");
24719
+ if (top === "custom-agents") return rw("agents");
23449
24720
  if (top === "settings" || top === "config") return rw("settings");
23450
24721
  if (top === "specs") return rw("backlog");
23451
24722
  if (top === "notifications") return rw("notifications");
23452
24723
  if (top === "tickets") return rw("support");
24724
+ if (top === "github-issues") return rw("support");
23453
24725
  if (top === "vps") return rw("vps");
23454
24726
  if (top === "prds") return rw("docs");
23455
24727
  if (top === "terminal") {
@@ -23459,6 +24731,7 @@ function capabilityForRoute(method, path) {
23459
24731
  if (top === "projects") {
23460
24732
  const sub = seg[2];
23461
24733
  if (sub === "docs" || sub === "prds") return rw("docs");
24734
+ if (sub === "github") return rw("support");
23462
24735
  if (sub && IDE_SUBS.has(sub)) return rw("ide");
23463
24736
  return rw("projects");
23464
24737
  }
@@ -23552,6 +24825,8 @@ function buildApp({ requireAuth = true } = {}) {
23552
24825
  await api.register(scheduler_default);
23553
24826
  await api.register(codex);
23554
24827
  await api.register(lead_default);
24828
+ await api.register(custom_agents_default);
24829
+ await api.register(githubIssues);
23555
24830
  }, { prefix: "/api" });
23556
24831
  if (process.env.NODE_ENV === "production") {
23557
24832
  const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
@@ -23837,8 +25112,9 @@ async function shutdown(sig) {
23837
25112
  }
23838
25113
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
23839
25114
  process.on("SIGINT", () => void shutdown("SIGINT"));
23840
- app.listen({ port, host }).then(() => {
25115
+ app.listen({ port, host }).then(async () => {
23841
25116
  console.log(`hanoman api ${host}:${port}`);
25117
+ await installCustomAgents();
23842
25118
  installSessionHistory();
23843
25119
  try {
23844
25120
  const liveIds = listSessions().map((s) => s.id);