hanoman 0.1.12 → 0.1.13
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 +155 -13
- package/dist/server.js +898 -88
- package/package.json +1 -1
- package/prisma/migrations/20260801230000_auto_merge/migration.sql +8 -0
- package/prisma/migrations/20260801230000_custom_agent_runtime/migration.sql +8 -0
- package/prisma/migrations/20260801234500_lead_flow/migration.sql +36 -0
- package/prisma/schema.prisma +42 -0
- package/web/assets/{index-W4J6iiyI.js → index-DRwKLKdp.js} +1566 -1566
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -4269,6 +4269,39 @@ var init_telegram = __esm({
|
|
|
4269
4269
|
}
|
|
4270
4270
|
});
|
|
4271
4271
|
|
|
4272
|
+
// ../shared/src/auto-merge.ts
|
|
4273
|
+
function autoMergeOf(raw) {
|
|
4274
|
+
if (raw === null || raw === void 0) return null;
|
|
4275
|
+
const p = zAutoMerge.safeParse(raw);
|
|
4276
|
+
return p.success ? p.data : null;
|
|
4277
|
+
}
|
|
4278
|
+
function resolveAutoMerge(projectRaw, specRaw) {
|
|
4279
|
+
return autoMergeOf(specRaw) ?? autoMergeOf(projectRaw) ?? AUTO_MERGE_OFF;
|
|
4280
|
+
}
|
|
4281
|
+
function autoMergeTargetOf(p, defaultBranch2) {
|
|
4282
|
+
if (p.mode === "off") return null;
|
|
4283
|
+
const name2 = p.mode === "branch" ? p.branch : defaultBranch2;
|
|
4284
|
+
return name2 ? `${p.dest}:${name2}` : null;
|
|
4285
|
+
}
|
|
4286
|
+
var AUTO_MERGE_MODES, zAutoMerge, AUTO_MERGE_OFF;
|
|
4287
|
+
var init_auto_merge = __esm({
|
|
4288
|
+
"../shared/src/auto-merge.ts"() {
|
|
4289
|
+
"use strict";
|
|
4290
|
+
init_zod();
|
|
4291
|
+
AUTO_MERGE_MODES = ["off", "default-branch", "branch"];
|
|
4292
|
+
zAutoMerge = external_exports.object({
|
|
4293
|
+
mode: external_exports.enum(AUTO_MERGE_MODES).default("off"),
|
|
4294
|
+
// Tujuan: `local` = perbarui ref/fast-forward di checkout ini; `origin` = push.
|
|
4295
|
+
dest: external_exports.enum(["local", "origin"]).default("local"),
|
|
4296
|
+
// Hanya bermakna saat mode = "branch". Untuk "default-branch" ia diresolve SAAT EKSEKUSI.
|
|
4297
|
+
branch: external_exports.string().min(1).nullable().default(null),
|
|
4298
|
+
// Hapus `hanoman/<spec>` (lokal + origin) — HANYA sesudah merge terbukti bersih.
|
|
4299
|
+
deleteBranch: external_exports.boolean().default(false)
|
|
4300
|
+
});
|
|
4301
|
+
AUTO_MERGE_OFF = { mode: "off", dest: "local", branch: null, deleteBranch: false };
|
|
4302
|
+
}
|
|
4303
|
+
});
|
|
4304
|
+
|
|
4272
4305
|
// ../shared/src/entities.ts
|
|
4273
4306
|
function codexModel(id) {
|
|
4274
4307
|
return CODEX_MODELS.find((m) => m.id === id);
|
|
@@ -4287,13 +4320,14 @@ function cmpVersion(a, b) {
|
|
|
4287
4320
|
}
|
|
4288
4321
|
return 0;
|
|
4289
4322
|
}
|
|
4290
|
-
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4323
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, MODELS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4291
4324
|
var init_entities = __esm({
|
|
4292
4325
|
"../shared/src/entities.ts"() {
|
|
4293
4326
|
"use strict";
|
|
4294
4327
|
init_zod();
|
|
4295
4328
|
init_enums();
|
|
4296
4329
|
init_telegram();
|
|
4330
|
+
init_auto_merge();
|
|
4297
4331
|
zProject = external_exports.object({
|
|
4298
4332
|
id: external_exports.string(),
|
|
4299
4333
|
name: external_exports.string(),
|
|
@@ -4359,7 +4393,10 @@ var init_entities = __esm({
|
|
|
4359
4393
|
dependsOn: external_exports.array(external_exports.string()).default([]),
|
|
4360
4394
|
// Turunan (bukan kolom): dihitung `liveSpecs` dari stage dependency + git. Klien tak pernah
|
|
4361
4395
|
// mengirimkannya — `.default([])` supaya bentuk lama tetap parse.
|
|
4362
|
-
blockedBy: external_exports.array(zSpecBlocker).default([])
|
|
4396
|
+
blockedBy: external_exports.array(zSpecBlocker).default([]),
|
|
4397
|
+
// SPEC-486 · ADR-0103 · override kebijakan auto-merge item ini; null = warisi project.
|
|
4398
|
+
// `.nullable().default(null)` menjaga respons/klien versi lama tetap parse.
|
|
4399
|
+
autoMerge: zAutoMerge.nullable().default(null)
|
|
4363
4400
|
});
|
|
4364
4401
|
NOTIFY_SOUNDS = [
|
|
4365
4402
|
"off",
|
|
@@ -4377,6 +4414,12 @@ var init_entities = __esm({
|
|
|
4377
4414
|
"marimba",
|
|
4378
4415
|
"fanfare"
|
|
4379
4416
|
];
|
|
4417
|
+
MODELS = [
|
|
4418
|
+
{ id: "claude-opus-5", label: "Opus 5" },
|
|
4419
|
+
{ id: "claude-sonnet-5", label: "Sonnet 5" },
|
|
4420
|
+
{ id: "claude-haiku-4-5", label: "Haiku 4.5" },
|
|
4421
|
+
{ id: "claude-fable-5", label: "Fable 5" }
|
|
4422
|
+
];
|
|
4380
4423
|
E_5_6 = ["ultra", "max", "xhigh", "high", "medium", "low"];
|
|
4381
4424
|
E_LUNA = ["max", "xhigh", "high", "medium", "low"];
|
|
4382
4425
|
E_BASE = ["xhigh", "high", "medium", "low"];
|
|
@@ -4460,6 +4503,11 @@ var init_entities = __esm({
|
|
|
4460
4503
|
// `requestTimeout: 0` (terukur dari `buildApp()`) — tak ada pihak lain yang akan memutus peminta.
|
|
4461
4504
|
// 0 = tanpa antrean sama sekali: penuh berarti langsung ditolak.
|
|
4462
4505
|
queueWaitSec: external_exports.number().int().min(0).max(900).default(120),
|
|
4506
|
+
// SPEC-485 · ADR-0102 · umur maksimum satu RANTAI keputusan yang dibiarkan terbuka. Peminta bisa
|
|
4507
|
+
// mati di tengah rantai (sesi ditutup, agen crash); tanpa batas ini alurnya `sebagian` selamanya
|
|
4508
|
+
// dan tak ada yang tahu apakah ia masih ditunggu. Penyapunya menumpang tick lead yang sudah ada —
|
|
4509
|
+
// ADR-0024 melarang timer/scheduler baru.
|
|
4510
|
+
flowTtlMin: external_exports.number().int().min(1).max(1440).default(60),
|
|
4463
4511
|
// OQ-3 · syarat objektif sebelum lead boleh mengintegrasikan ke branch utama. Default MENYALA:
|
|
4464
4512
|
// risiko "kode masuk main tanpa mata manusia" diterima sadar, tapi syaratnya tetap terukur.
|
|
4465
4513
|
requireGreenBeforeIntegrate: external_exports.boolean().default(true),
|
|
@@ -4642,6 +4690,9 @@ function mentionsOf(v) {
|
|
|
4642
4690
|
for (const x of v) if (typeof x === "string" && x && !out3.includes(x)) out3.push(x);
|
|
4643
4691
|
return out3;
|
|
4644
4692
|
}
|
|
4693
|
+
function runtimeOf(v) {
|
|
4694
|
+
return typeof v === "string" && AGENT_RUNTIMES.includes(v) ? v : null;
|
|
4695
|
+
}
|
|
4645
4696
|
function toolsOf(v) {
|
|
4646
4697
|
if (!Array.isArray(v)) return null;
|
|
4647
4698
|
const out3 = [];
|
|
@@ -4685,11 +4736,17 @@ function effectiveAgents(globals, project) {
|
|
|
4685
4736
|
for (const a of project) byName.set(a.name, a);
|
|
4686
4737
|
return [...byName.values()].filter((a) => a.enabled).sort((x, y) => x.name.localeCompare(y.name));
|
|
4687
4738
|
}
|
|
4688
|
-
var AGENT_NAME_RE, DEFAULT_AGENT_TOOLS, MENTION_TOOL, MENTION_MAX_HOPS, GLOBAL_SCOPE, zCustomAgent, zCreateCustomAgent, zUpdateCustomAgent, customAgentId;
|
|
4739
|
+
var AGENT_RUNTIMES, zAgentRuntime, AGENT_RUNTIME_LABELS, AGENT_NAME_RE, DEFAULT_AGENT_TOOLS, MENTION_TOOL, MENTION_MAX_HOPS, GLOBAL_SCOPE, zCustomAgent, zCreateCustomAgent, zUpdateCustomAgent, customAgentId;
|
|
4689
4740
|
var init_custom_agent = __esm({
|
|
4690
4741
|
"../shared/src/custom-agent.ts"() {
|
|
4691
4742
|
"use strict";
|
|
4692
4743
|
init_zod();
|
|
4744
|
+
AGENT_RUNTIMES = ["claude", "codex"];
|
|
4745
|
+
zAgentRuntime = external_exports.enum(AGENT_RUNTIMES);
|
|
4746
|
+
AGENT_RUNTIME_LABELS = {
|
|
4747
|
+
claude: "Claude Code",
|
|
4748
|
+
codex: "Codex CLI"
|
|
4749
|
+
};
|
|
4693
4750
|
AGENT_NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
|
|
4694
4751
|
DEFAULT_AGENT_TOOLS = [
|
|
4695
4752
|
"Read",
|
|
@@ -4713,6 +4770,7 @@ var init_custom_agent = __esm({
|
|
|
4713
4770
|
tools: external_exports.array(external_exports.string()).nullable(),
|
|
4714
4771
|
model: external_exports.string().nullable(),
|
|
4715
4772
|
mentions: external_exports.array(external_exports.string()).nullable(),
|
|
4773
|
+
runtime: external_exports.enum(AGENT_RUNTIMES).nullable(),
|
|
4716
4774
|
enabled: external_exports.boolean(),
|
|
4717
4775
|
createdAt: external_exports.string(),
|
|
4718
4776
|
updatedAt: external_exports.string()
|
|
@@ -4725,6 +4783,8 @@ var init_custom_agent = __esm({
|
|
|
4725
4783
|
tools: external_exports.array(external_exports.string()).nullable().optional(),
|
|
4726
4784
|
model: external_exports.string().nullable().optional(),
|
|
4727
4785
|
mentions: external_exports.array(external_exports.string()).nullable().optional(),
|
|
4786
|
+
// SPEC-484 · ADR-0101 · PENYARING mesin sesi, bukan pemilih proses. null/absen = ikut sesi induk.
|
|
4787
|
+
runtime: external_exports.enum(AGENT_RUNTIMES).nullable().optional(),
|
|
4728
4788
|
enabled: external_exports.boolean().optional()
|
|
4729
4789
|
});
|
|
4730
4790
|
zUpdateCustomAgent = zCreateCustomAgent.omit({ name: true, projectId: true }).partial();
|
|
@@ -4732,6 +4792,44 @@ var init_custom_agent = __esm({
|
|
|
4732
4792
|
}
|
|
4733
4793
|
});
|
|
4734
4794
|
|
|
4795
|
+
// ../shared/src/agent-catalog.ts
|
|
4796
|
+
function modelsForRuntime(rt) {
|
|
4797
|
+
const claude = MODELS.map((m) => ({ id: m.id, label: m.label, runtime: "claude" }));
|
|
4798
|
+
const codex2 = CODEX_MODELS.map((m) => ({ id: m.id, label: m.label, runtime: "codex" }));
|
|
4799
|
+
if (rt === "claude") return claude;
|
|
4800
|
+
if (rt === "codex") return codex2;
|
|
4801
|
+
return [...claude, ...codex2];
|
|
4802
|
+
}
|
|
4803
|
+
function expandTools(tools, catalogIds) {
|
|
4804
|
+
if (tools === null) return null;
|
|
4805
|
+
if (!tools.includes(ALL_TOOLS)) return tools;
|
|
4806
|
+
return catalogIds.filter((id) => id !== ALL_TOOLS);
|
|
4807
|
+
}
|
|
4808
|
+
var ALL_TOOLS, ALL_TOOLS_ENTRY, BUILTIN_AGENT_TOOLS, mcpToolEntry;
|
|
4809
|
+
var init_agent_catalog = __esm({
|
|
4810
|
+
"../shared/src/agent-catalog.ts"() {
|
|
4811
|
+
"use strict";
|
|
4812
|
+
init_custom_agent();
|
|
4813
|
+
init_entities();
|
|
4814
|
+
ALL_TOOLS = "*";
|
|
4815
|
+
ALL_TOOLS_ENTRY = {
|
|
4816
|
+
id: ALL_TOOLS,
|
|
4817
|
+
label: "Semua tools",
|
|
4818
|
+
group: "shortcut"
|
|
4819
|
+
};
|
|
4820
|
+
BUILTIN_AGENT_TOOLS = DEFAULT_AGENT_TOOLS.map((id) => ({
|
|
4821
|
+
id,
|
|
4822
|
+
label: id,
|
|
4823
|
+
group: "builtin"
|
|
4824
|
+
}));
|
|
4825
|
+
mcpToolEntry = (server) => ({
|
|
4826
|
+
id: `mcp__${server}__*`,
|
|
4827
|
+
label: `${server} \u2014 semua tool`,
|
|
4828
|
+
group: "mcp"
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
});
|
|
4832
|
+
|
|
4735
4833
|
// ../shared/src/github.ts
|
|
4736
4834
|
function sourceForLabels(labels) {
|
|
4737
4835
|
const hay = labels.map((l) => l.toLowerCase());
|
|
@@ -4807,6 +4905,36 @@ function resolveChoice(raw, options2) {
|
|
|
4807
4905
|
if (byPrefix.length === 1) return pick2(byPrefix[0]);
|
|
4808
4906
|
return null;
|
|
4809
4907
|
}
|
|
4908
|
+
function normalizeSelect(sel, optionCount) {
|
|
4909
|
+
const n2 = Math.max(0, optionCount);
|
|
4910
|
+
if (sel.mode === "single") return { mode: "single", min: Math.min(sel.min, 1), max: 1 };
|
|
4911
|
+
const max = Math.min(sel.max ?? n2, n2);
|
|
4912
|
+
return { mode: "multi", min: Math.min(sel.min, max), max };
|
|
4913
|
+
}
|
|
4914
|
+
function resolveChoices(raw, options2) {
|
|
4915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4916
|
+
const choices = [];
|
|
4917
|
+
const rejected = [];
|
|
4918
|
+
for (const r of raw ?? []) {
|
|
4919
|
+
const t = (r ?? "").trim();
|
|
4920
|
+
if (!t) continue;
|
|
4921
|
+
const hit = resolveChoice(t, options2);
|
|
4922
|
+
if (!hit) {
|
|
4923
|
+
rejected.push(t);
|
|
4924
|
+
continue;
|
|
4925
|
+
}
|
|
4926
|
+
if (seen.has(hit.index)) continue;
|
|
4927
|
+
seen.add(hit.index);
|
|
4928
|
+
choices.push(hit);
|
|
4929
|
+
}
|
|
4930
|
+
choices.sort((a, b) => a.index - b.index);
|
|
4931
|
+
return { choices, rejected };
|
|
4932
|
+
}
|
|
4933
|
+
function checkChoiceCount(n2, b) {
|
|
4934
|
+
if (n2 > b.max) return `pilihan terlalu banyak (${n2}) \u2014 paling banyak ${b.max}`;
|
|
4935
|
+
if (n2 < b.min) return `pilihan terlalu sedikit (${n2}) \u2014 paling sedikit ${b.min}`;
|
|
4936
|
+
return null;
|
|
4937
|
+
}
|
|
4810
4938
|
function clampProse(s2, max) {
|
|
4811
4939
|
const t = (s2 ?? "").replace(/\s+/g, " ").trim();
|
|
4812
4940
|
if (t.length <= max) return t;
|
|
@@ -4824,10 +4952,11 @@ function leadReplyText(d) {
|
|
|
4824
4952
|
const budget = LEAD_DECISION_MAX + LEAD_REASON_MAX;
|
|
4825
4953
|
if (d.missing.length)
|
|
4826
4954
|
return clampProse(`Belum bisa kuputuskan. Yang kurang: ${d.missing.join("; ")}.`, budget);
|
|
4827
|
-
|
|
4955
|
+
const picked = d.choices?.length ? d.choices : d.choice ? [d.choice] : [];
|
|
4956
|
+
if (picked.length) return clampProse(`Pilih: ${picked.map((c) => c.option).join("; ")}. ${d.reason}`, budget);
|
|
4828
4957
|
return clampProse(d.reply || d.decision, budget);
|
|
4829
4958
|
}
|
|
4830
|
-
var zLeadGate, zLeadKind, zLeadConfidence, zLeadStatus, LEAD_ACTIONS, zLeadAction, LEAD_FORBIDDEN, zLeadChoice, LEAD_DECISION_MAX, LEAD_REASON_MAX, norm, headOf, zLeadVerdict, zLeadAsk, zLeadOverride;
|
|
4959
|
+
var zLeadGate, zLeadKind, zLeadConfidence, zLeadStatus, zLeadFlowStatus, LEAD_FLOW_OPEN, LEAD_ACTIONS, zLeadAction, LEAD_FORBIDDEN, zLeadChoice, LEAD_DECISION_MAX, LEAD_REASON_MAX, norm, headOf, zLeadSelect, zLeadVerdict, zLeadAsk, zLeadOverride;
|
|
4831
4960
|
var init_lead = __esm({
|
|
4832
4961
|
"../shared/src/lead.ts"() {
|
|
4833
4962
|
"use strict";
|
|
@@ -4836,6 +4965,8 @@ var init_lead = __esm({
|
|
|
4836
4965
|
zLeadKind = external_exports.enum(["answer", "order", "collision", "quality", "refusal"]);
|
|
4837
4966
|
zLeadConfidence = external_exports.enum(["tinggi", "sedang", "ragu"]);
|
|
4838
4967
|
zLeadStatus = external_exports.enum(["berlaku", "ditimpa", "dibatalkan", "gagal"]);
|
|
4968
|
+
zLeadFlowStatus = external_exports.enum(["menunggu", "sebagian", "selesai", "dibatalkan"]);
|
|
4969
|
+
LEAD_FLOW_OPEN = ["menunggu", "sebagian"];
|
|
4839
4970
|
LEAD_ACTIONS = [
|
|
4840
4971
|
"none",
|
|
4841
4972
|
// keputusan murni jawaban; tak ada tindakan menyusul
|
|
@@ -4878,6 +5009,11 @@ var init_lead = __esm({
|
|
|
4878
5009
|
LEAD_REASON_MAX = 480;
|
|
4879
5010
|
norm = (s2) => s2.replace(/\s+/g, " ").trim().toLowerCase();
|
|
4880
5011
|
headOf = (s2) => norm(s2.split(/\s+[—–-]\s+|:/)[0] ?? s2);
|
|
5012
|
+
zLeadSelect = external_exports.object({
|
|
5013
|
+
mode: external_exports.enum(["single", "multi"]).default("single"),
|
|
5014
|
+
min: external_exports.number().int().min(0).max(20).default(0),
|
|
5015
|
+
max: external_exports.number().int().min(1).max(20).nullable().default(null)
|
|
5016
|
+
});
|
|
4881
5017
|
zLeadVerdict = external_exports.object({
|
|
4882
5018
|
decision: external_exports.string().min(1),
|
|
4883
5019
|
reason: external_exports.string().min(1),
|
|
@@ -4894,6 +5030,14 @@ var init_lead = __esm({
|
|
|
4894
5030
|
* yang sama persis dengan `action` di atas.
|
|
4895
5031
|
*/
|
|
4896
5032
|
choice: external_exports.string().default(""),
|
|
5033
|
+
/**
|
|
5034
|
+
* SPEC-485 · pilihan JAMAK. `string[]` dengan alasan yang sama persis seperti `choice` & `action`
|
|
5035
|
+
* di atas: pilihan di luar daftar harus BISA MASUK supaya server menolaknya secara sadar dan
|
|
5036
|
+
* mencatatnya. Kosong + `choice` terisi dibaca sebagai satu pilihan — keluaran agen berbentuk
|
|
5037
|
+
* ADR-0098 harus tetap terpakai, dan menuntut field baru berarti setiap agen lama mendadak
|
|
5038
|
+
* "tak memilih apa pun".
|
|
5039
|
+
*/
|
|
5040
|
+
choices: external_exports.array(external_exports.string().max(2e3)).max(20).default([]),
|
|
4897
5041
|
/**
|
|
4898
5042
|
* SPEC-480 · apa yang KURANG bila konteksnya memang tak cukup untuk memutuskan. Bukan pengganti
|
|
4899
5043
|
* `confidence: "ragu"` (bukti tipis, jawabannya tetap ada) melainkan untuk fakta konkret yang
|
|
@@ -4909,11 +5053,23 @@ var init_lead = __esm({
|
|
|
4909
5053
|
sessionId: external_exports.string().nullish(),
|
|
4910
5054
|
question: external_exports.string().min(1).max(8e3),
|
|
4911
5055
|
options: external_exports.array(external_exports.string().max(2e3)).max(20).default([]),
|
|
4912
|
-
context: external_exports.string().max(2e4).default("")
|
|
5056
|
+
context: external_exports.string().max(2e4).default(""),
|
|
5057
|
+
/** SPEC-485 · bentuk pilihan yang diminta peminta. Default = perilaku hari ini (single). */
|
|
5058
|
+
select: zLeadSelect.default({ mode: "single", min: 0, max: null }),
|
|
5059
|
+
/** `true` = peminta akan mengajukan pertanyaan lanjutan; alurnya dibiarkan terbuka sampai submit. */
|
|
5060
|
+
chain: external_exports.boolean().default(false),
|
|
5061
|
+
/** Lanjutkan rantai yang sudah ada. Alur tertutup ditolak 409 — bukan dibuatkan diam-diam. */
|
|
5062
|
+
flowId: external_exports.string().min(1).nullish().default(null)
|
|
4913
5063
|
});
|
|
4914
5064
|
zLeadOverride = external_exports.object({
|
|
4915
5065
|
answer: external_exports.string().min(1).max(8e3),
|
|
4916
|
-
reason: external_exports.string().max(8e3).default("")
|
|
5066
|
+
reason: external_exports.string().max(8e3).default(""),
|
|
5067
|
+
/**
|
|
5068
|
+
* SPEC-485 · centang operator sebagai DATA, bukan prosa. Dipetakan ke opsi baris yang ditimpa,
|
|
5069
|
+
* lalu ikut diketikkan ke pane — jadi manusia mencentang di dashboard dan kotaknya benar-benar
|
|
5070
|
+
* tercentang di dialog sesi.
|
|
5071
|
+
*/
|
|
5072
|
+
choices: external_exports.array(external_exports.string().max(2e3)).max(20).default([])
|
|
4917
5073
|
});
|
|
4918
5074
|
}
|
|
4919
5075
|
});
|
|
@@ -4922,13 +5078,14 @@ var init_lead = __esm({
|
|
|
4922
5078
|
function flowForSource(source) {
|
|
4923
5079
|
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
4924
5080
|
}
|
|
4925
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerState, zLeadDecisionView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
5081
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
4926
5082
|
var init_dto = __esm({
|
|
4927
5083
|
"../shared/src/dto.ts"() {
|
|
4928
5084
|
"use strict";
|
|
4929
5085
|
init_zod();
|
|
4930
5086
|
init_entities();
|
|
4931
5087
|
init_lead();
|
|
5088
|
+
init_auto_merge();
|
|
4932
5089
|
init_enums();
|
|
4933
5090
|
zIssueDeviceToken = external_exports.object({ name: external_exports.string().min(1) });
|
|
4934
5091
|
zSessionResult = external_exports.object({
|
|
@@ -4981,8 +5138,11 @@ var init_dto = __esm({
|
|
|
4981
5138
|
// SPEC-217 · path default/server editable (null = kosongkan)
|
|
4982
5139
|
schedulerOptIn: external_exports.boolean().optional(),
|
|
4983
5140
|
// SPEC-294 · opt-in scheduler otonom (lokal, tak disync)
|
|
4984
|
-
leadOptIn: external_exports.boolean().optional()
|
|
5141
|
+
leadOptIn: external_exports.boolean().optional(),
|
|
4985
5142
|
// SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal, tak disync)
|
|
5143
|
+
// SPEC-486 · ADR-0103 · kebijakan auto-merge project (null = kosongkan → tanpa auto-merge).
|
|
5144
|
+
// Divalidasi server terhadap repo (checkAutoMerge): repoDir wajib ada, branch wajib nyata.
|
|
5145
|
+
autoMerge: zAutoMerge.nullable().optional()
|
|
4986
5146
|
});
|
|
4987
5147
|
zCreateSpec = external_exports.object({
|
|
4988
5148
|
project: external_exports.string(),
|
|
@@ -5011,7 +5171,11 @@ var init_dto = __esm({
|
|
|
5011
5171
|
// SPEC-447 · ADR-0093 · SENGAJA di luar gerbang `editingContent` (SPEC-186): gerbang itu
|
|
5012
5172
|
// melindungi konten yang sudah jadi dasar kerja sesi berjalan, sedangkan dependsOn hanya
|
|
5013
5173
|
// menggerbangi peluncuran BERIKUTNYA. `[]` = kosongkan.
|
|
5014
|
-
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
5174
|
+
dependsOn: external_exports.array(external_exports.string()).optional(),
|
|
5175
|
+
// SPEC-486 · ADR-0103 · override per item; `null` mengembalikannya ke warisan project.
|
|
5176
|
+
// SENGAJA di luar gerbang `editingContent` (SPEC-186), sama seperti dependsOn: ia menggerbangi
|
|
5177
|
+
// apa yang terjadi SESUDAH kerja, bukan konten yang sedang dikerjakan sesi hidup.
|
|
5178
|
+
autoMerge: zAutoMerge.nullable().optional()
|
|
5015
5179
|
});
|
|
5016
5180
|
zIntegrate = external_exports.object({
|
|
5017
5181
|
op: external_exports.enum(["merge", "rebase"]),
|
|
@@ -5034,7 +5198,9 @@ var init_dto = __esm({
|
|
|
5034
5198
|
// SPEC-253 · Help Center publik aktif
|
|
5035
5199
|
schedulerOptIn: external_exports.boolean().default(false),
|
|
5036
5200
|
// SPEC-294 · opt-in scheduler otonom
|
|
5037
|
-
leadOptIn: external_exports.boolean().default(false)
|
|
5201
|
+
leadOptIn: external_exports.boolean().default(false),
|
|
5202
|
+
// SPEC-409 · ADR-0091 · opt-in hanoman-lead
|
|
5203
|
+
autoMerge: zAutoMerge.nullable().default(null)
|
|
5038
5204
|
});
|
|
5039
5205
|
zSchedulerQueueItem = external_exports.object({
|
|
5040
5206
|
id: external_exports.string(),
|
|
@@ -5091,11 +5257,32 @@ var init_dto = __esm({
|
|
|
5091
5257
|
choiceIndex: external_exports.number().nullable().default(null),
|
|
5092
5258
|
options: external_exports.array(external_exports.string()).default([]),
|
|
5093
5259
|
missing: external_exports.array(external_exports.string()).default([]),
|
|
5260
|
+
// SPEC-485 · ADR-0102 · jawaban SELALU daftar di permukaan baca. Baris pra-migrasi tak punya
|
|
5261
|
+
// kolomnya, jadi `toDecisionView` MENURUNKANNYA dari `choice`/`choiceIndex` — itulah yang
|
|
5262
|
+
// membuat riwayat lama tetap terbaca sesudah perubahan skema.
|
|
5263
|
+
choices: external_exports.array(zLeadChoice).default([]),
|
|
5264
|
+
select: zLeadSelect.nullable().default(null),
|
|
5265
|
+
flowId: external_exports.string().nullable().default(null),
|
|
5266
|
+
step: external_exports.number().nullable().default(null),
|
|
5094
5267
|
status: zLeadStatus,
|
|
5095
5268
|
weighty: external_exports.boolean(),
|
|
5096
5269
|
supersededById: external_exports.string().nullable(),
|
|
5097
5270
|
createdAt: external_exports.string()
|
|
5098
5271
|
});
|
|
5272
|
+
zLeadFlowView = external_exports.object({
|
|
5273
|
+
id: external_exports.string(),
|
|
5274
|
+
projectId: external_exports.string(),
|
|
5275
|
+
specId: external_exports.string().nullable(),
|
|
5276
|
+
sessionId: external_exports.string().nullable(),
|
|
5277
|
+
gate: zLeadGate,
|
|
5278
|
+
status: zLeadFlowStatus,
|
|
5279
|
+
title: external_exports.string(),
|
|
5280
|
+
steps: external_exports.number(),
|
|
5281
|
+
closeReason: external_exports.string().nullable(),
|
|
5282
|
+
openedAt: external_exports.string(),
|
|
5283
|
+
closedAt: external_exports.string().nullable(),
|
|
5284
|
+
expiresAt: external_exports.string()
|
|
5285
|
+
});
|
|
5099
5286
|
zLeadAnswer = external_exports.object({
|
|
5100
5287
|
id: external_exports.string(),
|
|
5101
5288
|
decision: external_exports.string(),
|
|
@@ -5107,7 +5294,12 @@ var init_dto = __esm({
|
|
|
5107
5294
|
// dipilih sebagai field, bukan sesuatu yang harus ditebak dari `decision`. `decision`/`reason`
|
|
5108
5295
|
// di sini TERPANGKAS (jejak penuh ada di `GET /lead/decisions`).
|
|
5109
5296
|
choice: zLeadChoice.nullable().default(null),
|
|
5110
|
-
missing: external_exports.array(external_exports.string()).default([])
|
|
5297
|
+
missing: external_exports.array(external_exports.string()).default([]),
|
|
5298
|
+
// SPEC-485 · ADR-0102 · `choices` adalah bentuk yang berlaku; `choice` di atas tinggal
|
|
5299
|
+
// `choices[0]`. `flowId`/`flowStatus` memberi peminta pegangan untuk melanjutkan rantainya.
|
|
5300
|
+
choices: external_exports.array(zLeadChoice).default([]),
|
|
5301
|
+
flowId: external_exports.string().nullable().default(null),
|
|
5302
|
+
flowStatus: zLeadFlowStatus.nullable().default(null)
|
|
5111
5303
|
});
|
|
5112
5304
|
zLeadProjectStatus = external_exports.object({
|
|
5113
5305
|
projectId: external_exports.string(),
|
|
@@ -5467,8 +5659,15 @@ var init_api = __esm({
|
|
|
5467
5659
|
leadDecisions: `${API}/lead/decisions`,
|
|
5468
5660
|
leadDecisionOverride: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/override`,
|
|
5469
5661
|
leadDecisionCancel: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/cancel`,
|
|
5662
|
+
// SPEC-485 · ADR-0102 · rantai keputusan. Di bawah prefix `lead` supaya capability-nya turunan
|
|
5663
|
+
// method (lead:read/lead:write) tanpa peta baru — kelas bug SPEC-405 sudah ditutup di sana.
|
|
5664
|
+
leadFlows: `${API}/lead/flows`,
|
|
5665
|
+
leadFlowSubmit: (id) => `${API}/lead/flows/${encodeURIComponent(id)}/submit`,
|
|
5666
|
+
leadFlowCancel: (id) => `${API}/lead/flows/${encodeURIComponent(id)}/cancel`,
|
|
5470
5667
|
// SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
|
|
5471
5668
|
customAgents: `${API}/custom-agents`,
|
|
5669
|
+
// SPEC-484 · ADR-0101 · sumber daftar tools/model/runtime untuk form (mention dari `customAgents`).
|
|
5670
|
+
customAgentCatalog: `${API}/custom-agents/catalog`,
|
|
5472
5671
|
// SPEC-481 · ADR-0100 · webhook keluar (cookie-only)
|
|
5473
5672
|
webhooks: `${API}/webhooks`,
|
|
5474
5673
|
webhook: (id) => `${API}/webhooks/${encodeURIComponent(id)}`,
|
|
@@ -5985,6 +6184,7 @@ var init_webhook = __esm({
|
|
|
5985
6184
|
"baseSha",
|
|
5986
6185
|
"headSha",
|
|
5987
6186
|
"dependsOn",
|
|
6187
|
+
"autoMerge",
|
|
5988
6188
|
"createdAt",
|
|
5989
6189
|
"startedAt",
|
|
5990
6190
|
"updatedAt"
|
|
@@ -6014,6 +6214,7 @@ var init_webhook = __esm({
|
|
|
6014
6214
|
baseSha: "5117298c5a3e63af76cbadaa46e2edfa50921d7",
|
|
6015
6215
|
headSha: null,
|
|
6016
6216
|
dependsOn: null,
|
|
6217
|
+
autoMerge: null,
|
|
6017
6218
|
createdAt: "2026-08-01T02:10:00.000Z",
|
|
6018
6219
|
startedAt: "2026-08-01T02:12:31.000Z",
|
|
6019
6220
|
updatedAt: "2026-08-01T09:41:22.000Z"
|
|
@@ -6033,6 +6234,7 @@ var init_webhook = __esm({
|
|
|
6033
6234
|
"helpEnabled",
|
|
6034
6235
|
"schedulerOptIn",
|
|
6035
6236
|
"leadOptIn",
|
|
6237
|
+
"autoMerge",
|
|
6036
6238
|
"createdAt",
|
|
6037
6239
|
"updatedAt"
|
|
6038
6240
|
],
|
|
@@ -6053,6 +6255,7 @@ var init_webhook = __esm({
|
|
|
6053
6255
|
helpEnabled: false,
|
|
6054
6256
|
schedulerOptIn: true,
|
|
6055
6257
|
leadOptIn: false,
|
|
6258
|
+
autoMerge: null,
|
|
6056
6259
|
createdAt: "2026-05-02T04:00:00.000Z",
|
|
6057
6260
|
updatedAt: "2026-08-01T09:00:00.000Z"
|
|
6058
6261
|
}
|
|
@@ -6819,7 +7022,7 @@ var init_mcp_catalog = __esm({
|
|
|
6819
7022
|
{
|
|
6820
7023
|
name: "hanoman_lead_ask",
|
|
6821
7024
|
title: "Minta putusan hanoman-lead",
|
|
6822
|
-
description: "Minta putusan ke hanoman-lead saat menemui persimpangan yang biasanya butuh manusia. Jawabannya terbaca mesin (`decision`, `reason`, `refs`, `confidence`, `action`) dan `refs` hanya memuat rujukan yang benar-benar ada di repo. Panggilan ini melahirkan jejak permanen dan putusannya bisa menggerakkan sesi \u2014 pakai hanya saat memang buntu. 409 = lead tak aktif atau proyek belum opt-in: kembali ke perilaku biasa, berhenti dan tunggu manusia.",
|
|
7025
|
+
description: "Minta putusan ke hanoman-lead saat menemui persimpangan yang biasanya butuh manusia. Jawabannya terbaca mesin (`decision`, `reason`, `refs`, `confidence`, `action`, `choices`) dan `refs` hanya memuat rujukan yang benar-benar ada di repo. Bila opsinya TIDAK saling eksklusif, set `multi: true` \u2014 balasannya memuat `choices` (daftar), bukan hanya `choice`. Panggilan ini melahirkan jejak permanen dan putusannya bisa menggerakkan sesi \u2014 pakai hanya saat memang buntu. 409 = lead tak aktif atau proyek belum opt-in: kembali ke perilaku biasa, berhenti dan tunggu manusia; 400 = bentuk `multi`/`minChoices`/`maxChoices` mustahil dipenuhi oleh daftar opsi yang kamu kirim.",
|
|
6823
7026
|
inputSchema: obj({
|
|
6824
7027
|
properties: {
|
|
6825
7028
|
project: str("Id proyek."),
|
|
@@ -6827,7 +7030,10 @@ var init_mcp_catalog = __esm({
|
|
|
6827
7030
|
spec: str("Opsional. Id backlog yang bersangkutan."),
|
|
6828
7031
|
session: str("Opsional. Id sesi yang bersangkutan."),
|
|
6829
7032
|
options: strArray("Opsional. Pilihan yang tersedia, maksimum 20, masing-masing maksimum 2000 karakter. Lead memilih salah satunya."),
|
|
6830
|
-
context: str("Opsional. Konteks pendukung, maksimum 20.000 karakter.")
|
|
7033
|
+
context: str("Opsional. Konteks pendukung, maksimum 20.000 karakter."),
|
|
7034
|
+
multi: bool("Opsional. `true` bila opsinya TIDAK saling eksklusif dan lead boleh memilih beberapa sekaligus. Menuntut `options` terisi."),
|
|
7035
|
+
minChoices: int("Opsional, hanya untuk `multi`. Paling sedikit berapa opsi harus dipilih."),
|
|
7036
|
+
maxChoices: int("Opsional, hanya untuk `multi`. Paling banyak berapa opsi boleh dipilih; tanpa ini sebanyak opsinya.")
|
|
6831
7037
|
},
|
|
6832
7038
|
required: ["project", "question"]
|
|
6833
7039
|
}),
|
|
@@ -6844,7 +7050,10 @@ var init_mcp_catalog = __esm({
|
|
|
6844
7050
|
...s(a.spec) ? { specId: a.spec } : {},
|
|
6845
7051
|
...s(a.session) ? { sessionId: a.session } : {},
|
|
6846
7052
|
...Array.isArray(a.options) ? { options: a.options } : {},
|
|
6847
|
-
...s(a.context) ? { context: a.context } : {}
|
|
7053
|
+
...s(a.context) ? { context: a.context } : {},
|
|
7054
|
+
// SPEC-485 · ADITIF: tanpa `multi` bentuk permintaannya identik dengan sebelum ADR-0102,
|
|
7055
|
+
// jadi klien MCP lama tak berubah perilakunya satu bit pun.
|
|
7056
|
+
...a.multi === true ? { select: { mode: "multi", min: n(a.minChoices) ?? 0, max: n(a.maxChoices) ?? null } } : {}
|
|
6848
7057
|
}
|
|
6849
7058
|
}),
|
|
6850
7059
|
shape: (raw) => raw
|
|
@@ -6883,6 +7092,7 @@ var init_src = __esm({
|
|
|
6883
7092
|
init_entities();
|
|
6884
7093
|
init_agent();
|
|
6885
7094
|
init_custom_agent();
|
|
7095
|
+
init_agent_catalog();
|
|
6886
7096
|
init_github();
|
|
6887
7097
|
init_lead();
|
|
6888
7098
|
init_dto();
|
|
@@ -6896,6 +7106,7 @@ var init_src = __esm({
|
|
|
6896
7106
|
init_telegram();
|
|
6897
7107
|
init_webhook();
|
|
6898
7108
|
init_mcp();
|
|
7109
|
+
init_auto_merge();
|
|
6899
7110
|
}
|
|
6900
7111
|
});
|
|
6901
7112
|
|
|
@@ -7726,9 +7937,9 @@ var init_custom_agents = __esm({
|
|
|
7726
7937
|
// ../runner/src/paths.ts
|
|
7727
7938
|
import { homedir } from "node:os";
|
|
7728
7939
|
import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve4 } from "node:path";
|
|
7729
|
-
function resolveHome(env = process.env,
|
|
7940
|
+
function resolveHome(env = process.env, home2 = homedir()) {
|
|
7730
7941
|
const v = env.HANOMAN_HOME?.trim();
|
|
7731
|
-
return v ? v : join(
|
|
7942
|
+
return v ? v : join(home2, ".hanoman");
|
|
7732
7943
|
}
|
|
7733
7944
|
function schemeOf(raw) {
|
|
7734
7945
|
return raw.split("://")[0] ?? "?";
|
|
@@ -8145,18 +8356,38 @@ function readChoiceDialog(paneText2) {
|
|
|
8145
8356
|
expected -= 1;
|
|
8146
8357
|
}
|
|
8147
8358
|
if (run2.length < 2 || run2[0].n !== 1) return null;
|
|
8148
|
-
const rows = run2.map((r) =>
|
|
8149
|
-
|
|
8150
|
-
label: r.label
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8359
|
+
const rows = run2.map((r) => {
|
|
8360
|
+
const m = CHECK.exec(r.label);
|
|
8361
|
+
const label = m ? (m[2] ?? "").trim() : r.label;
|
|
8362
|
+
return {
|
|
8363
|
+
n: r.n,
|
|
8364
|
+
label,
|
|
8365
|
+
checked: m ? m[1] !== " " : null,
|
|
8366
|
+
free: PLACEHOLDER.test(label),
|
|
8367
|
+
chat: CHAT_ROW.test(label)
|
|
8368
|
+
};
|
|
8369
|
+
});
|
|
8370
|
+
let submit = { present: false, focused: false };
|
|
8371
|
+
for (const line of lines2.slice(0, footer)) {
|
|
8372
|
+
const m = SUBMIT_BTN.exec(line);
|
|
8373
|
+
if (m) submit = { present: true, focused: !!m[1] };
|
|
8374
|
+
}
|
|
8154
8375
|
return {
|
|
8155
8376
|
rows,
|
|
8156
8377
|
freeIndex: rows.find((r) => r.free)?.n ?? null,
|
|
8157
|
-
options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
|
|
8378
|
+
options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label),
|
|
8379
|
+
multi: rows.some((r) => r.checked !== null),
|
|
8380
|
+
submit
|
|
8158
8381
|
};
|
|
8159
8382
|
}
|
|
8383
|
+
function focusedRow(paneText2) {
|
|
8384
|
+
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
8385
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
8386
|
+
const m = /^\s*[❯>›]\s*(\d{1,2})\.\s+\S/.exec(lines2[i] ?? "");
|
|
8387
|
+
if (m) return Number(m[1]);
|
|
8388
|
+
}
|
|
8389
|
+
return null;
|
|
8390
|
+
}
|
|
8160
8391
|
function readTabs(lines2) {
|
|
8161
8392
|
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
8162
8393
|
const line = lines2[i] ?? "";
|
|
@@ -8206,15 +8437,17 @@ function readDialogScreen(paneText2) {
|
|
|
8206
8437
|
options: d.options,
|
|
8207
8438
|
notes: d.freeIndex === null && NOTES_FOOTER.test(lines2[footer] ?? ""),
|
|
8208
8439
|
tabs,
|
|
8209
|
-
title: readTitle(lines2, at, footer)
|
|
8440
|
+
title: readTitle(lines2, at, footer),
|
|
8441
|
+
multi: d.multi,
|
|
8442
|
+
submit: d.submit
|
|
8210
8443
|
};
|
|
8211
8444
|
}
|
|
8212
8445
|
function dialogKey(paneText2) {
|
|
8213
8446
|
const s2 = readDialogScreen(paneText2);
|
|
8214
8447
|
if (!s2) return "none";
|
|
8215
8448
|
if (s2.kind === "review") return "review";
|
|
8216
|
-
const tabs = s2.tabs.map((t) => `${t.answered ? "x" : "o"}${t.header}`).join(",");
|
|
8217
|
-
return `q|${tabs}|${s2.title || s2.options.join("|")}`;
|
|
8449
|
+
const tabs = s2.multi ? s2.tabs.map((t) => t.header).join(",") : s2.tabs.map((t) => `${t.answered ? "x" : "o"}${t.header}`).join(",");
|
|
8450
|
+
return `q|${s2.multi ? "multi|" : ""}${tabs}|${s2.title || s2.options.join("|")}`;
|
|
8218
8451
|
}
|
|
8219
8452
|
function freeTextFilled(paneText2, n2) {
|
|
8220
8453
|
const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n2);
|
|
@@ -8233,6 +8466,42 @@ async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
|
|
|
8233
8466
|
io.enter();
|
|
8234
8467
|
return true;
|
|
8235
8468
|
}
|
|
8469
|
+
async function answerMultiSelectDialog(io, plan, chunkMs) {
|
|
8470
|
+
const want = new Set(plan.pick);
|
|
8471
|
+
const state = () => readChoiceDialog(io.capture());
|
|
8472
|
+
for (const row of state()?.rows ?? []) {
|
|
8473
|
+
if (row.checked === null || row.free || row.chat) continue;
|
|
8474
|
+
if (row.checked === want.has(row.n)) continue;
|
|
8475
|
+
io.literal(String(row.n));
|
|
8476
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8477
|
+
const after = state()?.rows.find((r) => r.n === row.n);
|
|
8478
|
+
if (!after || after.checked !== want.has(row.n)) return false;
|
|
8479
|
+
}
|
|
8480
|
+
if (plan.line && plan.freeIndex !== null) {
|
|
8481
|
+
let hop2 = 0;
|
|
8482
|
+
while (focusedRow(io.capture()) !== plan.freeIndex && hop2 < NAV_TRIES) {
|
|
8483
|
+
io.down();
|
|
8484
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8485
|
+
hop2++;
|
|
8486
|
+
}
|
|
8487
|
+
if (focusedRow(io.capture()) !== plan.freeIndex) return false;
|
|
8488
|
+
for (const chunk of goalChunks(plan.line)) {
|
|
8489
|
+
io.literal(chunk);
|
|
8490
|
+
await io.sleep(chunkMs);
|
|
8491
|
+
}
|
|
8492
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8493
|
+
if (!freeTextFilled(io.capture(), plan.freeIndex)) return false;
|
|
8494
|
+
}
|
|
8495
|
+
let hop = 0;
|
|
8496
|
+
while (!state()?.submit.focused && hop < NAV_TRIES) {
|
|
8497
|
+
io.down();
|
|
8498
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8499
|
+
hop++;
|
|
8500
|
+
}
|
|
8501
|
+
if (!state()?.submit.focused) return false;
|
|
8502
|
+
io.enter();
|
|
8503
|
+
return true;
|
|
8504
|
+
}
|
|
8236
8505
|
function notesFilled(paneText2) {
|
|
8237
8506
|
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
8238
8507
|
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
@@ -8263,7 +8532,7 @@ async function submitReview(io, submitRow) {
|
|
|
8263
8532
|
}
|
|
8264
8533
|
return false;
|
|
8265
8534
|
}
|
|
8266
|
-
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;
|
|
8535
|
+
var FOOTER, ROW, PLACEHOLDER, CHAT_ROW, SIDE_PANEL, cleanLabel, CHECK, SUBMIT_BTN, REVIEW_PROMPT, SUBMIT_ROW, NOTES_FOOTER, TAB_BOX, lastIndexOf, RULE, DIALOG_SETTLE_MS, NAV_TRIES, NOTES_PLACEHOLDER, NOTES_LINE, SUBMIT_TRIES;
|
|
8267
8536
|
var init_tui_dialog = __esm({
|
|
8268
8537
|
"src/services/tui-dialog.ts"() {
|
|
8269
8538
|
"use strict";
|
|
@@ -8274,6 +8543,8 @@ var init_tui_dialog = __esm({
|
|
|
8274
8543
|
CHAT_ROW = /^chat about this$/i;
|
|
8275
8544
|
SIDE_PANEL = /\s{2,}[│┃┌┐└┘├┤╭╮╰╯─━].*$/;
|
|
8276
8545
|
cleanLabel = (s2) => s2.replace(SIDE_PANEL, "").trim();
|
|
8546
|
+
CHECK = /^\[([ xX✔✓])\]\s*(.*)$/;
|
|
8547
|
+
SUBMIT_BTN = /^\s*([❯>›])?\s{2,}(Submit|Next)\s*$/;
|
|
8277
8548
|
REVIEW_PROMPT = /^\s*ready to submit your answers\?\s*$/i;
|
|
8278
8549
|
SUBMIT_ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+submit answers\s*$/i;
|
|
8279
8550
|
NOTES_FOOTER = /\bn to add notes\b/i;
|
|
@@ -8284,6 +8555,7 @@ var init_tui_dialog = __esm({
|
|
|
8284
8555
|
};
|
|
8285
8556
|
RULE = /^[\s─━╌╍_=]*$/;
|
|
8286
8557
|
DIALOG_SETTLE_MS = 250;
|
|
8558
|
+
NAV_TRIES = 24;
|
|
8287
8559
|
NOTES_PLACEHOLDER = /^(?:press n to add notes|add notes on this design(?:…|\.\.\.)?)$/i;
|
|
8288
8560
|
NOTES_LINE = /(?:^|\s)Notes:\s*(.*)$/;
|
|
8289
8561
|
SUBMIT_TRIES = 8;
|
|
@@ -8520,7 +8792,7 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
8520
8792
|
if (existing && !existing.exited) return existing;
|
|
8521
8793
|
if (existing) killSession(id);
|
|
8522
8794
|
const agentForDefs = opts.agent ?? "claude";
|
|
8523
|
-
const customDefs = opts.command ? [] : customAgentsFor(projectId);
|
|
8795
|
+
const customDefs = opts.command ? [] : customAgentsFor(projectId, agentForDefs);
|
|
8524
8796
|
const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : "";
|
|
8525
8797
|
let promptArg = "";
|
|
8526
8798
|
if (!opts.command && opts.prompt) {
|
|
@@ -8665,7 +8937,7 @@ function capturePane(id, lines2 = 200) {
|
|
|
8665
8937
|
return "";
|
|
8666
8938
|
}
|
|
8667
8939
|
}
|
|
8668
|
-
async function sendToPane(id, text, chunkMs = 50) {
|
|
8940
|
+
async function sendToPane(id, text, chunkMs = 50, choices = []) {
|
|
8669
8941
|
const p = getSession(id);
|
|
8670
8942
|
if (!p || p.exited) return false;
|
|
8671
8943
|
const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
@@ -8675,6 +8947,10 @@ async function sendToPane(id, text, chunkMs = 50) {
|
|
|
8675
8947
|
const screen = readDialogScreen(io.capture());
|
|
8676
8948
|
if (screen?.kind === "review") return await submitReview(io, screen.submitRow);
|
|
8677
8949
|
if (screen?.kind === "question") {
|
|
8950
|
+
if (screen.multi && screen.submit.present) {
|
|
8951
|
+
const pick2 = resolveChoices(choices, screen.options).choices.map((c) => c.index);
|
|
8952
|
+
return await answerMultiSelectDialog(io, { pick: pick2, line, freeIndex: screen.freeIndex }, chunkMs);
|
|
8953
|
+
}
|
|
8678
8954
|
if (screen.freeIndex !== null) return await answerChoiceDialog(io, screen.freeIndex, line, chunkMs);
|
|
8679
8955
|
if (screen.notes) return await answerNotesDialog(io, line, chunkMs);
|
|
8680
8956
|
}
|
|
@@ -8969,9 +9245,9 @@ var init_pty = __esm({
|
|
|
8969
9245
|
}
|
|
8970
9246
|
};
|
|
8971
9247
|
customAgentSource = () => [];
|
|
8972
|
-
customAgentsFor = (projectId) => {
|
|
9248
|
+
customAgentsFor = (projectId, agent) => {
|
|
8973
9249
|
try {
|
|
8974
|
-
return customAgentSource(projectId);
|
|
9250
|
+
return customAgentSource(projectId, agent);
|
|
8975
9251
|
} catch {
|
|
8976
9252
|
return [];
|
|
8977
9253
|
}
|
|
@@ -8994,6 +9270,11 @@ var init_pty = __esm({
|
|
|
8994
9270
|
enter: () => {
|
|
8995
9271
|
tmux("send-keys", "-t", name(id), "Enter");
|
|
8996
9272
|
},
|
|
9273
|
+
// SPEC-485 · SATU panah per pemanggilan: terukur, empat `Down` dalam satu `send-keys` memindahkan
|
|
9274
|
+
// fokus satu baris saja (jebakan burst ADR-0085, kali ini pada tombol kendali).
|
|
9275
|
+
down: () => {
|
|
9276
|
+
tmux("send-keys", "-t", name(id), "Down");
|
|
9277
|
+
},
|
|
8997
9278
|
sleep
|
|
8998
9279
|
});
|
|
8999
9280
|
DIALOG_CAPTURE_LINES = 60;
|
|
@@ -9273,7 +9554,10 @@ var init_sync = __esm({
|
|
|
9273
9554
|
// ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
|
|
9274
9555
|
// mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
|
|
9275
9556
|
// `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
|
|
9276
|
-
|
|
9557
|
+
// SPEC-484 · ADR-0101 · `runtime` ikut: ia menentukan sesi mesin mana yang memakai persona ini,
|
|
9558
|
+
// dan kolom yang terlewat di sini mendarat sebagai default palsu (= "warisi") di setiap mesin
|
|
9559
|
+
// lain tanpa satu pun error.
|
|
9560
|
+
customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "runtime", "enabled", "createdAt", "updatedAt"],
|
|
9277
9561
|
// SPEC-471 · ADR-0095 · SELURUH kolom bermakna ikut. `status`/`specId` termasuk: keputusan
|
|
9278
9562
|
// triase adalah bagian keadaan yang harus dilihat sama oleh semua mesin — tanpa itu satu
|
|
9279
9563
|
// mesin bisa menerima ulang issue yang di mesin lain sudah jadi backlog.
|
|
@@ -9387,8 +9671,8 @@ var init_settings2 = __esm({
|
|
|
9387
9671
|
// src/services/codex-trust.ts
|
|
9388
9672
|
import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
9389
9673
|
import { homedir as homedir2 } from "node:os";
|
|
9390
|
-
function ensureCodexTrust(repoDir,
|
|
9391
|
-
const path = codexConfigPath(
|
|
9674
|
+
function ensureCodexTrust(repoDir, home2) {
|
|
9675
|
+
const path = codexConfigPath(home2);
|
|
9392
9676
|
let canonical = repoDir;
|
|
9393
9677
|
try {
|
|
9394
9678
|
canonical = realpathSync2(repoDir);
|
|
@@ -9402,7 +9686,7 @@ function ensureCodexTrust(repoDir, home) {
|
|
|
9402
9686
|
} catch {
|
|
9403
9687
|
}
|
|
9404
9688
|
if (existing.includes(header2)) return;
|
|
9405
|
-
mkdirSync6(codexHome(
|
|
9689
|
+
mkdirSync6(codexHome(home2), { recursive: true });
|
|
9406
9690
|
const lead = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
9407
9691
|
appendFileSync(path, `${lead}
|
|
9408
9692
|
${header2}
|
|
@@ -9415,8 +9699,8 @@ var codexHome, codexConfigPath;
|
|
|
9415
9699
|
var init_codex_trust = __esm({
|
|
9416
9700
|
"src/services/codex-trust.ts"() {
|
|
9417
9701
|
"use strict";
|
|
9418
|
-
codexHome = (
|
|
9419
|
-
codexConfigPath = (
|
|
9702
|
+
codexHome = (home2) => home2 ?? process.env.CODEX_HOME ?? `${homedir2()}/.codex`;
|
|
9703
|
+
codexConfigPath = (home2) => `${codexHome(home2)}/config.toml`;
|
|
9420
9704
|
}
|
|
9421
9705
|
});
|
|
9422
9706
|
|
|
@@ -9992,12 +10276,12 @@ ${input.text}`;
|
|
|
9992
10276
|
});
|
|
9993
10277
|
|
|
9994
10278
|
// src/services/telegram/store.ts
|
|
9995
|
-
import { Prisma } from "@prisma/client";
|
|
10279
|
+
import { Prisma as Prisma3 } from "@prisma/client";
|
|
9996
10280
|
var isUnique, TelegramStore;
|
|
9997
10281
|
var init_store = __esm({
|
|
9998
10282
|
"src/services/telegram/store.ts"() {
|
|
9999
10283
|
"use strict";
|
|
10000
|
-
isUnique = (error) => error instanceof
|
|
10284
|
+
isUnique = (error) => error instanceof Prisma3.PrismaClientKnownRequestError && error.code === "P2002";
|
|
10001
10285
|
TelegramStore = class {
|
|
10002
10286
|
// SPEC-481 · klien yang diekspor kini ber-extension (tap webhook) sehingga tak lagi
|
|
10003
10287
|
// assignable ke `PrismaClient` polos. `Db` diturunkan dari nilai nyatanya.
|
|
@@ -14362,7 +14646,7 @@ function syncStatus() {
|
|
|
14362
14646
|
async function startSyncClient(base2, token, tickMs2) {
|
|
14363
14647
|
started = true;
|
|
14364
14648
|
const transport = fetchTransport(base2, token);
|
|
14365
|
-
const
|
|
14649
|
+
const tick6 = async () => {
|
|
14366
14650
|
try {
|
|
14367
14651
|
await syncOnce(transport);
|
|
14368
14652
|
} catch {
|
|
@@ -14373,13 +14657,13 @@ async function startSyncClient(base2, token, tickMs2) {
|
|
|
14373
14657
|
const wsUrl = base2.replace(/^http/, "ws").replace(/\/$/, "") + `/api/sync/ws?token=${encodeURIComponent(token)}`;
|
|
14374
14658
|
ws = new WebSocket2(wsUrl);
|
|
14375
14659
|
ws.on("open", () => {
|
|
14376
|
-
void
|
|
14660
|
+
void tick6();
|
|
14377
14661
|
});
|
|
14378
14662
|
ws.on("message", async (raw) => {
|
|
14379
14663
|
try {
|
|
14380
14664
|
const msg = JSON.parse(raw.toString());
|
|
14381
14665
|
if (msg.t !== "sync") return;
|
|
14382
|
-
if (!await applyFeedFrame(msg)) void
|
|
14666
|
+
if (!await applyFeedFrame(msg)) void tick6();
|
|
14383
14667
|
} catch {
|
|
14384
14668
|
}
|
|
14385
14669
|
});
|
|
@@ -14396,11 +14680,11 @@ async function startSyncClient(base2, token, tickMs2) {
|
|
|
14396
14680
|
}
|
|
14397
14681
|
});
|
|
14398
14682
|
};
|
|
14399
|
-
await
|
|
14683
|
+
await tick6();
|
|
14400
14684
|
void connectWs();
|
|
14401
14685
|
const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
|
|
14402
14686
|
timer2 = setInterval(() => {
|
|
14403
|
-
void
|
|
14687
|
+
void tick6();
|
|
14404
14688
|
}, ms);
|
|
14405
14689
|
timer2.unref?.();
|
|
14406
14690
|
}
|
|
@@ -17819,6 +18103,7 @@ init_src();
|
|
|
17819
18103
|
|
|
17820
18104
|
// src/services/project-view.ts
|
|
17821
18105
|
init_pty();
|
|
18106
|
+
init_src();
|
|
17822
18107
|
var IDLE = { status: "idle", phase: null, flow: null };
|
|
17823
18108
|
function sessionOf(projectId, sessions) {
|
|
17824
18109
|
const s2 = sessions.find((x) => x.projectId === projectId && x.specId && !x.exited);
|
|
@@ -17858,7 +18143,9 @@ async function toProjectView(p, sessions) {
|
|
|
17858
18143
|
// SPEC-294 · opt-in scheduler otonom (lokal per-instance).
|
|
17859
18144
|
schedulerOptIn: p.schedulerOptIn,
|
|
17860
18145
|
// SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal per-instance, cermin schedulerOptIn).
|
|
17861
|
-
leadOptIn: p.leadOptIn
|
|
18146
|
+
leadOptIn: p.leadOptIn,
|
|
18147
|
+
// SPEC-486 · ADR-0103 · kebijakan auto-merge (lokal per-instance, cermin schedulerOptIn).
|
|
18148
|
+
autoMerge: autoMergeOf(p.autoMerge)
|
|
17862
18149
|
};
|
|
17863
18150
|
}
|
|
17864
18151
|
|
|
@@ -17899,8 +18186,42 @@ async function branchFromCandidates(repoDir) {
|
|
|
17899
18186
|
const [local, remote] = await Promise.all([listRepoBranches(repoDir), listRepoRemoteBranches(repoDir)]);
|
|
17900
18187
|
return [.../* @__PURE__ */ new Set([...local, ...remote])].sort();
|
|
17901
18188
|
}
|
|
18189
|
+
async function defaultBranch(repoDir) {
|
|
18190
|
+
if (!repoDir) return null;
|
|
18191
|
+
try {
|
|
18192
|
+
const { stdout } = await exec2(
|
|
18193
|
+
"git",
|
|
18194
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
18195
|
+
{ cwd: repoDir, ...GIT }
|
|
18196
|
+
);
|
|
18197
|
+
const name2 = stdout.trim().replace(/^origin\//, "");
|
|
18198
|
+
if (name2 && name2 !== "HEAD") return name2;
|
|
18199
|
+
} catch {
|
|
18200
|
+
}
|
|
18201
|
+
const all = /* @__PURE__ */ new Set([...await listRepoBranches(repoDir), ...await listRepoRemoteBranches(repoDir)]);
|
|
18202
|
+
for (const c of ["main", "master"]) if (all.has(c)) return c;
|
|
18203
|
+
return null;
|
|
18204
|
+
}
|
|
18205
|
+
|
|
18206
|
+
// src/services/auto-merge-gate.ts
|
|
18207
|
+
init_src();
|
|
18208
|
+
async function checkAutoMerge(repoDir, raw) {
|
|
18209
|
+
if (raw === null || raw === void 0) return { ok: true };
|
|
18210
|
+
const p = autoMergeOf(raw);
|
|
18211
|
+
if (!p) return { ok: false, code: 400, error: "bentuk kebijakan auto-merge tak sah" };
|
|
18212
|
+
if (p.mode === "off") return { ok: true };
|
|
18213
|
+
if (!repoDir)
|
|
18214
|
+
return { ok: false, code: 409, error: "project belum di-bind ke checkout lokal \u2014 atur repoDir dulu sebelum menyalakan auto-merge" };
|
|
18215
|
+
if (p.mode === "default-branch") {
|
|
18216
|
+
return await defaultBranch(repoDir) ? { ok: true } : { ok: false, code: 400, error: "default branch repo tak bisa diresolve (tak ada origin/HEAD, main, maupun master) \u2014 pilih branch tujuan secara eksplisit" };
|
|
18217
|
+
}
|
|
18218
|
+
if (!p.branch) return { ok: false, code: 400, error: 'mode "branch" butuh branch tujuan' };
|
|
18219
|
+
const known = p.dest === "origin" ? await listRepoRemoteBranches(repoDir) : await listRepoBranches(repoDir);
|
|
18220
|
+
return known.includes(p.branch) ? { ok: true } : { ok: false, code: 400, error: `branch "${p.branch}" tidak ada di ${p.dest === "origin" ? "origin" : "repo lokal"} project` };
|
|
18221
|
+
}
|
|
17902
18222
|
|
|
17903
18223
|
// src/routes/projects.ts
|
|
18224
|
+
import { Prisma } from "@prisma/client";
|
|
17904
18225
|
init_pty();
|
|
17905
18226
|
|
|
17906
18227
|
// src/services/paginate.ts
|
|
@@ -17963,7 +18284,13 @@ async function projects_default(app2) {
|
|
|
17963
18284
|
const parsed = zUpdateProject.safeParse(req.body);
|
|
17964
18285
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
17965
18286
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
17966
|
-
|
|
18287
|
+
if ("autoMerge" in parsed.data) {
|
|
18288
|
+
const gate = await checkAutoMerge(await resolveRepoDir(id), parsed.data.autoMerge);
|
|
18289
|
+
if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
|
|
18290
|
+
}
|
|
18291
|
+
const data = { ...parsed.data };
|
|
18292
|
+
if ("autoMerge" in data && data.autoMerge === null) data.autoMerge = Prisma.DbNull;
|
|
18293
|
+
const updated = await prisma.project.update({ where: { id }, data });
|
|
17967
18294
|
await notifySynced("project", id);
|
|
17968
18295
|
return toProjectView(updated, listSessions());
|
|
17969
18296
|
});
|
|
@@ -17995,7 +18322,11 @@ async function projects_default(app2) {
|
|
|
17995
18322
|
const p = await prisma.project.findUnique({ where: { id } });
|
|
17996
18323
|
if (!p) return reply.code(404).send({ error: "not found" });
|
|
17997
18324
|
const repoDir = await resolveRepoDir(id);
|
|
17998
|
-
return {
|
|
18325
|
+
return {
|
|
18326
|
+
branches: await listRepoBranches(repoDir),
|
|
18327
|
+
remotes: await listRepoRemoteBranches(repoDir),
|
|
18328
|
+
defaultBranch: await defaultBranch(repoDir)
|
|
18329
|
+
};
|
|
17999
18330
|
});
|
|
18000
18331
|
app2.get("/projects/:id/help-center", async (req, reply) => {
|
|
18001
18332
|
const { id } = req.params;
|
|
@@ -18121,6 +18452,9 @@ async function resolveGraphSource(repoDir, source) {
|
|
|
18121
18452
|
if (await refExists(repoDir, cand)) return cand;
|
|
18122
18453
|
return null;
|
|
18123
18454
|
}
|
|
18455
|
+
async function discardMergeWorktree(repoDir, wt) {
|
|
18456
|
+
await reclaim(repoDir, wt);
|
|
18457
|
+
}
|
|
18124
18458
|
async function deleteMergedBranch(repoDir, branch) {
|
|
18125
18459
|
await sh(repoDir, ["branch", "-D", "--end-of-options", branch]);
|
|
18126
18460
|
if (await refExists(repoDir, `refs/remotes/origin/${branch}`))
|
|
@@ -18246,9 +18580,9 @@ async function revOk(wt, rev) {
|
|
|
18246
18580
|
return exec4("git", ["rev-parse", "--verify", "-q", "--end-of-options", `${rev}^{commit}`], { cwd: wt, ...GIT3 }).then(() => true).catch(() => false);
|
|
18247
18581
|
}
|
|
18248
18582
|
async function mergeBase(wt, baseSha, branchFrom) {
|
|
18249
|
-
const
|
|
18583
|
+
const candidates2 = [baseSha, branchFrom, "main", "master"].filter((c) => !!c);
|
|
18250
18584
|
let base2 = "HEAD";
|
|
18251
|
-
for (const c of
|
|
18585
|
+
for (const c of candidates2) if (await revOk(wt, c)) {
|
|
18252
18586
|
base2 = c;
|
|
18253
18587
|
break;
|
|
18254
18588
|
}
|
|
@@ -18641,6 +18975,9 @@ async function validateDependsOn(specId, projectId, raw) {
|
|
|
18641
18975
|
return { ok: true, ids };
|
|
18642
18976
|
}
|
|
18643
18977
|
|
|
18978
|
+
// src/routes/specs.ts
|
|
18979
|
+
import { Prisma as Prisma2 } from "@prisma/client";
|
|
18980
|
+
|
|
18644
18981
|
// src/services/doc-export.ts
|
|
18645
18982
|
import PDFDocument from "pdfkit";
|
|
18646
18983
|
|
|
@@ -21065,6 +21402,13 @@ async function recordFailure(specId, title, projectId, reason) {
|
|
|
21065
21402
|
}).catch(() => {
|
|
21066
21403
|
});
|
|
21067
21404
|
}
|
|
21405
|
+
async function recordAutoMerge(specId, projectId, title) {
|
|
21406
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
21407
|
+
await prisma.notification.create({
|
|
21408
|
+
data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
21409
|
+
}).catch(() => {
|
|
21410
|
+
});
|
|
21411
|
+
}
|
|
21068
21412
|
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
21069
21413
|
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
21070
21414
|
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
@@ -21274,7 +21618,7 @@ ${item.context}` : item.context;
|
|
|
21274
21618
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
21275
21619
|
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
21276
21620
|
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
21277
|
-
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload, dependsOn } = parsed.data;
|
|
21621
|
+
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload, dependsOn, autoMerge } = parsed.data;
|
|
21278
21622
|
const editingContent = title !== void 0 || newPriority !== void 0 || payload !== void 0;
|
|
21279
21623
|
if (editingContent && (spec.stage !== "brainstorming" || spec.baseSha !== null))
|
|
21280
21624
|
return reply.code(409).send({ error: "backlog item sudah dimulai \u2014 tak bisa diedit" });
|
|
@@ -21288,6 +21632,10 @@ ${item.context}` : item.context;
|
|
|
21288
21632
|
if (!d.ok) return reply.code(400).send({ error: d.error });
|
|
21289
21633
|
depIds = d.ids;
|
|
21290
21634
|
}
|
|
21635
|
+
if ("autoMerge" in parsed.data) {
|
|
21636
|
+
const gate = await checkAutoMerge(await resolveRepoDir(spec.projectId), autoMerge);
|
|
21637
|
+
if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
|
|
21638
|
+
}
|
|
21291
21639
|
if (stage !== void 0) {
|
|
21292
21640
|
if (STAGES.indexOf(stage) >= STAGES.indexOf(spec.stage))
|
|
21293
21641
|
return reply.code(422).send({ error: "stage hanya boleh dikembalikan mundur" });
|
|
@@ -21300,6 +21648,7 @@ ${item.context}` : item.context;
|
|
|
21300
21648
|
const data = {};
|
|
21301
21649
|
if (branchFrom !== void 0) data.branchFrom = branchFrom;
|
|
21302
21650
|
if (depIds !== void 0) data.dependsOn = depIds;
|
|
21651
|
+
if ("autoMerge" in parsed.data) data.autoMerge = autoMerge === null ? Prisma2.DbNull : autoMerge;
|
|
21303
21652
|
if (stage !== void 0) data.stage = stage;
|
|
21304
21653
|
if (editingContent) {
|
|
21305
21654
|
const effPayload = payload ?? spec.payload;
|
|
@@ -23202,9 +23551,9 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
23202
23551
|
return new Promise((resolve14) => {
|
|
23203
23552
|
const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
23204
23553
|
let out3 = "";
|
|
23205
|
-
const
|
|
23554
|
+
const timer7 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
23206
23555
|
const done = (r) => {
|
|
23207
|
-
clearTimeout(
|
|
23556
|
+
clearTimeout(timer7);
|
|
23208
23557
|
if (askpass) rmSync4(dirname10(askpass), { recursive: true, force: true });
|
|
23209
23558
|
resolve14(r);
|
|
23210
23559
|
};
|
|
@@ -25322,6 +25671,16 @@ function leadPrompt(q, c) {
|
|
|
25322
25671
|
}
|
|
25323
25672
|
lines2.push("");
|
|
25324
25673
|
}
|
|
25674
|
+
if (c.chainSteps?.length) {
|
|
25675
|
+
lines2.push("## Rantai keputusan ini (langkah yang sudah dijawab)");
|
|
25676
|
+
for (const [i, s2] of c.chainSteps.entries()) {
|
|
25677
|
+
lines2.push(`${i + 1}. "${s2.question.slice(0, 200)}" \u2192 ${s2.picked.length ? s2.picked.join("; ") : "(tanpa pilihan)"}`);
|
|
25678
|
+
if (s2.options.length) lines2.push(` opsi saat itu: ${s2.options.map((o) => o.slice(0, 80)).join(" \xB7 ")}`);
|
|
25679
|
+
}
|
|
25680
|
+
lines2.push("");
|
|
25681
|
+
lines2.push("Pertanyaan di bawah adalah lanjutan dari rantai itu. Jangan bertentangan dengan yang sudah kamu putuskan di atas, dan jangan mengulang penjelasannya.");
|
|
25682
|
+
lines2.push("");
|
|
25683
|
+
}
|
|
25325
25684
|
lines2.push("## Yang harus kamu putuskan");
|
|
25326
25685
|
lines2.push(q.question.trim());
|
|
25327
25686
|
if (q.options?.length) {
|
|
@@ -25329,7 +25688,12 @@ function leadPrompt(q, c) {
|
|
|
25329
25688
|
lines2.push("Opsi yang dilihat peminta:");
|
|
25330
25689
|
for (const [i, o] of q.options.entries()) lines2.push(`${i + 1}. ${o}`);
|
|
25331
25690
|
lines2.push("");
|
|
25332
|
-
|
|
25691
|
+
if (c.select?.mode === "multi") {
|
|
25692
|
+
lines2.push(`Opsinya TIDAK saling eksklusif. Isi \`choices\` dengan daftar nomor atau label yang kamu pilih (mis. ["1","3"]) \u2014 paling sedikit ${c.select.min}, paling banyak ${c.select.max}. Jumlah di luar itu membuat SELURUH pilihanmu dibatalkan, bukan dipangkas.`);
|
|
25693
|
+
} else {
|
|
25694
|
+
lines2.push('Salah satu dari daftar itu WAJIB kamu pilih lewat field `choice` \u2014 isi nomornya ("2") atau labelnya persis.');
|
|
25695
|
+
}
|
|
25696
|
+
lines2.push("Pilihan di luar daftar ditolak server, dicatat sebagai penolakan, dan peminta kembali menunggu manusia.");
|
|
25333
25697
|
}
|
|
25334
25698
|
lines2.push("");
|
|
25335
25699
|
lines2.push("## Batas waktu (BACA INI DULU)");
|
|
@@ -25356,6 +25720,7 @@ function leadPrompt(q, c) {
|
|
|
25356
25720
|
lines2.push(JSON.stringify({
|
|
25357
25721
|
decision: "keputusan yang dipilih, satu kalimat",
|
|
25358
25722
|
choice: "nomor atau label opsi yang kamu pilih; kosongkan bila tak ada daftar opsi",
|
|
25723
|
+
choices: [],
|
|
25359
25724
|
reason: "alasannya, dua-tiga kalimat, menyebut bukti",
|
|
25360
25725
|
refs: ["internal/docs/\u2026", "ADR-00xx"],
|
|
25361
25726
|
confidence: "tinggi | sedang | ragu",
|
|
@@ -25414,6 +25779,7 @@ function keepExistingRefs(refs2, repoDir) {
|
|
|
25414
25779
|
|
|
25415
25780
|
// src/services/lead/trail.ts
|
|
25416
25781
|
init_db();
|
|
25782
|
+
init_src();
|
|
25417
25783
|
async function recordDecision(i) {
|
|
25418
25784
|
return prisma.leadDecision.create({
|
|
25419
25785
|
data: {
|
|
@@ -25436,6 +25802,11 @@ async function recordDecision(i) {
|
|
|
25436
25802
|
// null eksplisit, sementara "tak disebut" pada create sudah berarti NULL di baris barunya.
|
|
25437
25803
|
options: i.options?.length ? i.options : void 0,
|
|
25438
25804
|
missing: i.missing?.length ? i.missing : void 0,
|
|
25805
|
+
// SPEC-485 · daftar kosong disimpan NULL dengan alasan yang sama seperti `options` di atas.
|
|
25806
|
+
choices: i.choices?.length ? i.choices : void 0,
|
|
25807
|
+
select: i.select ?? void 0,
|
|
25808
|
+
flowId: i.flowId ?? null,
|
|
25809
|
+
step: i.step ?? null,
|
|
25439
25810
|
status: i.status ?? "berlaku",
|
|
25440
25811
|
weighty: i.weighty ?? false,
|
|
25441
25812
|
actor: i.actor ?? "lead"
|
|
@@ -25448,16 +25819,21 @@ async function listDecisions(f = {}) {
|
|
|
25448
25819
|
...f.projectId ? { projectId: f.projectId } : {},
|
|
25449
25820
|
...f.specId ? { specId: f.specId } : {},
|
|
25450
25821
|
...f.sessionId ? { sessionId: f.sessionId } : {},
|
|
25822
|
+
...f.flowId ? { flowId: f.flowId } : {},
|
|
25451
25823
|
...f.status ? { status: f.status } : {}
|
|
25452
25824
|
},
|
|
25453
|
-
|
|
25825
|
+
// SPEC-485 · satu RANTAI dibaca dari awal: urutan pertanyaannya adalah isi jejaknya. Daftar
|
|
25826
|
+
// umum tetap terbaru-dulu (AC-24) — dua pertanyaan yang berbeda, dua urutan yang berbeda.
|
|
25827
|
+
orderBy: { createdAt: f.flowId ? "asc" : "desc" },
|
|
25454
25828
|
take: Math.min(f.take ?? 50, 200),
|
|
25455
25829
|
skip: f.skip ?? 0
|
|
25456
25830
|
});
|
|
25457
25831
|
}
|
|
25458
|
-
async function overrideDecision(id, answer, reason) {
|
|
25832
|
+
async function overrideDecision(id, answer, reason, rawChoices = []) {
|
|
25459
25833
|
const old = await prisma.leadDecision.findUnique({ where: { id } });
|
|
25460
25834
|
if (!old || old.status !== "berlaku") return null;
|
|
25835
|
+
const options2 = Array.isArray(old.options) ? old.options.map(String) : [];
|
|
25836
|
+
const { choices } = resolveChoices(rawChoices, options2);
|
|
25461
25837
|
const next = await recordDecision({
|
|
25462
25838
|
projectId: old.projectId,
|
|
25463
25839
|
specId: old.specId,
|
|
@@ -25470,7 +25846,14 @@ async function overrideDecision(id, answer, reason) {
|
|
|
25470
25846
|
refs: [],
|
|
25471
25847
|
confidence: "tinggi",
|
|
25472
25848
|
action: "none",
|
|
25473
|
-
actor: "operator"
|
|
25849
|
+
actor: "operator",
|
|
25850
|
+
choices,
|
|
25851
|
+
choice: choices[0]?.option ?? null,
|
|
25852
|
+
choiceIndex: choices[0]?.index ?? null,
|
|
25853
|
+
options: options2,
|
|
25854
|
+
select: old.select,
|
|
25855
|
+
flowId: old.flowId,
|
|
25856
|
+
step: old.step
|
|
25474
25857
|
});
|
|
25475
25858
|
const updated = await prisma.leadDecision.update({
|
|
25476
25859
|
where: { id },
|
|
@@ -25502,6 +25885,13 @@ function toDecisionView(r) {
|
|
|
25502
25885
|
choiceIndex: r.choiceIndex,
|
|
25503
25886
|
options: Array.isArray(r.options) ? r.options.map(String) : [],
|
|
25504
25887
|
missing: Array.isArray(r.missing) ? r.missing.map(String) : [],
|
|
25888
|
+
// SPEC-485 · ADR-0102 · jawaban SELALU daftar di permukaan baca. Baris pra-migrasi tak punya
|
|
25889
|
+
// kolomnya, jadi ia DITURUNKAN dari pasangan skalar lama — itulah yang membuat riwayat lama
|
|
25890
|
+
// tetap terbaca sesudah perubahan skema, tanpa satu pun backfill.
|
|
25891
|
+
choices: Array.isArray(r.choices) ? r.choices.map((c) => ({ index: Number(c?.index ?? 0) || 1, option: String(c?.option ?? "") })).filter((c) => c.option !== "") : r.choice ? [{ index: r.choiceIndex ?? 1, option: r.choice }] : [],
|
|
25892
|
+
select: r.select ?? null,
|
|
25893
|
+
flowId: r.flowId,
|
|
25894
|
+
step: r.step,
|
|
25505
25895
|
status: r.status,
|
|
25506
25896
|
weighty: r.weighty,
|
|
25507
25897
|
supersededById: r.supersededById,
|
|
@@ -25629,6 +26019,98 @@ async function runPool(items, limit, fn) {
|
|
|
25629
26019
|
await Promise.all(Array.from({ length: workers }, worker));
|
|
25630
26020
|
}
|
|
25631
26021
|
|
|
26022
|
+
// src/services/lead/flow.ts
|
|
26023
|
+
init_src();
|
|
26024
|
+
init_db();
|
|
26025
|
+
var LeadFlowClosedError = class extends Error {
|
|
26026
|
+
constructor(flowId, flowStatus) {
|
|
26027
|
+
super(`rantai keputusan ${flowId} sudah ${flowStatus === "tak-ada" ? "tidak ada" : flowStatus}`);
|
|
26028
|
+
this.flowId = flowId;
|
|
26029
|
+
this.flowStatus = flowStatus;
|
|
26030
|
+
this.name = "LeadFlowClosedError";
|
|
26031
|
+
}
|
|
26032
|
+
};
|
|
26033
|
+
var isOpen = (s2) => LEAD_FLOW_OPEN.includes(s2);
|
|
26034
|
+
var flowTitle = (q) => q.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
26035
|
+
async function openFlow(i) {
|
|
26036
|
+
return prisma.leadFlow.create({
|
|
26037
|
+
data: {
|
|
26038
|
+
projectId: i.projectId,
|
|
26039
|
+
specId: i.specId ?? null,
|
|
26040
|
+
sessionId: i.sessionId ?? null,
|
|
26041
|
+
gate: i.gate,
|
|
26042
|
+
title: flowTitle(i.title),
|
|
26043
|
+
expiresAt: new Date(Date.now() + Math.max(1, i.ttlMin) * 6e4)
|
|
26044
|
+
}
|
|
26045
|
+
});
|
|
26046
|
+
}
|
|
26047
|
+
async function joinFlow(id) {
|
|
26048
|
+
const row = await prisma.leadFlow.findUnique({ where: { id } });
|
|
26049
|
+
if (!row) throw new LeadFlowClosedError(id, "tak-ada");
|
|
26050
|
+
if (!isOpen(row.status)) throw new LeadFlowClosedError(id, row.status);
|
|
26051
|
+
return row;
|
|
26052
|
+
}
|
|
26053
|
+
async function markFlowStep(id, answered) {
|
|
26054
|
+
const row = await prisma.leadFlow.findUnique({ where: { id } });
|
|
26055
|
+
if (!row || !isOpen(row.status)) return;
|
|
26056
|
+
await prisma.leadFlow.update({
|
|
26057
|
+
where: { id },
|
|
26058
|
+
data: {
|
|
26059
|
+
steps: { increment: 1 },
|
|
26060
|
+
...answered && row.status === "menunggu" ? { status: "sebagian" } : {}
|
|
26061
|
+
}
|
|
26062
|
+
});
|
|
26063
|
+
}
|
|
26064
|
+
async function closeFlow(id, reason) {
|
|
26065
|
+
const row = await prisma.leadFlow.findUnique({ where: { id } });
|
|
26066
|
+
if (!row || !isOpen(row.status)) return null;
|
|
26067
|
+
const status = reason === "operator" || reason === "kedaluwarsa" ? "dibatalkan" : "selesai";
|
|
26068
|
+
return prisma.leadFlow.update({
|
|
26069
|
+
where: { id },
|
|
26070
|
+
data: { status, closeReason: reason, closedAt: /* @__PURE__ */ new Date() }
|
|
26071
|
+
});
|
|
26072
|
+
}
|
|
26073
|
+
async function listFlows(f = {}) {
|
|
26074
|
+
return prisma.leadFlow.findMany({
|
|
26075
|
+
where: {
|
|
26076
|
+
...f.projectId ? { projectId: f.projectId } : {},
|
|
26077
|
+
...f.status ? { status: f.status } : {}
|
|
26078
|
+
},
|
|
26079
|
+
orderBy: { createdAt: "desc" },
|
|
26080
|
+
take: Math.min(f.take ?? 50, 200),
|
|
26081
|
+
skip: f.skip ?? 0
|
|
26082
|
+
});
|
|
26083
|
+
}
|
|
26084
|
+
async function expireFlows(now) {
|
|
26085
|
+
const due = await prisma.leadFlow.findMany({
|
|
26086
|
+
where: { status: { in: [...LEAD_FLOW_OPEN] }, expiresAt: { lt: now } },
|
|
26087
|
+
orderBy: { createdAt: "asc" },
|
|
26088
|
+
take: 100
|
|
26089
|
+
});
|
|
26090
|
+
const out3 = [];
|
|
26091
|
+
for (const f of due) {
|
|
26092
|
+
const closed = await closeFlow(f.id, "kedaluwarsa");
|
|
26093
|
+
if (closed) out3.push(closed);
|
|
26094
|
+
}
|
|
26095
|
+
return out3;
|
|
26096
|
+
}
|
|
26097
|
+
function toFlowView(r) {
|
|
26098
|
+
return {
|
|
26099
|
+
id: r.id,
|
|
26100
|
+
projectId: r.projectId,
|
|
26101
|
+
specId: r.specId,
|
|
26102
|
+
sessionId: r.sessionId,
|
|
26103
|
+
gate: r.gate,
|
|
26104
|
+
status: r.status,
|
|
26105
|
+
title: r.title,
|
|
26106
|
+
steps: r.steps,
|
|
26107
|
+
closeReason: r.closeReason,
|
|
26108
|
+
openedAt: r.openedAt.toISOString(),
|
|
26109
|
+
closedAt: r.closedAt ? r.closedAt.toISOString() : null,
|
|
26110
|
+
expiresAt: r.expiresAt.toISOString()
|
|
26111
|
+
};
|
|
26112
|
+
}
|
|
26113
|
+
|
|
25632
26114
|
// src/services/lead/decide.ts
|
|
25633
26115
|
var prodDecideDeps = {
|
|
25634
26116
|
think,
|
|
@@ -25652,6 +26134,15 @@ var notifTitle = (kind, question, answer, confidence) => {
|
|
|
25652
26134
|
async function decide(req, deps = prodDecideDeps) {
|
|
25653
26135
|
const cfg = await getLead();
|
|
25654
26136
|
if (!leadActive(cfg, req.projectId)) return null;
|
|
26137
|
+
const flow = req.flowId ? await joinFlow(req.flowId) : await openFlow({
|
|
26138
|
+
projectId: req.projectId,
|
|
26139
|
+
specId: req.specId,
|
|
26140
|
+
sessionId: req.sessionId,
|
|
26141
|
+
gate: req.gate,
|
|
26142
|
+
title: req.question,
|
|
26143
|
+
ttlMin: cfg.flowTtlMin
|
|
26144
|
+
});
|
|
26145
|
+
const step = flow.steps + 1;
|
|
25655
26146
|
const project = await prisma.project.findUnique({ where: { id: req.projectId }, select: { name: true } });
|
|
25656
26147
|
const spec = req.specId ? await prisma.spec.findUnique({
|
|
25657
26148
|
where: { id: req.specId },
|
|
@@ -25663,6 +26154,9 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25663
26154
|
orderBy: { createdAt: "desc" },
|
|
25664
26155
|
take: 10
|
|
25665
26156
|
});
|
|
26157
|
+
const chainRows = req.flowId ? await prisma.leadDecision.findMany({ where: { flowId: flow.id }, orderBy: { createdAt: "asc" }, take: 10 }) : [];
|
|
26158
|
+
const options2 = req.options ?? [];
|
|
26159
|
+
const bounds = normalizeSelect(req.select ?? { mode: "single", min: 0, max: null }, options2.length);
|
|
25666
26160
|
const ctx = {
|
|
25667
26161
|
projectId: req.projectId,
|
|
25668
26162
|
projectName: project?.name ?? req.projectId,
|
|
@@ -25679,7 +26173,13 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25679
26173
|
reason: d.reason,
|
|
25680
26174
|
createdAt: d.createdAt.toISOString()
|
|
25681
26175
|
})),
|
|
25682
|
-
notes: req.notes
|
|
26176
|
+
notes: req.notes,
|
|
26177
|
+
select: bounds,
|
|
26178
|
+
chainSteps: chainRows.map((d) => ({
|
|
26179
|
+
question: d.question,
|
|
26180
|
+
options: Array.isArray(d.options) ? d.options.map(String) : [],
|
|
26181
|
+
picked: Array.isArray(d.choices) ? d.choices.map((c) => String(c?.option ?? "")).filter(Boolean) : d.choice ? [d.choice] : []
|
|
26182
|
+
}))
|
|
25683
26183
|
};
|
|
25684
26184
|
const { agent, model, effort } = await deps.defaults();
|
|
25685
26185
|
const prompt = leadPrompt({ kind: req.kind, question: req.question, options: req.options }, ctx);
|
|
@@ -25698,24 +26198,30 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25698
26198
|
}
|
|
25699
26199
|
});
|
|
25700
26200
|
} catch (e) {
|
|
25701
|
-
if (e instanceof LeadBusyError)
|
|
25702
|
-
|
|
26201
|
+
if (e instanceof LeadBusyError) {
|
|
26202
|
+
if (!req.flowId) await closeFlow(flow.id, "kedaluwarsa").catch(() => null);
|
|
26203
|
+
throw e;
|
|
26204
|
+
}
|
|
26205
|
+
return closeAfter(await fail(req, deps, `lead tak menghasilkan keputusan: ${e.message}`, flow.id, step), flow.id, req.chain);
|
|
25703
26206
|
} finally {
|
|
25704
26207
|
if (req.sessionId) clearQueued(req.sessionId);
|
|
25705
26208
|
}
|
|
25706
26209
|
const verdict = parseLeadVerdict(raw);
|
|
25707
|
-
if (!verdict) return fail(req, deps, "keluaran lead tak memuat blok json keputusan yang sah");
|
|
26210
|
+
if (!verdict) return closeAfter(await fail(req, deps, "keluaran lead tak memuat blok json keputusan yang sah", flow.id, step), flow.id, req.chain);
|
|
25708
26211
|
const refs2 = keepExistingRefs(verdict.refs, repoDir);
|
|
25709
26212
|
const allowed = leadActionAllowed(verdict.action);
|
|
25710
26213
|
const kind = allowed ? req.kind : "refusal";
|
|
25711
|
-
const
|
|
25712
|
-
const
|
|
25713
|
-
const
|
|
26214
|
+
const rawChoices = verdict.choices.length ? verdict.choices : verdict.choice.trim() ? [verdict.choice] : [];
|
|
26215
|
+
const resolved = resolveChoices(rawChoices, options2);
|
|
26216
|
+
const countProblem = options2.length && resolved.choices.length ? checkChoiceCount(resolved.choices.length, bounds) : null;
|
|
26217
|
+
const choices = countProblem ? [] : resolved.choices;
|
|
26218
|
+
const choice = choices[0] ?? null;
|
|
26219
|
+
const choiceRejected = options2.length > 0 && rawChoices.length > 0 && (resolved.rejected.length > 0 || !!countProblem);
|
|
25714
26220
|
const missing = verdict.missing.map((m) => m.trim()).filter(Boolean);
|
|
25715
26221
|
let action = allowed ? verdict.action : "none";
|
|
25716
26222
|
let actionNote = "";
|
|
25717
26223
|
let conflict = false;
|
|
25718
|
-
if (allowed && choice) {
|
|
26224
|
+
if (allowed && choices.length === 1 && choice) {
|
|
25719
26225
|
const hint = optionActionHint(choice.option);
|
|
25720
26226
|
if (hint && action === "none" && hint !== "none") {
|
|
25721
26227
|
action = hint;
|
|
@@ -25729,7 +26235,10 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25729
26235
|
const confidence = missing.length ? "ragu" : verdict.confidence;
|
|
25730
26236
|
const notes = [];
|
|
25731
26237
|
if (!allowed) notes.push(`DITOLAK: ${leadRefusalReason(verdict.action)} berada di luar permukaan tindakan lead (ADR-0091 \xB7 AC-31/32).`);
|
|
25732
|
-
if (
|
|
26238
|
+
if (options2.length && resolved.rejected.length)
|
|
26239
|
+
notes.push(`DITOLAK: pilihan ${resolved.rejected.map((r) => `"${r.slice(0, 120)}"`).join(", ")} tidak ada di daftar opsi yang dikirim peminta (SPEC-480 \xB7 ADR-0098).`);
|
|
26240
|
+
if (countProblem)
|
|
26241
|
+
notes.push(`DITOLAK: ${countProblem} (SPEC-485 \xB7 ADR-0102) \u2014 seluruh pilihan dibatalkan, bukan dipangkas.`);
|
|
25733
26242
|
if (actionNote) notes.push(actionNote);
|
|
25734
26243
|
if (missing.length) notes.push(`KONTEKS KURANG: ${missing.join("; ")}`);
|
|
25735
26244
|
const tail3 = notes.length ? `
|
|
@@ -25754,9 +26263,14 @@ ${notes.join("\n\n")}` : "";
|
|
|
25754
26263
|
weighty,
|
|
25755
26264
|
choice: choice?.option ?? null,
|
|
25756
26265
|
choiceIndex: choice?.index ?? null,
|
|
26266
|
+
choices,
|
|
26267
|
+
select: bounds,
|
|
26268
|
+
flowId: flow.id,
|
|
26269
|
+
step,
|
|
25757
26270
|
options: options2,
|
|
25758
26271
|
missing
|
|
25759
26272
|
});
|
|
26273
|
+
await closeAfter(row, flow.id, req.chain);
|
|
25760
26274
|
if (weighty) {
|
|
25761
26275
|
await deps.notify(
|
|
25762
26276
|
row.id,
|
|
@@ -25770,18 +26284,24 @@ ${notes.join("\n\n")}` : "";
|
|
|
25770
26284
|
decision: clampProse(verdict.decision, LEAD_DECISION_MAX),
|
|
25771
26285
|
reason: `${clampProse(verdict.reason, LEAD_REASON_MAX)}${tail3}`,
|
|
25772
26286
|
reply: verdict.reply,
|
|
26287
|
+
choices,
|
|
25773
26288
|
choice,
|
|
25774
26289
|
missing
|
|
25775
26290
|
});
|
|
25776
26291
|
return row;
|
|
25777
26292
|
}
|
|
26293
|
+
async function closeAfter(row, flowId, chain) {
|
|
26294
|
+
await markFlowStep(flowId, row.status === "berlaku");
|
|
26295
|
+
if (!chain) await closeFlow(flowId, "tunggal");
|
|
26296
|
+
return row;
|
|
26297
|
+
}
|
|
25778
26298
|
var lastDelivery = /* @__PURE__ */ new Map();
|
|
25779
26299
|
function takeDelivery(decisionId) {
|
|
25780
26300
|
const v = lastDelivery.get(decisionId) ?? null;
|
|
25781
26301
|
lastDelivery.delete(decisionId);
|
|
25782
26302
|
return v;
|
|
25783
26303
|
}
|
|
25784
|
-
async function fail(req, deps, reason) {
|
|
26304
|
+
async function fail(req, deps, reason, flowId = null, step = null) {
|
|
25785
26305
|
const prev = await prisma.leadDecision.findFirst({
|
|
25786
26306
|
where: { projectId: req.projectId, gate: req.gate, kind: req.kind },
|
|
25787
26307
|
orderBy: { createdAt: "desc" },
|
|
@@ -25799,6 +26319,10 @@ async function fail(req, deps, reason) {
|
|
|
25799
26319
|
refs: [],
|
|
25800
26320
|
confidence: "ragu",
|
|
25801
26321
|
action: "none",
|
|
26322
|
+
// SPEC-485 · langkah yang gagal tetap duduk di alurnya: alur yang seluruh langkahnya gagal
|
|
26323
|
+
// harus tetap terbaca sebagai satu urusan, bukan sebagai baris yatim.
|
|
26324
|
+
flowId,
|
|
26325
|
+
step,
|
|
25802
26326
|
status: "gagal",
|
|
25803
26327
|
weighty: true
|
|
25804
26328
|
});
|
|
@@ -26044,7 +26568,7 @@ var prodDetectDeps = {
|
|
|
26044
26568
|
return true;
|
|
26045
26569
|
}
|
|
26046
26570
|
},
|
|
26047
|
-
send: (id, text) => sendToPane(id, text),
|
|
26571
|
+
send: (id, text, choices) => sendToPane(id, text, 50, choices),
|
|
26048
26572
|
clearMarker: (file) => {
|
|
26049
26573
|
try {
|
|
26050
26574
|
writeFileSync5(file, "");
|
|
@@ -26208,7 +26732,8 @@ async function runChain(s2, agent, deps) {
|
|
|
26208
26732
|
return { acted, done: false, failed: true, reason: "lead tak menghasilkan keputusan yang berlaku" };
|
|
26209
26733
|
const sent = deps.delivery(row.id);
|
|
26210
26734
|
const reply = (sent ? leadReplyText(sent) : "") || row.answer;
|
|
26211
|
-
|
|
26735
|
+
const picked = sent?.choices.map((c) => c.option) ?? [];
|
|
26736
|
+
if (!await deps.send(s2.id, reply, picked))
|
|
26212
26737
|
return { acted, done: false, failed: true, reason: "gagal mengetik ke pane" };
|
|
26213
26738
|
acted = true;
|
|
26214
26739
|
if (!screen) return { acted, done: true, failed: false, reason: "" };
|
|
@@ -26531,6 +27056,21 @@ async function tick2(now, deps = {}) {
|
|
|
26531
27056
|
busyDetect = false;
|
|
26532
27057
|
}));
|
|
26533
27058
|
}
|
|
27059
|
+
jobs.push((async () => {
|
|
27060
|
+
const expire = deps.expire ?? expireFlows;
|
|
27061
|
+
const notify = deps.notify ?? recordLeadDecision;
|
|
27062
|
+
for (const f of await expire(/* @__PURE__ */ new Date())) {
|
|
27063
|
+
await notify(
|
|
27064
|
+
f.id,
|
|
27065
|
+
`Rantai keputusan lead ditutup karena kedaluwarsa: ${f.title.slice(0, 80)}`,
|
|
27066
|
+
f.projectId,
|
|
27067
|
+
f.specId,
|
|
27068
|
+
f.sessionId
|
|
27069
|
+
);
|
|
27070
|
+
}
|
|
27071
|
+
})().catch((e) => {
|
|
27072
|
+
console.error("lead expire:", e);
|
|
27073
|
+
}));
|
|
26534
27074
|
if (!cfg.paused && !busyPulse && now - Math.max(lastPulseAt, pulseEndedAt) >= cfg.everyMin * 6e4) {
|
|
26535
27075
|
busyPulse = true;
|
|
26536
27076
|
lastPulseAt = now;
|
|
@@ -26618,11 +27158,35 @@ async function lead_default(app2) {
|
|
|
26618
27158
|
specId: q.specId,
|
|
26619
27159
|
sessionId: q.sessionId,
|
|
26620
27160
|
status: q.status,
|
|
27161
|
+
// SPEC-485 · satu rantai dibaca lewat filter ini, urut NAIK (lihat `listDecisions`).
|
|
27162
|
+
flowId: q.flowId,
|
|
26621
27163
|
take: q.take ? Number(q.take) : void 0,
|
|
26622
27164
|
skip: q.skip ? Number(q.skip) : void 0
|
|
26623
27165
|
});
|
|
26624
27166
|
return { items: rows.map(toDecisionView) };
|
|
26625
27167
|
});
|
|
27168
|
+
app2.get("/lead/flows", async (req) => {
|
|
27169
|
+
const q = req.query;
|
|
27170
|
+
const rows = await listFlows({
|
|
27171
|
+
projectId: q.projectId,
|
|
27172
|
+
status: q.status,
|
|
27173
|
+
take: q.take ? Number(q.take) : void 0,
|
|
27174
|
+
skip: q.skip ? Number(q.skip) : void 0
|
|
27175
|
+
});
|
|
27176
|
+
return { items: rows.map(toFlowView) };
|
|
27177
|
+
});
|
|
27178
|
+
app2.post("/lead/flows/:id/submit", async (req, reply) => {
|
|
27179
|
+
const { id } = req.params;
|
|
27180
|
+
const row = await closeFlow(id, "submit");
|
|
27181
|
+
if (!row) return reply.code(409).send({ error: "rantai tak ada atau sudah tertutup" });
|
|
27182
|
+
return toFlowView(row);
|
|
27183
|
+
});
|
|
27184
|
+
app2.post("/lead/flows/:id/cancel", async (req, reply) => {
|
|
27185
|
+
const { id } = req.params;
|
|
27186
|
+
const row = await closeFlow(id, "operator");
|
|
27187
|
+
if (!row) return reply.code(409).send({ error: "rantai tak ada atau sudah tertutup" });
|
|
27188
|
+
return toFlowView(row);
|
|
27189
|
+
});
|
|
26626
27190
|
app2.post("/lead/decisions", async (req, reply) => {
|
|
26627
27191
|
const parsed = zLeadAsk.safeParse(req.body);
|
|
26628
27192
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
@@ -26632,6 +27196,15 @@ async function lead_default(app2) {
|
|
|
26632
27196
|
const cfg = await getLead();
|
|
26633
27197
|
if (!project.leadOptIn || !leadActive(cfg, ask.projectId))
|
|
26634
27198
|
return reply.code(409).send({ error: "lead tidak aktif untuk project ini" });
|
|
27199
|
+
const optionCount = ask.options.length;
|
|
27200
|
+
if (ask.select.mode === "multi" && optionCount === 0)
|
|
27201
|
+
return reply.code(400).send({ error: "select.mode `multi` menuntut daftar `options`" });
|
|
27202
|
+
if (ask.select.max !== null && ask.select.min > ask.select.max)
|
|
27203
|
+
return reply.code(400).send({ error: "select.min melebihi select.max" });
|
|
27204
|
+
if (ask.select.max !== null && optionCount > 0 && ask.select.max > optionCount)
|
|
27205
|
+
return reply.code(400).send({ error: `select.max (${ask.select.max}) melebihi jumlah opsi (${optionCount})` });
|
|
27206
|
+
if (ask.select.min > optionCount)
|
|
27207
|
+
return reply.code(400).send({ error: `select.min (${ask.select.min}) melebihi jumlah opsi (${optionCount})` });
|
|
26635
27208
|
let row;
|
|
26636
27209
|
try {
|
|
26637
27210
|
row = await decide({
|
|
@@ -26642,9 +27215,13 @@ async function lead_default(app2) {
|
|
|
26642
27215
|
kind: "answer",
|
|
26643
27216
|
question: ask.question,
|
|
26644
27217
|
options: ask.options,
|
|
26645
|
-
notes: ask.context ? [ask.context] : void 0
|
|
27218
|
+
notes: ask.context ? [ask.context] : void 0,
|
|
27219
|
+
select: ask.select,
|
|
27220
|
+
chain: ask.chain,
|
|
27221
|
+
flowId: ask.flowId ?? null
|
|
26646
27222
|
});
|
|
26647
27223
|
} catch (e) {
|
|
27224
|
+
if (e instanceof LeadFlowClosedError) return reply.code(409).send({ error: e.message });
|
|
26648
27225
|
if (!(e instanceof LeadBusyError)) throw e;
|
|
26649
27226
|
return reply.code(503).header("retry-after", String(Math.max(5, cfg.queueWaitSec))).send({ error: e.message, retryable: true, queued: e.queued });
|
|
26650
27227
|
}
|
|
@@ -26657,6 +27234,7 @@ async function lead_default(app2) {
|
|
|
26657
27234
|
} catch {
|
|
26658
27235
|
}
|
|
26659
27236
|
}
|
|
27237
|
+
const flowStatus = row.flowId ? (await prisma.leadFlow.findUnique({ where: { id: row.flowId }, select: { status: true } }))?.status ?? null : null;
|
|
26660
27238
|
const answer = {
|
|
26661
27239
|
id: row.id,
|
|
26662
27240
|
decision: sent?.decision ?? row.answer,
|
|
@@ -26666,7 +27244,12 @@ async function lead_default(app2) {
|
|
|
26666
27244
|
action: row.action,
|
|
26667
27245
|
// Saluran pengiriman bisa meleset (baris lahir dari jalur lain); kolomnya yang selalu ada.
|
|
26668
27246
|
choice: sent?.choice ?? (row.choice ? { index: row.choiceIndex ?? 1, option: row.choice } : null),
|
|
26669
|
-
missing: sent?.missing ?? (Array.isArray(row.missing) ? row.missing.map(String) : [])
|
|
27247
|
+
missing: sent?.missing ?? (Array.isArray(row.missing) ? row.missing.map(String) : []),
|
|
27248
|
+
// SPEC-485 · `choices` bentuk yang berlaku; `toDecisionView` sudah tahu cara menurunkannya
|
|
27249
|
+
// dari kolom lama, jadi pemetaannya tak diduplikasi di sini (kelas bug SPEC-431/448/475).
|
|
27250
|
+
choices: sent?.choices ?? toDecisionView(row).choices,
|
|
27251
|
+
flowId: row.flowId,
|
|
27252
|
+
flowStatus
|
|
26670
27253
|
};
|
|
26671
27254
|
return reply.code(201).send(answer);
|
|
26672
27255
|
});
|
|
@@ -26674,12 +27257,17 @@ async function lead_default(app2) {
|
|
|
26674
27257
|
const { id } = req.params;
|
|
26675
27258
|
const parsed = zLeadOverride.safeParse(req.body);
|
|
26676
27259
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
26677
|
-
const r = await overrideDecision(id, parsed.data.answer, parsed.data.reason);
|
|
27260
|
+
const r = await overrideDecision(id, parsed.data.answer, parsed.data.reason, parsed.data.choices);
|
|
26678
27261
|
if (!r) return reply.code(409).send({ error: "keputusan tak ada atau sudah tak berlaku" });
|
|
26679
27262
|
let delivered = false;
|
|
26680
27263
|
if (r.next.sessionId) {
|
|
26681
27264
|
resetSession(r.next.sessionId);
|
|
26682
|
-
delivered = await sendToPane(
|
|
27265
|
+
delivered = await sendToPane(
|
|
27266
|
+
r.next.sessionId,
|
|
27267
|
+
parsed.data.answer,
|
|
27268
|
+
50,
|
|
27269
|
+
toDecisionView(r.next).choices.map((c) => c.option)
|
|
27270
|
+
).catch(() => false);
|
|
26683
27271
|
}
|
|
26684
27272
|
return { old: toDecisionView(r.old), next: toDecisionView(r.next), delivered };
|
|
26685
27273
|
});
|
|
@@ -26696,11 +27284,61 @@ async function lead_default(app2) {
|
|
|
26696
27284
|
init_src();
|
|
26697
27285
|
init_db();
|
|
26698
27286
|
|
|
27287
|
+
// src/services/agent-tool-catalog.ts
|
|
27288
|
+
init_src();
|
|
27289
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
27290
|
+
import { homedir as homedir8 } from "node:os";
|
|
27291
|
+
import { join as join15 } from "node:path";
|
|
27292
|
+
var home = () => process.env.HOME || homedir8();
|
|
27293
|
+
var readJson = (path) => {
|
|
27294
|
+
try {
|
|
27295
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
27296
|
+
} catch {
|
|
27297
|
+
return null;
|
|
27298
|
+
}
|
|
27299
|
+
};
|
|
27300
|
+
var serversOf = (node) => {
|
|
27301
|
+
const ms = node?.mcpServers;
|
|
27302
|
+
if (!ms || typeof ms !== "object" || Array.isArray(ms)) return [];
|
|
27303
|
+
return Object.keys(ms);
|
|
27304
|
+
};
|
|
27305
|
+
var codexServers = () => {
|
|
27306
|
+
let text;
|
|
27307
|
+
try {
|
|
27308
|
+
text = readFileSync12(join15(home(), ".codex", "config.toml"), "utf8");
|
|
27309
|
+
} catch {
|
|
27310
|
+
return [];
|
|
27311
|
+
}
|
|
27312
|
+
const out3 = [];
|
|
27313
|
+
for (const m of text.matchAll(/^\s*\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.[^\]]*)?\]/gm)) {
|
|
27314
|
+
const name2 = m[1] ?? m[2];
|
|
27315
|
+
if (name2) out3.push(name2);
|
|
27316
|
+
}
|
|
27317
|
+
return out3;
|
|
27318
|
+
};
|
|
27319
|
+
function mcpServerNames(repoDir) {
|
|
27320
|
+
const names = [];
|
|
27321
|
+
const claudeJson = readJson(join15(home(), ".claude.json"));
|
|
27322
|
+
names.push(...serversOf(claudeJson));
|
|
27323
|
+
if (repoDir) {
|
|
27324
|
+
const projects = claudeJson?.projects;
|
|
27325
|
+
if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
|
|
27326
|
+
names.push(...serversOf(readJson(join15(repoDir, ".mcp.json"))));
|
|
27327
|
+
}
|
|
27328
|
+
names.push(...codexServers());
|
|
27329
|
+
return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
27330
|
+
}
|
|
27331
|
+
function agentToolCatalog(repoDir) {
|
|
27332
|
+
return [ALL_TOOLS_ENTRY, ...BUILTIN_AGENT_TOOLS, ...mcpServerNames(repoDir).map(mcpToolEntry)];
|
|
27333
|
+
}
|
|
27334
|
+
var agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
|
|
27335
|
+
|
|
26699
27336
|
// src/services/custom-agents.ts
|
|
26700
27337
|
init_db();
|
|
26701
27338
|
init_src();
|
|
26702
27339
|
init_pty();
|
|
26703
27340
|
var cache4 = [];
|
|
27341
|
+
var repoDirCache = /* @__PURE__ */ new Map();
|
|
26704
27342
|
var asCustomAgent = (r) => ({
|
|
26705
27343
|
id: r.id,
|
|
26706
27344
|
projectId: r.projectId,
|
|
@@ -26710,6 +27348,7 @@ var asCustomAgent = (r) => ({
|
|
|
26710
27348
|
tools: toolsOf(r.tools),
|
|
26711
27349
|
model: r.model,
|
|
26712
27350
|
mentions: mentionsOf(r.mentions),
|
|
27351
|
+
runtime: runtimeOf(r.runtime),
|
|
26713
27352
|
enabled: r.enabled,
|
|
26714
27353
|
createdAt: "",
|
|
26715
27354
|
updatedAt: ""
|
|
@@ -26718,18 +27357,31 @@ var asCustomAgent = (r) => ({
|
|
|
26718
27357
|
async function loadCustomAgents() {
|
|
26719
27358
|
try {
|
|
26720
27359
|
cache4 = await prisma.customAgent.findMany();
|
|
27360
|
+
const projects = await prisma.project.findMany({ select: { id: true, repoDir: true } });
|
|
27361
|
+
const bindings = await prisma.localBinding.findMany({ select: { projectId: true, repoDir: true } });
|
|
27362
|
+
const next = /* @__PURE__ */ new Map();
|
|
27363
|
+
for (const p of projects) next.set(p.id, p.repoDir ?? null);
|
|
27364
|
+
for (const b of bindings) next.set(b.projectId, b.repoDir ?? null);
|
|
27365
|
+
repoDirCache = next;
|
|
26721
27366
|
} catch {
|
|
26722
27367
|
cache4 = [];
|
|
27368
|
+
repoDirCache = /* @__PURE__ */ new Map();
|
|
26723
27369
|
}
|
|
26724
27370
|
}
|
|
26725
|
-
function agentDefsFor(projectId) {
|
|
27371
|
+
function agentDefsFor(projectId, agent) {
|
|
26726
27372
|
const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
|
|
26727
27373
|
const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
|
|
26728
|
-
|
|
27374
|
+
const eff = effectiveAgents(globals, project).filter((a) => a.runtime === null || a.runtime === agent);
|
|
27375
|
+
const needsCatalog = eff.some((a) => (a.tools ?? []).includes(ALL_TOOLS));
|
|
27376
|
+
const catalogIds = needsCatalog ? agentToolIds(repoDirCache.get(projectId) ?? null) : [];
|
|
27377
|
+
return eff.map((a) => ({
|
|
26729
27378
|
name: a.name,
|
|
26730
27379
|
description: a.description,
|
|
26731
27380
|
instructions: a.instructions,
|
|
26732
|
-
|
|
27381
|
+
// Ekspansi terjadi DI SINI, sebelum `resolveTools` di runner: meneruskan `"*"` apa adanya
|
|
27382
|
+
// membuat claude membuangnya senyap (agen tanpa alat), sementara menerjemahkannya jadi `null`
|
|
27383
|
+
// membuat agen mewarisi SELURUH tool termasuk `Task` — lapis 2 anti-loop lenyap tanpa jejak.
|
|
27384
|
+
tools: expandTools(a.tools, catalogIds),
|
|
26733
27385
|
model: a.model,
|
|
26734
27386
|
mentions: a.mentions ?? []
|
|
26735
27387
|
}));
|
|
@@ -26753,7 +27405,7 @@ function unknownMentions(row, all) {
|
|
|
26753
27405
|
}
|
|
26754
27406
|
async function installCustomAgents() {
|
|
26755
27407
|
await loadCustomAgents();
|
|
26756
|
-
registerCustomAgentSource((projectId) => agentDefsFor(projectId));
|
|
27408
|
+
registerCustomAgentSource((projectId, agent) => agentDefsFor(projectId, agent));
|
|
26757
27409
|
}
|
|
26758
27410
|
|
|
26759
27411
|
// src/routes/custom-agents.ts
|
|
@@ -26767,10 +27419,33 @@ var view6 = (r, projectId) => ({
|
|
|
26767
27419
|
tools: toolsOf(r.tools),
|
|
26768
27420
|
model: r.model,
|
|
26769
27421
|
mentions: mentionsOf(r.mentions),
|
|
27422
|
+
runtime: runtimeOf(r.runtime),
|
|
26770
27423
|
enabled: r.enabled,
|
|
26771
27424
|
...projectId ? { inherited: r.projectId === null } : {}
|
|
26772
27425
|
});
|
|
27426
|
+
var repoDirOf2 = async (projectId) => projectId ? await resolveRepoDir(projectId) : null;
|
|
27427
|
+
function toolsProblem(tools, catalogIds) {
|
|
27428
|
+
if (!tools || tools.length === 0) return null;
|
|
27429
|
+
if (tools.includes(ALL_TOOLS) && tools.length > 1) {
|
|
27430
|
+
return { error: "pintasan * harus jadi satu-satunya pilihan tools", unknownTools: [] };
|
|
27431
|
+
}
|
|
27432
|
+
const unknownTools = tools.filter((t) => !catalogIds.includes(t));
|
|
27433
|
+
return unknownTools.length ? { error: "tool tak dikenal di mesin ini", unknownTools } : null;
|
|
27434
|
+
}
|
|
27435
|
+
function modelProblem(model, runtime) {
|
|
27436
|
+
if (!model) return null;
|
|
27437
|
+
const ok2 = modelsForRuntime(runtime).some((m) => m.id === model);
|
|
27438
|
+
return ok2 ? null : { error: "model tak dikenal untuk runtime ini", model, runtime };
|
|
27439
|
+
}
|
|
26773
27440
|
async function custom_agents_default(app2) {
|
|
27441
|
+
app2.get("/custom-agents/catalog", async (req) => {
|
|
27442
|
+
const projectId = req.query.projectId;
|
|
27443
|
+
return {
|
|
27444
|
+
tools: agentToolCatalog(await repoDirOf2(projectId)),
|
|
27445
|
+
models: modelsForRuntime(null),
|
|
27446
|
+
runtimes: AGENT_RUNTIMES.map((id) => ({ id, label: AGENT_RUNTIME_LABELS[id] }))
|
|
27447
|
+
};
|
|
27448
|
+
});
|
|
26774
27449
|
app2.get("/custom-agents", async (req) => {
|
|
26775
27450
|
const projectId = req.query.projectId;
|
|
26776
27451
|
const rows = await prisma.customAgent.findMany({
|
|
@@ -26789,6 +27464,10 @@ async function custom_agents_default(app2) {
|
|
|
26789
27464
|
const projectId = p.projectId ?? null;
|
|
26790
27465
|
if (projectId && !await prisma.project.findUnique({ where: { id: projectId } }))
|
|
26791
27466
|
return reply.code(400).send({ error: "project tak ditemukan", projectId });
|
|
27467
|
+
const tp = toolsProblem(p.tools ?? null, agentToolIds(await repoDirOf2(projectId)));
|
|
27468
|
+
if (tp) return reply.code(400).send(tp);
|
|
27469
|
+
const mp = modelProblem(p.model ?? null, p.runtime ?? null);
|
|
27470
|
+
if (mp) return reply.code(400).send(mp);
|
|
26792
27471
|
const id = customAgentId(projectId, p.name);
|
|
26793
27472
|
if (await prisma.customAgent.findUnique({ where: { id } }))
|
|
26794
27473
|
return reply.code(409).send({ error: "nama sudah dipakai di scope ini", id });
|
|
@@ -26801,6 +27480,7 @@ async function custom_agents_default(app2) {
|
|
|
26801
27480
|
tools: p.tools ?? null,
|
|
26802
27481
|
model: p.model ?? null,
|
|
26803
27482
|
mentions: p.mentions ?? [],
|
|
27483
|
+
runtime: p.runtime ?? null,
|
|
26804
27484
|
enabled: p.enabled ?? true
|
|
26805
27485
|
};
|
|
26806
27486
|
const all = [...await rowsOf(), candidate];
|
|
@@ -26817,6 +27497,7 @@ async function custom_agents_default(app2) {
|
|
|
26817
27497
|
tools: candidate.tools,
|
|
26818
27498
|
model: candidate.model,
|
|
26819
27499
|
mentions: candidate.mentions,
|
|
27500
|
+
runtime: candidate.runtime,
|
|
26820
27501
|
enabled: candidate.enabled
|
|
26821
27502
|
} });
|
|
26822
27503
|
await loadCustomAgents();
|
|
@@ -26833,11 +27514,24 @@ async function custom_agents_default(app2) {
|
|
|
26833
27514
|
const existing = await prisma.customAgent.findUnique({ where: { id } });
|
|
26834
27515
|
if (!existing) return reply.code(404).send({ error: "not found" });
|
|
26835
27516
|
const before = existing;
|
|
27517
|
+
const effRuntime = "runtime" in parsed.data ? parsed.data.runtime ?? null : runtimeOf(before.runtime);
|
|
27518
|
+
if (parsed.data.tools !== void 0) {
|
|
27519
|
+
const tp = toolsProblem(parsed.data.tools, agentToolIds(await repoDirOf2(before.projectId)));
|
|
27520
|
+
if (tp) return reply.code(400).send(tp);
|
|
27521
|
+
}
|
|
27522
|
+
if (parsed.data.model !== void 0 || "runtime" in parsed.data) {
|
|
27523
|
+
const mp = modelProblem(
|
|
27524
|
+
parsed.data.model !== void 0 ? parsed.data.model : before.model,
|
|
27525
|
+
effRuntime
|
|
27526
|
+
);
|
|
27527
|
+
if (mp) return reply.code(400).send(mp);
|
|
27528
|
+
}
|
|
26836
27529
|
const candidate = {
|
|
26837
27530
|
...before,
|
|
26838
27531
|
...parsed.data,
|
|
26839
27532
|
mentions: parsed.data.mentions ?? mentionsOf(before.mentions),
|
|
26840
|
-
tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before.tools)
|
|
27533
|
+
tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before.tools),
|
|
27534
|
+
runtime: effRuntime
|
|
26841
27535
|
};
|
|
26842
27536
|
const all = (await rowsOf()).map((r) => r.id === id ? candidate : r);
|
|
26843
27537
|
const unknown = unknownMentions(candidate, all);
|
|
@@ -26850,6 +27544,7 @@ async function custom_agents_default(app2) {
|
|
|
26850
27544
|
tools: candidate.tools,
|
|
26851
27545
|
model: candidate.model,
|
|
26852
27546
|
mentions: candidate.mentions,
|
|
27547
|
+
runtime: candidate.runtime,
|
|
26853
27548
|
enabled: candidate.enabled
|
|
26854
27549
|
} });
|
|
26855
27550
|
await loadCustomAgents();
|
|
@@ -26900,24 +27595,24 @@ function githubSlugFromUrl(url2) {
|
|
|
26900
27595
|
async function resolveGithubRepo(projectId) {
|
|
26901
27596
|
const project = await prisma.project.findUnique({ where: { id: projectId } });
|
|
26902
27597
|
if (!project) return { ok: false, kind: "no-project", error: `project "${projectId}" tidak ada` };
|
|
26903
|
-
const
|
|
26904
|
-
if (project.gitRemote)
|
|
27598
|
+
const candidates2 = [];
|
|
27599
|
+
if (project.gitRemote) candidates2.push(project.gitRemote);
|
|
26905
27600
|
const repoDir = await resolveRepoDir(projectId).catch(() => null);
|
|
26906
27601
|
if (repoDir) {
|
|
26907
27602
|
const origin = (await listRemotes(repoDir)).find((r) => r.name === "origin");
|
|
26908
|
-
if (origin?.fetch)
|
|
27603
|
+
if (origin?.fetch) candidates2.push(origin.fetch);
|
|
26909
27604
|
}
|
|
26910
|
-
if (
|
|
27605
|
+
if (candidates2.length === 0)
|
|
26911
27606
|
return {
|
|
26912
27607
|
ok: false,
|
|
26913
27608
|
kind: "no-remote",
|
|
26914
27609
|
error: "project belum punya remote GitHub (isi gitRemote atau tambahkan origin di repo lokalnya)"
|
|
26915
27610
|
};
|
|
26916
|
-
for (const url2 of
|
|
27611
|
+
for (const url2 of candidates2) {
|
|
26917
27612
|
const gh = githubSlugFromUrl(url2);
|
|
26918
27613
|
if (gh) return { ok: true, repo: gh };
|
|
26919
27614
|
}
|
|
26920
|
-
const host2 = parse2(
|
|
27615
|
+
const host2 = parse2(candidates2[0])?.host ?? candidates2[0];
|
|
26921
27616
|
return {
|
|
26922
27617
|
ok: false,
|
|
26923
27618
|
kind: "not-github",
|
|
@@ -27349,7 +28044,7 @@ async function testTelegramConnection(deps = {}) {
|
|
|
27349
28044
|
}
|
|
27350
28045
|
const timeoutMs = deps.timeoutMs ?? 1e4;
|
|
27351
28046
|
const controller = new AbortController();
|
|
27352
|
-
const
|
|
28047
|
+
const timer7 = setTimeout(() => controller.abort(), timeoutMs);
|
|
27353
28048
|
const send = deps.transport ?? ((url2, init) => fetch(url2, init));
|
|
27354
28049
|
const transport = (url2, init) => send(url2, { ...init, signal: controller.signal });
|
|
27355
28050
|
const client = new TelegramApiClient(botToken, transport);
|
|
@@ -27363,7 +28058,7 @@ async function testTelegramConnection(deps = {}) {
|
|
|
27363
28058
|
const message = controller.signal.aborted ? `Timeout ${timeoutMs} ms \u2014 Telegram tidak menjawab.` : raw;
|
|
27364
28059
|
return { ok: false, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
|
|
27365
28060
|
} finally {
|
|
27366
|
-
clearTimeout(
|
|
28061
|
+
clearTimeout(timer7);
|
|
27367
28062
|
}
|
|
27368
28063
|
}
|
|
27369
28064
|
function resolveTestChatId() {
|
|
@@ -27669,7 +28364,7 @@ async function sendOnce(o, deps = {}) {
|
|
|
27669
28364
|
nowSec: o.nowSec ?? Math.floor(Date.now() / 1e3)
|
|
27670
28365
|
});
|
|
27671
28366
|
const ac = new AbortController();
|
|
27672
|
-
const
|
|
28367
|
+
const timer7 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
|
|
27673
28368
|
try {
|
|
27674
28369
|
const f = deps.fetcher ?? ((u, init) => fetch(u, init));
|
|
27675
28370
|
const res = await f(parsed.url.toString(), { method: "POST", headers, body: o.body, signal: ac.signal });
|
|
@@ -27683,7 +28378,7 @@ async function sendOnce(o, deps = {}) {
|
|
|
27683
28378
|
const msg = e.name === "AbortError" ? `timeout ${WEBHOOK_TIMEOUT_MS} ms` : e.message;
|
|
27684
28379
|
return fail2(msg);
|
|
27685
28380
|
} finally {
|
|
27686
|
-
clearTimeout(
|
|
28381
|
+
clearTimeout(timer7);
|
|
27687
28382
|
}
|
|
27688
28383
|
}
|
|
27689
28384
|
|
|
@@ -28885,6 +29580,120 @@ function startWebhookEngine() {
|
|
|
28885
29580
|
}).catch((e) => console.error("webhook reset:", e));
|
|
28886
29581
|
}
|
|
28887
29582
|
|
|
29583
|
+
// src/services/auto-merge.ts
|
|
29584
|
+
init_src();
|
|
29585
|
+
init_db();
|
|
29586
|
+
import { execFile as execFile11 } from "node:child_process";
|
|
29587
|
+
import { promisify as promisify10 } from "node:util";
|
|
29588
|
+
var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
29589
|
+
var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
|
|
29590
|
+
var TICK_MS4 = 6e4;
|
|
29591
|
+
var exec8 = promisify10(execFile11);
|
|
29592
|
+
var GIT7 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
|
|
29593
|
+
var gitOut = async (cwd, args) => {
|
|
29594
|
+
try {
|
|
29595
|
+
return (await exec8("git", args, { cwd, ...GIT7 })).stdout.trim();
|
|
29596
|
+
} catch {
|
|
29597
|
+
return null;
|
|
29598
|
+
}
|
|
29599
|
+
};
|
|
29600
|
+
var prodAutoMergeDeps = {
|
|
29601
|
+
repoDir: resolveRepoDir,
|
|
29602
|
+
defaultBranch,
|
|
29603
|
+
// Tanpa `fetch`: `git push` dari worktree repo yang sama memperbarui remote-tracking ref
|
|
29604
|
+
// di repo itu juga, jadi origin/<branch> sudah mutakhir tepat setelah sesi mem-push.
|
|
29605
|
+
sourceTip: async (repoDir, branch) => await gitOut(repoDir, ["rev-parse", "--verify", "-q", "--end-of-options", `refs/remotes/origin/${branch}^{commit}`]) ?? await gitOut(repoDir, ["rev-parse", "--verify", "-q", "--end-of-options", `refs/heads/${branch}^{commit}`]),
|
|
29606
|
+
// `merge-base --is-ancestor` keluar 0/1 tanpa output → "" saat benar, null saat salah/galat.
|
|
29607
|
+
contains: async (repoDir, tip, sha) => await gitOut(repoDir, ["merge-base", "--is-ancestor", "--end-of-options", sha, tip]) !== null,
|
|
29608
|
+
integrate,
|
|
29609
|
+
discardWorktree: discardMergeWorktree,
|
|
29610
|
+
deleteBranch: deleteMergedBranch
|
|
29611
|
+
};
|
|
29612
|
+
async function candidates(now) {
|
|
29613
|
+
const since = new Date(now.getTime() - AUTO_MERGE_WINDOW_MS);
|
|
29614
|
+
const done = await prisma.notification.findMany({
|
|
29615
|
+
where: { type: "done", createdAt: { gte: since }, specId: { not: null } },
|
|
29616
|
+
select: { specId: true, projectId: true, createdAt: true }
|
|
29617
|
+
});
|
|
29618
|
+
if (!done.length) return [];
|
|
29619
|
+
const keys = done.map((d) => `automerge:${d.specId}`);
|
|
29620
|
+
const marked2 = new Set(
|
|
29621
|
+
(await prisma.notification.findMany({ where: { key: { in: keys } }, select: { key: true } })).map((n2) => n2.key)
|
|
29622
|
+
);
|
|
29623
|
+
return done.filter((d) => !marked2.has(`automerge:${d.specId}`)).map((d) => ({ specId: d.specId, projectId: d.projectId ?? "", doneAt: d.createdAt }));
|
|
29624
|
+
}
|
|
29625
|
+
async function sweepAutoMerge(deps = prodAutoMergeDeps, now = /* @__PURE__ */ new Date()) {
|
|
29626
|
+
const list2 = await candidates(now);
|
|
29627
|
+
if (!list2.length) return 0;
|
|
29628
|
+
let settled = 0;
|
|
29629
|
+
for (const c of list2) {
|
|
29630
|
+
try {
|
|
29631
|
+
if (await settleOne(c, deps, now)) settled++;
|
|
29632
|
+
} catch (e) {
|
|
29633
|
+
console.error(`auto-merge ${c.specId}:`, e);
|
|
29634
|
+
}
|
|
29635
|
+
}
|
|
29636
|
+
return settled;
|
|
29637
|
+
}
|
|
29638
|
+
async function settleOne(c, deps, now) {
|
|
29639
|
+
const spec = await prisma.spec.findUnique({ where: { id: c.specId } });
|
|
29640
|
+
if (!spec || spec.stage !== "done") return false;
|
|
29641
|
+
const project = await prisma.project.findUnique({ where: { id: spec.projectId } });
|
|
29642
|
+
if (!project) return false;
|
|
29643
|
+
const policy = resolveAutoMerge(
|
|
29644
|
+
project.autoMerge,
|
|
29645
|
+
spec.autoMerge
|
|
29646
|
+
);
|
|
29647
|
+
if (policy.mode === "off") return false;
|
|
29648
|
+
const report = (t) => recordAutoMerge(spec.id, spec.projectId, t).then(() => true);
|
|
29649
|
+
const repoDir = await deps.repoDir(spec.projectId);
|
|
29650
|
+
if (!repoDir)
|
|
29651
|
+
return report(`Auto-merge ${spec.id} dilewati \u2014 project belum di-bind ke checkout lokal`);
|
|
29652
|
+
const target2 = autoMergeTargetOf(
|
|
29653
|
+
policy,
|
|
29654
|
+
policy.mode === "default-branch" ? await deps.defaultBranch(repoDir) : null
|
|
29655
|
+
);
|
|
29656
|
+
if (!target2)
|
|
29657
|
+
return report(`Auto-merge ${spec.id} dilewati \u2014 default branch repo tak bisa diresolve; pilih branch tujuan di Settings project`);
|
|
29658
|
+
const branch = sourceBranch(spec.id);
|
|
29659
|
+
const tip = await deps.sourceTip(repoDir, branch);
|
|
29660
|
+
const ready = tip !== null && (spec.headSha === null || await deps.contains(repoDir, tip, spec.headSha));
|
|
29661
|
+
if (!ready) {
|
|
29662
|
+
if (now.getTime() - c.doneAt.getTime() <= AUTO_MERGE_GRACE_MS) return false;
|
|
29663
|
+
return report(`Auto-merge ${spec.id} dilewati \u2014 branch kerja \`${branch}\` belum ter-push ke origin`);
|
|
29664
|
+
}
|
|
29665
|
+
const res = await deps.integrate(repoDir, spec.id, "merge", target2);
|
|
29666
|
+
if (res.status === "clean") {
|
|
29667
|
+
if (policy.deleteBranch) await deps.deleteBranch(repoDir, branch).catch(() => {
|
|
29668
|
+
});
|
|
29669
|
+
return report(`Auto-merge ${spec.id} \u2192 ${target2} bersih (${res.detail})`);
|
|
29670
|
+
}
|
|
29671
|
+
if (res.status === "conflict") {
|
|
29672
|
+
await deps.discardWorktree(repoDir, res.worktree).catch(() => {
|
|
29673
|
+
});
|
|
29674
|
+
return report(`Auto-merge ${spec.id} \u2192 ${target2} GAGAL: konflik \u2014 branch kerja \`${branch}\` utuh, selesaikan lewat Rebase / Merge di backlog`);
|
|
29675
|
+
}
|
|
29676
|
+
return report(`Auto-merge ${spec.id} \u2192 ${target2} GAGAL: ${res.error}`);
|
|
29677
|
+
}
|
|
29678
|
+
var timer6;
|
|
29679
|
+
var busy3 = false;
|
|
29680
|
+
async function tick5() {
|
|
29681
|
+
if (busy3) return;
|
|
29682
|
+
busy3 = true;
|
|
29683
|
+
try {
|
|
29684
|
+
await sweepAutoMerge();
|
|
29685
|
+
} catch (e) {
|
|
29686
|
+
console.error("auto-merge sweep:", e);
|
|
29687
|
+
} finally {
|
|
29688
|
+
busy3 = false;
|
|
29689
|
+
}
|
|
29690
|
+
}
|
|
29691
|
+
function startAutoMerge() {
|
|
29692
|
+
if (timer6) return;
|
|
29693
|
+
timer6 = setInterval(() => void tick5(), TICK_MS4);
|
|
29694
|
+
timer6.unref();
|
|
29695
|
+
}
|
|
29696
|
+
|
|
28888
29697
|
// src/server.ts
|
|
28889
29698
|
var app = buildApp();
|
|
28890
29699
|
var port = Number(process.env.PORT ?? 8787);
|
|
@@ -28931,6 +29740,7 @@ app.listen({ port, host }).then(async () => {
|
|
|
28931
29740
|
startScheduler();
|
|
28932
29741
|
startLead();
|
|
28933
29742
|
startWebhookEngine();
|
|
29743
|
+
startAutoMerge();
|
|
28934
29744
|
}).catch((err) => {
|
|
28935
29745
|
console.error("listen gagal:", err);
|
|
28936
29746
|
process.exit(1);
|