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/build-info.json +3 -3
- package/dist/cli.js +138 -84
- package/dist/server.js +589 -33
- package/package.json +1 -1
- package/prisma/migrations/20260801120000_custom_agent/migration.sql +27 -0
- package/prisma/schema.prisma +37 -2
- package/web/assets/{index-D6AOUdRy.js → index-D5MU9hT6.js} +1527 -1527
- package/web/index.html +1 -1
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
|
-
|
|
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
|
-
|
|
6937
|
+
io.literal(chunk);
|
|
6665
6938
|
await sleep(chunkMs);
|
|
6666
6939
|
}
|
|
6667
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
23394
|
-
return reply.code(201).send({ ...
|
|
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
|
|
23401
|
-
return
|
|
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);
|