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