hanoman 0.1.8 → 0.1.9

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,103 @@ 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
+
4505
4609
  // ../shared/src/lead.ts
4506
4610
  function leadActionAllowed(action) {
4507
4611
  return LEAD_ACTIONS.includes(action);
@@ -5114,6 +5218,9 @@ var init_api = __esm({
5114
5218
  leadDecisions: `${API}/lead/decisions`,
5115
5219
  leadDecisionOverride: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/override`,
5116
5220
  leadDecisionCancel: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/cancel`,
5221
+ // SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
5222
+ customAgents: `${API}/custom-agents`,
5223
+ customAgent: (id) => `${API}/custom-agents/${encodeURIComponent(id)}`,
5117
5224
  // SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
5118
5225
  syncNow: `${API}/sync/now`,
5119
5226
  // SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
@@ -5434,6 +5541,7 @@ var init_src = __esm({
5434
5541
  init_enums();
5435
5542
  init_entities();
5436
5543
  init_agent();
5544
+ init_custom_agent();
5437
5545
  init_lead();
5438
5546
  init_dto();
5439
5547
  init_api();
@@ -6202,6 +6310,74 @@ var init_agent_cli = __esm({
6202
6310
  }
6203
6311
  });
6204
6312
 
6313
+ // ../runner/src/custom-agents.ts
6314
+ function agentPromptOf(def2, roster) {
6315
+ const can = liveMentions(def2, roster);
6316
+ if (can.length === 0) {
6317
+ return `${def2.instructions}
6318
+
6319
+ ---
6320
+ Kamu TIDAK boleh mendelegasikan ke agen lain. Selesaikan sendiri lalu laporkan hasilnya.`;
6321
+ }
6322
+ const list2 = can.map((m) => `@${m}`).join(", ");
6323
+ return [
6324
+ def2.instructions,
6325
+ "",
6326
+ "---",
6327
+ `Kamu boleh mendelegasikan HANYA ke: ${list2}. Panggil lewat ${MENTION_TOOL} dengan nama agennya.`,
6328
+ `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.`,
6329
+ "Sebutkan hop keberapa kamu berada saat mendelegasikan, dan jangan pernah memanggil agen yang sudah ada di rantai yang membawamu ke sini."
6330
+ ].join("\n");
6331
+ }
6332
+ function renderAgentsJson(defs) {
6333
+ if (defs.length === 0) return "";
6334
+ const out3 = {};
6335
+ for (const d of defs) {
6336
+ out3[d.name] = {
6337
+ description: d.description,
6338
+ prompt: agentPromptOf(d, defs),
6339
+ tools: resolveTools({ tools: d.tools, mentions: d.mentions }),
6340
+ ...d.model ? { model: d.model } : {}
6341
+ };
6342
+ }
6343
+ return JSON.stringify(out3);
6344
+ }
6345
+ function agentRosterBlock(defs) {
6346
+ if (defs.length === 0) return "";
6347
+ const lines2 = [
6348
+ "",
6349
+ "## Custom agent hanoman",
6350
+ "",
6351
+ "Peran berikut tersedia untuk sesi ini. Saat sebuah tugas cocok dengan salah satunya, ADOPSI",
6352
+ "perannya (baca instruksinya, kerjakan dengan sudut pandang itu) lalu kembali ke peranmu sendiri.",
6353
+ "Jangan melahirkan proses agen baru.",
6354
+ ""
6355
+ ];
6356
+ for (const d of defs) {
6357
+ const can = liveMentions(d, defs);
6358
+ lines2.push(`### @${d.name} \u2014 ${d.description}`);
6359
+ lines2.push("");
6360
+ lines2.push(d.instructions);
6361
+ lines2.push("");
6362
+ lines2.push(
6363
+ can.length ? `Boleh berkonsultasi ke: ${can.map((m) => `@${m}`).join(", ")} (maks ${MENTION_MAX_HOPS} hop berantai).` : "Tidak boleh berkonsultasi ke peran lain."
6364
+ );
6365
+ lines2.push("");
6366
+ }
6367
+ return lines2.join("\n");
6368
+ }
6369
+ var liveMentions;
6370
+ var init_custom_agents = __esm({
6371
+ "../runner/src/custom-agents.ts"() {
6372
+ "use strict";
6373
+ init_src();
6374
+ liveMentions = (def2, roster) => {
6375
+ const names = new Set(roster.map((r) => r.name));
6376
+ return def2.mentions.filter((m) => names.has(m) && m !== def2.name);
6377
+ };
6378
+ }
6379
+ });
6380
+
6205
6381
  // ../runner/src/paths.ts
6206
6382
  import { homedir } from "node:os";
6207
6383
  import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve4 } from "node:path";
@@ -6262,6 +6438,7 @@ var init_src2 = __esm({
6262
6438
  init_goal_spec();
6263
6439
  init_codex_settings();
6264
6440
  init_agent_cli();
6441
+ init_custom_agents();
6265
6442
  init_verify_scope();
6266
6443
  init_paths();
6267
6444
  }
@@ -6391,6 +6568,74 @@ var init_session_phases = __esm({
6391
6568
  }
6392
6569
  });
6393
6570
 
6571
+ // src/services/tui-dialog.ts
6572
+ function readChoiceDialog(paneText2) {
6573
+ const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
6574
+ let footer = -1;
6575
+ for (let i = lines2.length - 1; i >= 0; i--) {
6576
+ if (FOOTER.test(lines2[i] ?? "")) {
6577
+ footer = i;
6578
+ break;
6579
+ }
6580
+ }
6581
+ if (footer < 0) return null;
6582
+ const found = [];
6583
+ for (const line of lines2.slice(0, footer)) {
6584
+ const m = ROW.exec(line);
6585
+ if (m) found.push({ n: Number(m[1]), label: (m[2] ?? "").trim() });
6586
+ }
6587
+ if (found.length < 2) return null;
6588
+ const run2 = [];
6589
+ let expected = found[found.length - 1].n;
6590
+ for (let i = found.length - 1; i >= 0 && expected >= 1; i--) {
6591
+ const row = found[i];
6592
+ if (row.n !== expected) break;
6593
+ run2.unshift(row);
6594
+ expected -= 1;
6595
+ }
6596
+ if (run2.length < 2 || run2[0].n !== 1) return null;
6597
+ const rows = run2.map((r) => ({
6598
+ n: r.n,
6599
+ label: r.label,
6600
+ free: PLACEHOLDER.test(r.label),
6601
+ chat: CHAT_ROW.test(r.label)
6602
+ }));
6603
+ return {
6604
+ rows,
6605
+ freeIndex: rows.find((r) => r.free)?.n ?? null,
6606
+ options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
6607
+ };
6608
+ }
6609
+ function freeTextFilled(paneText2, n) {
6610
+ const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n);
6611
+ if (!row) return false;
6612
+ return row.label.length > 0 && !PLACEHOLDER.test(row.label);
6613
+ }
6614
+ async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
6615
+ io.literal(String(freeIndex));
6616
+ await io.sleep(DIALOG_SETTLE_MS);
6617
+ for (const chunk of goalChunks(line)) {
6618
+ io.literal(chunk);
6619
+ await io.sleep(chunkMs);
6620
+ }
6621
+ await io.sleep(DIALOG_SETTLE_MS);
6622
+ if (!freeTextFilled(io.capture(), freeIndex)) return false;
6623
+ io.enter();
6624
+ return true;
6625
+ }
6626
+ var FOOTER, ROW, PLACEHOLDER, CHAT_ROW, DIALOG_SETTLE_MS;
6627
+ var init_tui_dialog = __esm({
6628
+ "src/services/tui-dialog.ts"() {
6629
+ "use strict";
6630
+ init_src2();
6631
+ FOOTER = /enter to (?:select|confirm)\b/i;
6632
+ ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+(\S.*)$/;
6633
+ PLACEHOLDER = /^(?:type something\.?|other)$/i;
6634
+ CHAT_ROW = /^chat about this$/i;
6635
+ DIALOG_SETTLE_MS = 250;
6636
+ }
6637
+ });
6638
+
6394
6639
  // src/config.ts
6395
6640
  var config_exports = {};
6396
6641
  __export(config_exports, {
@@ -6500,6 +6745,9 @@ function listPanes() {
6500
6745
  function registerSessionHooks(h) {
6501
6746
  hooks = h;
6502
6747
  }
6748
+ function registerCustomAgentSource(fn) {
6749
+ customAgentSource = fn;
6750
+ }
6503
6751
  function sessionKind(o, projectId, cwd) {
6504
6752
  if (o.specId) return "spec";
6505
6753
  if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
@@ -6521,11 +6769,14 @@ function createSession(projectId, cwd, opts = {}) {
6521
6769
  const existing = getSession(id);
6522
6770
  if (existing && !existing.exited) return existing;
6523
6771
  if (existing) killSession(id);
6772
+ const agentForDefs = opts.agent ?? "claude";
6773
+ const customDefs = opts.command ? [] : customAgentsFor(projectId);
6774
+ const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : "";
6524
6775
  let promptArg = "";
6525
6776
  if (!opts.command && opts.prompt) {
6526
6777
  const promptFile = promptFilePath(id);
6527
6778
  mkdirSync3(dirname4(promptFile), { recursive: true });
6528
- writeFileSync(promptFile, opts.prompt);
6779
+ writeFileSync(promptFile, opts.prompt + rosterBlock);
6529
6780
  promptArg = `"$(cat ${sq(promptFile)})"`;
6530
6781
  }
6531
6782
  const agent = opts.agent ?? "claude";
@@ -6547,6 +6798,15 @@ function createSession(projectId, cwd, opts = {}) {
6547
6798
  }), { mode: 493 });
6548
6799
  }
6549
6800
  const effort = agent === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort;
6801
+ let agentsFile;
6802
+ if (agent === "claude") {
6803
+ const json = renderAgentsJson(customDefs);
6804
+ if (json) {
6805
+ agentsFile = agentsFilePath(id);
6806
+ mkdirSync3(dirname4(agentsFile), { recursive: true });
6807
+ writeFileSync(agentsFile, json);
6808
+ }
6809
+ }
6550
6810
  const flags = agentFlags({
6551
6811
  agent,
6552
6812
  model: opts.model,
@@ -6555,7 +6815,8 @@ function createSession(projectId, cwd, opts = {}) {
6555
6815
  goal: opts.goal,
6556
6816
  goalGate
6557
6817
  }).map(sq).join(" ");
6558
- argv = [sq(agentBin(agent)), promptArg, flags].filter(Boolean).join(" ");
6818
+ const agentsArg = agentsFile ? `--agents "$(cat ${sq(agentsFile)})"` : "";
6819
+ argv = [sq(agentBin(agent)), promptArg, flags, agentsArg].filter(Boolean).join(" ");
6559
6820
  }
6560
6821
  const envPairs = [];
6561
6822
  if (!opts.command && agent === "claude") {
@@ -6660,11 +6921,23 @@ async function sendToPane(id, text, chunkMs = 50) {
6660
6921
  const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
6661
6922
  if (!line) return false;
6662
6923
  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);
6663
6936
  for (const chunk of goalChunks(line)) {
6664
- tmux("send-keys", "-t", name(id), "-l", chunk);
6937
+ io.literal(chunk);
6665
6938
  await sleep(chunkMs);
6666
6939
  }
6667
- tmux("send-keys", "-t", name(id), "Enter");
6940
+ io.enter();
6668
6941
  return true;
6669
6942
  } catch {
6670
6943
  return false;
@@ -6758,7 +7031,7 @@ function end(id, code) {
6758
7031
  function pollPhases(p, a) {
6759
7032
  if (!p.flow || !p.phaseFile) return;
6760
7033
  const phases = readPhases(p.phaseFile, p.flow);
6761
- const complete = sessionComplete(phases, p.cwd, p.specId);
7034
+ const complete = paneComplete(p);
6762
7035
  const json = phaseKey(phases, complete);
6763
7036
  if (json === a.lastPhases) return;
6764
7037
  a.lastPhases = json;
@@ -6805,7 +7078,7 @@ function attach(id, c) {
6805
7078
  if (a.scrollback) c.send(frame({ t: "data", d: a.scrollback }));
6806
7079
  if (p.flow && p.phaseFile) {
6807
7080
  const phases = readPhases(p.phaseFile, p.flow);
6808
- const complete = sessionComplete(phases, p.cwd, p.specId);
7081
+ const complete = paneComplete(p);
6809
7082
  a.lastPhases = phaseKey(phases, complete);
6810
7083
  c.send(frame({ t: "phase", phases, complete }));
6811
7084
  }
@@ -6844,13 +7117,14 @@ function spawnPty(...args) {
6844
7117
  );
6845
7118
  }
6846
7119
  }
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;
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;
6848
7121
  var init_pty = __esm({
6849
7122
  "src/services/pty.ts"() {
6850
7123
  "use strict";
6851
7124
  init_src2();
6852
7125
  init_src();
6853
7126
  init_session_phases();
7127
+ init_tui_dialog();
6854
7128
  init_config2();
6855
7129
  socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
6856
7130
  PREFIX = "hanoman-";
@@ -6874,6 +7148,7 @@ var init_pty = __esm({
6874
7148
  promptFilePath = (id) => `${tmpdir()}/hanoman-prompts/${id}`;
6875
7149
  goalGatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.sh`;
6876
7150
  goalStatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.count`;
7151
+ agentsFilePath = (id) => `${tmpdir()}/hanoman-agents/${id}.json`;
6877
7152
  NO_SERVER = /no server running|error connecting to/i;
6878
7153
  TmuxError = class extends Error {
6879
7154
  constructor(message, noServer) {
@@ -6926,7 +7201,17 @@ var init_pty = __esm({
6926
7201
  } catch {
6927
7202
  }
6928
7203
  };
6929
- sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7204
+ customAgentSource = () => [];
7205
+ customAgentsFor = (projectId) => {
7206
+ try {
7207
+ return customAgentSource(projectId);
7208
+ } catch {
7209
+ return [];
7210
+ }
7211
+ };
7212
+ sleep = (ms) => new Promise((r) => {
7213
+ setTimeout(r, ms);
7214
+ });
6930
7215
  paneText = (id) => {
6931
7216
  try {
6932
7217
  return tmux("capture-pane", "-p", "-t", name(id));
@@ -6934,6 +7219,7 @@ var init_pty = __esm({
6934
7219
  return "";
6935
7220
  }
6936
7221
  };
7222
+ DIALOG_CAPTURE_LINES = 60;
6937
7223
  GOAL_ARMED_MARKERS = {
6938
7224
  claude: ["/goal"],
6939
7225
  codex: ["Goal active", "Pursuing goal", "Goal achieved"]
@@ -6943,6 +7229,11 @@ var init_pty = __esm({
6943
7229
  return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
6944
7230
  };
6945
7231
  phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
7232
+ paneComplete = (p) => !!p.flow && !!p.phaseFile && sessionComplete(readPhases(p.phaseFile, p.flow), p.cwd, p.specId);
7233
+ sessionFinished = (id) => {
7234
+ const p = getSession(id);
7235
+ return !!p && paneComplete(p);
7236
+ };
6946
7237
  detach = (id, c) => {
6947
7238
  attached.get(id)?.clients.delete(c);
6948
7239
  };
@@ -7174,14 +7465,15 @@ var init_sync = __esm({
7174
7465
  "use strict";
7175
7466
  init_db();
7176
7467
  init_rename_project();
7177
- SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment"];
7468
+ SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent"];
7178
7469
  DELEGATE = {
7179
7470
  project: prisma.project,
7180
7471
  spec: prisma.spec,
7181
7472
  vps: prisma.vps,
7182
7473
  sessionResult: prisma.sessionResult,
7183
7474
  ticket: prisma.ticket,
7184
- ticketAttachment: prisma.ticketAttachment
7475
+ ticketAttachment: prisma.ticketAttachment,
7476
+ customAgent: prisma.customAgent
7185
7477
  };
7186
7478
  FIELDS = {
7187
7479
  project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
@@ -7196,7 +7488,12 @@ var init_sync = __esm({
7196
7488
  // (kolom required @unique tanpa default); kunci plaintext tak pernah menyeberang.
7197
7489
  ticket: ["projectId", "number", "category", "title", "detail", "reporterEmail", "status", "accessKeyHash", "specId", "createdAt", "updatedAt"],
7198
7490
  // 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"]
7491
+ ticketAttachment: ["ticketId", "projectId", "filename", "mimeType", "size", "storageKey", "createdAt", "updatedAt"],
7492
+ // SPEC-450 · ADR-0094 · SELURUH kolom bermakna ikut menyeberang. `enabled` & `mentions` wajib
7493
+ // ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
7494
+ // mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
7495
+ // `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
7496
+ customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"]
7200
7497
  };
7201
7498
  DATE_FIELDS = {
7202
7499
  project: ["updatedAt"],
@@ -7204,7 +7501,8 @@ var init_sync = __esm({
7204
7501
  vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
7205
7502
  sessionResult: ["createdAt", "updatedAt"],
7206
7503
  ticket: ["createdAt", "updatedAt"],
7207
- ticketAttachment: ["createdAt", "updatedAt"]
7504
+ ticketAttachment: ["createdAt", "updatedAt"],
7505
+ customAgent: ["createdAt", "updatedAt"]
7208
7506
  };
7209
7507
  __FIELDS_FOR_TEST = FIELDS;
7210
7508
  }
@@ -22573,12 +22871,11 @@ async function integrateMain(row, deps) {
22573
22871
  if (!repoDir) return { ok: false, detail: `project ${row.projectId} belum di-bind ke checkout lokal` };
22574
22872
  const spec = await prisma.spec.findUnique({ where: { id: row.specId } });
22575
22873
  if (!spec) return { ok: false, detail: `backlog ${row.specId} tak ada` };
22874
+ const sid = sessionIdForSpec(spec.id);
22875
+ const done = deps.planDone(worktreeDir(repoDir, spec.id), spec.id);
22576
22876
  const evidence = [];
22577
22877
  if (cfg.requireGreenBeforeIntegrate) {
22578
- const wt = worktreeDir(repoDir, spec.id);
22579
- const done = deps.planDone(wt, spec.id);
22580
22878
  evidence.push(done ? "plan tak menyisakan `- [ ]`" : "plan MASIH menyisakan `- [ ]`");
22581
- const sid = sessionIdForSpec(spec.id);
22582
22879
  const pane = deps.sessionExists(sid);
22583
22880
  evidence.push(pane ? `pane ${sid} masih ada` : `pane ${sid} sudah tak ada`);
22584
22881
  if (!done) {
@@ -22597,6 +22894,9 @@ async function integrateMain(row, deps) {
22597
22894
  }
22598
22895
  const res = await deps.integrate(repoDir, spec.id, "merge", "local:main");
22599
22896
  evidence.push(`integrate \u2192 ${res.status}`);
22897
+ if (res.status === "clean" && done && deps.sessionExists(sid)) {
22898
+ evidence.push(deps.killSession(sid) ? `pane ${sid} dilepas (worktree utuh)` : `pane ${sid} gagal dilepas`);
22899
+ }
22600
22900
  await recordEvidence(row, evidence, res.status === "clean" ? "integrasi bersih" : `integrasi tak bersih: ${res.status}`);
22601
22901
  if (res.status !== "clean") {
22602
22902
  await deps.notify(
@@ -22621,8 +22921,10 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
22621
22921
 
22622
22922
  // src/services/lead/detect.ts
22623
22923
  init_pty();
22924
+ import { writeFileSync as writeFileSync4 } from "node:fs";
22624
22925
 
22625
22926
  // src/services/lead/pane.ts
22927
+ init_tui_dialog();
22626
22928
  var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
22627
22929
  var tail = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
22628
22930
  var CODEX_FINISHED = [
@@ -22639,15 +22941,16 @@ var ASK_SIGNALS = [
22639
22941
  function readPaneQuestion(text, agent) {
22640
22942
  const lines2 = tail(text, 40);
22641
22943
  const body = lines2.join("\n").trim();
22642
- if (!body) return { asking: false, question: "", reason: "layar kosong" };
22944
+ const choices = readChoiceDialog(text)?.options ?? [];
22945
+ if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
22643
22946
  const question = tail(text, 25).join("\n").trim();
22644
22947
  if (agent === "codex") {
22645
22948
  const finished = CODEX_FINISHED.find((re) => re.test(body));
22646
- if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)" };
22949
+ if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
22647
22950
  if (!ASK_SIGNALS.some((re) => re.test(body)))
22648
- return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex" };
22951
+ return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex", choices };
22649
22952
  }
22650
- return { asking: true, question, reason: "" };
22953
+ return { asking: true, question, reason: "", choices };
22651
22954
  }
22652
22955
 
22653
22956
  // src/services/lead/detect.ts
@@ -22685,6 +22988,12 @@ var prodDetectDeps = {
22685
22988
  }
22686
22989
  },
22687
22990
  send: (id, text) => sendToPane(id, text),
22991
+ clearMarker: (file) => {
22992
+ try {
22993
+ writeFileSync4(file, "");
22994
+ } catch {
22995
+ }
22996
+ },
22688
22997
  decide,
22689
22998
  decideDeps: prodDecideDeps,
22690
22999
  optIn: leadProjects,
@@ -22747,6 +23056,10 @@ async function scanAndAnswer(deps = prodDetectDeps) {
22747
23056
  skip(read.reason);
22748
23057
  continue;
22749
23058
  }
23059
+ 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
+ if (read.choices.length) {
23061
+ 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
+ }
22750
23063
  const row = await deps.decide({
22751
23064
  projectId: s.projectId,
22752
23065
  specId: s.specId,
@@ -22754,7 +23067,8 @@ async function scanAndAnswer(deps = prodDetectDeps) {
22754
23067
  gate: "detected",
22755
23068
  kind: "answer",
22756
23069
  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\`).`]
23070
+ options: read.choices.length ? read.choices : void 0,
23071
+ notes
22758
23072
  }, deps.decideDeps);
22759
23073
  if (!row || row.status !== "berlaku") {
22760
23074
  skip("lead tak menghasilkan keputusan yang berlaku");
@@ -22766,6 +23080,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
22766
23080
  skip("gagal mengetik ke pane");
22767
23081
  continue;
22768
23082
  }
23083
+ deps.clearMarker(s.decisionFile);
22769
23084
  answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
22770
23085
  out3.answered.push(s.id);
22771
23086
  }
@@ -22818,6 +23133,13 @@ var prodPulseDeps = {
22818
23133
  }
22819
23134
  },
22820
23135
  planDone: planComplete,
23136
+ finished: (id) => {
23137
+ try {
23138
+ return sessionFinished(id);
23139
+ } catch {
23140
+ return false;
23141
+ }
23142
+ },
22821
23143
  decide,
22822
23144
  decideDeps: prodDecideDeps,
22823
23145
  apply: applyAction,
@@ -22838,6 +23160,10 @@ async function pulse(deps = prodPulseDeps) {
22838
23160
  res.quality = await followUpFinished(cfg, optIn, deps);
22839
23161
  } catch {
22840
23162
  }
23163
+ try {
23164
+ res.quality += await followUpComplete(optIn, deps);
23165
+ } catch {
23166
+ }
22841
23167
  try {
22842
23168
  res.collisions = await detectCollisions(optIn, deps);
22843
23169
  } catch {
@@ -22890,6 +23216,49 @@ async function followUpFinished(cfg, optIn, deps) {
22890
23216
  }
22891
23217
  return n;
22892
23218
  }
23219
+ async function followUpComplete(optIn, deps) {
23220
+ let n = 0;
23221
+ const opt = new Set(optIn);
23222
+ for (const s of deps.sessions()) {
23223
+ if (!s.specId || !opt.has(s.projectId)) continue;
23224
+ if ((s.exitCode ?? 0) !== 0) continue;
23225
+ if (!deps.finished(s.id)) continue;
23226
+ const mark = `Backlog ${s.specId} sudah selesai di sesi ${s.id}`;
23227
+ const seen = await prisma.leadDecision.findFirst({
23228
+ where: { sessionId: s.id, gate: "pulse", question: { startsWith: mark } }
23229
+ });
23230
+ if (seen) continue;
23231
+ const row = await deps.decide({
23232
+ projectId: s.projectId,
23233
+ specId: s.specId,
23234
+ sessionId: s.id,
23235
+ gate: "pulse",
23236
+ kind: "quality",
23237
+ 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?`,
23238
+ options: [
23239
+ "integrate-main \u2014 merge branch sesi ini ke main; panenya ikut dilepas, worktree tetap utuh",
23240
+ "stop-session \u2014 lepas panenya tanpa mengintegrasikan (worktree tetap utuh, ADR-0084 masih bisa melanjutkan)",
23241
+ "none \u2014 biarkan sesinya berdiri, sertakan alasan kenapa slot itu layak ditahan"
23242
+ ],
23243
+ notes: [
23244
+ `Worktree sesi: ${s.cwd}`,
23245
+ // Rebase sengaja tak ditawarkan: `LEAD_ACTIONS` adalah konstanta tertutup (AC-31), dan
23246
+ // merge adalah yang PALING MUDAH DIBATALKAN dari keduanya — kriteria yang diperintahkan
23247
+ // prompt lead sendiri. Rebase tetap tindakan operator lewat POST /specs/:id/integrate.
23248
+ "Rebase tidak tersedia untukmu; bila hasilnya menuntut rebase, pilih `none` dan katakan begitu."
23249
+ ]
23250
+ }, deps.decideDeps);
23251
+ if (!row) continue;
23252
+ n++;
23253
+ if (row.status === "berlaku" && row.action !== "none") {
23254
+ try {
23255
+ await deps.apply(row);
23256
+ } catch {
23257
+ }
23258
+ }
23259
+ }
23260
+ return n;
23261
+ }
22893
23262
  async function detectCollisions(optIn, deps) {
22894
23263
  const opt = new Set(optIn);
22895
23264
  const live = deps.sessions().filter((s) => !s.exited && s.specId && opt.has(s.projectId));
@@ -23162,6 +23531,190 @@ async function lead_default(app2) {
23162
23531
  });
23163
23532
  }
23164
23533
 
23534
+ // src/routes/custom-agents.ts
23535
+ init_src();
23536
+ init_db();
23537
+
23538
+ // src/services/custom-agents.ts
23539
+ init_db();
23540
+ init_src();
23541
+ init_pty();
23542
+ var cache4 = [];
23543
+ var asCustomAgent = (r) => ({
23544
+ id: r.id,
23545
+ projectId: r.projectId,
23546
+ name: r.name,
23547
+ description: r.description,
23548
+ instructions: r.instructions,
23549
+ tools: toolsOf(r.tools),
23550
+ model: r.model,
23551
+ mentions: mentionsOf(r.mentions),
23552
+ enabled: r.enabled,
23553
+ createdAt: "",
23554
+ updatedAt: ""
23555
+ // tak dipakai lapis ini
23556
+ });
23557
+ async function loadCustomAgents() {
23558
+ try {
23559
+ cache4 = await prisma.customAgent.findMany();
23560
+ } catch {
23561
+ cache4 = [];
23562
+ }
23563
+ }
23564
+ function agentDefsFor(projectId) {
23565
+ const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
23566
+ const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
23567
+ return effectiveAgents(globals, project).map((a) => ({
23568
+ name: a.name,
23569
+ description: a.description,
23570
+ instructions: a.instructions,
23571
+ tools: a.tools,
23572
+ model: a.model,
23573
+ mentions: a.mentions ?? []
23574
+ }));
23575
+ }
23576
+ function validateGraph(rows) {
23577
+ const projectScopes = [...new Set(rows.map((r) => r.projectId).filter((p) => p !== null))];
23578
+ const globals = rows.filter((r) => r.projectId === null).map(asCustomAgent);
23579
+ for (const scope of [null, ...projectScopes]) {
23580
+ const project = scope === null ? [] : rows.filter((r) => r.projectId === scope).map(asCustomAgent);
23581
+ const nodes = effectiveAgents(globals, project).map((a) => ({ name: a.name, mentions: a.mentions ?? [] }));
23582
+ const cycle = detectCycle(nodes);
23583
+ if (cycle) return { scope: scope ?? GLOBAL_SCOPE, cycle };
23584
+ }
23585
+ return null;
23586
+ }
23587
+ function unknownMentions(row, all) {
23588
+ const visible = new Set(
23589
+ all.filter((r) => r.projectId === null || row.projectId !== null && r.projectId === row.projectId).map((r) => r.name)
23590
+ );
23591
+ return mentionsOf(row.mentions).filter((m) => !visible.has(m));
23592
+ }
23593
+ async function installCustomAgents() {
23594
+ await loadCustomAgents();
23595
+ registerCustomAgentSource((projectId) => agentDefsFor(projectId));
23596
+ }
23597
+
23598
+ // src/routes/custom-agents.ts
23599
+ var rowsOf = async () => await prisma.customAgent.findMany();
23600
+ var view6 = (r, projectId) => ({
23601
+ id: r.id,
23602
+ projectId: r.projectId,
23603
+ name: r.name,
23604
+ description: r.description,
23605
+ instructions: r.instructions,
23606
+ tools: toolsOf(r.tools),
23607
+ model: r.model,
23608
+ mentions: mentionsOf(r.mentions),
23609
+ enabled: r.enabled,
23610
+ ...projectId ? { inherited: r.projectId === null } : {}
23611
+ });
23612
+ async function custom_agents_default(app2) {
23613
+ app2.get("/custom-agents", async (req) => {
23614
+ const projectId = req.query.projectId;
23615
+ const rows = await prisma.customAgent.findMany({
23616
+ where: projectId ? { OR: [{ projectId: null }, { projectId }] } : { projectId: null },
23617
+ orderBy: { name: "asc" }
23618
+ });
23619
+ const byName = /* @__PURE__ */ new Map();
23620
+ for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
23621
+ for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
23622
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view6(r, projectId));
23623
+ });
23624
+ app2.post("/custom-agents", async (req, reply) => {
23625
+ const parsed = zCreateCustomAgent.safeParse(req.body);
23626
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
23627
+ const p = parsed.data;
23628
+ const projectId = p.projectId ?? null;
23629
+ if (projectId && !await prisma.project.findUnique({ where: { id: projectId } }))
23630
+ return reply.code(400).send({ error: "project tak ditemukan", projectId });
23631
+ const id = customAgentId(projectId, p.name);
23632
+ if (await prisma.customAgent.findUnique({ where: { id } }))
23633
+ return reply.code(409).send({ error: "nama sudah dipakai di scope ini", id });
23634
+ const candidate = {
23635
+ id,
23636
+ projectId,
23637
+ name: p.name,
23638
+ description: p.description,
23639
+ instructions: p.instructions,
23640
+ tools: p.tools ?? null,
23641
+ model: p.model ?? null,
23642
+ mentions: p.mentions ?? [],
23643
+ enabled: p.enabled ?? true
23644
+ };
23645
+ const all = [...await rowsOf(), candidate];
23646
+ const unknown = unknownMentions(candidate, all);
23647
+ if (unknown.length) return reply.code(400).send({ error: "mention tak dikenal", unknown });
23648
+ const cycle = validateGraph(all);
23649
+ if (cycle) return reply.code(409).send({ error: "mention membentuk siklus", ...cycle });
23650
+ const row = await prisma.customAgent.create({ data: {
23651
+ id,
23652
+ projectId,
23653
+ name: p.name,
23654
+ description: p.description,
23655
+ instructions: p.instructions,
23656
+ tools: candidate.tools,
23657
+ model: candidate.model,
23658
+ mentions: candidate.mentions,
23659
+ enabled: candidate.enabled
23660
+ } });
23661
+ await loadCustomAgents();
23662
+ await notifySynced("customAgent", id);
23663
+ return reply.code(201).send(view6(row));
23664
+ });
23665
+ app2.patch("/custom-agents/:id", async (req, reply) => {
23666
+ const { id } = req.params;
23667
+ const body = req.body ?? {};
23668
+ if ("name" in body || "projectId" in body)
23669
+ return reply.code(400).send({ error: "name & projectId tak bisa diubah \u2014 hapus lalu buat baru" });
23670
+ const parsed = zUpdateCustomAgent.safeParse(body);
23671
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
23672
+ const existing = await prisma.customAgent.findUnique({ where: { id } });
23673
+ if (!existing) return reply.code(404).send({ error: "not found" });
23674
+ const before = existing;
23675
+ const candidate = {
23676
+ ...before,
23677
+ ...parsed.data,
23678
+ mentions: parsed.data.mentions ?? mentionsOf(before.mentions),
23679
+ tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before.tools)
23680
+ };
23681
+ const all = (await rowsOf()).map((r) => r.id === id ? candidate : r);
23682
+ const unknown = unknownMentions(candidate, all);
23683
+ if (unknown.length) return reply.code(400).send({ error: "mention tak dikenal", unknown });
23684
+ const cycle = validateGraph(all);
23685
+ if (cycle) return reply.code(409).send({ error: "mention membentuk siklus", ...cycle });
23686
+ const row = await prisma.customAgent.update({ where: { id }, data: {
23687
+ description: candidate.description,
23688
+ instructions: candidate.instructions,
23689
+ tools: candidate.tools,
23690
+ model: candidate.model,
23691
+ mentions: candidate.mentions,
23692
+ enabled: candidate.enabled
23693
+ } });
23694
+ await loadCustomAgents();
23695
+ await notifySynced("customAgent", id);
23696
+ return view6(row);
23697
+ });
23698
+ app2.delete("/custom-agents/:id", async (req, reply) => {
23699
+ const { id } = req.params;
23700
+ const existing = await prisma.customAgent.findUnique({ where: { id } });
23701
+ if (!existing) return reply.code(404).send({ error: "not found" });
23702
+ await prisma.customAgent.delete({ where: { id } });
23703
+ const name2 = existing.name;
23704
+ for (const r of await rowsOf()) {
23705
+ const m = mentionsOf(r.mentions);
23706
+ if (!m.includes(name2)) continue;
23707
+ await prisma.customAgent.update({
23708
+ where: { id: r.id },
23709
+ data: { mentions: m.filter((x) => x !== name2) }
23710
+ });
23711
+ await notifySynced("customAgent", r.id);
23712
+ }
23713
+ await loadCustomAgents();
23714
+ return reply.code(204).send();
23715
+ });
23716
+ }
23717
+
23165
23718
  // src/app.ts
23166
23719
  var import_multipart = __toESM(require_multipart2(), 1);
23167
23720
 
@@ -23243,7 +23796,7 @@ function cookieOpts() {
23243
23796
  }
23244
23797
 
23245
23798
  // src/routes/auth.ts
23246
- var view6 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
23799
+ var view7 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
23247
23800
  async function issue(reply, userId) {
23248
23801
  const token = await createSession2(userId);
23249
23802
  reply.setCookie(COOKIE_NAME, token, cookieOpts());
@@ -23261,7 +23814,7 @@ async function auth_default(app2) {
23261
23814
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
23262
23815
  });
23263
23816
  await issue(reply, user.id);
23264
- return { user: view6(user) };
23817
+ return { user: view7(user) };
23265
23818
  });
23266
23819
  app2.post("/auth/login", async (req, reply) => {
23267
23820
  if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
@@ -23274,7 +23827,7 @@ async function auth_default(app2) {
23274
23827
  }
23275
23828
  clearLoginFails(req.ip);
23276
23829
  await issue(reply, user.id);
23277
- return { user: view6(user) };
23830
+ return { user: view7(user) };
23278
23831
  });
23279
23832
  app2.post("/auth/logout", async (req, reply) => {
23280
23833
  const token = req.cookies?.[COOKIE_NAME];
@@ -23282,7 +23835,7 @@ async function auth_default(app2) {
23282
23835
  reply.clearCookie(COOKIE_NAME, { path: "/" });
23283
23836
  return reply.code(204).send();
23284
23837
  });
23285
- app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view6));
23838
+ app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view7));
23286
23839
  app2.post("/auth/users", async (req, reply) => {
23287
23840
  const p = zSignup.safeParse(req.body);
23288
23841
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
@@ -23291,7 +23844,7 @@ async function auth_default(app2) {
23291
23844
  const user = await prisma.user.create({
23292
23845
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
23293
23846
  });
23294
- return view6(user);
23847
+ return view7(user);
23295
23848
  });
23296
23849
  app2.delete("/auth/users/:id", async (req, reply) => {
23297
23850
  if (await prisma.user.count() <= 1) return reply.code(400).send({ error: "tak bisa hapus user terakhir" });
@@ -23311,7 +23864,7 @@ async function auth_default(app2) {
23311
23864
  });
23312
23865
  await deleteUserSessions(user.id);
23313
23866
  await issue(reply, user.id);
23314
- return { user: view6(user) };
23867
+ return { user: view7(user) };
23315
23868
  });
23316
23869
  }
23317
23870
 
@@ -23390,15 +23943,15 @@ async function agent_tokens_default(app2) {
23390
23943
  app2.post("/agent-tokens", async (req, reply) => {
23391
23944
  const p = zAgentTokenCreate.safeParse(req.body);
23392
23945
  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 });
23946
+ const { view: view8, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
23947
+ return reply.code(201).send({ ...view8, token });
23395
23948
  });
23396
23949
  app2.patch("/agent-tokens/:id", async (req, reply) => {
23397
23950
  const p = zAgentTokenPatch.safeParse(req.body);
23398
23951
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
23399
23952
  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" });
23953
+ const view8 = await patchAgentToken(id, p.data);
23954
+ return view8 ? reply.send(view8) : reply.code(404).send({ error: "not found" });
23402
23955
  });
23403
23956
  app2.delete("/agent-tokens/:id", async (req, reply) => {
23404
23957
  const { id } = req.params;
@@ -23446,6 +23999,7 @@ function capabilityForRoute(method, path) {
23446
23999
  return read ? "GLOBAL_READ" : "COOKIE_ONLY";
23447
24000
  if (top === "scheduler") return rw("settings");
23448
24001
  if (top === "lead") return rw("lead");
24002
+ if (top === "custom-agents") return rw("agents");
23449
24003
  if (top === "settings" || top === "config") return rw("settings");
23450
24004
  if (top === "specs") return rw("backlog");
23451
24005
  if (top === "notifications") return rw("notifications");
@@ -23552,6 +24106,7 @@ function buildApp({ requireAuth = true } = {}) {
23552
24106
  await api.register(scheduler_default);
23553
24107
  await api.register(codex);
23554
24108
  await api.register(lead_default);
24109
+ await api.register(custom_agents_default);
23555
24110
  }, { prefix: "/api" });
23556
24111
  if (process.env.NODE_ENV === "production") {
23557
24112
  const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
@@ -23837,8 +24392,9 @@ async function shutdown(sig) {
23837
24392
  }
23838
24393
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
23839
24394
  process.on("SIGINT", () => void shutdown("SIGINT"));
23840
- app.listen({ port, host }).then(() => {
24395
+ app.listen({ port, host }).then(async () => {
23841
24396
  console.log(`hanoman api ${host}:${port}`);
24397
+ await installCustomAgents();
23842
24398
  installSessionHistory();
23843
24399
  try {
23844
24400
  const liveIds = listSessions().map((s) => s.id);