hanoman 0.1.12 → 0.1.14
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 +192 -27
- package/dist/server.js +1172 -189
- package/docs/agent-integration.md +429 -0
- package/package.json +2 -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-CB1_4DSI.js} +1570 -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(),
|
|
@@ -5361,6 +5553,9 @@ var init_api = __esm({
|
|
|
5361
5553
|
limits: `${API}/limits`,
|
|
5362
5554
|
// SPEC-339 · versi codex CLI (peringatan lunak model GPT-5.6).
|
|
5363
5555
|
codexVersion: `${API}/codex/version`,
|
|
5556
|
+
// SPEC-489 · panduan AI agent — markdown MENTAH & PUBLIC (tanpa auth). Satu definisi URL untuk
|
|
5557
|
+
// klien web dan untuk tautan yang disalin operator ke agennya.
|
|
5558
|
+
agentDoc: `${API}/agent-integration.md`,
|
|
5364
5559
|
docs: (id) => `${API}/projects/${id}/docs`,
|
|
5365
5560
|
docFile: (id, path) => `${API}/projects/${id}/docs/${path}`,
|
|
5366
5561
|
// SPEC-210 · dokumen PRD project (freshest-wins: worktree sesi prd hidup > repoDir)
|
|
@@ -5467,8 +5662,15 @@ var init_api = __esm({
|
|
|
5467
5662
|
leadDecisions: `${API}/lead/decisions`,
|
|
5468
5663
|
leadDecisionOverride: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/override`,
|
|
5469
5664
|
leadDecisionCancel: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/cancel`,
|
|
5665
|
+
// SPEC-485 · ADR-0102 · rantai keputusan. Di bawah prefix `lead` supaya capability-nya turunan
|
|
5666
|
+
// method (lead:read/lead:write) tanpa peta baru — kelas bug SPEC-405 sudah ditutup di sana.
|
|
5667
|
+
leadFlows: `${API}/lead/flows`,
|
|
5668
|
+
leadFlowSubmit: (id) => `${API}/lead/flows/${encodeURIComponent(id)}/submit`,
|
|
5669
|
+
leadFlowCancel: (id) => `${API}/lead/flows/${encodeURIComponent(id)}/cancel`,
|
|
5470
5670
|
// SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
|
|
5471
5671
|
customAgents: `${API}/custom-agents`,
|
|
5672
|
+
// SPEC-484 · ADR-0101 · sumber daftar tools/model/runtime untuk form (mention dari `customAgents`).
|
|
5673
|
+
customAgentCatalog: `${API}/custom-agents/catalog`,
|
|
5472
5674
|
// SPEC-481 · ADR-0100 · webhook keluar (cookie-only)
|
|
5473
5675
|
webhooks: `${API}/webhooks`,
|
|
5474
5676
|
webhook: (id) => `${API}/webhooks/${encodeURIComponent(id)}`,
|
|
@@ -5680,6 +5882,7 @@ var init_config_registry = __esm({
|
|
|
5680
5882
|
kind: "url",
|
|
5681
5883
|
apply: "live",
|
|
5682
5884
|
category: "knob",
|
|
5885
|
+
example: "https://hanoman.nafanesia.id",
|
|
5683
5886
|
help: "Base URL hub tujuan sync (REST + WS). Kosong = instance ini murni HUB."
|
|
5684
5887
|
},
|
|
5685
5888
|
{
|
|
@@ -5689,6 +5892,7 @@ var init_config_registry = __esm({
|
|
|
5689
5892
|
kind: "secret",
|
|
5690
5893
|
apply: "live",
|
|
5691
5894
|
category: "credential",
|
|
5895
|
+
example: "43 karakter base64url dari tab Perangkat hub",
|
|
5692
5896
|
help: "Token yang diterbitkan hub (tab Perangkat di hub). Dikirim sebagai Bearer."
|
|
5693
5897
|
},
|
|
5694
5898
|
{
|
|
@@ -5698,6 +5902,7 @@ var init_config_registry = __esm({
|
|
|
5698
5902
|
kind: "int",
|
|
5699
5903
|
apply: "live",
|
|
5700
5904
|
category: "knob",
|
|
5905
|
+
example: "15000",
|
|
5701
5906
|
default: "15000",
|
|
5702
5907
|
min: 1e3
|
|
5703
5908
|
},
|
|
@@ -5709,13 +5914,14 @@ var init_config_registry = __esm({
|
|
|
5709
5914
|
kind: "secret",
|
|
5710
5915
|
apply: "new-session",
|
|
5711
5916
|
category: "credential",
|
|
5917
|
+
example: "sk-ant-oat01-\u2026",
|
|
5712
5918
|
inheritEnv: true,
|
|
5713
5919
|
help: "Token `claude setup-token`. Diwarisi proses claude yang di-spawn."
|
|
5714
5920
|
},
|
|
5715
|
-
{ key: "ANTHROPIC_API_KEY", group: "claude", label: "Anthropic API key", kind: "secret", apply: "new-session", category: "credential", inheritEnv: true },
|
|
5716
|
-
{ key: "HANOMAN_CLAUDE_BIN", group: "claude", label: "Biner claude", kind: "path", apply: "new-session", category: "knob", default: "claude" },
|
|
5921
|
+
{ key: "ANTHROPIC_API_KEY", group: "claude", label: "Anthropic API key", kind: "secret", apply: "new-session", category: "credential", example: "sk-ant-api03-\u2026", inheritEnv: true },
|
|
5922
|
+
{ key: "HANOMAN_CLAUDE_BIN", group: "claude", label: "Biner claude", kind: "path", apply: "new-session", category: "knob", example: "/opt/homebrew/bin/claude", default: "claude" },
|
|
5717
5923
|
// codex (SPEC-338 · ADR-0074)
|
|
5718
|
-
{ key: "HANOMAN_CODEX_BIN", group: "claude", label: "Biner codex", kind: "path", apply: "new-session", category: "knob", default: "codex" },
|
|
5924
|
+
{ key: "HANOMAN_CODEX_BIN", group: "claude", label: "Biner codex", kind: "path", apply: "new-session", category: "knob", example: "/opt/homebrew/bin/codex", default: "codex" },
|
|
5719
5925
|
{
|
|
5720
5926
|
key: "CLAUDE_CONFIG_DIR",
|
|
5721
5927
|
group: "claude",
|
|
@@ -5723,11 +5929,12 @@ var init_config_registry = __esm({
|
|
|
5723
5929
|
kind: "path",
|
|
5724
5930
|
apply: "new-session",
|
|
5725
5931
|
category: "knob",
|
|
5932
|
+
example: "~/.claude",
|
|
5726
5933
|
help: "Default ~/.claude. Sumber .credentials.json untuk panel usage/limit."
|
|
5727
5934
|
},
|
|
5728
5935
|
// vps
|
|
5729
|
-
{ key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", help: "Default ~/.hanoman." },
|
|
5730
|
-
{ key: "HANOMAN_SSH_BIN", group: "vps", label: "Biner ssh", kind: "path", apply: "new-session", category: "knob", default: "ssh" },
|
|
5936
|
+
{ key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", example: "~/.hanoman", help: "Default ~/.hanoman." },
|
|
5937
|
+
{ key: "HANOMAN_SSH_BIN", group: "vps", label: "Biner ssh", kind: "path", apply: "new-session", category: "knob", example: "/usr/bin/ssh", default: "ssh" },
|
|
5731
5938
|
// github (SPEC-471 · ADR-0095 · tarik issue → backlog). Dua mode auth, satu jalur kode:
|
|
5732
5939
|
// `gh` memakai keyring mesin; bila GITHUB_TOKEN diisi ia diteruskan sebagai GH_TOKEN ke
|
|
5733
5940
|
// proses gh DAN dipakai jalur REST (hub VPS yang tak punya keyring).
|
|
@@ -5738,6 +5945,7 @@ var init_config_registry = __esm({
|
|
|
5738
5945
|
kind: "secret",
|
|
5739
5946
|
apply: "live",
|
|
5740
5947
|
category: "credential",
|
|
5948
|
+
example: "ghp_\u2026",
|
|
5741
5949
|
help: "PAT scope `repo` (atau `public_repo`). Kosong = andalkan `gh auth login` di mesin ini."
|
|
5742
5950
|
},
|
|
5743
5951
|
{
|
|
@@ -5747,6 +5955,7 @@ var init_config_registry = __esm({
|
|
|
5747
5955
|
kind: "path",
|
|
5748
5956
|
apply: "live",
|
|
5749
5957
|
category: "knob",
|
|
5958
|
+
example: "/opt/homebrew/bin/gh",
|
|
5750
5959
|
default: "gh",
|
|
5751
5960
|
help: "Absen = tarik issue lewat HTTPS langsung ke api.github.com."
|
|
5752
5961
|
},
|
|
@@ -5759,6 +5968,7 @@ var init_config_registry = __esm({
|
|
|
5759
5968
|
kind: "secret",
|
|
5760
5969
|
apply: "live",
|
|
5761
5970
|
category: "credential",
|
|
5971
|
+
example: "123456789:AAE\u2026",
|
|
5762
5972
|
pattern: "^\\d{5,}:[A-Za-z0-9_-]{30,}$",
|
|
5763
5973
|
patternError: "format BotFather: <bot_id>:<secret>",
|
|
5764
5974
|
help: "Token dari BotFather. Disimpan terenkripsi; tak pernah dikembalikan utuh."
|
|
@@ -5770,6 +5980,7 @@ var init_config_registry = __esm({
|
|
|
5770
5980
|
kind: "secret",
|
|
5771
5981
|
apply: "live",
|
|
5772
5982
|
category: "credential",
|
|
5983
|
+
example: "hnm_agt_\u2026",
|
|
5773
5984
|
pattern: "^\\S{20,}$",
|
|
5774
5985
|
patternError: "plaintext AgentToken (hnm_agt_\u2026)",
|
|
5775
5986
|
help: "Plaintext AgentToken ber-capability Telegram. Dipakai session operator, bukan bot."
|
|
@@ -5781,6 +5992,7 @@ var init_config_registry = __esm({
|
|
|
5781
5992
|
kind: "string",
|
|
5782
5993
|
apply: "live",
|
|
5783
5994
|
category: "credential",
|
|
5995
|
+
example: "123456789, 987654321",
|
|
5784
5996
|
pattern: "^\\d+(?:[\\s,]+\\d+)*$",
|
|
5785
5997
|
patternError: "numeric user id, dipisah koma/spasi",
|
|
5786
5998
|
help: "Siapa yang boleh memerintah bot. Numeric Telegram user id, bukan username."
|
|
@@ -5792,12 +6004,13 @@ var init_config_registry = __esm({
|
|
|
5792
6004
|
kind: "string",
|
|
5793
6005
|
apply: "live",
|
|
5794
6006
|
category: "knob",
|
|
6007
|
+
example: "-1001234567890",
|
|
5795
6008
|
pattern: "^-?\\d+$",
|
|
5796
6009
|
patternError: "numeric chat id (channel/supergroup negatif)",
|
|
5797
6010
|
help: "Tujuan Test Connection. Kosong = pakai satu-satunya id di allowlist."
|
|
5798
6011
|
},
|
|
5799
6012
|
// runtime
|
|
5800
|
-
{ key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", default: "1000", min: 100 },
|
|
6013
|
+
{ key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", example: "1000", default: "1000", min: 100 },
|
|
5801
6014
|
{ key: "HANOMAN_UPDATE_FETCH", group: "runtime", label: "Deteksi update saat boot", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
5802
6015
|
// SPEC-398 · ADR-0087 · deteksi update kini membandingkan semver dengan registry npm, bukan
|
|
5803
6016
|
// menghitung commit di repo git (HANOMAN_REPO_ROOT dicabut bersama jalur git itu).
|
|
@@ -5808,6 +6021,7 @@ var init_config_registry = __esm({
|
|
|
5808
6021
|
kind: "string",
|
|
5809
6022
|
apply: "live",
|
|
5810
6023
|
category: "knob",
|
|
6024
|
+
example: "https://registry.npmjs.org",
|
|
5811
6025
|
default: "https://registry.npmjs.org",
|
|
5812
6026
|
help: "Sumber versi terbaru paket `hanoman`."
|
|
5813
6027
|
},
|
|
@@ -5818,15 +6032,16 @@ var init_config_registry = __esm({
|
|
|
5818
6032
|
kind: "string",
|
|
5819
6033
|
apply: "restart",
|
|
5820
6034
|
category: "knob",
|
|
6035
|
+
example: "hanoman",
|
|
5821
6036
|
default: "hanoman",
|
|
5822
6037
|
help: "Mengubah ini TIDAK memindahkan sesi tmux yang sudah hidup \u2014 berlaku setelah restart."
|
|
5823
6038
|
},
|
|
5824
6039
|
// gitGraph (SPEC-233 · preferensi tampilan git graph; semua live, dikonsumsi client)
|
|
5825
|
-
{ key: "gitGraph.style", group: "gitGraph", label: "Gaya graph", kind: "string", apply: "live", category: "knob", default: "rounded", help: "rounded | angular" },
|
|
5826
|
-
{ key: "gitGraph.colours", group: "gitGraph", label: "Warna lane (CSV)", kind: "string", apply: "live", category: "knob", help: "Daftar warna hex dipisah koma; kosong = palet bawaan." },
|
|
5827
|
-
{ key: "gitGraph.dateType", group: "gitGraph", label: "Jenis tanggal", kind: "string", apply: "live", category: "knob", default: "author", help: "author | commit" },
|
|
5828
|
-
{ key: "gitGraph.commitsInitialLoad", group: "gitGraph", label: "Muat awal", kind: "int", apply: "live", category: "knob", default: "200", min: 1 },
|
|
5829
|
-
{ key: "gitGraph.commitsLoadMore", group: "gitGraph", label: "Muat lagi", kind: "int", apply: "live", category: "knob", default: "100", min: 1 },
|
|
6040
|
+
{ key: "gitGraph.style", group: "gitGraph", label: "Gaya graph", kind: "string", apply: "live", category: "knob", example: "rounded", default: "rounded", help: "rounded | angular" },
|
|
6041
|
+
{ key: "gitGraph.colours", group: "gitGraph", label: "Warna lane (CSV)", kind: "string", apply: "live", category: "knob", example: "#c9a227, #6b8f71, #b4614e", help: "Daftar warna hex dipisah koma; kosong = palet bawaan." },
|
|
6042
|
+
{ key: "gitGraph.dateType", group: "gitGraph", label: "Jenis tanggal", kind: "string", apply: "live", category: "knob", example: "author", default: "author", help: "author | commit" },
|
|
6043
|
+
{ key: "gitGraph.commitsInitialLoad", group: "gitGraph", label: "Muat awal", kind: "int", apply: "live", category: "knob", example: "200", default: "200", min: 1 },
|
|
6044
|
+
{ key: "gitGraph.commitsLoadMore", group: "gitGraph", label: "Muat lagi", kind: "int", apply: "live", category: "knob", example: "100", default: "100", min: 1 },
|
|
5830
6045
|
{ key: "gitGraph.showRemoteBranches", group: "gitGraph", label: "Tampilkan remote", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
5831
6046
|
{ key: "gitGraph.showTags", group: "gitGraph", label: "Tampilkan tag", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
5832
6047
|
{ key: "gitGraph.showStashes", group: "gitGraph", label: "Tampilkan stash", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
@@ -5836,7 +6051,7 @@ var init_config_registry = __esm({
|
|
|
5836
6051
|
{ key: "gitGraph.combineLocalRemote", group: "gitGraph", label: "Gabung label local+remote", kind: "bool", apply: "live", category: "knob", default: "0" },
|
|
5837
6052
|
{ key: "gitGraph.markdown", group: "gitGraph", label: "Render markdown pesan", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
5838
6053
|
{ key: "gitGraph.emoji", group: "gitGraph", label: "Render emoji shortcode", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
5839
|
-
{ key: "gitGraph.issueLinkPattern", group: "gitGraph", label: "Pola link issue", kind: "string", apply: "live", category: "knob", help: "URL dengan $1 untuk nomor issue, mis. https://github.com/acme/app/issues/$1" },
|
|
6054
|
+
{ key: "gitGraph.issueLinkPattern", group: "gitGraph", label: "Pola link issue", kind: "string", apply: "live", category: "knob", example: "https://github.com/acme/app/issues/$1", help: "URL dengan $1 untuk nomor issue, mis. https://github.com/acme/app/issues/$1" },
|
|
5840
6055
|
// bootstrap (read-only)
|
|
5841
6056
|
{ key: "DATABASE_URL", group: "bootstrap", label: "DATABASE_URL", kind: "secret", apply: "restart", category: "bootstrap" },
|
|
5842
6057
|
{ key: "TEST_DATABASE_URL", group: "bootstrap", label: "TEST_DATABASE_URL", kind: "secret", apply: "restart", category: "bootstrap" },
|
|
@@ -5985,6 +6200,7 @@ var init_webhook = __esm({
|
|
|
5985
6200
|
"baseSha",
|
|
5986
6201
|
"headSha",
|
|
5987
6202
|
"dependsOn",
|
|
6203
|
+
"autoMerge",
|
|
5988
6204
|
"createdAt",
|
|
5989
6205
|
"startedAt",
|
|
5990
6206
|
"updatedAt"
|
|
@@ -6014,6 +6230,7 @@ var init_webhook = __esm({
|
|
|
6014
6230
|
baseSha: "5117298c5a3e63af76cbadaa46e2edfa50921d7",
|
|
6015
6231
|
headSha: null,
|
|
6016
6232
|
dependsOn: null,
|
|
6233
|
+
autoMerge: null,
|
|
6017
6234
|
createdAt: "2026-08-01T02:10:00.000Z",
|
|
6018
6235
|
startedAt: "2026-08-01T02:12:31.000Z",
|
|
6019
6236
|
updatedAt: "2026-08-01T09:41:22.000Z"
|
|
@@ -6033,6 +6250,7 @@ var init_webhook = __esm({
|
|
|
6033
6250
|
"helpEnabled",
|
|
6034
6251
|
"schedulerOptIn",
|
|
6035
6252
|
"leadOptIn",
|
|
6253
|
+
"autoMerge",
|
|
6036
6254
|
"createdAt",
|
|
6037
6255
|
"updatedAt"
|
|
6038
6256
|
],
|
|
@@ -6053,6 +6271,7 @@ var init_webhook = __esm({
|
|
|
6053
6271
|
helpEnabled: false,
|
|
6054
6272
|
schedulerOptIn: true,
|
|
6055
6273
|
leadOptIn: false,
|
|
6274
|
+
autoMerge: null,
|
|
6056
6275
|
createdAt: "2026-05-02T04:00:00.000Z",
|
|
6057
6276
|
updatedAt: "2026-08-01T09:00:00.000Z"
|
|
6058
6277
|
}
|
|
@@ -6819,7 +7038,7 @@ var init_mcp_catalog = __esm({
|
|
|
6819
7038
|
{
|
|
6820
7039
|
name: "hanoman_lead_ask",
|
|
6821
7040
|
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.",
|
|
7041
|
+
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
7042
|
inputSchema: obj({
|
|
6824
7043
|
properties: {
|
|
6825
7044
|
project: str("Id proyek."),
|
|
@@ -6827,7 +7046,10 @@ var init_mcp_catalog = __esm({
|
|
|
6827
7046
|
spec: str("Opsional. Id backlog yang bersangkutan."),
|
|
6828
7047
|
session: str("Opsional. Id sesi yang bersangkutan."),
|
|
6829
7048
|
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.")
|
|
7049
|
+
context: str("Opsional. Konteks pendukung, maksimum 20.000 karakter."),
|
|
7050
|
+
multi: bool("Opsional. `true` bila opsinya TIDAK saling eksklusif dan lead boleh memilih beberapa sekaligus. Menuntut `options` terisi."),
|
|
7051
|
+
minChoices: int("Opsional, hanya untuk `multi`. Paling sedikit berapa opsi harus dipilih."),
|
|
7052
|
+
maxChoices: int("Opsional, hanya untuk `multi`. Paling banyak berapa opsi boleh dipilih; tanpa ini sebanyak opsinya.")
|
|
6831
7053
|
},
|
|
6832
7054
|
required: ["project", "question"]
|
|
6833
7055
|
}),
|
|
@@ -6844,7 +7066,10 @@ var init_mcp_catalog = __esm({
|
|
|
6844
7066
|
...s(a.spec) ? { specId: a.spec } : {},
|
|
6845
7067
|
...s(a.session) ? { sessionId: a.session } : {},
|
|
6846
7068
|
...Array.isArray(a.options) ? { options: a.options } : {},
|
|
6847
|
-
...s(a.context) ? { context: a.context } : {}
|
|
7069
|
+
...s(a.context) ? { context: a.context } : {},
|
|
7070
|
+
// SPEC-485 · ADITIF: tanpa `multi` bentuk permintaannya identik dengan sebelum ADR-0102,
|
|
7071
|
+
// jadi klien MCP lama tak berubah perilakunya satu bit pun.
|
|
7072
|
+
...a.multi === true ? { select: { mode: "multi", min: n(a.minChoices) ?? 0, max: n(a.maxChoices) ?? null } } : {}
|
|
6848
7073
|
}
|
|
6849
7074
|
}),
|
|
6850
7075
|
shape: (raw) => raw
|
|
@@ -6883,6 +7108,7 @@ var init_src = __esm({
|
|
|
6883
7108
|
init_entities();
|
|
6884
7109
|
init_agent();
|
|
6885
7110
|
init_custom_agent();
|
|
7111
|
+
init_agent_catalog();
|
|
6886
7112
|
init_github();
|
|
6887
7113
|
init_lead();
|
|
6888
7114
|
init_dto();
|
|
@@ -6896,12 +7122,13 @@ var init_src = __esm({
|
|
|
6896
7122
|
init_telegram();
|
|
6897
7123
|
init_webhook();
|
|
6898
7124
|
init_mcp();
|
|
7125
|
+
init_auto_merge();
|
|
6899
7126
|
}
|
|
6900
7127
|
});
|
|
6901
7128
|
|
|
6902
7129
|
// src/env.ts
|
|
6903
7130
|
import { readFileSync, existsSync } from "node:fs";
|
|
6904
|
-
import { dirname, resolve as
|
|
7131
|
+
import { dirname, resolve as resolve3 } from "node:path";
|
|
6905
7132
|
import { fileURLToPath } from "node:url";
|
|
6906
7133
|
var dir;
|
|
6907
7134
|
var init_env = __esm({
|
|
@@ -6909,7 +7136,7 @@ var init_env = __esm({
|
|
|
6909
7136
|
"use strict";
|
|
6910
7137
|
dir = dirname(fileURLToPath(import.meta.url));
|
|
6911
7138
|
for (let i = 0; i < 6; i++) {
|
|
6912
|
-
const file =
|
|
7139
|
+
const file = resolve3(dir, ".env");
|
|
6913
7140
|
if (existsSync(file)) {
|
|
6914
7141
|
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
6915
7142
|
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
@@ -7363,7 +7590,7 @@ ${lines2.join("\n")}` : "";
|
|
|
7363
7590
|
// ../runner/src/git.ts
|
|
7364
7591
|
import { spawnSync } from "node:child_process";
|
|
7365
7592
|
import { rmSync, mkdirSync, realpathSync } from "node:fs";
|
|
7366
|
-
import { isAbsolute, resolve as
|
|
7593
|
+
import { isAbsolute, resolve as resolve4 } from "node:path";
|
|
7367
7594
|
function git(cwd, args) {
|
|
7368
7595
|
const r = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
7369
7596
|
if (r.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${r.stderr || r.stdout || r.error?.message || "gagal spawn"}`);
|
|
@@ -7398,7 +7625,7 @@ var init_git = __esm({
|
|
|
7398
7625
|
addWorktree: (repo, path, branchFrom) => {
|
|
7399
7626
|
tryGit(repo, ["worktree", "remove", "--force", path]);
|
|
7400
7627
|
tryGit(repo, ["worktree", "prune"]);
|
|
7401
|
-
rmSync(isAbsolute(path) ? path :
|
|
7628
|
+
rmSync(isAbsolute(path) ? path : resolve4(repo, path), { recursive: true, force: true });
|
|
7402
7629
|
const base2 = resolveCommit(repo, branchFrom);
|
|
7403
7630
|
git(repo, ["worktree", "add", "--detach", path, base2]);
|
|
7404
7631
|
return base2;
|
|
@@ -7408,8 +7635,8 @@ var init_git = __esm({
|
|
|
7408
7635
|
// `fatal: not a working tree` → membuat DELETE /terminal/sessions balas 500. remove+prune+rm
|
|
7409
7636
|
// semuanya toleran, jadi penutupan sesi selalu 204 dan registrasi worktree tak stale (SPEC-197).
|
|
7410
7637
|
removeWorktree: (repo, path) => {
|
|
7411
|
-
const target2 = isAbsolute(path) ?
|
|
7412
|
-
if (target2 ===
|
|
7638
|
+
const target2 = isAbsolute(path) ? resolve4(path) : resolve4(repo, path);
|
|
7639
|
+
if (target2 === resolve4(repo)) {
|
|
7413
7640
|
throw new Error(`removeWorktree menolak menghapus repo itu sendiri: ${target2}`);
|
|
7414
7641
|
}
|
|
7415
7642
|
tryGit(repo, ["worktree", "remove", "--force", target2]);
|
|
@@ -7725,10 +7952,10 @@ var init_custom_agents = __esm({
|
|
|
7725
7952
|
|
|
7726
7953
|
// ../runner/src/paths.ts
|
|
7727
7954
|
import { homedir } from "node:os";
|
|
7728
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as
|
|
7729
|
-
function resolveHome(env = process.env,
|
|
7955
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve5 } from "node:path";
|
|
7956
|
+
function resolveHome(env = process.env, home2 = homedir()) {
|
|
7730
7957
|
const v = env.HANOMAN_HOME?.trim();
|
|
7731
|
-
return v ? v : join(
|
|
7958
|
+
return v ? v : join(home2, ".hanoman");
|
|
7732
7959
|
}
|
|
7733
7960
|
function schemeOf(raw) {
|
|
7734
7961
|
return raw.split("://")[0] ?? "?";
|
|
@@ -7750,7 +7977,7 @@ function resolveDbUrl(env, schemaDir2) {
|
|
|
7750
7977
|
function absoluteFileUrl(raw, schemaDir2) {
|
|
7751
7978
|
const p = raw.slice("file:".length);
|
|
7752
7979
|
if (p.startsWith(":")) return raw;
|
|
7753
|
-
return `file:${isAbsolute2(p) ? p :
|
|
7980
|
+
return `file:${isAbsolute2(p) ? p : resolve5(schemaDir2, p)}`;
|
|
7754
7981
|
}
|
|
7755
7982
|
function dbUrlNotice(env) {
|
|
7756
7983
|
if (env.HANOMAN_DATABASE_URL?.trim()) return null;
|
|
@@ -7986,7 +8213,7 @@ var init_tap = __esm({
|
|
|
7986
8213
|
|
|
7987
8214
|
// src/db.ts
|
|
7988
8215
|
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
7989
|
-
import { dirname as dirname3, resolve as
|
|
8216
|
+
import { dirname as dirname3, resolve as resolve6 } from "node:path";
|
|
7990
8217
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7991
8218
|
import { PrismaClient } from "@prisma/client";
|
|
7992
8219
|
var schemaDir, notice, url, base, prisma;
|
|
@@ -7996,7 +8223,7 @@ var init_db = __esm({
|
|
|
7996
8223
|
init_env();
|
|
7997
8224
|
init_src2();
|
|
7998
8225
|
init_tap();
|
|
7999
|
-
schemaDir =
|
|
8226
|
+
schemaDir = resolve6(dirname3(fileURLToPath2(import.meta.url)), "../prisma");
|
|
8000
8227
|
notice = dbUrlNotice(process.env);
|
|
8001
8228
|
if (notice) console.warn(notice);
|
|
8002
8229
|
url = resolveDbUrl(process.env, schemaDir);
|
|
@@ -8145,18 +8372,38 @@ function readChoiceDialog(paneText2) {
|
|
|
8145
8372
|
expected -= 1;
|
|
8146
8373
|
}
|
|
8147
8374
|
if (run2.length < 2 || run2[0].n !== 1) return null;
|
|
8148
|
-
const rows = run2.map((r) =>
|
|
8149
|
-
|
|
8150
|
-
label: r.label
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8375
|
+
const rows = run2.map((r) => {
|
|
8376
|
+
const m = CHECK.exec(r.label);
|
|
8377
|
+
const label = m ? (m[2] ?? "").trim() : r.label;
|
|
8378
|
+
return {
|
|
8379
|
+
n: r.n,
|
|
8380
|
+
label,
|
|
8381
|
+
checked: m ? m[1] !== " " : null,
|
|
8382
|
+
free: PLACEHOLDER.test(label),
|
|
8383
|
+
chat: CHAT_ROW.test(label)
|
|
8384
|
+
};
|
|
8385
|
+
});
|
|
8386
|
+
let submit = { present: false, focused: false };
|
|
8387
|
+
for (const line of lines2.slice(0, footer)) {
|
|
8388
|
+
const m = SUBMIT_BTN.exec(line);
|
|
8389
|
+
if (m) submit = { present: true, focused: !!m[1] };
|
|
8390
|
+
}
|
|
8154
8391
|
return {
|
|
8155
8392
|
rows,
|
|
8156
8393
|
freeIndex: rows.find((r) => r.free)?.n ?? null,
|
|
8157
|
-
options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
|
|
8394
|
+
options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label),
|
|
8395
|
+
multi: rows.some((r) => r.checked !== null),
|
|
8396
|
+
submit
|
|
8158
8397
|
};
|
|
8159
8398
|
}
|
|
8399
|
+
function focusedRow(paneText2) {
|
|
8400
|
+
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
8401
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
8402
|
+
const m = /^\s*[❯>›]\s*(\d{1,2})\.\s+\S/.exec(lines2[i] ?? "");
|
|
8403
|
+
if (m) return Number(m[1]);
|
|
8404
|
+
}
|
|
8405
|
+
return null;
|
|
8406
|
+
}
|
|
8160
8407
|
function readTabs(lines2) {
|
|
8161
8408
|
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
8162
8409
|
const line = lines2[i] ?? "";
|
|
@@ -8206,15 +8453,17 @@ function readDialogScreen(paneText2) {
|
|
|
8206
8453
|
options: d.options,
|
|
8207
8454
|
notes: d.freeIndex === null && NOTES_FOOTER.test(lines2[footer] ?? ""),
|
|
8208
8455
|
tabs,
|
|
8209
|
-
title: readTitle(lines2, at, footer)
|
|
8456
|
+
title: readTitle(lines2, at, footer),
|
|
8457
|
+
multi: d.multi,
|
|
8458
|
+
submit: d.submit
|
|
8210
8459
|
};
|
|
8211
8460
|
}
|
|
8212
8461
|
function dialogKey(paneText2) {
|
|
8213
8462
|
const s2 = readDialogScreen(paneText2);
|
|
8214
8463
|
if (!s2) return "none";
|
|
8215
8464
|
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("|")}`;
|
|
8465
|
+
const tabs = s2.multi ? s2.tabs.map((t) => t.header).join(",") : s2.tabs.map((t) => `${t.answered ? "x" : "o"}${t.header}`).join(",");
|
|
8466
|
+
return `q|${s2.multi ? "multi|" : ""}${tabs}|${s2.title || s2.options.join("|")}`;
|
|
8218
8467
|
}
|
|
8219
8468
|
function freeTextFilled(paneText2, n2) {
|
|
8220
8469
|
const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n2);
|
|
@@ -8233,6 +8482,42 @@ async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
|
|
|
8233
8482
|
io.enter();
|
|
8234
8483
|
return true;
|
|
8235
8484
|
}
|
|
8485
|
+
async function answerMultiSelectDialog(io, plan, chunkMs) {
|
|
8486
|
+
const want = new Set(plan.pick);
|
|
8487
|
+
const state = () => readChoiceDialog(io.capture());
|
|
8488
|
+
for (const row of state()?.rows ?? []) {
|
|
8489
|
+
if (row.checked === null || row.free || row.chat) continue;
|
|
8490
|
+
if (row.checked === want.has(row.n)) continue;
|
|
8491
|
+
io.literal(String(row.n));
|
|
8492
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8493
|
+
const after = state()?.rows.find((r) => r.n === row.n);
|
|
8494
|
+
if (!after || after.checked !== want.has(row.n)) return false;
|
|
8495
|
+
}
|
|
8496
|
+
if (plan.line && plan.freeIndex !== null) {
|
|
8497
|
+
let hop2 = 0;
|
|
8498
|
+
while (focusedRow(io.capture()) !== plan.freeIndex && hop2 < NAV_TRIES) {
|
|
8499
|
+
io.down();
|
|
8500
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8501
|
+
hop2++;
|
|
8502
|
+
}
|
|
8503
|
+
if (focusedRow(io.capture()) !== plan.freeIndex) return false;
|
|
8504
|
+
for (const chunk of goalChunks(plan.line)) {
|
|
8505
|
+
io.literal(chunk);
|
|
8506
|
+
await io.sleep(chunkMs);
|
|
8507
|
+
}
|
|
8508
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8509
|
+
if (!freeTextFilled(io.capture(), plan.freeIndex)) return false;
|
|
8510
|
+
}
|
|
8511
|
+
let hop = 0;
|
|
8512
|
+
while (!state()?.submit.focused && hop < NAV_TRIES) {
|
|
8513
|
+
io.down();
|
|
8514
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
8515
|
+
hop++;
|
|
8516
|
+
}
|
|
8517
|
+
if (!state()?.submit.focused) return false;
|
|
8518
|
+
io.enter();
|
|
8519
|
+
return true;
|
|
8520
|
+
}
|
|
8236
8521
|
function notesFilled(paneText2) {
|
|
8237
8522
|
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
8238
8523
|
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
@@ -8263,7 +8548,7 @@ async function submitReview(io, submitRow) {
|
|
|
8263
8548
|
}
|
|
8264
8549
|
return false;
|
|
8265
8550
|
}
|
|
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;
|
|
8551
|
+
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
8552
|
var init_tui_dialog = __esm({
|
|
8268
8553
|
"src/services/tui-dialog.ts"() {
|
|
8269
8554
|
"use strict";
|
|
@@ -8274,6 +8559,8 @@ var init_tui_dialog = __esm({
|
|
|
8274
8559
|
CHAT_ROW = /^chat about this$/i;
|
|
8275
8560
|
SIDE_PANEL = /\s{2,}[│┃┌┐└┘├┤╭╮╰╯─━].*$/;
|
|
8276
8561
|
cleanLabel = (s2) => s2.replace(SIDE_PANEL, "").trim();
|
|
8562
|
+
CHECK = /^\[([ xX✔✓])\]\s*(.*)$/;
|
|
8563
|
+
SUBMIT_BTN = /^\s*([❯>›])?\s{2,}(Submit|Next)\s*$/;
|
|
8277
8564
|
REVIEW_PROMPT = /^\s*ready to submit your answers\?\s*$/i;
|
|
8278
8565
|
SUBMIT_ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+submit answers\s*$/i;
|
|
8279
8566
|
NOTES_FOOTER = /\bn to add notes\b/i;
|
|
@@ -8284,6 +8571,7 @@ var init_tui_dialog = __esm({
|
|
|
8284
8571
|
};
|
|
8285
8572
|
RULE = /^[\s─━╌╍_=]*$/;
|
|
8286
8573
|
DIALOG_SETTLE_MS = 250;
|
|
8574
|
+
NAV_TRIES = 24;
|
|
8287
8575
|
NOTES_PLACEHOLDER = /^(?:press n to add notes|add notes on this design(?:…|\.\.\.)?)$/i;
|
|
8288
8576
|
NOTES_LINE = /(?:^|\s)Notes:\s*(.*)$/;
|
|
8289
8577
|
SUBMIT_TRIES = 8;
|
|
@@ -8520,7 +8808,7 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
8520
8808
|
if (existing && !existing.exited) return existing;
|
|
8521
8809
|
if (existing) killSession(id);
|
|
8522
8810
|
const agentForDefs = opts.agent ?? "claude";
|
|
8523
|
-
const customDefs = opts.command ? [] : customAgentsFor(projectId);
|
|
8811
|
+
const customDefs = opts.command ? [] : customAgentsFor(projectId, agentForDefs);
|
|
8524
8812
|
const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : "";
|
|
8525
8813
|
let promptArg = "";
|
|
8526
8814
|
if (!opts.command && opts.prompt) {
|
|
@@ -8665,7 +8953,7 @@ function capturePane(id, lines2 = 200) {
|
|
|
8665
8953
|
return "";
|
|
8666
8954
|
}
|
|
8667
8955
|
}
|
|
8668
|
-
async function sendToPane(id, text, chunkMs = 50) {
|
|
8956
|
+
async function sendToPane(id, text, chunkMs = 50, choices = []) {
|
|
8669
8957
|
const p = getSession(id);
|
|
8670
8958
|
if (!p || p.exited) return false;
|
|
8671
8959
|
const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
@@ -8675,6 +8963,10 @@ async function sendToPane(id, text, chunkMs = 50) {
|
|
|
8675
8963
|
const screen = readDialogScreen(io.capture());
|
|
8676
8964
|
if (screen?.kind === "review") return await submitReview(io, screen.submitRow);
|
|
8677
8965
|
if (screen?.kind === "question") {
|
|
8966
|
+
if (screen.multi && screen.submit.present) {
|
|
8967
|
+
const pick2 = resolveChoices(choices, screen.options).choices.map((c) => c.index);
|
|
8968
|
+
return await answerMultiSelectDialog(io, { pick: pick2, line, freeIndex: screen.freeIndex }, chunkMs);
|
|
8969
|
+
}
|
|
8678
8970
|
if (screen.freeIndex !== null) return await answerChoiceDialog(io, screen.freeIndex, line, chunkMs);
|
|
8679
8971
|
if (screen.notes) return await answerNotesDialog(io, line, chunkMs);
|
|
8680
8972
|
}
|
|
@@ -8969,9 +9261,9 @@ var init_pty = __esm({
|
|
|
8969
9261
|
}
|
|
8970
9262
|
};
|
|
8971
9263
|
customAgentSource = () => [];
|
|
8972
|
-
customAgentsFor = (projectId) => {
|
|
9264
|
+
customAgentsFor = (projectId, agent) => {
|
|
8973
9265
|
try {
|
|
8974
|
-
return customAgentSource(projectId);
|
|
9266
|
+
return customAgentSource(projectId, agent);
|
|
8975
9267
|
} catch {
|
|
8976
9268
|
return [];
|
|
8977
9269
|
}
|
|
@@ -8994,6 +9286,11 @@ var init_pty = __esm({
|
|
|
8994
9286
|
enter: () => {
|
|
8995
9287
|
tmux("send-keys", "-t", name(id), "Enter");
|
|
8996
9288
|
},
|
|
9289
|
+
// SPEC-485 · SATU panah per pemanggilan: terukur, empat `Down` dalam satu `send-keys` memindahkan
|
|
9290
|
+
// fokus satu baris saja (jebakan burst ADR-0085, kali ini pada tombol kendali).
|
|
9291
|
+
down: () => {
|
|
9292
|
+
tmux("send-keys", "-t", name(id), "Down");
|
|
9293
|
+
},
|
|
8997
9294
|
sleep
|
|
8998
9295
|
});
|
|
8999
9296
|
DIALOG_CAPTURE_LINES = 60;
|
|
@@ -9273,7 +9570,10 @@ var init_sync = __esm({
|
|
|
9273
9570
|
// ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
|
|
9274
9571
|
// mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
|
|
9275
9572
|
// `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
|
|
9276
|
-
|
|
9573
|
+
// SPEC-484 · ADR-0101 · `runtime` ikut: ia menentukan sesi mesin mana yang memakai persona ini,
|
|
9574
|
+
// dan kolom yang terlewat di sini mendarat sebagai default palsu (= "warisi") di setiap mesin
|
|
9575
|
+
// lain tanpa satu pun error.
|
|
9576
|
+
customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "runtime", "enabled", "createdAt", "updatedAt"],
|
|
9277
9577
|
// SPEC-471 · ADR-0095 · SELURUH kolom bermakna ikut. `status`/`specId` termasuk: keputusan
|
|
9278
9578
|
// triase adalah bagian keadaan yang harus dilihat sama oleh semua mesin — tanpa itu satu
|
|
9279
9579
|
// mesin bisa menerima ulang issue yang di mesin lain sudah jadi backlog.
|
|
@@ -9387,8 +9687,8 @@ var init_settings2 = __esm({
|
|
|
9387
9687
|
// src/services/codex-trust.ts
|
|
9388
9688
|
import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
9389
9689
|
import { homedir as homedir2 } from "node:os";
|
|
9390
|
-
function ensureCodexTrust(repoDir,
|
|
9391
|
-
const path = codexConfigPath(
|
|
9690
|
+
function ensureCodexTrust(repoDir, home2) {
|
|
9691
|
+
const path = codexConfigPath(home2);
|
|
9392
9692
|
let canonical = repoDir;
|
|
9393
9693
|
try {
|
|
9394
9694
|
canonical = realpathSync2(repoDir);
|
|
@@ -9402,7 +9702,7 @@ function ensureCodexTrust(repoDir, home) {
|
|
|
9402
9702
|
} catch {
|
|
9403
9703
|
}
|
|
9404
9704
|
if (existing.includes(header2)) return;
|
|
9405
|
-
mkdirSync6(codexHome(
|
|
9705
|
+
mkdirSync6(codexHome(home2), { recursive: true });
|
|
9406
9706
|
const lead = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
9407
9707
|
appendFileSync(path, `${lead}
|
|
9408
9708
|
${header2}
|
|
@@ -9415,8 +9715,8 @@ var codexHome, codexConfigPath;
|
|
|
9415
9715
|
var init_codex_trust = __esm({
|
|
9416
9716
|
"src/services/codex-trust.ts"() {
|
|
9417
9717
|
"use strict";
|
|
9418
|
-
codexHome = (
|
|
9419
|
-
codexConfigPath = (
|
|
9718
|
+
codexHome = (home2) => home2 ?? process.env.CODEX_HOME ?? `${homedir2()}/.codex`;
|
|
9719
|
+
codexConfigPath = (home2) => `${codexHome(home2)}/config.toml`;
|
|
9420
9720
|
}
|
|
9421
9721
|
});
|
|
9422
9722
|
|
|
@@ -9740,13 +10040,15 @@ var init_runtime = __esm({
|
|
|
9740
10040
|
|
|
9741
10041
|
// src/services/telegram/gateway.ts
|
|
9742
10042
|
import { createHash as createHash3 } from "node:crypto";
|
|
9743
|
-
var digestRaw, TelegramGateway;
|
|
10043
|
+
var GATEWAY_PROGRESS_KIND, GATEWAY_FAILURE_KIND, digestRaw, TelegramGateway;
|
|
9744
10044
|
var init_gateway = __esm({
|
|
9745
10045
|
"src/services/telegram/gateway.ts"() {
|
|
9746
10046
|
"use strict";
|
|
9747
10047
|
init_client();
|
|
9748
10048
|
init_protocol();
|
|
9749
10049
|
init_runtime();
|
|
10050
|
+
GATEWAY_PROGRESS_KIND = "gateway-progress";
|
|
10051
|
+
GATEWAY_FAILURE_KIND = "gateway-failure";
|
|
9750
10052
|
digestRaw = (update2) => createHash3("sha256").update(JSON.stringify(update2)).digest("hex");
|
|
9751
10053
|
TelegramGateway = class {
|
|
9752
10054
|
constructor(deps) {
|
|
@@ -9832,8 +10134,18 @@ var init_gateway = __esm({
|
|
|
9832
10134
|
outcome: target2.created ? "session-created" : "session-reused",
|
|
9833
10135
|
correlationId: `tg:${input.updateId}`
|
|
9834
10136
|
});
|
|
10137
|
+
if (this.deps.progress) {
|
|
10138
|
+
await this.deps.store.enqueueReply({
|
|
10139
|
+
chatId: input.chatId,
|
|
10140
|
+
updateId: input.updateId,
|
|
10141
|
+
kind: GATEWAY_PROGRESS_KIND,
|
|
10142
|
+
text: target2.created ? "Diterima. Sesi operator Hanoman untuk chat ini sedang dijalankan \u2014 jawabannya menyusul." : "Diterima. Diteruskan ke sesi operator."
|
|
10143
|
+
}).catch(() => {
|
|
10144
|
+
});
|
|
10145
|
+
}
|
|
9835
10146
|
updateTelegramRuntimeStatus({ lastUpdateAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null });
|
|
9836
10147
|
} catch (error) {
|
|
10148
|
+
const reason = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 200);
|
|
9837
10149
|
await this.deps.store.markUpdateUncertain(input.updateId, "dispatch-failed");
|
|
9838
10150
|
await this.deps.store.audit({
|
|
9839
10151
|
chatId: input.chatId,
|
|
@@ -9843,7 +10155,14 @@ var init_gateway = __esm({
|
|
|
9843
10155
|
outcome: "uncertain",
|
|
9844
10156
|
correlationId: `tg:${input.updateId}`
|
|
9845
10157
|
});
|
|
9846
|
-
|
|
10158
|
+
await this.deps.store.enqueueReply({
|
|
10159
|
+
chatId: input.chatId,
|
|
10160
|
+
updateId: input.updateId,
|
|
10161
|
+
kind: GATEWAY_FAILURE_KIND,
|
|
10162
|
+
text: `Pesan Anda tertangkap tapi gagal diteruskan ke sesi operator: ${reason || "sebab tak diketahui"}. Kirim ulang setelah masalahnya beres \u2014 hanoman tidak pernah mengulanginya sendiri (at-most-once).`
|
|
10163
|
+
}).catch(() => {
|
|
10164
|
+
});
|
|
10165
|
+
updateTelegramRuntimeStatus({ lastError: reason });
|
|
9847
10166
|
}
|
|
9848
10167
|
}
|
|
9849
10168
|
async flushOutbox() {
|
|
@@ -9902,14 +10221,20 @@ var init_gateway = __esm({
|
|
|
9902
10221
|
timeout: this.deps.pollTimeout ?? 25,
|
|
9903
10222
|
signal
|
|
9904
10223
|
});
|
|
9905
|
-
|
|
9906
|
-
|
|
10224
|
+
if (telegramRuntimeStatus().readiness === "error") {
|
|
10225
|
+
updateTelegramRuntimeStatus({ running: true, readiness: "running", lastError: null });
|
|
10226
|
+
}
|
|
10227
|
+
try {
|
|
10228
|
+
await this.processUpdates(updates);
|
|
10229
|
+
} finally {
|
|
10230
|
+
await this.flushOutbox();
|
|
10231
|
+
}
|
|
9907
10232
|
} catch (error) {
|
|
9908
10233
|
if (signal.aborted) return;
|
|
9909
10234
|
const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
|
|
9910
10235
|
updateTelegramRuntimeStatus({ running: false, readiness: "error", lastError: message });
|
|
9911
10236
|
if (error instanceof TelegramApiError && error.code === 409) return;
|
|
9912
|
-
await new Promise((
|
|
10237
|
+
await new Promise((resolve15) => setTimeout(resolve15, 1e3));
|
|
9913
10238
|
}
|
|
9914
10239
|
}
|
|
9915
10240
|
}
|
|
@@ -9992,12 +10317,12 @@ ${input.text}`;
|
|
|
9992
10317
|
});
|
|
9993
10318
|
|
|
9994
10319
|
// src/services/telegram/store.ts
|
|
9995
|
-
import { Prisma } from "@prisma/client";
|
|
10320
|
+
import { Prisma as Prisma3 } from "@prisma/client";
|
|
9996
10321
|
var isUnique, TelegramStore;
|
|
9997
10322
|
var init_store = __esm({
|
|
9998
10323
|
"src/services/telegram/store.ts"() {
|
|
9999
10324
|
"use strict";
|
|
10000
|
-
isUnique = (error) => error instanceof
|
|
10325
|
+
isUnique = (error) => error instanceof Prisma3.PrismaClientKnownRequestError && error.code === "P2002";
|
|
10001
10326
|
TelegramStore = class {
|
|
10002
10327
|
// SPEC-481 · klien yang diekspor kini ber-extension (tap webhook) sehingga tak lagi
|
|
10003
10328
|
// assignable ke `PrismaClient` polos. `Db` diturunkan dari nilai nyatanya.
|
|
@@ -14362,7 +14687,7 @@ function syncStatus() {
|
|
|
14362
14687
|
async function startSyncClient(base2, token, tickMs2) {
|
|
14363
14688
|
started = true;
|
|
14364
14689
|
const transport = fetchTransport(base2, token);
|
|
14365
|
-
const
|
|
14690
|
+
const tick6 = async () => {
|
|
14366
14691
|
try {
|
|
14367
14692
|
await syncOnce(transport);
|
|
14368
14693
|
} catch {
|
|
@@ -14373,13 +14698,13 @@ async function startSyncClient(base2, token, tickMs2) {
|
|
|
14373
14698
|
const wsUrl = base2.replace(/^http/, "ws").replace(/\/$/, "") + `/api/sync/ws?token=${encodeURIComponent(token)}`;
|
|
14374
14699
|
ws = new WebSocket2(wsUrl);
|
|
14375
14700
|
ws.on("open", () => {
|
|
14376
|
-
void
|
|
14701
|
+
void tick6();
|
|
14377
14702
|
});
|
|
14378
14703
|
ws.on("message", async (raw) => {
|
|
14379
14704
|
try {
|
|
14380
14705
|
const msg = JSON.parse(raw.toString());
|
|
14381
14706
|
if (msg.t !== "sync") return;
|
|
14382
|
-
if (!await applyFeedFrame(msg)) void
|
|
14707
|
+
if (!await applyFeedFrame(msg)) void tick6();
|
|
14383
14708
|
} catch {
|
|
14384
14709
|
}
|
|
14385
14710
|
});
|
|
@@ -14396,11 +14721,11 @@ async function startSyncClient(base2, token, tickMs2) {
|
|
|
14396
14721
|
}
|
|
14397
14722
|
});
|
|
14398
14723
|
};
|
|
14399
|
-
await
|
|
14724
|
+
await tick6();
|
|
14400
14725
|
void connectWs();
|
|
14401
14726
|
const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
|
|
14402
14727
|
timer2 = setInterval(() => {
|
|
14403
|
-
void
|
|
14728
|
+
void tick6();
|
|
14404
14729
|
}, ms);
|
|
14405
14730
|
timer2.unref?.();
|
|
14406
14731
|
}
|
|
@@ -16938,17 +17263,17 @@ var require_stream_wormhole = __commonJS({
|
|
|
16938
17263
|
"../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
|
|
16939
17264
|
"use strict";
|
|
16940
17265
|
module.exports = (stream, throwError) => {
|
|
16941
|
-
return new Promise((
|
|
17266
|
+
return new Promise((resolve15, reject2) => {
|
|
16942
17267
|
if (typeof stream.resume !== "function") {
|
|
16943
|
-
return
|
|
17268
|
+
return resolve15();
|
|
16944
17269
|
}
|
|
16945
17270
|
stream.unpipe && stream.unpipe();
|
|
16946
17271
|
stream.resume();
|
|
16947
17272
|
if (stream._readableState && stream._readableState.ended) {
|
|
16948
|
-
return
|
|
17273
|
+
return resolve15();
|
|
16949
17274
|
}
|
|
16950
17275
|
if (!stream.readable || stream.destroyed) {
|
|
16951
|
-
return
|
|
17276
|
+
return resolve15();
|
|
16952
17277
|
}
|
|
16953
17278
|
function cleanup() {
|
|
16954
17279
|
stream.removeListener("end", onEnd);
|
|
@@ -16957,14 +17282,14 @@ var require_stream_wormhole = __commonJS({
|
|
|
16957
17282
|
}
|
|
16958
17283
|
function onEnd() {
|
|
16959
17284
|
cleanup();
|
|
16960
|
-
|
|
17285
|
+
resolve15();
|
|
16961
17286
|
}
|
|
16962
17287
|
function onError(err) {
|
|
16963
17288
|
cleanup();
|
|
16964
17289
|
if (throwError) {
|
|
16965
17290
|
reject2(err);
|
|
16966
17291
|
} else {
|
|
16967
|
-
|
|
17292
|
+
resolve15();
|
|
16968
17293
|
}
|
|
16969
17294
|
}
|
|
16970
17295
|
stream.on("end", onEnd);
|
|
@@ -17399,16 +17724,16 @@ var require_multipart2 = __commonJS({
|
|
|
17399
17724
|
}
|
|
17400
17725
|
};
|
|
17401
17726
|
const parts = () => {
|
|
17402
|
-
return new Promise((
|
|
17727
|
+
return new Promise((resolve15, reject2) => {
|
|
17403
17728
|
handle((val) => {
|
|
17404
17729
|
if (val instanceof Error) {
|
|
17405
17730
|
if (val.message === "Unexpected end of multipart data") {
|
|
17406
|
-
|
|
17731
|
+
resolve15(null);
|
|
17407
17732
|
} else {
|
|
17408
17733
|
reject2(val);
|
|
17409
17734
|
}
|
|
17410
17735
|
} else {
|
|
17411
|
-
|
|
17736
|
+
resolve15(val);
|
|
17412
17737
|
}
|
|
17413
17738
|
});
|
|
17414
17739
|
});
|
|
@@ -17691,11 +18016,39 @@ function pickWebDir(distDir, env, exists) {
|
|
|
17691
18016
|
return null;
|
|
17692
18017
|
}
|
|
17693
18018
|
|
|
18019
|
+
// src/guide-file.ts
|
|
18020
|
+
import { resolve as resolve2 } from "node:path";
|
|
18021
|
+
var AGENT_DOC_REL = "docs/agent-integration.md";
|
|
18022
|
+
function pickGuideFile(distDir, env, exists) {
|
|
18023
|
+
const forced = env.HANOMAN_AGENT_DOC?.trim();
|
|
18024
|
+
if (forced) {
|
|
18025
|
+
if (!exists(forced)) throw new Error(`HANOMAN_AGENT_DOC menunjuk berkas yang tak ada: ${forced}`);
|
|
18026
|
+
return forced;
|
|
18027
|
+
}
|
|
18028
|
+
for (const c of [resolve2(distDir, "..", AGENT_DOC_REL), resolve2(distDir, "../..", AGENT_DOC_REL)]) {
|
|
18029
|
+
if (exists(c)) return c;
|
|
18030
|
+
}
|
|
18031
|
+
return null;
|
|
18032
|
+
}
|
|
18033
|
+
|
|
17694
18034
|
// src/routes/health.ts
|
|
17695
18035
|
async function health_default(app2) {
|
|
17696
18036
|
app2.get("/health", async () => ({ ok: true }));
|
|
17697
18037
|
}
|
|
17698
18038
|
|
|
18039
|
+
// src/routes/agent-doc.ts
|
|
18040
|
+
import { readFile } from "node:fs/promises";
|
|
18041
|
+
async function agent_doc_default(app2, opts) {
|
|
18042
|
+
app2.get("/agent-integration.md", async (_req, reply) => {
|
|
18043
|
+
if (!opts.file)
|
|
18044
|
+
return reply.code(404).send({
|
|
18045
|
+
error: `dokumen panduan tak ada di instalasi ini (${AGENT_DOC_REL}) \u2014 pasang ulang paket hanoman atau set HANOMAN_AGENT_DOC`
|
|
18046
|
+
});
|
|
18047
|
+
const text = await readFile(opts.file, "utf8");
|
|
18048
|
+
return reply.type("text/markdown; charset=utf-8").send(text);
|
|
18049
|
+
});
|
|
18050
|
+
}
|
|
18051
|
+
|
|
17699
18052
|
// src/routes/projects.ts
|
|
17700
18053
|
init_src();
|
|
17701
18054
|
init_db();
|
|
@@ -17710,7 +18063,7 @@ init_src();
|
|
|
17710
18063
|
import { execFile } from "node:child_process";
|
|
17711
18064
|
import { promisify } from "node:util";
|
|
17712
18065
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync5 } from "node:fs";
|
|
17713
|
-
import { resolve as
|
|
18066
|
+
import { resolve as resolve7, dirname as dirname6, sep } from "node:path";
|
|
17714
18067
|
var exec = promisify(execFile);
|
|
17715
18068
|
async function listRepoDocs(repoDir) {
|
|
17716
18069
|
try {
|
|
@@ -17726,7 +18079,7 @@ async function listRepoDocs(repoDir) {
|
|
|
17726
18079
|
}
|
|
17727
18080
|
function docsDirOf(repoDir) {
|
|
17728
18081
|
try {
|
|
17729
|
-
const raw = readFileSync4(
|
|
18082
|
+
const raw = readFileSync4(resolve7(repoDir, "hanoman.config.json"), "utf8");
|
|
17730
18083
|
return zHanomanConfig.parse(JSON.parse(raw)).docsDir;
|
|
17731
18084
|
} catch {
|
|
17732
18085
|
return zHanomanConfig.parse({}).docsDir;
|
|
@@ -17734,7 +18087,7 @@ function docsDirOf(repoDir) {
|
|
|
17734
18087
|
}
|
|
17735
18088
|
function resolveIndex(repoDir, docsDir) {
|
|
17736
18089
|
const rel = `${docsDir}/README.md`;
|
|
17737
|
-
return existsSync3(
|
|
18090
|
+
return existsSync3(resolve7(repoDir, rel)) ? rel : "";
|
|
17738
18091
|
}
|
|
17739
18092
|
var catOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ".";
|
|
17740
18093
|
var nameOf = (rel) => rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
@@ -17745,7 +18098,7 @@ async function scanRepoDocs(repoDir) {
|
|
|
17745
18098
|
const index = resolveIndex(repoDir, docsDir);
|
|
17746
18099
|
const read = (rel) => {
|
|
17747
18100
|
try {
|
|
17748
|
-
return readFileSync4(
|
|
18101
|
+
return readFileSync4(resolve7(repoDir, rel), "utf8");
|
|
17749
18102
|
} catch {
|
|
17750
18103
|
return null;
|
|
17751
18104
|
}
|
|
@@ -17768,7 +18121,7 @@ async function scanRepoDocs(repoDir) {
|
|
|
17768
18121
|
function docAbsPath(repoDir, rel) {
|
|
17769
18122
|
if (!rel.endsWith(".md")) throw new Error("hanya file .md yang diizinkan");
|
|
17770
18123
|
if (rel.split("/").includes(".git")) throw new Error("tidak boleh menyentuh .git");
|
|
17771
|
-
const abs =
|
|
18124
|
+
const abs = resolve7(repoDir, rel);
|
|
17772
18125
|
if (abs !== repoDir && !abs.startsWith(repoDir + sep)) throw new Error("path keluar dari repo");
|
|
17773
18126
|
return abs;
|
|
17774
18127
|
}
|
|
@@ -17819,6 +18172,7 @@ init_src();
|
|
|
17819
18172
|
|
|
17820
18173
|
// src/services/project-view.ts
|
|
17821
18174
|
init_pty();
|
|
18175
|
+
init_src();
|
|
17822
18176
|
var IDLE = { status: "idle", phase: null, flow: null };
|
|
17823
18177
|
function sessionOf(projectId, sessions) {
|
|
17824
18178
|
const s2 = sessions.find((x) => x.projectId === projectId && x.specId && !x.exited);
|
|
@@ -17858,7 +18212,9 @@ async function toProjectView(p, sessions) {
|
|
|
17858
18212
|
// SPEC-294 · opt-in scheduler otonom (lokal per-instance).
|
|
17859
18213
|
schedulerOptIn: p.schedulerOptIn,
|
|
17860
18214
|
// SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal per-instance, cermin schedulerOptIn).
|
|
17861
|
-
leadOptIn: p.leadOptIn
|
|
18215
|
+
leadOptIn: p.leadOptIn,
|
|
18216
|
+
// SPEC-486 · ADR-0103 · kebijakan auto-merge (lokal per-instance, cermin schedulerOptIn).
|
|
18217
|
+
autoMerge: autoMergeOf(p.autoMerge)
|
|
17862
18218
|
};
|
|
17863
18219
|
}
|
|
17864
18220
|
|
|
@@ -17899,8 +18255,42 @@ async function branchFromCandidates(repoDir) {
|
|
|
17899
18255
|
const [local, remote] = await Promise.all([listRepoBranches(repoDir), listRepoRemoteBranches(repoDir)]);
|
|
17900
18256
|
return [.../* @__PURE__ */ new Set([...local, ...remote])].sort();
|
|
17901
18257
|
}
|
|
18258
|
+
async function defaultBranch(repoDir) {
|
|
18259
|
+
if (!repoDir) return null;
|
|
18260
|
+
try {
|
|
18261
|
+
const { stdout } = await exec2(
|
|
18262
|
+
"git",
|
|
18263
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
18264
|
+
{ cwd: repoDir, ...GIT }
|
|
18265
|
+
);
|
|
18266
|
+
const name2 = stdout.trim().replace(/^origin\//, "");
|
|
18267
|
+
if (name2 && name2 !== "HEAD") return name2;
|
|
18268
|
+
} catch {
|
|
18269
|
+
}
|
|
18270
|
+
const all = /* @__PURE__ */ new Set([...await listRepoBranches(repoDir), ...await listRepoRemoteBranches(repoDir)]);
|
|
18271
|
+
for (const c of ["main", "master"]) if (all.has(c)) return c;
|
|
18272
|
+
return null;
|
|
18273
|
+
}
|
|
18274
|
+
|
|
18275
|
+
// src/services/auto-merge-gate.ts
|
|
18276
|
+
init_src();
|
|
18277
|
+
async function checkAutoMerge(repoDir, raw) {
|
|
18278
|
+
if (raw === null || raw === void 0) return { ok: true };
|
|
18279
|
+
const p = autoMergeOf(raw);
|
|
18280
|
+
if (!p) return { ok: false, code: 400, error: "bentuk kebijakan auto-merge tak sah" };
|
|
18281
|
+
if (p.mode === "off") return { ok: true };
|
|
18282
|
+
if (!repoDir)
|
|
18283
|
+
return { ok: false, code: 409, error: "project belum di-bind ke checkout lokal \u2014 atur repoDir dulu sebelum menyalakan auto-merge" };
|
|
18284
|
+
if (p.mode === "default-branch") {
|
|
18285
|
+
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" };
|
|
18286
|
+
}
|
|
18287
|
+
if (!p.branch) return { ok: false, code: 400, error: 'mode "branch" butuh branch tujuan' };
|
|
18288
|
+
const known = p.dest === "origin" ? await listRepoRemoteBranches(repoDir) : await listRepoBranches(repoDir);
|
|
18289
|
+
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` };
|
|
18290
|
+
}
|
|
17902
18291
|
|
|
17903
18292
|
// src/routes/projects.ts
|
|
18293
|
+
import { Prisma } from "@prisma/client";
|
|
17904
18294
|
init_pty();
|
|
17905
18295
|
|
|
17906
18296
|
// src/services/paginate.ts
|
|
@@ -17963,7 +18353,13 @@ async function projects_default(app2) {
|
|
|
17963
18353
|
const parsed = zUpdateProject.safeParse(req.body);
|
|
17964
18354
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
17965
18355
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
17966
|
-
|
|
18356
|
+
if ("autoMerge" in parsed.data) {
|
|
18357
|
+
const gate = await checkAutoMerge(await resolveRepoDir(id), parsed.data.autoMerge);
|
|
18358
|
+
if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
|
|
18359
|
+
}
|
|
18360
|
+
const data = { ...parsed.data };
|
|
18361
|
+
if ("autoMerge" in data && data.autoMerge === null) data.autoMerge = Prisma.DbNull;
|
|
18362
|
+
const updated = await prisma.project.update({ where: { id }, data });
|
|
17967
18363
|
await notifySynced("project", id);
|
|
17968
18364
|
return toProjectView(updated, listSessions());
|
|
17969
18365
|
});
|
|
@@ -17995,7 +18391,11 @@ async function projects_default(app2) {
|
|
|
17995
18391
|
const p = await prisma.project.findUnique({ where: { id } });
|
|
17996
18392
|
if (!p) return reply.code(404).send({ error: "not found" });
|
|
17997
18393
|
const repoDir = await resolveRepoDir(id);
|
|
17998
|
-
return {
|
|
18394
|
+
return {
|
|
18395
|
+
branches: await listRepoBranches(repoDir),
|
|
18396
|
+
remotes: await listRepoRemoteBranches(repoDir),
|
|
18397
|
+
defaultBranch: await defaultBranch(repoDir)
|
|
18398
|
+
};
|
|
17999
18399
|
});
|
|
18000
18400
|
app2.get("/projects/:id/help-center", async (req, reply) => {
|
|
18001
18401
|
const { id } = req.params;
|
|
@@ -18121,6 +18521,9 @@ async function resolveGraphSource(repoDir, source) {
|
|
|
18121
18521
|
if (await refExists(repoDir, cand)) return cand;
|
|
18122
18522
|
return null;
|
|
18123
18523
|
}
|
|
18524
|
+
async function discardMergeWorktree(repoDir, wt) {
|
|
18525
|
+
await reclaim(repoDir, wt);
|
|
18526
|
+
}
|
|
18124
18527
|
async function deleteMergedBranch(repoDir, branch) {
|
|
18125
18528
|
await sh(repoDir, ["branch", "-D", "--end-of-options", branch]);
|
|
18126
18529
|
if (await refExists(repoDir, `refs/remotes/origin/${branch}`))
|
|
@@ -18218,9 +18621,9 @@ init_db();
|
|
|
18218
18621
|
// src/services/spec-review.ts
|
|
18219
18622
|
import { execFile as execFile4 } from "node:child_process";
|
|
18220
18623
|
import { promisify as promisify4 } from "node:util";
|
|
18221
|
-
import { readFile, mkdtemp, copyFile, rm } from "node:fs/promises";
|
|
18624
|
+
import { readFile as readFile2, mkdtemp, copyFile, rm } from "node:fs/promises";
|
|
18222
18625
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
18223
|
-
import { join as join4, resolve as
|
|
18626
|
+
import { join as join4, resolve as resolve8 } from "node:path";
|
|
18224
18627
|
var exec4 = promisify4(execFile4);
|
|
18225
18628
|
var GIT3 = { maxBuffer: 1 << 24 };
|
|
18226
18629
|
var MAX = 256 * 1024;
|
|
@@ -18229,7 +18632,7 @@ async function withTempIndex(wt, fn) {
|
|
|
18229
18632
|
const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
|
|
18230
18633
|
const dir2 = await mkdtemp(join4(tmpdir2(), "hanoman-idx-"));
|
|
18231
18634
|
const tmp = join4(dir2, "index");
|
|
18232
|
-
await copyFile(
|
|
18635
|
+
await copyFile(resolve8(wt, idx), tmp);
|
|
18233
18636
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
18234
18637
|
try {
|
|
18235
18638
|
await exec4("git", ["add", "-A", "-N"], { cwd: wt, env, ...GIT3 });
|
|
@@ -18246,9 +18649,9 @@ async function revOk(wt, rev) {
|
|
|
18246
18649
|
return exec4("git", ["rev-parse", "--verify", "-q", "--end-of-options", `${rev}^{commit}`], { cwd: wt, ...GIT3 }).then(() => true).catch(() => false);
|
|
18247
18650
|
}
|
|
18248
18651
|
async function mergeBase(wt, baseSha, branchFrom) {
|
|
18249
|
-
const
|
|
18652
|
+
const candidates2 = [baseSha, branchFrom, "main", "master"].filter((c) => !!c);
|
|
18250
18653
|
let base2 = "HEAD";
|
|
18251
|
-
for (const c of
|
|
18654
|
+
for (const c of candidates2) if (await revOk(wt, c)) {
|
|
18252
18655
|
base2 = c;
|
|
18253
18656
|
break;
|
|
18254
18657
|
}
|
|
@@ -18353,7 +18756,7 @@ async function reviewFile(repoDir, specId, baseSha, branchFrom, path) {
|
|
|
18353
18756
|
let contentRaw = null;
|
|
18354
18757
|
if (status !== "D") {
|
|
18355
18758
|
try {
|
|
18356
|
-
contentRaw = await
|
|
18759
|
+
contentRaw = await readFile2(join4(wt, path), "utf8");
|
|
18357
18760
|
} catch {
|
|
18358
18761
|
contentRaw = null;
|
|
18359
18762
|
}
|
|
@@ -18641,6 +19044,9 @@ async function validateDependsOn(specId, projectId, raw) {
|
|
|
18641
19044
|
return { ok: true, ids };
|
|
18642
19045
|
}
|
|
18643
19046
|
|
|
19047
|
+
// src/routes/specs.ts
|
|
19048
|
+
import { Prisma as Prisma2 } from "@prisma/client";
|
|
19049
|
+
|
|
18644
19050
|
// src/services/doc-export.ts
|
|
18645
19051
|
import PDFDocument from "pdfkit";
|
|
18646
19052
|
|
|
@@ -20963,7 +21369,7 @@ function renderDocPdf(text, name2, meta) {
|
|
|
20963
21369
|
});
|
|
20964
21370
|
const chunks = [];
|
|
20965
21371
|
doc.on("data", (c) => chunks.push(c));
|
|
20966
|
-
const done = new Promise((
|
|
21372
|
+
const done = new Promise((resolve15) => doc.on("end", () => resolve15(Buffer.concat(chunks))));
|
|
20967
21373
|
doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
|
|
20968
21374
|
doc.moveDown(0.25);
|
|
20969
21375
|
doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
|
|
@@ -21065,6 +21471,13 @@ async function recordFailure(specId, title, projectId, reason) {
|
|
|
21065
21471
|
}).catch(() => {
|
|
21066
21472
|
});
|
|
21067
21473
|
}
|
|
21474
|
+
async function recordAutoMerge(specId, projectId, title) {
|
|
21475
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
21476
|
+
await prisma.notification.create({
|
|
21477
|
+
data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
21478
|
+
}).catch(() => {
|
|
21479
|
+
});
|
|
21480
|
+
}
|
|
21068
21481
|
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
21069
21482
|
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
21070
21483
|
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
@@ -21274,7 +21687,7 @@ ${item.context}` : item.context;
|
|
|
21274
21687
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
21275
21688
|
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
21276
21689
|
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
21277
|
-
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload, dependsOn } = parsed.data;
|
|
21690
|
+
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload, dependsOn, autoMerge } = parsed.data;
|
|
21278
21691
|
const editingContent = title !== void 0 || newPriority !== void 0 || payload !== void 0;
|
|
21279
21692
|
if (editingContent && (spec.stage !== "brainstorming" || spec.baseSha !== null))
|
|
21280
21693
|
return reply.code(409).send({ error: "backlog item sudah dimulai \u2014 tak bisa diedit" });
|
|
@@ -21288,6 +21701,10 @@ ${item.context}` : item.context;
|
|
|
21288
21701
|
if (!d.ok) return reply.code(400).send({ error: d.error });
|
|
21289
21702
|
depIds = d.ids;
|
|
21290
21703
|
}
|
|
21704
|
+
if ("autoMerge" in parsed.data) {
|
|
21705
|
+
const gate = await checkAutoMerge(await resolveRepoDir(spec.projectId), autoMerge);
|
|
21706
|
+
if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
|
|
21707
|
+
}
|
|
21291
21708
|
if (stage !== void 0) {
|
|
21292
21709
|
if (STAGES.indexOf(stage) >= STAGES.indexOf(spec.stage))
|
|
21293
21710
|
return reply.code(422).send({ error: "stage hanya boleh dikembalikan mundur" });
|
|
@@ -21300,6 +21717,7 @@ ${item.context}` : item.context;
|
|
|
21300
21717
|
const data = {};
|
|
21301
21718
|
if (branchFrom !== void 0) data.branchFrom = branchFrom;
|
|
21302
21719
|
if (depIds !== void 0) data.dependsOn = depIds;
|
|
21720
|
+
if ("autoMerge" in parsed.data) data.autoMerge = autoMerge === null ? Prisma2.DbNull : autoMerge;
|
|
21303
21721
|
if (stage !== void 0) data.stage = stage;
|
|
21304
21722
|
if (editingContent) {
|
|
21305
21723
|
const effPayload = payload ?? spec.payload;
|
|
@@ -21660,16 +22078,16 @@ import { promisify as promisify7 } from "node:util";
|
|
|
21660
22078
|
// src/services/git-ide.ts
|
|
21661
22079
|
import { execFile as execFile6 } from "node:child_process";
|
|
21662
22080
|
import { promisify as promisify6 } from "node:util";
|
|
21663
|
-
import { readFile as
|
|
22081
|
+
import { readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
|
|
21664
22082
|
import { existsSync as existsSync6 } from "node:fs";
|
|
21665
|
-
import { resolve as
|
|
22083
|
+
import { resolve as resolve9, sep as sep2, dirname as dirname8 } from "node:path";
|
|
21666
22084
|
var exec6 = promisify6(execFile6);
|
|
21667
22085
|
var GIT5 = { maxBuffer: 1 << 24 };
|
|
21668
22086
|
var MAX2 = 256 * 1024;
|
|
21669
22087
|
var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
|
|
21670
22088
|
function repoAbsPath(repoDir, rel) {
|
|
21671
22089
|
if (rel.split(/[\\/]/).includes(".git")) throw new Error("tidak boleh menyentuh .git");
|
|
21672
|
-
const abs =
|
|
22090
|
+
const abs = resolve9(repoDir, rel);
|
|
21673
22091
|
if (abs !== repoDir && !abs.startsWith(repoDir + sep2)) throw new Error("path keluar dari repo");
|
|
21674
22092
|
return abs;
|
|
21675
22093
|
}
|
|
@@ -21687,7 +22105,7 @@ async function readRepoFile(repoDir, rel, ref = "") {
|
|
|
21687
22105
|
repoAbsPath(repoDir, rel);
|
|
21688
22106
|
let raw;
|
|
21689
22107
|
try {
|
|
21690
|
-
raw = ref ? (await exec6("git", ["show", `${ref}:${rel}`], { cwd: repoDir, ...GIT5 })).stdout : await
|
|
22108
|
+
raw = ref ? (await exec6("git", ["show", `${ref}:${rel}`], { cwd: repoDir, ...GIT5 })).stdout : await readFile3(repoAbsPath(repoDir, rel), "utf8");
|
|
21691
22109
|
} catch {
|
|
21692
22110
|
return null;
|
|
21693
22111
|
}
|
|
@@ -22088,7 +22506,7 @@ async function workingFileDiff(repoDir, path, staged) {
|
|
|
22088
22506
|
let contentRaw = null;
|
|
22089
22507
|
if (cf.status !== "D") {
|
|
22090
22508
|
try {
|
|
22091
|
-
contentRaw = staged ? (await exec6("git", ["show", `:${path}`], { cwd: repoDir, ...GIT5 })).stdout : await
|
|
22509
|
+
contentRaw = staged ? (await exec6("git", ["show", `:${path}`], { cwd: repoDir, ...GIT5 })).stdout : await readFile3(repoAbsPath(repoDir, path), "utf8");
|
|
22092
22510
|
} catch {
|
|
22093
22511
|
contentRaw = null;
|
|
22094
22512
|
}
|
|
@@ -22554,11 +22972,11 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
|
|
|
22554
22972
|
// src/routes/fs.ts
|
|
22555
22973
|
import { readdir } from "node:fs/promises";
|
|
22556
22974
|
import { homedir as homedir3 } from "node:os";
|
|
22557
|
-
import { resolve as
|
|
22975
|
+
import { resolve as resolve10, dirname as dirname9, join as join5 } from "node:path";
|
|
22558
22976
|
async function fs_default(app2) {
|
|
22559
22977
|
app2.get("/fs/browse", async (req, reply) => {
|
|
22560
22978
|
const q = req.query.path;
|
|
22561
|
-
const dir2 = q && q.trim() ?
|
|
22979
|
+
const dir2 = q && q.trim() ? resolve10(q.trim()) : homedir3();
|
|
22562
22980
|
try {
|
|
22563
22981
|
const ents = await readdir(dir2, { withFileTypes: true });
|
|
22564
22982
|
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join5(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -22694,11 +23112,11 @@ async function startSpecSession(spec, opts) {
|
|
|
22694
23112
|
}
|
|
22695
23113
|
|
|
22696
23114
|
// src/services/session-worktree.ts
|
|
22697
|
-
import { resolve as
|
|
23115
|
+
import { resolve as resolve11, sep as sep3 } from "node:path";
|
|
22698
23116
|
function ownsWorktree(repoDir, cwd) {
|
|
22699
|
-
const base2 =
|
|
22700
|
-
const dir2 =
|
|
22701
|
-
const container =
|
|
23117
|
+
const base2 = resolve11(repoDir);
|
|
23118
|
+
const dir2 = resolve11(cwd);
|
|
23119
|
+
const container = resolve11(base2, ".worktrees");
|
|
22702
23120
|
return dir2 !== base2 && dir2 !== container && dir2.startsWith(container + sep3);
|
|
22703
23121
|
}
|
|
22704
23122
|
|
|
@@ -23199,14 +23617,14 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
23199
23617
|
SSH_ASKPASS_REQUIRE: "force",
|
|
23200
23618
|
HANOMAN_SSH_PASSWORD: opts.password
|
|
23201
23619
|
} : process.env;
|
|
23202
|
-
return new Promise((
|
|
23620
|
+
return new Promise((resolve15) => {
|
|
23203
23621
|
const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
23204
23622
|
let out3 = "";
|
|
23205
|
-
const
|
|
23623
|
+
const timer7 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
23206
23624
|
const done = (r) => {
|
|
23207
|
-
clearTimeout(
|
|
23625
|
+
clearTimeout(timer7);
|
|
23208
23626
|
if (askpass) rmSync4(dirname10(askpass), { recursive: true, force: true });
|
|
23209
|
-
|
|
23627
|
+
resolve15(r);
|
|
23210
23628
|
};
|
|
23211
23629
|
p.stdout.on("data", (d) => {
|
|
23212
23630
|
out3 += d;
|
|
@@ -23983,7 +24401,7 @@ init_src();
|
|
|
23983
24401
|
init_src();
|
|
23984
24402
|
init_config2();
|
|
23985
24403
|
import { readFileSync as readFileSync11 } from "node:fs";
|
|
23986
|
-
import { resolve as
|
|
24404
|
+
import { resolve as resolve12, dirname as dirname12 } from "node:path";
|
|
23987
24405
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
23988
24406
|
var UPDATE_COMMAND = "npm i -g hanoman@latest";
|
|
23989
24407
|
function composeUpdate(x) {
|
|
@@ -24009,7 +24427,7 @@ var lastLatest = null;
|
|
|
24009
24427
|
var lastStatus = "unavailable";
|
|
24010
24428
|
function runningVersion() {
|
|
24011
24429
|
const here = dirname12(fileURLToPath3(import.meta.url));
|
|
24012
|
-
for (const p of [
|
|
24430
|
+
for (const p of [resolve12(here, "build-info.json"), resolve12(here, "../package.json")]) {
|
|
24013
24431
|
try {
|
|
24014
24432
|
const v = JSON.parse(readFileSync11(p, "utf8")).version;
|
|
24015
24433
|
if (typeof v === "string" && v) return v;
|
|
@@ -24358,8 +24776,8 @@ init_conflicts();
|
|
|
24358
24776
|
// src/services/uploads.ts
|
|
24359
24777
|
init_config2();
|
|
24360
24778
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
24361
|
-
import { mkdir as mkdir2, writeFile as writeFile2, readFile as
|
|
24362
|
-
import { join as join12, resolve as
|
|
24779
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile4, unlink } from "node:fs/promises";
|
|
24780
|
+
import { join as join12, resolve as resolve13 } from "node:path";
|
|
24363
24781
|
var EXT = {
|
|
24364
24782
|
"image/png": ".png",
|
|
24365
24783
|
"image/jpeg": ".jpg",
|
|
@@ -24369,7 +24787,7 @@ function extFor(mimeType) {
|
|
|
24369
24787
|
return EXT[mimeType] ?? ".bin";
|
|
24370
24788
|
}
|
|
24371
24789
|
function uploadDir() {
|
|
24372
|
-
return
|
|
24790
|
+
return resolve13(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join12(process.cwd(), "data", "uploads"));
|
|
24373
24791
|
}
|
|
24374
24792
|
async function saveUpload(buf, mimeType) {
|
|
24375
24793
|
const dir2 = uploadDir();
|
|
@@ -24380,13 +24798,13 @@ async function saveUpload(buf, mimeType) {
|
|
|
24380
24798
|
}
|
|
24381
24799
|
async function readUpload(storageKey) {
|
|
24382
24800
|
const safe = storageKey.replace(/[/\\]/g, "");
|
|
24383
|
-
return
|
|
24801
|
+
return readFile4(join12(uploadDir(), safe));
|
|
24384
24802
|
}
|
|
24385
24803
|
async function readUploadOrFetch(storageKey) {
|
|
24386
24804
|
const safe = storageKey.replace(/[/\\]/g, "");
|
|
24387
24805
|
const target2 = join12(uploadDir(), safe);
|
|
24388
24806
|
try {
|
|
24389
|
-
return await
|
|
24807
|
+
return await readFile4(target2);
|
|
24390
24808
|
} catch {
|
|
24391
24809
|
const base2 = effectiveStr("SYNC_SERVER_URL");
|
|
24392
24810
|
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
@@ -24533,11 +24951,11 @@ import { randomUUID as randomUUID5 } from "node:crypto";
|
|
|
24533
24951
|
// src/services/transcript-store.ts
|
|
24534
24952
|
init_config2();
|
|
24535
24953
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
24536
|
-
import { mkdir as mkdir3, writeFile as writeFile3, readFile as
|
|
24537
|
-
import { join as join13, resolve as
|
|
24954
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile5, unlink as unlink2 } from "node:fs/promises";
|
|
24955
|
+
import { join as join13, resolve as resolve14, basename as basename2 } from "node:path";
|
|
24538
24956
|
var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
|
|
24539
24957
|
function transcriptDir() {
|
|
24540
|
-
return
|
|
24958
|
+
return resolve14(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join13(process.cwd(), "data", "transcripts"));
|
|
24541
24959
|
}
|
|
24542
24960
|
function clamp(text) {
|
|
24543
24961
|
const buf = Buffer.from(text, "utf8");
|
|
@@ -24561,7 +24979,7 @@ async function saveTranscript(text) {
|
|
|
24561
24979
|
async function readTranscript(key) {
|
|
24562
24980
|
if (!key) return null;
|
|
24563
24981
|
try {
|
|
24564
|
-
return await
|
|
24982
|
+
return await readFile5(join13(transcriptDir(), basename2(key)), "utf8");
|
|
24565
24983
|
} catch {
|
|
24566
24984
|
return null;
|
|
24567
24985
|
}
|
|
@@ -25322,6 +25740,16 @@ function leadPrompt(q, c) {
|
|
|
25322
25740
|
}
|
|
25323
25741
|
lines2.push("");
|
|
25324
25742
|
}
|
|
25743
|
+
if (c.chainSteps?.length) {
|
|
25744
|
+
lines2.push("## Rantai keputusan ini (langkah yang sudah dijawab)");
|
|
25745
|
+
for (const [i, s2] of c.chainSteps.entries()) {
|
|
25746
|
+
lines2.push(`${i + 1}. "${s2.question.slice(0, 200)}" \u2192 ${s2.picked.length ? s2.picked.join("; ") : "(tanpa pilihan)"}`);
|
|
25747
|
+
if (s2.options.length) lines2.push(` opsi saat itu: ${s2.options.map((o) => o.slice(0, 80)).join(" \xB7 ")}`);
|
|
25748
|
+
}
|
|
25749
|
+
lines2.push("");
|
|
25750
|
+
lines2.push("Pertanyaan di bawah adalah lanjutan dari rantai itu. Jangan bertentangan dengan yang sudah kamu putuskan di atas, dan jangan mengulang penjelasannya.");
|
|
25751
|
+
lines2.push("");
|
|
25752
|
+
}
|
|
25325
25753
|
lines2.push("## Yang harus kamu putuskan");
|
|
25326
25754
|
lines2.push(q.question.trim());
|
|
25327
25755
|
if (q.options?.length) {
|
|
@@ -25329,7 +25757,12 @@ function leadPrompt(q, c) {
|
|
|
25329
25757
|
lines2.push("Opsi yang dilihat peminta:");
|
|
25330
25758
|
for (const [i, o] of q.options.entries()) lines2.push(`${i + 1}. ${o}`);
|
|
25331
25759
|
lines2.push("");
|
|
25332
|
-
|
|
25760
|
+
if (c.select?.mode === "multi") {
|
|
25761
|
+
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.`);
|
|
25762
|
+
} else {
|
|
25763
|
+
lines2.push('Salah satu dari daftar itu WAJIB kamu pilih lewat field `choice` \u2014 isi nomornya ("2") atau labelnya persis.');
|
|
25764
|
+
}
|
|
25765
|
+
lines2.push("Pilihan di luar daftar ditolak server, dicatat sebagai penolakan, dan peminta kembali menunggu manusia.");
|
|
25333
25766
|
}
|
|
25334
25767
|
lines2.push("");
|
|
25335
25768
|
lines2.push("## Batas waktu (BACA INI DULU)");
|
|
@@ -25356,6 +25789,7 @@ function leadPrompt(q, c) {
|
|
|
25356
25789
|
lines2.push(JSON.stringify({
|
|
25357
25790
|
decision: "keputusan yang dipilih, satu kalimat",
|
|
25358
25791
|
choice: "nomor atau label opsi yang kamu pilih; kosongkan bila tak ada daftar opsi",
|
|
25792
|
+
choices: [],
|
|
25359
25793
|
reason: "alasannya, dua-tiga kalimat, menyebut bukti",
|
|
25360
25794
|
refs: ["internal/docs/\u2026", "ADR-00xx"],
|
|
25361
25795
|
confidence: "tinggi | sedang | ragu",
|
|
@@ -25414,6 +25848,7 @@ function keepExistingRefs(refs2, repoDir) {
|
|
|
25414
25848
|
|
|
25415
25849
|
// src/services/lead/trail.ts
|
|
25416
25850
|
init_db();
|
|
25851
|
+
init_src();
|
|
25417
25852
|
async function recordDecision(i) {
|
|
25418
25853
|
return prisma.leadDecision.create({
|
|
25419
25854
|
data: {
|
|
@@ -25436,6 +25871,11 @@ async function recordDecision(i) {
|
|
|
25436
25871
|
// null eksplisit, sementara "tak disebut" pada create sudah berarti NULL di baris barunya.
|
|
25437
25872
|
options: i.options?.length ? i.options : void 0,
|
|
25438
25873
|
missing: i.missing?.length ? i.missing : void 0,
|
|
25874
|
+
// SPEC-485 · daftar kosong disimpan NULL dengan alasan yang sama seperti `options` di atas.
|
|
25875
|
+
choices: i.choices?.length ? i.choices : void 0,
|
|
25876
|
+
select: i.select ?? void 0,
|
|
25877
|
+
flowId: i.flowId ?? null,
|
|
25878
|
+
step: i.step ?? null,
|
|
25439
25879
|
status: i.status ?? "berlaku",
|
|
25440
25880
|
weighty: i.weighty ?? false,
|
|
25441
25881
|
actor: i.actor ?? "lead"
|
|
@@ -25448,16 +25888,21 @@ async function listDecisions(f = {}) {
|
|
|
25448
25888
|
...f.projectId ? { projectId: f.projectId } : {},
|
|
25449
25889
|
...f.specId ? { specId: f.specId } : {},
|
|
25450
25890
|
...f.sessionId ? { sessionId: f.sessionId } : {},
|
|
25891
|
+
...f.flowId ? { flowId: f.flowId } : {},
|
|
25451
25892
|
...f.status ? { status: f.status } : {}
|
|
25452
25893
|
},
|
|
25453
|
-
|
|
25894
|
+
// SPEC-485 · satu RANTAI dibaca dari awal: urutan pertanyaannya adalah isi jejaknya. Daftar
|
|
25895
|
+
// umum tetap terbaru-dulu (AC-24) — dua pertanyaan yang berbeda, dua urutan yang berbeda.
|
|
25896
|
+
orderBy: { createdAt: f.flowId ? "asc" : "desc" },
|
|
25454
25897
|
take: Math.min(f.take ?? 50, 200),
|
|
25455
25898
|
skip: f.skip ?? 0
|
|
25456
25899
|
});
|
|
25457
25900
|
}
|
|
25458
|
-
async function overrideDecision(id, answer, reason) {
|
|
25901
|
+
async function overrideDecision(id, answer, reason, rawChoices = []) {
|
|
25459
25902
|
const old = await prisma.leadDecision.findUnique({ where: { id } });
|
|
25460
25903
|
if (!old || old.status !== "berlaku") return null;
|
|
25904
|
+
const options2 = Array.isArray(old.options) ? old.options.map(String) : [];
|
|
25905
|
+
const { choices } = resolveChoices(rawChoices, options2);
|
|
25461
25906
|
const next = await recordDecision({
|
|
25462
25907
|
projectId: old.projectId,
|
|
25463
25908
|
specId: old.specId,
|
|
@@ -25470,7 +25915,14 @@ async function overrideDecision(id, answer, reason) {
|
|
|
25470
25915
|
refs: [],
|
|
25471
25916
|
confidence: "tinggi",
|
|
25472
25917
|
action: "none",
|
|
25473
|
-
actor: "operator"
|
|
25918
|
+
actor: "operator",
|
|
25919
|
+
choices,
|
|
25920
|
+
choice: choices[0]?.option ?? null,
|
|
25921
|
+
choiceIndex: choices[0]?.index ?? null,
|
|
25922
|
+
options: options2,
|
|
25923
|
+
select: old.select,
|
|
25924
|
+
flowId: old.flowId,
|
|
25925
|
+
step: old.step
|
|
25474
25926
|
});
|
|
25475
25927
|
const updated = await prisma.leadDecision.update({
|
|
25476
25928
|
where: { id },
|
|
@@ -25502,6 +25954,13 @@ function toDecisionView(r) {
|
|
|
25502
25954
|
choiceIndex: r.choiceIndex,
|
|
25503
25955
|
options: Array.isArray(r.options) ? r.options.map(String) : [],
|
|
25504
25956
|
missing: Array.isArray(r.missing) ? r.missing.map(String) : [],
|
|
25957
|
+
// SPEC-485 · ADR-0102 · jawaban SELALU daftar di permukaan baca. Baris pra-migrasi tak punya
|
|
25958
|
+
// kolomnya, jadi ia DITURUNKAN dari pasangan skalar lama — itulah yang membuat riwayat lama
|
|
25959
|
+
// tetap terbaca sesudah perubahan skema, tanpa satu pun backfill.
|
|
25960
|
+
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 }] : [],
|
|
25961
|
+
select: r.select ?? null,
|
|
25962
|
+
flowId: r.flowId,
|
|
25963
|
+
step: r.step,
|
|
25505
25964
|
status: r.status,
|
|
25506
25965
|
weighty: r.weighty,
|
|
25507
25966
|
supersededById: r.supersededById,
|
|
@@ -25548,7 +26007,7 @@ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
|
|
|
25548
26007
|
function think(prompt, o) {
|
|
25549
26008
|
const bin = binFor(o.agent);
|
|
25550
26009
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
25551
|
-
return new Promise((
|
|
26010
|
+
return new Promise((resolve15, reject2) => {
|
|
25552
26011
|
const child = execFile9(bin, args, {
|
|
25553
26012
|
cwd: o.cwd,
|
|
25554
26013
|
timeout: o.timeoutMs,
|
|
@@ -25561,7 +26020,7 @@ function think(prompt, o) {
|
|
|
25561
26020
|
reject2(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
|
|
25562
26021
|
return;
|
|
25563
26022
|
}
|
|
25564
|
-
|
|
26023
|
+
resolve15(stdout);
|
|
25565
26024
|
});
|
|
25566
26025
|
child.stdin?.end();
|
|
25567
26026
|
});
|
|
@@ -25598,9 +26057,9 @@ function acquire(cap, waitMs) {
|
|
|
25598
26057
|
return Promise.resolve();
|
|
25599
26058
|
}
|
|
25600
26059
|
const startedAt = Date.now();
|
|
25601
|
-
return new Promise((
|
|
26060
|
+
return new Promise((resolve15, reject2) => {
|
|
25602
26061
|
const w = {
|
|
25603
|
-
grant:
|
|
26062
|
+
grant: resolve15,
|
|
25604
26063
|
deny: reject2,
|
|
25605
26064
|
timer: setTimeout(() => {
|
|
25606
26065
|
const i = queue.indexOf(w);
|
|
@@ -25629,6 +26088,98 @@ async function runPool(items, limit, fn) {
|
|
|
25629
26088
|
await Promise.all(Array.from({ length: workers }, worker));
|
|
25630
26089
|
}
|
|
25631
26090
|
|
|
26091
|
+
// src/services/lead/flow.ts
|
|
26092
|
+
init_src();
|
|
26093
|
+
init_db();
|
|
26094
|
+
var LeadFlowClosedError = class extends Error {
|
|
26095
|
+
constructor(flowId, flowStatus) {
|
|
26096
|
+
super(`rantai keputusan ${flowId} sudah ${flowStatus === "tak-ada" ? "tidak ada" : flowStatus}`);
|
|
26097
|
+
this.flowId = flowId;
|
|
26098
|
+
this.flowStatus = flowStatus;
|
|
26099
|
+
this.name = "LeadFlowClosedError";
|
|
26100
|
+
}
|
|
26101
|
+
};
|
|
26102
|
+
var isOpen = (s2) => LEAD_FLOW_OPEN.includes(s2);
|
|
26103
|
+
var flowTitle = (q) => q.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
26104
|
+
async function openFlow(i) {
|
|
26105
|
+
return prisma.leadFlow.create({
|
|
26106
|
+
data: {
|
|
26107
|
+
projectId: i.projectId,
|
|
26108
|
+
specId: i.specId ?? null,
|
|
26109
|
+
sessionId: i.sessionId ?? null,
|
|
26110
|
+
gate: i.gate,
|
|
26111
|
+
title: flowTitle(i.title),
|
|
26112
|
+
expiresAt: new Date(Date.now() + Math.max(1, i.ttlMin) * 6e4)
|
|
26113
|
+
}
|
|
26114
|
+
});
|
|
26115
|
+
}
|
|
26116
|
+
async function joinFlow(id) {
|
|
26117
|
+
const row = await prisma.leadFlow.findUnique({ where: { id } });
|
|
26118
|
+
if (!row) throw new LeadFlowClosedError(id, "tak-ada");
|
|
26119
|
+
if (!isOpen(row.status)) throw new LeadFlowClosedError(id, row.status);
|
|
26120
|
+
return row;
|
|
26121
|
+
}
|
|
26122
|
+
async function markFlowStep(id, answered) {
|
|
26123
|
+
const row = await prisma.leadFlow.findUnique({ where: { id } });
|
|
26124
|
+
if (!row || !isOpen(row.status)) return;
|
|
26125
|
+
await prisma.leadFlow.update({
|
|
26126
|
+
where: { id },
|
|
26127
|
+
data: {
|
|
26128
|
+
steps: { increment: 1 },
|
|
26129
|
+
...answered && row.status === "menunggu" ? { status: "sebagian" } : {}
|
|
26130
|
+
}
|
|
26131
|
+
});
|
|
26132
|
+
}
|
|
26133
|
+
async function closeFlow(id, reason) {
|
|
26134
|
+
const row = await prisma.leadFlow.findUnique({ where: { id } });
|
|
26135
|
+
if (!row || !isOpen(row.status)) return null;
|
|
26136
|
+
const status = reason === "operator" || reason === "kedaluwarsa" ? "dibatalkan" : "selesai";
|
|
26137
|
+
return prisma.leadFlow.update({
|
|
26138
|
+
where: { id },
|
|
26139
|
+
data: { status, closeReason: reason, closedAt: /* @__PURE__ */ new Date() }
|
|
26140
|
+
});
|
|
26141
|
+
}
|
|
26142
|
+
async function listFlows(f = {}) {
|
|
26143
|
+
return prisma.leadFlow.findMany({
|
|
26144
|
+
where: {
|
|
26145
|
+
...f.projectId ? { projectId: f.projectId } : {},
|
|
26146
|
+
...f.status ? { status: f.status } : {}
|
|
26147
|
+
},
|
|
26148
|
+
orderBy: { createdAt: "desc" },
|
|
26149
|
+
take: Math.min(f.take ?? 50, 200),
|
|
26150
|
+
skip: f.skip ?? 0
|
|
26151
|
+
});
|
|
26152
|
+
}
|
|
26153
|
+
async function expireFlows(now) {
|
|
26154
|
+
const due = await prisma.leadFlow.findMany({
|
|
26155
|
+
where: { status: { in: [...LEAD_FLOW_OPEN] }, expiresAt: { lt: now } },
|
|
26156
|
+
orderBy: { createdAt: "asc" },
|
|
26157
|
+
take: 100
|
|
26158
|
+
});
|
|
26159
|
+
const out3 = [];
|
|
26160
|
+
for (const f of due) {
|
|
26161
|
+
const closed = await closeFlow(f.id, "kedaluwarsa");
|
|
26162
|
+
if (closed) out3.push(closed);
|
|
26163
|
+
}
|
|
26164
|
+
return out3;
|
|
26165
|
+
}
|
|
26166
|
+
function toFlowView(r) {
|
|
26167
|
+
return {
|
|
26168
|
+
id: r.id,
|
|
26169
|
+
projectId: r.projectId,
|
|
26170
|
+
specId: r.specId,
|
|
26171
|
+
sessionId: r.sessionId,
|
|
26172
|
+
gate: r.gate,
|
|
26173
|
+
status: r.status,
|
|
26174
|
+
title: r.title,
|
|
26175
|
+
steps: r.steps,
|
|
26176
|
+
closeReason: r.closeReason,
|
|
26177
|
+
openedAt: r.openedAt.toISOString(),
|
|
26178
|
+
closedAt: r.closedAt ? r.closedAt.toISOString() : null,
|
|
26179
|
+
expiresAt: r.expiresAt.toISOString()
|
|
26180
|
+
};
|
|
26181
|
+
}
|
|
26182
|
+
|
|
25632
26183
|
// src/services/lead/decide.ts
|
|
25633
26184
|
var prodDecideDeps = {
|
|
25634
26185
|
think,
|
|
@@ -25652,6 +26203,15 @@ var notifTitle = (kind, question, answer, confidence) => {
|
|
|
25652
26203
|
async function decide(req, deps = prodDecideDeps) {
|
|
25653
26204
|
const cfg = await getLead();
|
|
25654
26205
|
if (!leadActive(cfg, req.projectId)) return null;
|
|
26206
|
+
const flow = req.flowId ? await joinFlow(req.flowId) : await openFlow({
|
|
26207
|
+
projectId: req.projectId,
|
|
26208
|
+
specId: req.specId,
|
|
26209
|
+
sessionId: req.sessionId,
|
|
26210
|
+
gate: req.gate,
|
|
26211
|
+
title: req.question,
|
|
26212
|
+
ttlMin: cfg.flowTtlMin
|
|
26213
|
+
});
|
|
26214
|
+
const step = flow.steps + 1;
|
|
25655
26215
|
const project = await prisma.project.findUnique({ where: { id: req.projectId }, select: { name: true } });
|
|
25656
26216
|
const spec = req.specId ? await prisma.spec.findUnique({
|
|
25657
26217
|
where: { id: req.specId },
|
|
@@ -25663,6 +26223,9 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25663
26223
|
orderBy: { createdAt: "desc" },
|
|
25664
26224
|
take: 10
|
|
25665
26225
|
});
|
|
26226
|
+
const chainRows = req.flowId ? await prisma.leadDecision.findMany({ where: { flowId: flow.id }, orderBy: { createdAt: "asc" }, take: 10 }) : [];
|
|
26227
|
+
const options2 = req.options ?? [];
|
|
26228
|
+
const bounds = normalizeSelect(req.select ?? { mode: "single", min: 0, max: null }, options2.length);
|
|
25666
26229
|
const ctx = {
|
|
25667
26230
|
projectId: req.projectId,
|
|
25668
26231
|
projectName: project?.name ?? req.projectId,
|
|
@@ -25679,7 +26242,13 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25679
26242
|
reason: d.reason,
|
|
25680
26243
|
createdAt: d.createdAt.toISOString()
|
|
25681
26244
|
})),
|
|
25682
|
-
notes: req.notes
|
|
26245
|
+
notes: req.notes,
|
|
26246
|
+
select: bounds,
|
|
26247
|
+
chainSteps: chainRows.map((d) => ({
|
|
26248
|
+
question: d.question,
|
|
26249
|
+
options: Array.isArray(d.options) ? d.options.map(String) : [],
|
|
26250
|
+
picked: Array.isArray(d.choices) ? d.choices.map((c) => String(c?.option ?? "")).filter(Boolean) : d.choice ? [d.choice] : []
|
|
26251
|
+
}))
|
|
25683
26252
|
};
|
|
25684
26253
|
const { agent, model, effort } = await deps.defaults();
|
|
25685
26254
|
const prompt = leadPrompt({ kind: req.kind, question: req.question, options: req.options }, ctx);
|
|
@@ -25698,24 +26267,30 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25698
26267
|
}
|
|
25699
26268
|
});
|
|
25700
26269
|
} catch (e) {
|
|
25701
|
-
if (e instanceof LeadBusyError)
|
|
25702
|
-
|
|
26270
|
+
if (e instanceof LeadBusyError) {
|
|
26271
|
+
if (!req.flowId) await closeFlow(flow.id, "kedaluwarsa").catch(() => null);
|
|
26272
|
+
throw e;
|
|
26273
|
+
}
|
|
26274
|
+
return closeAfter(await fail(req, deps, `lead tak menghasilkan keputusan: ${e.message}`, flow.id, step), flow.id, req.chain);
|
|
25703
26275
|
} finally {
|
|
25704
26276
|
if (req.sessionId) clearQueued(req.sessionId);
|
|
25705
26277
|
}
|
|
25706
26278
|
const verdict = parseLeadVerdict(raw);
|
|
25707
|
-
if (!verdict) return fail(req, deps, "keluaran lead tak memuat blok json keputusan yang sah");
|
|
26279
|
+
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
26280
|
const refs2 = keepExistingRefs(verdict.refs, repoDir);
|
|
25709
26281
|
const allowed = leadActionAllowed(verdict.action);
|
|
25710
26282
|
const kind = allowed ? req.kind : "refusal";
|
|
25711
|
-
const
|
|
25712
|
-
const
|
|
25713
|
-
const
|
|
26283
|
+
const rawChoices = verdict.choices.length ? verdict.choices : verdict.choice.trim() ? [verdict.choice] : [];
|
|
26284
|
+
const resolved = resolveChoices(rawChoices, options2);
|
|
26285
|
+
const countProblem = options2.length && resolved.choices.length ? checkChoiceCount(resolved.choices.length, bounds) : null;
|
|
26286
|
+
const choices = countProblem ? [] : resolved.choices;
|
|
26287
|
+
const choice = choices[0] ?? null;
|
|
26288
|
+
const choiceRejected = options2.length > 0 && rawChoices.length > 0 && (resolved.rejected.length > 0 || !!countProblem);
|
|
25714
26289
|
const missing = verdict.missing.map((m) => m.trim()).filter(Boolean);
|
|
25715
26290
|
let action = allowed ? verdict.action : "none";
|
|
25716
26291
|
let actionNote = "";
|
|
25717
26292
|
let conflict = false;
|
|
25718
|
-
if (allowed && choice) {
|
|
26293
|
+
if (allowed && choices.length === 1 && choice) {
|
|
25719
26294
|
const hint = optionActionHint(choice.option);
|
|
25720
26295
|
if (hint && action === "none" && hint !== "none") {
|
|
25721
26296
|
action = hint;
|
|
@@ -25729,7 +26304,10 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
25729
26304
|
const confidence = missing.length ? "ragu" : verdict.confidence;
|
|
25730
26305
|
const notes = [];
|
|
25731
26306
|
if (!allowed) notes.push(`DITOLAK: ${leadRefusalReason(verdict.action)} berada di luar permukaan tindakan lead (ADR-0091 \xB7 AC-31/32).`);
|
|
25732
|
-
if (
|
|
26307
|
+
if (options2.length && resolved.rejected.length)
|
|
26308
|
+
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).`);
|
|
26309
|
+
if (countProblem)
|
|
26310
|
+
notes.push(`DITOLAK: ${countProblem} (SPEC-485 \xB7 ADR-0102) \u2014 seluruh pilihan dibatalkan, bukan dipangkas.`);
|
|
25733
26311
|
if (actionNote) notes.push(actionNote);
|
|
25734
26312
|
if (missing.length) notes.push(`KONTEKS KURANG: ${missing.join("; ")}`);
|
|
25735
26313
|
const tail3 = notes.length ? `
|
|
@@ -25754,9 +26332,14 @@ ${notes.join("\n\n")}` : "";
|
|
|
25754
26332
|
weighty,
|
|
25755
26333
|
choice: choice?.option ?? null,
|
|
25756
26334
|
choiceIndex: choice?.index ?? null,
|
|
26335
|
+
choices,
|
|
26336
|
+
select: bounds,
|
|
26337
|
+
flowId: flow.id,
|
|
26338
|
+
step,
|
|
25757
26339
|
options: options2,
|
|
25758
26340
|
missing
|
|
25759
26341
|
});
|
|
26342
|
+
await closeAfter(row, flow.id, req.chain);
|
|
25760
26343
|
if (weighty) {
|
|
25761
26344
|
await deps.notify(
|
|
25762
26345
|
row.id,
|
|
@@ -25770,18 +26353,24 @@ ${notes.join("\n\n")}` : "";
|
|
|
25770
26353
|
decision: clampProse(verdict.decision, LEAD_DECISION_MAX),
|
|
25771
26354
|
reason: `${clampProse(verdict.reason, LEAD_REASON_MAX)}${tail3}`,
|
|
25772
26355
|
reply: verdict.reply,
|
|
26356
|
+
choices,
|
|
25773
26357
|
choice,
|
|
25774
26358
|
missing
|
|
25775
26359
|
});
|
|
25776
26360
|
return row;
|
|
25777
26361
|
}
|
|
26362
|
+
async function closeAfter(row, flowId, chain) {
|
|
26363
|
+
await markFlowStep(flowId, row.status === "berlaku");
|
|
26364
|
+
if (!chain) await closeFlow(flowId, "tunggal");
|
|
26365
|
+
return row;
|
|
26366
|
+
}
|
|
25778
26367
|
var lastDelivery = /* @__PURE__ */ new Map();
|
|
25779
26368
|
function takeDelivery(decisionId) {
|
|
25780
26369
|
const v = lastDelivery.get(decisionId) ?? null;
|
|
25781
26370
|
lastDelivery.delete(decisionId);
|
|
25782
26371
|
return v;
|
|
25783
26372
|
}
|
|
25784
|
-
async function fail(req, deps, reason) {
|
|
26373
|
+
async function fail(req, deps, reason, flowId = null, step = null) {
|
|
25785
26374
|
const prev = await prisma.leadDecision.findFirst({
|
|
25786
26375
|
where: { projectId: req.projectId, gate: req.gate, kind: req.kind },
|
|
25787
26376
|
orderBy: { createdAt: "desc" },
|
|
@@ -25799,6 +26388,10 @@ async function fail(req, deps, reason) {
|
|
|
25799
26388
|
refs: [],
|
|
25800
26389
|
confidence: "ragu",
|
|
25801
26390
|
action: "none",
|
|
26391
|
+
// SPEC-485 · langkah yang gagal tetap duduk di alurnya: alur yang seluruh langkahnya gagal
|
|
26392
|
+
// harus tetap terbaca sebagai satu urusan, bukan sebagai baris yatim.
|
|
26393
|
+
flowId,
|
|
26394
|
+
step,
|
|
25802
26395
|
status: "gagal",
|
|
25803
26396
|
weighty: true
|
|
25804
26397
|
});
|
|
@@ -25983,6 +26576,7 @@ var CODEX_FINISHED = [
|
|
|
25983
26576
|
/\btokens used\b/i,
|
|
25984
26577
|
/\bTo continue this session\b/i
|
|
25985
26578
|
];
|
|
26579
|
+
var AGENT_TURN_LINE = /(?:\bfor\s+\d+\s*[hms]\b|\(\s*\d+\s*[hms][^)]*·)/i;
|
|
25986
26580
|
var ASK_SIGNALS = [
|
|
25987
26581
|
/\?\s*$/m,
|
|
25988
26582
|
/^\s*\[?\d+[).\]]\s+\S/m,
|
|
@@ -25991,9 +26585,12 @@ var ASK_SIGNALS = [
|
|
|
25991
26585
|
function readPaneQuestion(text, agent) {
|
|
25992
26586
|
const lines2 = tail2(text, 40);
|
|
25993
26587
|
const body = lines2.join("\n").trim();
|
|
25994
|
-
const
|
|
26588
|
+
const dialog = readChoiceDialog(text);
|
|
26589
|
+
const choices = dialog?.options ?? [];
|
|
25995
26590
|
if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
|
|
25996
26591
|
const question = tail2(text, 25).join("\n").trim();
|
|
26592
|
+
if (!dialog && AGENT_TURN_LINE.test(body))
|
|
26593
|
+
return { asking: false, question, reason: "giliran agen, bukan pertanyaan (baris giliran claude di layar)", choices };
|
|
25997
26594
|
if (agent === "codex") {
|
|
25998
26595
|
const finished = CODEX_FINISHED.find((re) => re.test(body));
|
|
25999
26596
|
if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
|
|
@@ -26008,14 +26605,20 @@ var answers = /* @__PURE__ */ new Map();
|
|
|
26008
26605
|
var capped = /* @__PURE__ */ new Set();
|
|
26009
26606
|
var failures = /* @__PURE__ */ new Map();
|
|
26010
26607
|
var failCapped = /* @__PURE__ */ new Set();
|
|
26608
|
+
var failedAt = /* @__PURE__ */ new Map();
|
|
26609
|
+
var FAIL_COOLDOWN_MS = 15 * 6e4;
|
|
26610
|
+
var chainFlows = /* @__PURE__ */ new Map();
|
|
26011
26611
|
var MAX_CHAIN_STEPS = 6;
|
|
26012
26612
|
var CHAIN_POLL_MS = 300;
|
|
26013
26613
|
var CHAIN_POLL_TRIES = 20;
|
|
26614
|
+
var CHAIN_END_TRIES = 5;
|
|
26014
26615
|
function resetSession(sessionId2) {
|
|
26015
26616
|
answers.delete(sessionId2);
|
|
26016
26617
|
capped.delete(sessionId2);
|
|
26017
26618
|
failures.delete(sessionId2);
|
|
26018
26619
|
failCapped.delete(sessionId2);
|
|
26620
|
+
failedAt.delete(sessionId2);
|
|
26621
|
+
chainFlows.delete(sessionId2);
|
|
26019
26622
|
}
|
|
26020
26623
|
var prodDetectDeps = {
|
|
26021
26624
|
live: () => {
|
|
@@ -26044,7 +26647,7 @@ var prodDetectDeps = {
|
|
|
26044
26647
|
return true;
|
|
26045
26648
|
}
|
|
26046
26649
|
},
|
|
26047
|
-
send: (id, text) => sendToPane(id, text),
|
|
26650
|
+
send: (id, text, choices) => sendToPane(id, text, 50, choices),
|
|
26048
26651
|
clearMarker: (file) => {
|
|
26049
26652
|
try {
|
|
26050
26653
|
writeFileSync5(file, "");
|
|
@@ -26053,6 +26656,12 @@ var prodDetectDeps = {
|
|
|
26053
26656
|
},
|
|
26054
26657
|
submit: (id) => submitPaneDialog(id),
|
|
26055
26658
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
26659
|
+
// Alur yang gagal ditutup bukan alasan menggagalkan rantai yang sudah berhasil: penyapu TTL
|
|
26660
|
+
// (ADR-0102) tetap menutupnya.
|
|
26661
|
+
closeChain: async (flowId) => {
|
|
26662
|
+
await closeFlow(flowId, "submit").catch(() => null);
|
|
26663
|
+
},
|
|
26664
|
+
now: Date.now,
|
|
26056
26665
|
decide,
|
|
26057
26666
|
decideDeps: prodDecideDeps,
|
|
26058
26667
|
delivery: takeDelivery,
|
|
@@ -26078,7 +26687,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
26078
26687
|
skip("lead dijeda untuk project ini");
|
|
26079
26688
|
continue;
|
|
26080
26689
|
}
|
|
26081
|
-
if (!deps.filled(s2.decisionFile)) continue;
|
|
26690
|
+
if (!deps.filled(s2.decisionFile) && !(!deps.exited(s2.id) && readDialogScreen(deps.pane(s2.id)))) continue;
|
|
26082
26691
|
if (deps.exited(s2.id)) {
|
|
26083
26692
|
skip("pane mati");
|
|
26084
26693
|
continue;
|
|
@@ -26111,6 +26720,12 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
26111
26720
|
skip("batas jawaban otomatis tercapai");
|
|
26112
26721
|
continue;
|
|
26113
26722
|
}
|
|
26723
|
+
const lastFail = failedAt.get(s2.id);
|
|
26724
|
+
if (lastFail !== void 0 && deps.now() - lastFail > FAIL_COOLDOWN_MS) {
|
|
26725
|
+
failures.delete(s2.id);
|
|
26726
|
+
failCapped.delete(s2.id);
|
|
26727
|
+
failedAt.delete(s2.id);
|
|
26728
|
+
}
|
|
26114
26729
|
if ((failures.get(s2.id) ?? 0) >= cfg.maxAutoAnswers) {
|
|
26115
26730
|
if (!failCapped.has(s2.id)) {
|
|
26116
26731
|
failCapped.add(s2.id);
|
|
@@ -26156,15 +26771,27 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
26156
26771
|
out3.answered.push(s2.id);
|
|
26157
26772
|
return;
|
|
26158
26773
|
}
|
|
26159
|
-
if (chain.failed)
|
|
26774
|
+
if (chain.failed) {
|
|
26775
|
+
failures.set(s2.id, (failures.get(s2.id) ?? 0) + 1);
|
|
26776
|
+
failedAt.set(s2.id, deps.now());
|
|
26777
|
+
}
|
|
26160
26778
|
out3.skipped.push({ id: s2.id, reason: chain.reason });
|
|
26161
26779
|
});
|
|
26162
26780
|
return out3;
|
|
26163
26781
|
}
|
|
26164
26782
|
async function runChain(s2, agent, deps) {
|
|
26165
26783
|
let acted = false;
|
|
26784
|
+
const done = async (r) => {
|
|
26785
|
+
const flowId = chainFlows.get(s2.id);
|
|
26786
|
+
if (flowId) {
|
|
26787
|
+
chainFlows.delete(s2.id);
|
|
26788
|
+
await deps.closeChain(flowId).catch(() => {
|
|
26789
|
+
});
|
|
26790
|
+
}
|
|
26791
|
+
return r;
|
|
26792
|
+
};
|
|
26166
26793
|
for (let step = 0; step < MAX_CHAIN_STEPS; step++) {
|
|
26167
|
-
const text = deps.pane(s2.id);
|
|
26794
|
+
const text = step === 0 ? deps.pane(s2.id) : await settledPane(s2.id, deps);
|
|
26168
26795
|
const screen = readDialogScreen(text);
|
|
26169
26796
|
if (screen?.kind === "review") {
|
|
26170
26797
|
if (!await deps.submit(s2.id))
|
|
@@ -26172,10 +26799,10 @@ async function runChain(s2, agent, deps) {
|
|
|
26172
26799
|
acted = true;
|
|
26173
26800
|
continue;
|
|
26174
26801
|
}
|
|
26175
|
-
if (step > 0 && !screen) return { acted, done: true, failed: false, reason: "" };
|
|
26802
|
+
if (step > 0 && !screen) return done({ acted, done: true, failed: false, reason: "" });
|
|
26176
26803
|
const read = readPaneQuestion(text, agent);
|
|
26177
26804
|
if (!read.asking) {
|
|
26178
|
-
return step === 0 ? { acted, done: false, failed: false, reason: read.reason } : { acted, done: true, failed: false, reason: "" };
|
|
26805
|
+
return step === 0 ? { acted, done: false, failed: false, reason: read.reason } : done({ acted, done: true, failed: false, reason: "" });
|
|
26179
26806
|
}
|
|
26180
26807
|
const notes = [`Sesi ini menunggu di terminal; teks di bawah adalah layar terakhirnya. Jawablah sebagai masukan yang bisa langsung diketik ke terminal itu (isi \`reply\`).`];
|
|
26181
26808
|
if (read.choices.length) {
|
|
@@ -26187,36 +26814,55 @@ async function runChain(s2, agent, deps) {
|
|
|
26187
26814
|
`Dialog ini BERANTAI: ${screen.tabs.length} pertanyaan dalam satu tanya (${screen.tabs.map((t) => `${t.answered ? "sudah" : "belum"}: ${t.header}`).join(", ")}). Yang sedang tampil pertanyaan ke-${at + 1}; jawab HANYA pertanyaan itu \u2014 sisanya akan ditanyakan sesudah ini.`
|
|
26188
26815
|
);
|
|
26189
26816
|
}
|
|
26817
|
+
const ask = {
|
|
26818
|
+
projectId: s2.projectId,
|
|
26819
|
+
specId: s2.specId,
|
|
26820
|
+
sessionId: s2.id,
|
|
26821
|
+
gate: "detected",
|
|
26822
|
+
kind: "answer",
|
|
26823
|
+
question: read.question,
|
|
26824
|
+
options: read.choices.length ? read.choices : void 0,
|
|
26825
|
+
notes,
|
|
26826
|
+
chain: true
|
|
26827
|
+
};
|
|
26190
26828
|
let row;
|
|
26191
26829
|
try {
|
|
26192
|
-
row = await deps.decide({
|
|
26193
|
-
projectId: s2.projectId,
|
|
26194
|
-
specId: s2.specId,
|
|
26195
|
-
sessionId: s2.id,
|
|
26196
|
-
gate: "detected",
|
|
26197
|
-
kind: "answer",
|
|
26198
|
-
question: read.question,
|
|
26199
|
-
options: read.choices.length ? read.choices : void 0,
|
|
26200
|
-
notes
|
|
26201
|
-
}, deps.decideDeps);
|
|
26830
|
+
row = await deps.decide({ ...ask, flowId: chainFlows.get(s2.id) ?? null }, deps.decideDeps);
|
|
26202
26831
|
} catch (e) {
|
|
26203
26832
|
if (e instanceof LeadBusyError) return { acted, done: false, failed: false, reason: `lead penuh \u2014 ${e.message}` };
|
|
26204
|
-
throw e;
|
|
26833
|
+
if (!(e instanceof LeadFlowClosedError)) throw e;
|
|
26834
|
+
chainFlows.delete(s2.id);
|
|
26835
|
+
try {
|
|
26836
|
+
row = await deps.decide({ ...ask, flowId: null }, deps.decideDeps);
|
|
26837
|
+
} catch (e2) {
|
|
26838
|
+
if (e2 instanceof LeadBusyError) return { acted, done: false, failed: false, reason: `lead penuh \u2014 ${e2.message}` };
|
|
26839
|
+
throw e2;
|
|
26840
|
+
}
|
|
26205
26841
|
}
|
|
26842
|
+
if (row?.flowId) chainFlows.set(s2.id, row.flowId);
|
|
26206
26843
|
if (!row) return { acted, done: false, failed: false, reason: "lead tak menghasilkan keputusan yang berlaku" };
|
|
26207
26844
|
if (row.status !== "berlaku")
|
|
26208
26845
|
return { acted, done: false, failed: true, reason: "lead tak menghasilkan keputusan yang berlaku" };
|
|
26209
26846
|
const sent = deps.delivery(row.id);
|
|
26210
26847
|
const reply = (sent ? leadReplyText(sent) : "") || row.answer;
|
|
26211
|
-
|
|
26848
|
+
const picked = sent?.choices.map((c) => c.option) ?? [];
|
|
26849
|
+
if (!await deps.send(s2.id, reply, picked))
|
|
26212
26850
|
return { acted, done: false, failed: true, reason: "gagal mengetik ke pane" };
|
|
26213
26851
|
acted = true;
|
|
26214
|
-
if (!screen) return { acted, done: true, failed: false, reason: "" };
|
|
26852
|
+
if (!screen) return done({ acted, done: true, failed: false, reason: "" });
|
|
26215
26853
|
if (!await waitScreenChange(s2.id, dialogKey(text), deps))
|
|
26216
26854
|
return { acted, done: false, failed: true, reason: "layar dialog tak berubah sesudah dijawab" };
|
|
26217
26855
|
}
|
|
26218
26856
|
return { acted, done: false, failed: true, reason: "batas langkah rantai dialog tercapai" };
|
|
26219
26857
|
}
|
|
26858
|
+
async function settledPane(id, deps) {
|
|
26859
|
+
let text = deps.pane(id);
|
|
26860
|
+
for (let i = 0; i < CHAIN_END_TRIES && !readDialogScreen(text); i++) {
|
|
26861
|
+
await deps.sleep(CHAIN_POLL_MS);
|
|
26862
|
+
text = deps.pane(id);
|
|
26863
|
+
}
|
|
26864
|
+
return text;
|
|
26865
|
+
}
|
|
26220
26866
|
async function waitScreenChange(id, before, deps) {
|
|
26221
26867
|
for (let i = 0; i < CHAIN_POLL_TRIES; i++) {
|
|
26222
26868
|
await deps.sleep(CHAIN_POLL_MS);
|
|
@@ -26230,6 +26876,8 @@ function sweep(liveIds) {
|
|
|
26230
26876
|
for (const id of [...capped]) if (!live.has(id)) capped.delete(id);
|
|
26231
26877
|
for (const id of [...failures.keys()]) if (!live.has(id)) failures.delete(id);
|
|
26232
26878
|
for (const id of [...failCapped]) if (!live.has(id)) failCapped.delete(id);
|
|
26879
|
+
for (const id of [...failedAt.keys()]) if (!live.has(id)) failedAt.delete(id);
|
|
26880
|
+
for (const id of [...chainFlows.keys()]) if (!live.has(id)) chainFlows.delete(id);
|
|
26233
26881
|
}
|
|
26234
26882
|
|
|
26235
26883
|
// src/services/lead/pulse.ts
|
|
@@ -26531,6 +27179,21 @@ async function tick2(now, deps = {}) {
|
|
|
26531
27179
|
busyDetect = false;
|
|
26532
27180
|
}));
|
|
26533
27181
|
}
|
|
27182
|
+
jobs.push((async () => {
|
|
27183
|
+
const expire = deps.expire ?? expireFlows;
|
|
27184
|
+
const notify = deps.notify ?? recordLeadDecision;
|
|
27185
|
+
for (const f of await expire(/* @__PURE__ */ new Date())) {
|
|
27186
|
+
await notify(
|
|
27187
|
+
f.id,
|
|
27188
|
+
`Rantai keputusan lead ditutup karena kedaluwarsa: ${f.title.slice(0, 80)}`,
|
|
27189
|
+
f.projectId,
|
|
27190
|
+
f.specId,
|
|
27191
|
+
f.sessionId
|
|
27192
|
+
);
|
|
27193
|
+
}
|
|
27194
|
+
})().catch((e) => {
|
|
27195
|
+
console.error("lead expire:", e);
|
|
27196
|
+
}));
|
|
26534
27197
|
if (!cfg.paused && !busyPulse && now - Math.max(lastPulseAt, pulseEndedAt) >= cfg.everyMin * 6e4) {
|
|
26535
27198
|
busyPulse = true;
|
|
26536
27199
|
lastPulseAt = now;
|
|
@@ -26618,11 +27281,35 @@ async function lead_default(app2) {
|
|
|
26618
27281
|
specId: q.specId,
|
|
26619
27282
|
sessionId: q.sessionId,
|
|
26620
27283
|
status: q.status,
|
|
27284
|
+
// SPEC-485 · satu rantai dibaca lewat filter ini, urut NAIK (lihat `listDecisions`).
|
|
27285
|
+
flowId: q.flowId,
|
|
26621
27286
|
take: q.take ? Number(q.take) : void 0,
|
|
26622
27287
|
skip: q.skip ? Number(q.skip) : void 0
|
|
26623
27288
|
});
|
|
26624
27289
|
return { items: rows.map(toDecisionView) };
|
|
26625
27290
|
});
|
|
27291
|
+
app2.get("/lead/flows", async (req) => {
|
|
27292
|
+
const q = req.query;
|
|
27293
|
+
const rows = await listFlows({
|
|
27294
|
+
projectId: q.projectId,
|
|
27295
|
+
status: q.status,
|
|
27296
|
+
take: q.take ? Number(q.take) : void 0,
|
|
27297
|
+
skip: q.skip ? Number(q.skip) : void 0
|
|
27298
|
+
});
|
|
27299
|
+
return { items: rows.map(toFlowView) };
|
|
27300
|
+
});
|
|
27301
|
+
app2.post("/lead/flows/:id/submit", async (req, reply) => {
|
|
27302
|
+
const { id } = req.params;
|
|
27303
|
+
const row = await closeFlow(id, "submit");
|
|
27304
|
+
if (!row) return reply.code(409).send({ error: "rantai tak ada atau sudah tertutup" });
|
|
27305
|
+
return toFlowView(row);
|
|
27306
|
+
});
|
|
27307
|
+
app2.post("/lead/flows/:id/cancel", async (req, reply) => {
|
|
27308
|
+
const { id } = req.params;
|
|
27309
|
+
const row = await closeFlow(id, "operator");
|
|
27310
|
+
if (!row) return reply.code(409).send({ error: "rantai tak ada atau sudah tertutup" });
|
|
27311
|
+
return toFlowView(row);
|
|
27312
|
+
});
|
|
26626
27313
|
app2.post("/lead/decisions", async (req, reply) => {
|
|
26627
27314
|
const parsed = zLeadAsk.safeParse(req.body);
|
|
26628
27315
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
@@ -26632,6 +27319,15 @@ async function lead_default(app2) {
|
|
|
26632
27319
|
const cfg = await getLead();
|
|
26633
27320
|
if (!project.leadOptIn || !leadActive(cfg, ask.projectId))
|
|
26634
27321
|
return reply.code(409).send({ error: "lead tidak aktif untuk project ini" });
|
|
27322
|
+
const optionCount = ask.options.length;
|
|
27323
|
+
if (ask.select.mode === "multi" && optionCount === 0)
|
|
27324
|
+
return reply.code(400).send({ error: "select.mode `multi` menuntut daftar `options`" });
|
|
27325
|
+
if (ask.select.max !== null && ask.select.min > ask.select.max)
|
|
27326
|
+
return reply.code(400).send({ error: "select.min melebihi select.max" });
|
|
27327
|
+
if (ask.select.max !== null && optionCount > 0 && ask.select.max > optionCount)
|
|
27328
|
+
return reply.code(400).send({ error: `select.max (${ask.select.max}) melebihi jumlah opsi (${optionCount})` });
|
|
27329
|
+
if (ask.select.min > optionCount)
|
|
27330
|
+
return reply.code(400).send({ error: `select.min (${ask.select.min}) melebihi jumlah opsi (${optionCount})` });
|
|
26635
27331
|
let row;
|
|
26636
27332
|
try {
|
|
26637
27333
|
row = await decide({
|
|
@@ -26642,9 +27338,13 @@ async function lead_default(app2) {
|
|
|
26642
27338
|
kind: "answer",
|
|
26643
27339
|
question: ask.question,
|
|
26644
27340
|
options: ask.options,
|
|
26645
|
-
notes: ask.context ? [ask.context] : void 0
|
|
27341
|
+
notes: ask.context ? [ask.context] : void 0,
|
|
27342
|
+
select: ask.select,
|
|
27343
|
+
chain: ask.chain,
|
|
27344
|
+
flowId: ask.flowId ?? null
|
|
26646
27345
|
});
|
|
26647
27346
|
} catch (e) {
|
|
27347
|
+
if (e instanceof LeadFlowClosedError) return reply.code(409).send({ error: e.message });
|
|
26648
27348
|
if (!(e instanceof LeadBusyError)) throw e;
|
|
26649
27349
|
return reply.code(503).header("retry-after", String(Math.max(5, cfg.queueWaitSec))).send({ error: e.message, retryable: true, queued: e.queued });
|
|
26650
27350
|
}
|
|
@@ -26657,6 +27357,7 @@ async function lead_default(app2) {
|
|
|
26657
27357
|
} catch {
|
|
26658
27358
|
}
|
|
26659
27359
|
}
|
|
27360
|
+
const flowStatus = row.flowId ? (await prisma.leadFlow.findUnique({ where: { id: row.flowId }, select: { status: true } }))?.status ?? null : null;
|
|
26660
27361
|
const answer = {
|
|
26661
27362
|
id: row.id,
|
|
26662
27363
|
decision: sent?.decision ?? row.answer,
|
|
@@ -26666,7 +27367,12 @@ async function lead_default(app2) {
|
|
|
26666
27367
|
action: row.action,
|
|
26667
27368
|
// Saluran pengiriman bisa meleset (baris lahir dari jalur lain); kolomnya yang selalu ada.
|
|
26668
27369
|
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) : [])
|
|
27370
|
+
missing: sent?.missing ?? (Array.isArray(row.missing) ? row.missing.map(String) : []),
|
|
27371
|
+
// SPEC-485 · `choices` bentuk yang berlaku; `toDecisionView` sudah tahu cara menurunkannya
|
|
27372
|
+
// dari kolom lama, jadi pemetaannya tak diduplikasi di sini (kelas bug SPEC-431/448/475).
|
|
27373
|
+
choices: sent?.choices ?? toDecisionView(row).choices,
|
|
27374
|
+
flowId: row.flowId,
|
|
27375
|
+
flowStatus
|
|
26670
27376
|
};
|
|
26671
27377
|
return reply.code(201).send(answer);
|
|
26672
27378
|
});
|
|
@@ -26674,12 +27380,17 @@ async function lead_default(app2) {
|
|
|
26674
27380
|
const { id } = req.params;
|
|
26675
27381
|
const parsed = zLeadOverride.safeParse(req.body);
|
|
26676
27382
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
26677
|
-
const r = await overrideDecision(id, parsed.data.answer, parsed.data.reason);
|
|
27383
|
+
const r = await overrideDecision(id, parsed.data.answer, parsed.data.reason, parsed.data.choices);
|
|
26678
27384
|
if (!r) return reply.code(409).send({ error: "keputusan tak ada atau sudah tak berlaku" });
|
|
26679
27385
|
let delivered = false;
|
|
26680
27386
|
if (r.next.sessionId) {
|
|
26681
27387
|
resetSession(r.next.sessionId);
|
|
26682
|
-
delivered = await sendToPane(
|
|
27388
|
+
delivered = await sendToPane(
|
|
27389
|
+
r.next.sessionId,
|
|
27390
|
+
parsed.data.answer,
|
|
27391
|
+
50,
|
|
27392
|
+
toDecisionView(r.next).choices.map((c) => c.option)
|
|
27393
|
+
).catch(() => false);
|
|
26683
27394
|
}
|
|
26684
27395
|
return { old: toDecisionView(r.old), next: toDecisionView(r.next), delivered };
|
|
26685
27396
|
});
|
|
@@ -26696,11 +27407,61 @@ async function lead_default(app2) {
|
|
|
26696
27407
|
init_src();
|
|
26697
27408
|
init_db();
|
|
26698
27409
|
|
|
27410
|
+
// src/services/agent-tool-catalog.ts
|
|
27411
|
+
init_src();
|
|
27412
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
27413
|
+
import { homedir as homedir8 } from "node:os";
|
|
27414
|
+
import { join as join15 } from "node:path";
|
|
27415
|
+
var home = () => process.env.HOME || homedir8();
|
|
27416
|
+
var readJson = (path) => {
|
|
27417
|
+
try {
|
|
27418
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
27419
|
+
} catch {
|
|
27420
|
+
return null;
|
|
27421
|
+
}
|
|
27422
|
+
};
|
|
27423
|
+
var serversOf = (node) => {
|
|
27424
|
+
const ms = node?.mcpServers;
|
|
27425
|
+
if (!ms || typeof ms !== "object" || Array.isArray(ms)) return [];
|
|
27426
|
+
return Object.keys(ms);
|
|
27427
|
+
};
|
|
27428
|
+
var codexServers = () => {
|
|
27429
|
+
let text;
|
|
27430
|
+
try {
|
|
27431
|
+
text = readFileSync12(join15(home(), ".codex", "config.toml"), "utf8");
|
|
27432
|
+
} catch {
|
|
27433
|
+
return [];
|
|
27434
|
+
}
|
|
27435
|
+
const out3 = [];
|
|
27436
|
+
for (const m of text.matchAll(/^\s*\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.[^\]]*)?\]/gm)) {
|
|
27437
|
+
const name2 = m[1] ?? m[2];
|
|
27438
|
+
if (name2) out3.push(name2);
|
|
27439
|
+
}
|
|
27440
|
+
return out3;
|
|
27441
|
+
};
|
|
27442
|
+
function mcpServerNames(repoDir) {
|
|
27443
|
+
const names = [];
|
|
27444
|
+
const claudeJson = readJson(join15(home(), ".claude.json"));
|
|
27445
|
+
names.push(...serversOf(claudeJson));
|
|
27446
|
+
if (repoDir) {
|
|
27447
|
+
const projects = claudeJson?.projects;
|
|
27448
|
+
if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
|
|
27449
|
+
names.push(...serversOf(readJson(join15(repoDir, ".mcp.json"))));
|
|
27450
|
+
}
|
|
27451
|
+
names.push(...codexServers());
|
|
27452
|
+
return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
27453
|
+
}
|
|
27454
|
+
function agentToolCatalog(repoDir) {
|
|
27455
|
+
return [ALL_TOOLS_ENTRY, ...BUILTIN_AGENT_TOOLS, ...mcpServerNames(repoDir).map(mcpToolEntry)];
|
|
27456
|
+
}
|
|
27457
|
+
var agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
|
|
27458
|
+
|
|
26699
27459
|
// src/services/custom-agents.ts
|
|
26700
27460
|
init_db();
|
|
26701
27461
|
init_src();
|
|
26702
27462
|
init_pty();
|
|
26703
27463
|
var cache4 = [];
|
|
27464
|
+
var repoDirCache = /* @__PURE__ */ new Map();
|
|
26704
27465
|
var asCustomAgent = (r) => ({
|
|
26705
27466
|
id: r.id,
|
|
26706
27467
|
projectId: r.projectId,
|
|
@@ -26710,6 +27471,7 @@ var asCustomAgent = (r) => ({
|
|
|
26710
27471
|
tools: toolsOf(r.tools),
|
|
26711
27472
|
model: r.model,
|
|
26712
27473
|
mentions: mentionsOf(r.mentions),
|
|
27474
|
+
runtime: runtimeOf(r.runtime),
|
|
26713
27475
|
enabled: r.enabled,
|
|
26714
27476
|
createdAt: "",
|
|
26715
27477
|
updatedAt: ""
|
|
@@ -26718,18 +27480,31 @@ var asCustomAgent = (r) => ({
|
|
|
26718
27480
|
async function loadCustomAgents() {
|
|
26719
27481
|
try {
|
|
26720
27482
|
cache4 = await prisma.customAgent.findMany();
|
|
27483
|
+
const projects = await prisma.project.findMany({ select: { id: true, repoDir: true } });
|
|
27484
|
+
const bindings = await prisma.localBinding.findMany({ select: { projectId: true, repoDir: true } });
|
|
27485
|
+
const next = /* @__PURE__ */ new Map();
|
|
27486
|
+
for (const p of projects) next.set(p.id, p.repoDir ?? null);
|
|
27487
|
+
for (const b of bindings) next.set(b.projectId, b.repoDir ?? null);
|
|
27488
|
+
repoDirCache = next;
|
|
26721
27489
|
} catch {
|
|
26722
27490
|
cache4 = [];
|
|
27491
|
+
repoDirCache = /* @__PURE__ */ new Map();
|
|
26723
27492
|
}
|
|
26724
27493
|
}
|
|
26725
|
-
function agentDefsFor(projectId) {
|
|
27494
|
+
function agentDefsFor(projectId, agent) {
|
|
26726
27495
|
const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
|
|
26727
27496
|
const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
|
|
26728
|
-
|
|
27497
|
+
const eff = effectiveAgents(globals, project).filter((a) => a.runtime === null || a.runtime === agent);
|
|
27498
|
+
const needsCatalog = eff.some((a) => (a.tools ?? []).includes(ALL_TOOLS));
|
|
27499
|
+
const catalogIds = needsCatalog ? agentToolIds(repoDirCache.get(projectId) ?? null) : [];
|
|
27500
|
+
return eff.map((a) => ({
|
|
26729
27501
|
name: a.name,
|
|
26730
27502
|
description: a.description,
|
|
26731
27503
|
instructions: a.instructions,
|
|
26732
|
-
|
|
27504
|
+
// Ekspansi terjadi DI SINI, sebelum `resolveTools` di runner: meneruskan `"*"` apa adanya
|
|
27505
|
+
// membuat claude membuangnya senyap (agen tanpa alat), sementara menerjemahkannya jadi `null`
|
|
27506
|
+
// membuat agen mewarisi SELURUH tool termasuk `Task` — lapis 2 anti-loop lenyap tanpa jejak.
|
|
27507
|
+
tools: expandTools(a.tools, catalogIds),
|
|
26733
27508
|
model: a.model,
|
|
26734
27509
|
mentions: a.mentions ?? []
|
|
26735
27510
|
}));
|
|
@@ -26753,7 +27528,7 @@ function unknownMentions(row, all) {
|
|
|
26753
27528
|
}
|
|
26754
27529
|
async function installCustomAgents() {
|
|
26755
27530
|
await loadCustomAgents();
|
|
26756
|
-
registerCustomAgentSource((projectId) => agentDefsFor(projectId));
|
|
27531
|
+
registerCustomAgentSource((projectId, agent) => agentDefsFor(projectId, agent));
|
|
26757
27532
|
}
|
|
26758
27533
|
|
|
26759
27534
|
// src/routes/custom-agents.ts
|
|
@@ -26767,10 +27542,33 @@ var view6 = (r, projectId) => ({
|
|
|
26767
27542
|
tools: toolsOf(r.tools),
|
|
26768
27543
|
model: r.model,
|
|
26769
27544
|
mentions: mentionsOf(r.mentions),
|
|
27545
|
+
runtime: runtimeOf(r.runtime),
|
|
26770
27546
|
enabled: r.enabled,
|
|
26771
27547
|
...projectId ? { inherited: r.projectId === null } : {}
|
|
26772
27548
|
});
|
|
27549
|
+
var repoDirOf2 = async (projectId) => projectId ? await resolveRepoDir(projectId) : null;
|
|
27550
|
+
function toolsProblem(tools, catalogIds) {
|
|
27551
|
+
if (!tools || tools.length === 0) return null;
|
|
27552
|
+
if (tools.includes(ALL_TOOLS) && tools.length > 1) {
|
|
27553
|
+
return { error: "pintasan * harus jadi satu-satunya pilihan tools", unknownTools: [] };
|
|
27554
|
+
}
|
|
27555
|
+
const unknownTools = tools.filter((t) => !catalogIds.includes(t));
|
|
27556
|
+
return unknownTools.length ? { error: "tool tak dikenal di mesin ini", unknownTools } : null;
|
|
27557
|
+
}
|
|
27558
|
+
function modelProblem(model, runtime) {
|
|
27559
|
+
if (!model) return null;
|
|
27560
|
+
const ok2 = modelsForRuntime(runtime).some((m) => m.id === model);
|
|
27561
|
+
return ok2 ? null : { error: "model tak dikenal untuk runtime ini", model, runtime };
|
|
27562
|
+
}
|
|
26773
27563
|
async function custom_agents_default(app2) {
|
|
27564
|
+
app2.get("/custom-agents/catalog", async (req) => {
|
|
27565
|
+
const projectId = req.query.projectId;
|
|
27566
|
+
return {
|
|
27567
|
+
tools: agentToolCatalog(await repoDirOf2(projectId)),
|
|
27568
|
+
models: modelsForRuntime(null),
|
|
27569
|
+
runtimes: AGENT_RUNTIMES.map((id) => ({ id, label: AGENT_RUNTIME_LABELS[id] }))
|
|
27570
|
+
};
|
|
27571
|
+
});
|
|
26774
27572
|
app2.get("/custom-agents", async (req) => {
|
|
26775
27573
|
const projectId = req.query.projectId;
|
|
26776
27574
|
const rows = await prisma.customAgent.findMany({
|
|
@@ -26789,6 +27587,10 @@ async function custom_agents_default(app2) {
|
|
|
26789
27587
|
const projectId = p.projectId ?? null;
|
|
26790
27588
|
if (projectId && !await prisma.project.findUnique({ where: { id: projectId } }))
|
|
26791
27589
|
return reply.code(400).send({ error: "project tak ditemukan", projectId });
|
|
27590
|
+
const tp = toolsProblem(p.tools ?? null, agentToolIds(await repoDirOf2(projectId)));
|
|
27591
|
+
if (tp) return reply.code(400).send(tp);
|
|
27592
|
+
const mp = modelProblem(p.model ?? null, p.runtime ?? null);
|
|
27593
|
+
if (mp) return reply.code(400).send(mp);
|
|
26792
27594
|
const id = customAgentId(projectId, p.name);
|
|
26793
27595
|
if (await prisma.customAgent.findUnique({ where: { id } }))
|
|
26794
27596
|
return reply.code(409).send({ error: "nama sudah dipakai di scope ini", id });
|
|
@@ -26801,6 +27603,7 @@ async function custom_agents_default(app2) {
|
|
|
26801
27603
|
tools: p.tools ?? null,
|
|
26802
27604
|
model: p.model ?? null,
|
|
26803
27605
|
mentions: p.mentions ?? [],
|
|
27606
|
+
runtime: p.runtime ?? null,
|
|
26804
27607
|
enabled: p.enabled ?? true
|
|
26805
27608
|
};
|
|
26806
27609
|
const all = [...await rowsOf(), candidate];
|
|
@@ -26817,6 +27620,7 @@ async function custom_agents_default(app2) {
|
|
|
26817
27620
|
tools: candidate.tools,
|
|
26818
27621
|
model: candidate.model,
|
|
26819
27622
|
mentions: candidate.mentions,
|
|
27623
|
+
runtime: candidate.runtime,
|
|
26820
27624
|
enabled: candidate.enabled
|
|
26821
27625
|
} });
|
|
26822
27626
|
await loadCustomAgents();
|
|
@@ -26833,11 +27637,24 @@ async function custom_agents_default(app2) {
|
|
|
26833
27637
|
const existing = await prisma.customAgent.findUnique({ where: { id } });
|
|
26834
27638
|
if (!existing) return reply.code(404).send({ error: "not found" });
|
|
26835
27639
|
const before = existing;
|
|
27640
|
+
const effRuntime = "runtime" in parsed.data ? parsed.data.runtime ?? null : runtimeOf(before.runtime);
|
|
27641
|
+
if (parsed.data.tools !== void 0) {
|
|
27642
|
+
const tp = toolsProblem(parsed.data.tools, agentToolIds(await repoDirOf2(before.projectId)));
|
|
27643
|
+
if (tp) return reply.code(400).send(tp);
|
|
27644
|
+
}
|
|
27645
|
+
if (parsed.data.model !== void 0 || "runtime" in parsed.data) {
|
|
27646
|
+
const mp = modelProblem(
|
|
27647
|
+
parsed.data.model !== void 0 ? parsed.data.model : before.model,
|
|
27648
|
+
effRuntime
|
|
27649
|
+
);
|
|
27650
|
+
if (mp) return reply.code(400).send(mp);
|
|
27651
|
+
}
|
|
26836
27652
|
const candidate = {
|
|
26837
27653
|
...before,
|
|
26838
27654
|
...parsed.data,
|
|
26839
27655
|
mentions: parsed.data.mentions ?? mentionsOf(before.mentions),
|
|
26840
|
-
tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before.tools)
|
|
27656
|
+
tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before.tools),
|
|
27657
|
+
runtime: effRuntime
|
|
26841
27658
|
};
|
|
26842
27659
|
const all = (await rowsOf()).map((r) => r.id === id ? candidate : r);
|
|
26843
27660
|
const unknown = unknownMentions(candidate, all);
|
|
@@ -26850,6 +27667,7 @@ async function custom_agents_default(app2) {
|
|
|
26850
27667
|
tools: candidate.tools,
|
|
26851
27668
|
model: candidate.model,
|
|
26852
27669
|
mentions: candidate.mentions,
|
|
27670
|
+
runtime: candidate.runtime,
|
|
26853
27671
|
enabled: candidate.enabled
|
|
26854
27672
|
} });
|
|
26855
27673
|
await loadCustomAgents();
|
|
@@ -26900,24 +27718,24 @@ function githubSlugFromUrl(url2) {
|
|
|
26900
27718
|
async function resolveGithubRepo(projectId) {
|
|
26901
27719
|
const project = await prisma.project.findUnique({ where: { id: projectId } });
|
|
26902
27720
|
if (!project) return { ok: false, kind: "no-project", error: `project "${projectId}" tidak ada` };
|
|
26903
|
-
const
|
|
26904
|
-
if (project.gitRemote)
|
|
27721
|
+
const candidates2 = [];
|
|
27722
|
+
if (project.gitRemote) candidates2.push(project.gitRemote);
|
|
26905
27723
|
const repoDir = await resolveRepoDir(projectId).catch(() => null);
|
|
26906
27724
|
if (repoDir) {
|
|
26907
27725
|
const origin = (await listRemotes(repoDir)).find((r) => r.name === "origin");
|
|
26908
|
-
if (origin?.fetch)
|
|
27726
|
+
if (origin?.fetch) candidates2.push(origin.fetch);
|
|
26909
27727
|
}
|
|
26910
|
-
if (
|
|
27728
|
+
if (candidates2.length === 0)
|
|
26911
27729
|
return {
|
|
26912
27730
|
ok: false,
|
|
26913
27731
|
kind: "no-remote",
|
|
26914
27732
|
error: "project belum punya remote GitHub (isi gitRemote atau tambahkan origin di repo lokalnya)"
|
|
26915
27733
|
};
|
|
26916
|
-
for (const url2 of
|
|
27734
|
+
for (const url2 of candidates2) {
|
|
26917
27735
|
const gh = githubSlugFromUrl(url2);
|
|
26918
27736
|
if (gh) return { ok: true, repo: gh };
|
|
26919
27737
|
}
|
|
26920
|
-
const host2 = parse2(
|
|
27738
|
+
const host2 = parse2(candidates2[0])?.host ?? candidates2[0];
|
|
26921
27739
|
return {
|
|
26922
27740
|
ok: false,
|
|
26923
27741
|
kind: "not-github",
|
|
@@ -26967,7 +27785,7 @@ function issuesFromRest(raw) {
|
|
|
26967
27785
|
}
|
|
26968
27786
|
var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
|
|
26969
27787
|
var API2 = "https://api.github.com";
|
|
26970
|
-
var defaultRunGh = (args, env) => new Promise((
|
|
27788
|
+
var defaultRunGh = (args, env) => new Promise((resolve15, reject2) => {
|
|
26971
27789
|
execFile10(
|
|
26972
27790
|
args[0],
|
|
26973
27791
|
args.slice(1),
|
|
@@ -26975,7 +27793,7 @@ var defaultRunGh = (args, env) => new Promise((resolve14, reject2) => {
|
|
|
26975
27793
|
(err, stdout, stderr) => {
|
|
26976
27794
|
const e = err;
|
|
26977
27795
|
if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject2(e);
|
|
26978
|
-
|
|
27796
|
+
resolve15({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
|
|
26979
27797
|
}
|
|
26980
27798
|
);
|
|
26981
27799
|
});
|
|
@@ -27294,8 +28112,42 @@ import { randomBytes as randomBytes5 } from "node:crypto";
|
|
|
27294
28112
|
// src/services/telegram/credentials.ts
|
|
27295
28113
|
init_src();
|
|
27296
28114
|
init_config2();
|
|
28115
|
+
init_agent_token();
|
|
28116
|
+
init_settings2();
|
|
28117
|
+
init_bootstrap();
|
|
27297
28118
|
init_client();
|
|
27298
28119
|
init_protocol();
|
|
28120
|
+
init_runtime();
|
|
28121
|
+
async function verifyTelegramAgentToken(token, deps = {}) {
|
|
28122
|
+
const agent = await (deps.verify ?? verifyAgentToken)(token);
|
|
28123
|
+
if (!agent) {
|
|
28124
|
+
return {
|
|
28125
|
+
ok: false,
|
|
28126
|
+
missing: [...TELEGRAM_REQUIRED_CAPABILITIES],
|
|
28127
|
+
reason: "AgentToken tidak dikenal atau sudah dicabut \u2014 salin plaintext `hnm_agt_\u2026` dari Akses AI Agent, bukan hash-nya."
|
|
28128
|
+
};
|
|
28129
|
+
}
|
|
28130
|
+
const missing = TELEGRAM_REQUIRED_CAPABILITIES.filter((c) => !agent.capabilities.includes(c));
|
|
28131
|
+
return missing.length ? { ok: false, missing, reason: `AgentToken kurang ${missing.length} capability.` } : { ok: true };
|
|
28132
|
+
}
|
|
28133
|
+
async function telegramInboundReadiness(deps = {}) {
|
|
28134
|
+
const polling = deps.polling ?? telegramRuntimeStatus().running;
|
|
28135
|
+
const empty = [];
|
|
28136
|
+
const token = deps.agentToken ?? effectiveStr("HANOMAN_TELEGRAM_AGENT_TOKEN")?.trim();
|
|
28137
|
+
if (!token) {
|
|
28138
|
+
return { ok: false, reason: "AgentToken gateway belum diisi.", missingCapabilities: [...TELEGRAM_REQUIRED_CAPABILITIES], polling };
|
|
28139
|
+
}
|
|
28140
|
+
const setting = await (deps.getSetting ?? getSetting)();
|
|
28141
|
+
if (!setting.agentAccessEnabled) {
|
|
28142
|
+
return { ok: false, reason: "Akses agent mati \u2014 nyalakan master switch di Akses AI Agent.", missingCapabilities: empty, polling };
|
|
28143
|
+
}
|
|
28144
|
+
const gate = await verifyTelegramAgentToken(token, deps);
|
|
28145
|
+
if (!gate.ok) return { ok: false, reason: gate.reason, missingCapabilities: gate.missing, polling };
|
|
28146
|
+
if (!polling) {
|
|
28147
|
+
return { ok: false, reason: "Kredensial sudah sah tapi gateway belum polling \u2014 nyalakan \u201CGateway aktif\u201D.", missingCapabilities: empty, polling };
|
|
28148
|
+
}
|
|
28149
|
+
return { ok: true, reason: null, missingCapabilities: empty, polling };
|
|
28150
|
+
}
|
|
27299
28151
|
var KEYS = TELEGRAM_CONFIG_KEYS;
|
|
27300
28152
|
function telegramCredentialView() {
|
|
27301
28153
|
return {
|
|
@@ -27313,7 +28165,7 @@ function telegramCredentialView() {
|
|
|
27313
28165
|
})
|
|
27314
28166
|
};
|
|
27315
28167
|
}
|
|
27316
|
-
async function saveTelegramCredentials(patch) {
|
|
28168
|
+
async function saveTelegramCredentials(patch, deps = {}) {
|
|
27317
28169
|
const writes = [];
|
|
27318
28170
|
for (const [key, raw] of Object.entries(patch)) {
|
|
27319
28171
|
if (!KEYS.includes(key)) return { ok: false, key, error: "key bukan kredensial Telegram" };
|
|
@@ -27327,6 +28179,14 @@ async function saveTelegramCredentials(patch) {
|
|
|
27327
28179
|
if (!parsed.ok) return { ok: false, key, error: parsed.error };
|
|
27328
28180
|
writes.push([key, parsed.value]);
|
|
27329
28181
|
}
|
|
28182
|
+
const agentToken = writes.find(([key]) => key === "HANOMAN_TELEGRAM_AGENT_TOKEN")?.[1];
|
|
28183
|
+
if (agentToken !== void 0) {
|
|
28184
|
+
const gate = await verifyTelegramAgentToken(agentToken, deps);
|
|
28185
|
+
if (!gate.ok) {
|
|
28186
|
+
const detail = gate.missing.length < TELEGRAM_REQUIRED_CAPABILITIES.length ? `${gate.reason} Kurang: ${gate.missing.join(", ")}.` : gate.reason;
|
|
28187
|
+
return { ok: false, key: "HANOMAN_TELEGRAM_AGENT_TOKEN", error: detail };
|
|
28188
|
+
}
|
|
28189
|
+
}
|
|
27330
28190
|
for (const [key, value] of writes) await setConfig(key, value);
|
|
27331
28191
|
return { ok: true };
|
|
27332
28192
|
}
|
|
@@ -27341,15 +28201,16 @@ async function clearTelegramCredentials() {
|
|
|
27341
28201
|
return { cleared, envFallback: KEYS.filter((k) => sourceOf(k) === "env") };
|
|
27342
28202
|
}
|
|
27343
28203
|
async function testTelegramConnection(deps = {}) {
|
|
28204
|
+
const inbound = await telegramInboundReadiness(deps);
|
|
27344
28205
|
const botToken = deps.botToken ?? effectiveStr("HANOMAN_TELEGRAM_BOT_TOKEN");
|
|
27345
|
-
if (!botToken) return { ok: false, error: "Bot token belum diisi." };
|
|
28206
|
+
if (!botToken) return { ok: false, error: "Bot token belum diisi.", inbound };
|
|
27346
28207
|
const chatId = deps.chatId ?? resolveTestChatId();
|
|
27347
28208
|
if (!chatId) {
|
|
27348
|
-
return { ok: false, error: "Isi Chat / Channel ID target \u2014 atau isi allowlist dengan tepat satu user id." };
|
|
28209
|
+
return { ok: false, inbound, error: "Isi Chat / Channel ID target \u2014 atau isi allowlist dengan tepat satu user id." };
|
|
27349
28210
|
}
|
|
27350
28211
|
const timeoutMs = deps.timeoutMs ?? 1e4;
|
|
27351
28212
|
const controller = new AbortController();
|
|
27352
|
-
const
|
|
28213
|
+
const timer7 = setTimeout(() => controller.abort(), timeoutMs);
|
|
27353
28214
|
const send = deps.transport ?? ((url2, init) => fetch(url2, init));
|
|
27354
28215
|
const transport = (url2, init) => send(url2, { ...init, signal: controller.signal });
|
|
27355
28216
|
const client = new TelegramApiClient(botToken, transport);
|
|
@@ -27357,13 +28218,13 @@ async function testTelegramConnection(deps = {}) {
|
|
|
27357
28218
|
const me = await client.getMe();
|
|
27358
28219
|
const stamp2 = (deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
27359
28220
|
await client.sendMessage({ chatId, text: `hanoman: uji koneksi Telegram berhasil (${stamp2}).` });
|
|
27360
|
-
return { ok: true, botUsername: me.username ?? null, chatId };
|
|
28221
|
+
return { ok: true, botUsername: me.username ?? null, chatId, inbound };
|
|
27361
28222
|
} catch (error) {
|
|
27362
28223
|
const raw = error.message || "gagal menghubungi Telegram";
|
|
27363
28224
|
const message = controller.signal.aborted ? `Timeout ${timeoutMs} ms \u2014 Telegram tidak menjawab.` : raw;
|
|
27364
|
-
return { ok: false, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
|
|
28225
|
+
return { ok: false, inbound, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
|
|
27365
28226
|
} finally {
|
|
27366
|
-
clearTimeout(
|
|
28227
|
+
clearTimeout(timer7);
|
|
27367
28228
|
}
|
|
27368
28229
|
}
|
|
27369
28230
|
function resolveTestChatId() {
|
|
@@ -27669,7 +28530,7 @@ async function sendOnce(o, deps = {}) {
|
|
|
27669
28530
|
nowSec: o.nowSec ?? Math.floor(Date.now() / 1e3)
|
|
27670
28531
|
});
|
|
27671
28532
|
const ac = new AbortController();
|
|
27672
|
-
const
|
|
28533
|
+
const timer7 = setTimeout(() => ac.abort(), WEBHOOK_TIMEOUT_MS);
|
|
27673
28534
|
try {
|
|
27674
28535
|
const f = deps.fetcher ?? ((u, init) => fetch(u, init));
|
|
27675
28536
|
const res = await f(parsed.url.toString(), { method: "POST", headers, body: o.body, signal: ac.signal });
|
|
@@ -27683,7 +28544,7 @@ async function sendOnce(o, deps = {}) {
|
|
|
27683
28544
|
const msg = e.name === "AbortError" ? `timeout ${WEBHOOK_TIMEOUT_MS} ms` : e.message;
|
|
27684
28545
|
return fail2(msg);
|
|
27685
28546
|
} finally {
|
|
27686
|
-
clearTimeout(
|
|
28547
|
+
clearTimeout(timer7);
|
|
27687
28548
|
}
|
|
27688
28549
|
}
|
|
27689
28550
|
|
|
@@ -28266,9 +29127,15 @@ var PUBLIC = /* @__PURE__ */ new Set([
|
|
|
28266
29127
|
"GET /api/health",
|
|
28267
29128
|
"GET /api/auth/status",
|
|
28268
29129
|
"POST /api/auth/login",
|
|
28269
|
-
"POST /api/auth/setup"
|
|
29130
|
+
"POST /api/auth/setup",
|
|
29131
|
+
// SPEC-489 · panduan AI agent. Sengaja tanpa auth: byte-nya sudah publik di GitHub, dan
|
|
29132
|
+
// "cukup diberi link + token" hanya benar bila link-nya terbaca SEBELUM token disetel —
|
|
29133
|
+
// menggerbanginya berarti agen yang capability-nya kurang menerima 403 pada dokumen yang
|
|
29134
|
+
// justru menjelaskan arti 403 itu.
|
|
29135
|
+
"GET /api/agent-integration.md"
|
|
28270
29136
|
]);
|
|
28271
|
-
function buildApp({ requireAuth = true } = {}) {
|
|
29137
|
+
function buildApp({ requireAuth = true, agentDocFile } = {}) {
|
|
29138
|
+
const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync12);
|
|
28272
29139
|
const app2 = Fastify({ logger: false, trustProxy: true });
|
|
28273
29140
|
app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
28274
29141
|
if (!body) return done(null, void 0);
|
|
@@ -28322,6 +29189,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
28322
29189
|
api.addHook("onResponse", auditTelegramGatewayResponse);
|
|
28323
29190
|
await api.register(auth_default);
|
|
28324
29191
|
await api.register(health_default);
|
|
29192
|
+
await api.register(agent_doc_default, { file: docFile });
|
|
28325
29193
|
await api.register(projects_default);
|
|
28326
29194
|
await api.register(specs_default);
|
|
28327
29195
|
await api.register(notifications_default);
|
|
@@ -28885,6 +29753,120 @@ function startWebhookEngine() {
|
|
|
28885
29753
|
}).catch((e) => console.error("webhook reset:", e));
|
|
28886
29754
|
}
|
|
28887
29755
|
|
|
29756
|
+
// src/services/auto-merge.ts
|
|
29757
|
+
init_src();
|
|
29758
|
+
init_db();
|
|
29759
|
+
import { execFile as execFile11 } from "node:child_process";
|
|
29760
|
+
import { promisify as promisify10 } from "node:util";
|
|
29761
|
+
var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
29762
|
+
var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
|
|
29763
|
+
var TICK_MS4 = 6e4;
|
|
29764
|
+
var exec8 = promisify10(execFile11);
|
|
29765
|
+
var GIT7 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
|
|
29766
|
+
var gitOut = async (cwd, args) => {
|
|
29767
|
+
try {
|
|
29768
|
+
return (await exec8("git", args, { cwd, ...GIT7 })).stdout.trim();
|
|
29769
|
+
} catch {
|
|
29770
|
+
return null;
|
|
29771
|
+
}
|
|
29772
|
+
};
|
|
29773
|
+
var prodAutoMergeDeps = {
|
|
29774
|
+
repoDir: resolveRepoDir,
|
|
29775
|
+
defaultBranch,
|
|
29776
|
+
// Tanpa `fetch`: `git push` dari worktree repo yang sama memperbarui remote-tracking ref
|
|
29777
|
+
// di repo itu juga, jadi origin/<branch> sudah mutakhir tepat setelah sesi mem-push.
|
|
29778
|
+
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}`]),
|
|
29779
|
+
// `merge-base --is-ancestor` keluar 0/1 tanpa output → "" saat benar, null saat salah/galat.
|
|
29780
|
+
contains: async (repoDir, tip, sha) => await gitOut(repoDir, ["merge-base", "--is-ancestor", "--end-of-options", sha, tip]) !== null,
|
|
29781
|
+
integrate,
|
|
29782
|
+
discardWorktree: discardMergeWorktree,
|
|
29783
|
+
deleteBranch: deleteMergedBranch
|
|
29784
|
+
};
|
|
29785
|
+
async function candidates(now) {
|
|
29786
|
+
const since = new Date(now.getTime() - AUTO_MERGE_WINDOW_MS);
|
|
29787
|
+
const done = await prisma.notification.findMany({
|
|
29788
|
+
where: { type: "done", createdAt: { gte: since }, specId: { not: null } },
|
|
29789
|
+
select: { specId: true, projectId: true, createdAt: true }
|
|
29790
|
+
});
|
|
29791
|
+
if (!done.length) return [];
|
|
29792
|
+
const keys = done.map((d) => `automerge:${d.specId}`);
|
|
29793
|
+
const marked2 = new Set(
|
|
29794
|
+
(await prisma.notification.findMany({ where: { key: { in: keys } }, select: { key: true } })).map((n2) => n2.key)
|
|
29795
|
+
);
|
|
29796
|
+
return done.filter((d) => !marked2.has(`automerge:${d.specId}`)).map((d) => ({ specId: d.specId, projectId: d.projectId ?? "", doneAt: d.createdAt }));
|
|
29797
|
+
}
|
|
29798
|
+
async function sweepAutoMerge(deps = prodAutoMergeDeps, now = /* @__PURE__ */ new Date()) {
|
|
29799
|
+
const list2 = await candidates(now);
|
|
29800
|
+
if (!list2.length) return 0;
|
|
29801
|
+
let settled = 0;
|
|
29802
|
+
for (const c of list2) {
|
|
29803
|
+
try {
|
|
29804
|
+
if (await settleOne(c, deps, now)) settled++;
|
|
29805
|
+
} catch (e) {
|
|
29806
|
+
console.error(`auto-merge ${c.specId}:`, e);
|
|
29807
|
+
}
|
|
29808
|
+
}
|
|
29809
|
+
return settled;
|
|
29810
|
+
}
|
|
29811
|
+
async function settleOne(c, deps, now) {
|
|
29812
|
+
const spec = await prisma.spec.findUnique({ where: { id: c.specId } });
|
|
29813
|
+
if (!spec || spec.stage !== "done") return false;
|
|
29814
|
+
const project = await prisma.project.findUnique({ where: { id: spec.projectId } });
|
|
29815
|
+
if (!project) return false;
|
|
29816
|
+
const policy = resolveAutoMerge(
|
|
29817
|
+
project.autoMerge,
|
|
29818
|
+
spec.autoMerge
|
|
29819
|
+
);
|
|
29820
|
+
if (policy.mode === "off") return false;
|
|
29821
|
+
const report = (t) => recordAutoMerge(spec.id, spec.projectId, t).then(() => true);
|
|
29822
|
+
const repoDir = await deps.repoDir(spec.projectId);
|
|
29823
|
+
if (!repoDir)
|
|
29824
|
+
return report(`Auto-merge ${spec.id} dilewati \u2014 project belum di-bind ke checkout lokal`);
|
|
29825
|
+
const target2 = autoMergeTargetOf(
|
|
29826
|
+
policy,
|
|
29827
|
+
policy.mode === "default-branch" ? await deps.defaultBranch(repoDir) : null
|
|
29828
|
+
);
|
|
29829
|
+
if (!target2)
|
|
29830
|
+
return report(`Auto-merge ${spec.id} dilewati \u2014 default branch repo tak bisa diresolve; pilih branch tujuan di Settings project`);
|
|
29831
|
+
const branch = sourceBranch(spec.id);
|
|
29832
|
+
const tip = await deps.sourceTip(repoDir, branch);
|
|
29833
|
+
const ready = tip !== null && (spec.headSha === null || await deps.contains(repoDir, tip, spec.headSha));
|
|
29834
|
+
if (!ready) {
|
|
29835
|
+
if (now.getTime() - c.doneAt.getTime() <= AUTO_MERGE_GRACE_MS) return false;
|
|
29836
|
+
return report(`Auto-merge ${spec.id} dilewati \u2014 branch kerja \`${branch}\` belum ter-push ke origin`);
|
|
29837
|
+
}
|
|
29838
|
+
const res = await deps.integrate(repoDir, spec.id, "merge", target2);
|
|
29839
|
+
if (res.status === "clean") {
|
|
29840
|
+
if (policy.deleteBranch) await deps.deleteBranch(repoDir, branch).catch(() => {
|
|
29841
|
+
});
|
|
29842
|
+
return report(`Auto-merge ${spec.id} \u2192 ${target2} bersih (${res.detail})`);
|
|
29843
|
+
}
|
|
29844
|
+
if (res.status === "conflict") {
|
|
29845
|
+
await deps.discardWorktree(repoDir, res.worktree).catch(() => {
|
|
29846
|
+
});
|
|
29847
|
+
return report(`Auto-merge ${spec.id} \u2192 ${target2} GAGAL: konflik \u2014 branch kerja \`${branch}\` utuh, selesaikan lewat Rebase / Merge di backlog`);
|
|
29848
|
+
}
|
|
29849
|
+
return report(`Auto-merge ${spec.id} \u2192 ${target2} GAGAL: ${res.error}`);
|
|
29850
|
+
}
|
|
29851
|
+
var timer6;
|
|
29852
|
+
var busy3 = false;
|
|
29853
|
+
async function tick5() {
|
|
29854
|
+
if (busy3) return;
|
|
29855
|
+
busy3 = true;
|
|
29856
|
+
try {
|
|
29857
|
+
await sweepAutoMerge();
|
|
29858
|
+
} catch (e) {
|
|
29859
|
+
console.error("auto-merge sweep:", e);
|
|
29860
|
+
} finally {
|
|
29861
|
+
busy3 = false;
|
|
29862
|
+
}
|
|
29863
|
+
}
|
|
29864
|
+
function startAutoMerge() {
|
|
29865
|
+
if (timer6) return;
|
|
29866
|
+
timer6 = setInterval(() => void tick5(), TICK_MS4);
|
|
29867
|
+
timer6.unref();
|
|
29868
|
+
}
|
|
29869
|
+
|
|
28888
29870
|
// src/server.ts
|
|
28889
29871
|
var app = buildApp();
|
|
28890
29872
|
var port = Number(process.env.PORT ?? 8787);
|
|
@@ -28931,6 +29913,7 @@ app.listen({ port, host }).then(async () => {
|
|
|
28931
29913
|
startScheduler();
|
|
28932
29914
|
startLead();
|
|
28933
29915
|
startWebhookEngine();
|
|
29916
|
+
startAutoMerge();
|
|
28934
29917
|
}).catch((err) => {
|
|
28935
29918
|
console.error("listen gagal:", err);
|
|
28936
29919
|
process.exit(1);
|