hanoman 0.1.9 → 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 +73 -1
- package/dist/server.js +784 -64
- package/package.json +1 -1
- package/prisma/migrations/20260801170000_github_issue/migration.sql +31 -0
- package/prisma/schema.prisma +33 -0
- package/web/assets/{index-D5MU9hT6.js → index-CXJBr-3-.js} +1523 -1523
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -4606,6 +4606,47 @@ var init_custom_agent = __esm({
|
|
|
4606
4606
|
}
|
|
4607
4607
|
});
|
|
4608
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
|
+
|
|
4609
4650
|
// ../shared/src/lead.ts
|
|
4610
4651
|
function leadActionAllowed(action) {
|
|
4611
4652
|
return LEAD_ACTIONS.includes(action);
|
|
@@ -5243,6 +5284,14 @@ var init_api = __esm({
|
|
|
5243
5284
|
ticketUnlink: (id) => `${API}/tickets/${id}/unlink`,
|
|
5244
5285
|
// SPEC-271 · lepas tautan backlog
|
|
5245
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`,
|
|
5246
5295
|
// SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
|
|
5247
5296
|
// (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
|
|
5248
5297
|
download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
|
|
@@ -5450,6 +5499,28 @@ var init_config_registry = __esm({
|
|
|
5450
5499
|
// vps
|
|
5451
5500
|
{ key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", help: "Default ~/.hanoman." },
|
|
5452
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
|
+
},
|
|
5453
5524
|
// runtime
|
|
5454
5525
|
{ key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", default: "1000", min: 100 },
|
|
5455
5526
|
{ key: "HANOMAN_UPDATE_FETCH", group: "runtime", label: "Deteksi update saat boot", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
@@ -5542,6 +5613,7 @@ var init_src = __esm({
|
|
|
5542
5613
|
init_entities();
|
|
5543
5614
|
init_agent();
|
|
5544
5615
|
init_custom_agent();
|
|
5616
|
+
init_github();
|
|
5545
5617
|
init_lead();
|
|
5546
5618
|
init_dto();
|
|
5547
5619
|
init_api();
|
|
@@ -6582,7 +6654,7 @@ function readChoiceDialog(paneText2) {
|
|
|
6582
6654
|
const found = [];
|
|
6583
6655
|
for (const line of lines2.slice(0, footer)) {
|
|
6584
6656
|
const m = ROW.exec(line);
|
|
6585
|
-
if (m) found.push({ n: Number(m[1]), label: (m[2] ?? "")
|
|
6657
|
+
if (m) found.push({ n: Number(m[1]), label: cleanLabel(m[2] ?? "") });
|
|
6586
6658
|
}
|
|
6587
6659
|
if (found.length < 2) return null;
|
|
6588
6660
|
const run2 = [];
|
|
@@ -6606,6 +6678,65 @@ function readChoiceDialog(paneText2) {
|
|
|
6606
6678
|
options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
|
|
6607
6679
|
};
|
|
6608
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
|
+
}
|
|
6609
6740
|
function freeTextFilled(paneText2, n) {
|
|
6610
6741
|
const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n);
|
|
6611
6742
|
if (!row) return false;
|
|
@@ -6623,7 +6754,37 @@ async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
|
|
|
6623
6754
|
io.enter();
|
|
6624
6755
|
return true;
|
|
6625
6756
|
}
|
|
6626
|
-
|
|
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;
|
|
6627
6788
|
var init_tui_dialog = __esm({
|
|
6628
6789
|
"src/services/tui-dialog.ts"() {
|
|
6629
6790
|
"use strict";
|
|
@@ -6632,7 +6793,21 @@ var init_tui_dialog = __esm({
|
|
|
6632
6793
|
ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+(\S.*)$/;
|
|
6633
6794
|
PLACEHOLDER = /^(?:type something\.?|other)$/i;
|
|
6634
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─━╌╍_=]*$/;
|
|
6635
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;
|
|
6636
6811
|
}
|
|
6637
6812
|
});
|
|
6638
6813
|
|
|
@@ -6921,18 +7096,13 @@ async function sendToPane(id, text, chunkMs = 50) {
|
|
|
6921
7096
|
const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
6922
7097
|
if (!line) return false;
|
|
6923
7098
|
try {
|
|
6924
|
-
const io =
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
},
|
|
6932
|
-
sleep
|
|
6933
|
-
};
|
|
6934
|
-
const free = readChoiceDialog(io.capture())?.freeIndex ?? null;
|
|
6935
|
-
if (free !== null) return await answerChoiceDialog(io, free, line, chunkMs);
|
|
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
|
+
}
|
|
6936
7106
|
for (const chunk of goalChunks(line)) {
|
|
6937
7107
|
io.literal(chunk);
|
|
6938
7108
|
await sleep(chunkMs);
|
|
@@ -6943,6 +7113,18 @@ async function sendToPane(id, text, chunkMs = 50) {
|
|
|
6943
7113
|
return false;
|
|
6944
7114
|
}
|
|
6945
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
|
+
}
|
|
6946
7128
|
async function armGoalInTui(id, condition, o = {}) {
|
|
6947
7129
|
const pollMs = o.pollMs ?? 500, readyTries = o.readyTries ?? 20;
|
|
6948
7130
|
const settleMs = o.settleMs ?? 1200, verifyTries = o.verifyTries ?? 12;
|
|
@@ -7117,7 +7299,7 @@ function spawnPty(...args) {
|
|
|
7117
7299
|
);
|
|
7118
7300
|
}
|
|
7119
7301
|
}
|
|
7120
|
-
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
|
|
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;
|
|
7121
7303
|
var init_pty = __esm({
|
|
7122
7304
|
"src/services/pty.ts"() {
|
|
7123
7305
|
"use strict";
|
|
@@ -7219,6 +7401,16 @@ var init_pty = __esm({
|
|
|
7219
7401
|
return "";
|
|
7220
7402
|
}
|
|
7221
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
|
+
});
|
|
7222
7414
|
DIALOG_CAPTURE_LINES = 60;
|
|
7223
7415
|
GOAL_ARMED_MARKERS = {
|
|
7224
7416
|
claude: ["/goal"],
|
|
@@ -7321,6 +7513,8 @@ var init_rename_project = __esm({
|
|
|
7321
7513
|
var sync_exports = {};
|
|
7322
7514
|
__export(sync_exports, {
|
|
7323
7515
|
SYNCED: () => SYNCED,
|
|
7516
|
+
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
7517
|
+
__FIELDS: () => __FIELDS,
|
|
7324
7518
|
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
7325
7519
|
applyPush: () => applyPush,
|
|
7326
7520
|
backfillFeed: () => backfillFeed,
|
|
@@ -7459,13 +7653,13 @@ async function upsertLocal(entity, id, version, data) {
|
|
|
7459
7653
|
function setAcceptedHook(hook) {
|
|
7460
7654
|
onAccepted = hook;
|
|
7461
7655
|
}
|
|
7462
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
7656
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, __FIELDS, __DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
7463
7657
|
var init_sync = __esm({
|
|
7464
7658
|
"src/services/sync.ts"() {
|
|
7465
7659
|
"use strict";
|
|
7466
7660
|
init_db();
|
|
7467
7661
|
init_rename_project();
|
|
7468
|
-
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent"];
|
|
7662
|
+
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue"];
|
|
7469
7663
|
DELEGATE = {
|
|
7470
7664
|
project: prisma.project,
|
|
7471
7665
|
spec: prisma.spec,
|
|
@@ -7473,7 +7667,8 @@ var init_sync = __esm({
|
|
|
7473
7667
|
sessionResult: prisma.sessionResult,
|
|
7474
7668
|
ticket: prisma.ticket,
|
|
7475
7669
|
ticketAttachment: prisma.ticketAttachment,
|
|
7476
|
-
customAgent: prisma.customAgent
|
|
7670
|
+
customAgent: prisma.customAgent,
|
|
7671
|
+
githubIssue: prisma.githubIssue
|
|
7477
7672
|
};
|
|
7478
7673
|
FIELDS = {
|
|
7479
7674
|
project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
|
|
@@ -7493,7 +7688,28 @@ var init_sync = __esm({
|
|
|
7493
7688
|
// ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
|
|
7494
7689
|
// mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
|
|
7495
7690
|
// `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
|
|
7496
|
-
customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"]
|
|
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
|
+
]
|
|
7497
7713
|
};
|
|
7498
7714
|
DATE_FIELDS = {
|
|
7499
7715
|
project: ["updatedAt"],
|
|
@@ -7502,8 +7718,11 @@ var init_sync = __esm({
|
|
|
7502
7718
|
sessionResult: ["createdAt", "updatedAt"],
|
|
7503
7719
|
ticket: ["createdAt", "updatedAt"],
|
|
7504
7720
|
ticketAttachment: ["createdAt", "updatedAt"],
|
|
7505
|
-
customAgent: ["createdAt", "updatedAt"]
|
|
7721
|
+
customAgent: ["createdAt", "updatedAt"],
|
|
7722
|
+
githubIssue: ["issueCreatedAt", "issueUpdatedAt", "pulledAt", "createdAt", "updatedAt"]
|
|
7506
7723
|
};
|
|
7724
|
+
__FIELDS = FIELDS;
|
|
7725
|
+
__DATE_FIELDS = DATE_FIELDS;
|
|
7507
7726
|
__FIELDS_FOR_TEST = FIELDS;
|
|
7508
7727
|
}
|
|
7509
7728
|
});
|
|
@@ -9670,7 +9889,7 @@ var require_extension = __commonJS({
|
|
|
9670
9889
|
if (dest[name2] === void 0) dest[name2] = [elem];
|
|
9671
9890
|
else dest[name2].push(elem);
|
|
9672
9891
|
}
|
|
9673
|
-
function
|
|
9892
|
+
function parse3(header) {
|
|
9674
9893
|
const offers = /* @__PURE__ */ Object.create(null);
|
|
9675
9894
|
let params = /* @__PURE__ */ Object.create(null);
|
|
9676
9895
|
let mustUnescape = false;
|
|
@@ -9810,7 +10029,7 @@ var require_extension = __commonJS({
|
|
|
9810
10029
|
}).join(", ");
|
|
9811
10030
|
}).join(", ");
|
|
9812
10031
|
}
|
|
9813
|
-
module.exports = { format, parse:
|
|
10032
|
+
module.exports = { format, parse: parse3 };
|
|
9814
10033
|
}
|
|
9815
10034
|
});
|
|
9816
10035
|
|
|
@@ -9844,7 +10063,7 @@ var require_websocket = __commonJS({
|
|
|
9844
10063
|
var {
|
|
9845
10064
|
EventTarget: { addEventListener, removeEventListener }
|
|
9846
10065
|
} = require_event_target();
|
|
9847
|
-
var { format, parse:
|
|
10066
|
+
var { format, parse: parse3 } = require_extension();
|
|
9848
10067
|
var { toBuffer } = require_buffer_util();
|
|
9849
10068
|
var kAborted = Symbol("kAborted");
|
|
9850
10069
|
var protocolVersions = [8, 13];
|
|
@@ -10521,7 +10740,7 @@ var require_websocket = __commonJS({
|
|
|
10521
10740
|
}
|
|
10522
10741
|
let extensions;
|
|
10523
10742
|
try {
|
|
10524
|
-
extensions =
|
|
10743
|
+
extensions = parse3(secWebSocketExtensions);
|
|
10525
10744
|
} catch (err) {
|
|
10526
10745
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
10527
10746
|
abortHandshake(websocket2, socket2, message);
|
|
@@ -10813,7 +11032,7 @@ var require_subprotocol = __commonJS({
|
|
|
10813
11032
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
|
|
10814
11033
|
"use strict";
|
|
10815
11034
|
var { tokenChars } = require_validation();
|
|
10816
|
-
function
|
|
11035
|
+
function parse3(header) {
|
|
10817
11036
|
const protocols = /* @__PURE__ */ new Set();
|
|
10818
11037
|
let start = -1;
|
|
10819
11038
|
let end2 = -1;
|
|
@@ -10849,7 +11068,7 @@ var require_subprotocol = __commonJS({
|
|
|
10849
11068
|
protocols.add(protocol);
|
|
10850
11069
|
return protocols;
|
|
10851
11070
|
}
|
|
10852
|
-
module.exports = { parse:
|
|
11071
|
+
module.exports = { parse: parse3 };
|
|
10853
11072
|
}
|
|
10854
11073
|
});
|
|
10855
11074
|
|
|
@@ -14280,7 +14499,7 @@ var require_secure_json_parse = __commonJS({
|
|
|
14280
14499
|
}
|
|
14281
14500
|
return obj;
|
|
14282
14501
|
}
|
|
14283
|
-
function
|
|
14502
|
+
function parse3(text, reviver, options2) {
|
|
14284
14503
|
const stackTraceLimit = Error.stackTraceLimit;
|
|
14285
14504
|
Error.stackTraceLimit = 0;
|
|
14286
14505
|
try {
|
|
@@ -14300,9 +14519,9 @@ var require_secure_json_parse = __commonJS({
|
|
|
14300
14519
|
Error.stackTraceLimit = stackTraceLimit;
|
|
14301
14520
|
}
|
|
14302
14521
|
}
|
|
14303
|
-
module.exports =
|
|
14304
|
-
module.exports.default =
|
|
14305
|
-
module.exports.parse =
|
|
14522
|
+
module.exports = parse3;
|
|
14523
|
+
module.exports.default = parse3;
|
|
14524
|
+
module.exports.parse = parse3;
|
|
14306
14525
|
module.exports.safeParse = safeParse;
|
|
14307
14526
|
module.exports.scan = filter;
|
|
14308
14527
|
}
|
|
@@ -21665,11 +21884,11 @@ function clamp(text) {
|
|
|
21665
21884
|
const buf = Buffer.from(text, "utf8");
|
|
21666
21885
|
if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
|
|
21667
21886
|
const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
|
|
21668
|
-
let
|
|
21669
|
-
const nl =
|
|
21670
|
-
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);
|
|
21671
21890
|
return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
|
|
21672
|
-
${
|
|
21891
|
+
${tail3}`, truncated: true };
|
|
21673
21892
|
}
|
|
21674
21893
|
async function saveTranscript(text) {
|
|
21675
21894
|
if (!text.trim()) return { key: "", bytes: 0, truncated: false };
|
|
@@ -22629,6 +22848,18 @@ function leadArgv(o) {
|
|
|
22629
22848
|
];
|
|
22630
22849
|
}
|
|
22631
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
|
+
}
|
|
22632
22863
|
function think(prompt, o) {
|
|
22633
22864
|
const bin = binFor(o.agent);
|
|
22634
22865
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
@@ -22642,8 +22873,7 @@ function think(prompt, o) {
|
|
|
22642
22873
|
killSignal: "SIGTERM"
|
|
22643
22874
|
}, (err, stdout, stderr) => {
|
|
22644
22875
|
if (err) {
|
|
22645
|
-
|
|
22646
|
-
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)));
|
|
22647
22877
|
return;
|
|
22648
22878
|
}
|
|
22649
22879
|
resolve14(stdout);
|
|
@@ -22921,12 +23151,13 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
|
|
|
22921
23151
|
|
|
22922
23152
|
// src/services/lead/detect.ts
|
|
22923
23153
|
init_pty();
|
|
23154
|
+
init_tui_dialog();
|
|
22924
23155
|
import { writeFileSync as writeFileSync4 } from "node:fs";
|
|
22925
23156
|
|
|
22926
23157
|
// src/services/lead/pane.ts
|
|
22927
23158
|
init_tui_dialog();
|
|
22928
23159
|
var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
|
|
22929
|
-
var
|
|
23160
|
+
var tail2 = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
|
|
22930
23161
|
var CODEX_FINISHED = [
|
|
22931
23162
|
/Goal achieved/i,
|
|
22932
23163
|
/Goal unmet/i,
|
|
@@ -22939,11 +23170,11 @@ var ASK_SIGNALS = [
|
|
|
22939
23170
|
/\b(pilih|apakah|haruskah|mana yang|opsi|which|should i|do you want|proceed\?)\b/i
|
|
22940
23171
|
];
|
|
22941
23172
|
function readPaneQuestion(text, agent) {
|
|
22942
|
-
const lines2 =
|
|
23173
|
+
const lines2 = tail2(text, 40);
|
|
22943
23174
|
const body = lines2.join("\n").trim();
|
|
22944
23175
|
const choices = readChoiceDialog(text)?.options ?? [];
|
|
22945
23176
|
if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
|
|
22946
|
-
const question =
|
|
23177
|
+
const question = tail2(text, 25).join("\n").trim();
|
|
22947
23178
|
if (agent === "codex") {
|
|
22948
23179
|
const finished = CODEX_FINISHED.find((re) => re.test(body));
|
|
22949
23180
|
if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
|
|
@@ -22956,9 +23187,16 @@ function readPaneQuestion(text, agent) {
|
|
|
22956
23187
|
// src/services/lead/detect.ts
|
|
22957
23188
|
var answers = /* @__PURE__ */ new Map();
|
|
22958
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;
|
|
22959
23195
|
function resetSession(sessionId2) {
|
|
22960
23196
|
answers.delete(sessionId2);
|
|
22961
23197
|
capped.delete(sessionId2);
|
|
23198
|
+
failures.delete(sessionId2);
|
|
23199
|
+
failCapped.delete(sessionId2);
|
|
22962
23200
|
}
|
|
22963
23201
|
var prodDetectDeps = {
|
|
22964
23202
|
live: () => {
|
|
@@ -22994,6 +23232,8 @@ var prodDetectDeps = {
|
|
|
22994
23232
|
} catch {
|
|
22995
23233
|
}
|
|
22996
23234
|
},
|
|
23235
|
+
submit: (id) => submitPaneDialog(id),
|
|
23236
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
22997
23237
|
decide,
|
|
22998
23238
|
decideDeps: prodDecideDeps,
|
|
22999
23239
|
optIn: leadProjects,
|
|
@@ -23025,7 +23265,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23025
23265
|
if ((answers.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
|
|
23026
23266
|
if (!capped.has(s.id)) {
|
|
23027
23267
|
capped.add(s.id);
|
|
23028
|
-
const
|
|
23268
|
+
const row = await recordDecision({
|
|
23029
23269
|
projectId: s.projectId,
|
|
23030
23270
|
specId: s.specId,
|
|
23031
23271
|
sessionId: s.id,
|
|
@@ -23040,7 +23280,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23040
23280
|
weighty: true
|
|
23041
23281
|
});
|
|
23042
23282
|
await deps.notify(
|
|
23043
|
-
|
|
23283
|
+
row.id,
|
|
23044
23284
|
`Lead berhenti menjawab sesi ${s.id} (batas ${cfg.maxAutoAnswers}\xD7 tercapai)`,
|
|
23045
23285
|
s.projectId,
|
|
23046
23286
|
s.specId ?? null,
|
|
@@ -23050,16 +23290,79 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23050
23290
|
skip("batas jawaban otomatis tercapai");
|
|
23051
23291
|
continue;
|
|
23052
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
|
+
}
|
|
23053
23321
|
const agent = deps.agentOf(s.id) ?? "claude";
|
|
23054
23322
|
const read = readPaneQuestion(deps.pane(s.id), agent);
|
|
23055
23323
|
if (!read.asking) {
|
|
23056
23324
|
skip(read.reason);
|
|
23057
23325
|
continue;
|
|
23058
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
|
+
}
|
|
23059
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\`).`];
|
|
23060
23357
|
if (read.choices.length) {
|
|
23061
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.");
|
|
23062
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
|
+
}
|
|
23063
23366
|
const row = await deps.decide({
|
|
23064
23367
|
projectId: s.projectId,
|
|
23065
23368
|
specId: s.specId,
|
|
@@ -23070,26 +23373,32 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23070
23373
|
options: read.choices.length ? read.choices : void 0,
|
|
23071
23374
|
notes
|
|
23072
23375
|
}, deps.decideDeps);
|
|
23073
|
-
if (!row
|
|
23074
|
-
|
|
23075
|
-
|
|
23076
|
-
}
|
|
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" };
|
|
23077
23379
|
const reply = takeReply(row.id) || row.answer;
|
|
23078
|
-
|
|
23079
|
-
|
|
23080
|
-
|
|
23081
|
-
|
|
23082
|
-
|
|
23083
|
-
|
|
23084
|
-
answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
|
|
23085
|
-
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" };
|
|
23086
23386
|
}
|
|
23087
|
-
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;
|
|
23088
23395
|
}
|
|
23089
23396
|
function sweep(liveIds) {
|
|
23090
23397
|
const live = new Set(liveIds);
|
|
23091
23398
|
for (const id of [...answers.keys()]) if (!live.has(id)) answers.delete(id);
|
|
23092
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);
|
|
23093
23402
|
}
|
|
23094
23403
|
|
|
23095
23404
|
// src/services/lead/pulse.ts
|
|
@@ -23715,6 +24024,414 @@ async function custom_agents_default(app2) {
|
|
|
23715
24024
|
});
|
|
23716
24025
|
}
|
|
23717
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
|
+
|
|
23718
24435
|
// src/app.ts
|
|
23719
24436
|
var import_multipart = __toESM(require_multipart2(), 1);
|
|
23720
24437
|
|
|
@@ -23796,7 +24513,7 @@ function cookieOpts() {
|
|
|
23796
24513
|
}
|
|
23797
24514
|
|
|
23798
24515
|
// src/routes/auth.ts
|
|
23799
|
-
var
|
|
24516
|
+
var view8 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
|
|
23800
24517
|
async function issue(reply, userId) {
|
|
23801
24518
|
const token = await createSession2(userId);
|
|
23802
24519
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -23814,7 +24531,7 @@ async function auth_default(app2) {
|
|
|
23814
24531
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
23815
24532
|
});
|
|
23816
24533
|
await issue(reply, user.id);
|
|
23817
|
-
return { user:
|
|
24534
|
+
return { user: view8(user) };
|
|
23818
24535
|
});
|
|
23819
24536
|
app2.post("/auth/login", async (req, reply) => {
|
|
23820
24537
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
@@ -23827,7 +24544,7 @@ async function auth_default(app2) {
|
|
|
23827
24544
|
}
|
|
23828
24545
|
clearLoginFails(req.ip);
|
|
23829
24546
|
await issue(reply, user.id);
|
|
23830
|
-
return { user:
|
|
24547
|
+
return { user: view8(user) };
|
|
23831
24548
|
});
|
|
23832
24549
|
app2.post("/auth/logout", async (req, reply) => {
|
|
23833
24550
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -23835,7 +24552,7 @@ async function auth_default(app2) {
|
|
|
23835
24552
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
23836
24553
|
return reply.code(204).send();
|
|
23837
24554
|
});
|
|
23838
|
-
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));
|
|
23839
24556
|
app2.post("/auth/users", async (req, reply) => {
|
|
23840
24557
|
const p = zSignup.safeParse(req.body);
|
|
23841
24558
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -23844,7 +24561,7 @@ async function auth_default(app2) {
|
|
|
23844
24561
|
const user = await prisma.user.create({
|
|
23845
24562
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
23846
24563
|
});
|
|
23847
|
-
return
|
|
24564
|
+
return view8(user);
|
|
23848
24565
|
});
|
|
23849
24566
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
23850
24567
|
if (await prisma.user.count() <= 1) return reply.code(400).send({ error: "tak bisa hapus user terakhir" });
|
|
@@ -23864,7 +24581,7 @@ async function auth_default(app2) {
|
|
|
23864
24581
|
});
|
|
23865
24582
|
await deleteUserSessions(user.id);
|
|
23866
24583
|
await issue(reply, user.id);
|
|
23867
|
-
return { user:
|
|
24584
|
+
return { user: view8(user) };
|
|
23868
24585
|
});
|
|
23869
24586
|
}
|
|
23870
24587
|
|
|
@@ -23943,15 +24660,15 @@ async function agent_tokens_default(app2) {
|
|
|
23943
24660
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
23944
24661
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
23945
24662
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
23946
|
-
const { view:
|
|
23947
|
-
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 });
|
|
23948
24665
|
});
|
|
23949
24666
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
23950
24667
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
23951
24668
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
23952
24669
|
const { id } = req.params;
|
|
23953
|
-
const
|
|
23954
|
-
return
|
|
24670
|
+
const view9 = await patchAgentToken(id, p.data);
|
|
24671
|
+
return view9 ? reply.send(view9) : reply.code(404).send({ error: "not found" });
|
|
23955
24672
|
});
|
|
23956
24673
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
23957
24674
|
const { id } = req.params;
|
|
@@ -24004,6 +24721,7 @@ function capabilityForRoute(method, path) {
|
|
|
24004
24721
|
if (top === "specs") return rw("backlog");
|
|
24005
24722
|
if (top === "notifications") return rw("notifications");
|
|
24006
24723
|
if (top === "tickets") return rw("support");
|
|
24724
|
+
if (top === "github-issues") return rw("support");
|
|
24007
24725
|
if (top === "vps") return rw("vps");
|
|
24008
24726
|
if (top === "prds") return rw("docs");
|
|
24009
24727
|
if (top === "terminal") {
|
|
@@ -24013,6 +24731,7 @@ function capabilityForRoute(method, path) {
|
|
|
24013
24731
|
if (top === "projects") {
|
|
24014
24732
|
const sub = seg[2];
|
|
24015
24733
|
if (sub === "docs" || sub === "prds") return rw("docs");
|
|
24734
|
+
if (sub === "github") return rw("support");
|
|
24016
24735
|
if (sub && IDE_SUBS.has(sub)) return rw("ide");
|
|
24017
24736
|
return rw("projects");
|
|
24018
24737
|
}
|
|
@@ -24107,6 +24826,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24107
24826
|
await api.register(codex);
|
|
24108
24827
|
await api.register(lead_default);
|
|
24109
24828
|
await api.register(custom_agents_default);
|
|
24829
|
+
await api.register(githubIssues);
|
|
24110
24830
|
}, { prefix: "/api" });
|
|
24111
24831
|
if (process.env.NODE_ENV === "production") {
|
|
24112
24832
|
const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
|