hanoman 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +162 -89
- package/dist/server.js +834 -55
- package/package.json +1 -1
- package/prisma/migrations/20260731210000_spec_depends_on/migration.sql +7 -0
- package/prisma/migrations/20260801120000_custom_agent/migration.sql +27 -0
- package/prisma/schema.prisma +43 -2
- package/web/assets/{index-BWyQwoAk.js → index-D5MU9hT6.js} +1534 -1534
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -4182,7 +4182,7 @@ function cmpVersion(a, b) {
|
|
|
4182
4182
|
}
|
|
4183
4183
|
return 0;
|
|
4184
4184
|
}
|
|
4185
|
-
var zProject, zBriefPayload, zQaPayload, zGoalPayload, 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;
|
|
4185
|
+
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;
|
|
4186
4186
|
var init_entities = __esm({
|
|
4187
4187
|
"../shared/src/entities.ts"() {
|
|
4188
4188
|
"use strict";
|
|
@@ -4225,6 +4225,10 @@ var init_entities = __esm({
|
|
|
4225
4225
|
constraints: external_exports.string(),
|
|
4226
4226
|
priority: zPriority
|
|
4227
4227
|
});
|
|
4228
|
+
zSpecBlocker = external_exports.object({
|
|
4229
|
+
id: external_exports.string(),
|
|
4230
|
+
reason: external_exports.enum(["missing", "unfinished", "unmerged"])
|
|
4231
|
+
});
|
|
4228
4232
|
zSpec = external_exports.object({
|
|
4229
4233
|
id: external_exports.string(),
|
|
4230
4234
|
projectId: external_exports.string(),
|
|
@@ -4243,7 +4247,13 @@ var init_entities = __esm({
|
|
|
4243
4247
|
// SPEC-408 · ADR-0090 · stempel waktu backlog (ISO string di wire — kolom DateTime di DB).
|
|
4244
4248
|
// `startedAt` null = belum pernah dikerjakan; ia tak pernah ditulis ulang saat sesi dilanjutkan.
|
|
4245
4249
|
createdAt: external_exports.string(),
|
|
4246
|
-
startedAt: external_exports.string().nullable()
|
|
4250
|
+
startedAt: external_exports.string().nullable(),
|
|
4251
|
+
// SPEC-447 · ADR-0093 · id backlog yang harus selesai & ter-merge lebih dulu. Server selalu
|
|
4252
|
+
// menormalkannya ke array (kolom DB-nya `Json?`); `.default([])` menjaga respons lama.
|
|
4253
|
+
dependsOn: external_exports.array(external_exports.string()).default([]),
|
|
4254
|
+
// Turunan (bukan kolom): dihitung `liveSpecs` dari stage dependency + git. Klien tak pernah
|
|
4255
|
+
// mengirimkannya — `.default([])` supaya bentuk lama tetap parse.
|
|
4256
|
+
blockedBy: external_exports.array(zSpecBlocker).default([])
|
|
4247
4257
|
});
|
|
4248
4258
|
NOTIFY_SOUNDS = [
|
|
4249
4259
|
"off",
|
|
@@ -4436,7 +4446,12 @@ var init_agent = __esm({
|
|
|
4436
4446
|
// baris jejak & bisa menggerakkan sesi), dan SPEC-405 sudah membuktikan apa yang terjadi saat
|
|
4437
4447
|
// endpoint tulis menumpang prefix status yang dipetakan tanpa melihat method (AC-5).
|
|
4438
4448
|
"lead:read",
|
|
4439
|
-
"lead:write"
|
|
4449
|
+
"lead:write",
|
|
4450
|
+
// SPEC-450 · ADR-0094 · custom agent. Domain TERSENDIRI, dipetakan MENURUT METHOD: menulis
|
|
4451
|
+
// definisi agen mengubah apa yang dilihat SETIAP sesi baru di seluruh workspace, jadi izin
|
|
4452
|
+
// baca tak pernah cukup untuk itu (kelas bug SPEC-405).
|
|
4453
|
+
"agents:read",
|
|
4454
|
+
"agents:write"
|
|
4440
4455
|
];
|
|
4441
4456
|
zCapability = external_exports.enum(CAPABILITY_IDS);
|
|
4442
4457
|
zCapabilityInfo = external_exports.object({
|
|
@@ -4467,7 +4482,9 @@ var init_agent = __esm({
|
|
|
4467
4482
|
{ id: "notifications:read", domain: "notifications", access: "read", label: "Notifikasi \u2014 baca", desc: "Lihat notifikasi." },
|
|
4468
4483
|
{ id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." },
|
|
4469
4484
|
{ id: "lead:read", domain: "lead", access: "read", label: "Lead \u2014 baca", desc: "Baca jejak keputusan hanoman-lead & statusnya." },
|
|
4470
|
-
{ id: "lead:write", domain: "lead", access: "write", label: "Lead \u2014 tulis", desc: "Minta putusan ke hanoman-lead (keputusan bisa menggerakkan sesi).", risk: "exec" }
|
|
4485
|
+
{ id: "lead:write", domain: "lead", access: "write", label: "Lead \u2014 tulis", desc: "Minta putusan ke hanoman-lead (keputusan bisa menggerakkan sesi).", risk: "exec" },
|
|
4486
|
+
{ id: "agents:read", domain: "agents", access: "read", label: "Custom agent \u2014 baca", desc: "Lihat katalog custom agent global & per project." },
|
|
4487
|
+
{ id: "agents:write", domain: "agents", access: "write", label: "Custom agent \u2014 tulis", desc: "Buat/ubah/hapus custom agent; definisinya dipakai setiap sesi baru.", risk: "exec" }
|
|
4471
4488
|
];
|
|
4472
4489
|
zAgentTokenView = external_exports.object({
|
|
4473
4490
|
id: external_exports.string(),
|
|
@@ -4492,6 +4509,103 @@ var init_agent = __esm({
|
|
|
4492
4509
|
}
|
|
4493
4510
|
});
|
|
4494
4511
|
|
|
4512
|
+
// ../shared/src/custom-agent.ts
|
|
4513
|
+
function mentionsOf(v) {
|
|
4514
|
+
if (!Array.isArray(v)) return [];
|
|
4515
|
+
const out3 = [];
|
|
4516
|
+
for (const x of v) if (typeof x === "string" && x && !out3.includes(x)) out3.push(x);
|
|
4517
|
+
return out3;
|
|
4518
|
+
}
|
|
4519
|
+
function toolsOf(v) {
|
|
4520
|
+
if (!Array.isArray(v)) return null;
|
|
4521
|
+
const out3 = [];
|
|
4522
|
+
for (const x of v) if (typeof x === "string" && x && !out3.includes(x)) out3.push(x);
|
|
4523
|
+
return out3;
|
|
4524
|
+
}
|
|
4525
|
+
function resolveTools(a) {
|
|
4526
|
+
const base = a.tools ?? [...DEFAULT_AGENT_TOOLS];
|
|
4527
|
+
const canMention = (a.mentions ?? []).length > 0;
|
|
4528
|
+
const out3 = base.filter((t) => t !== MENTION_TOOL);
|
|
4529
|
+
if (canMention) out3.push(MENTION_TOOL);
|
|
4530
|
+
return out3;
|
|
4531
|
+
}
|
|
4532
|
+
function detectCycle(nodes) {
|
|
4533
|
+
const edges = new Map(nodes.map((n) => [n.name, n.mentions]));
|
|
4534
|
+
const state = /* @__PURE__ */ new Map();
|
|
4535
|
+
const stack = [];
|
|
4536
|
+
const walk = (name2) => {
|
|
4537
|
+
if (state.get(name2) === 1) return [...stack.slice(stack.indexOf(name2)), name2];
|
|
4538
|
+
if (state.get(name2) === 2) return null;
|
|
4539
|
+
state.set(name2, 1);
|
|
4540
|
+
stack.push(name2);
|
|
4541
|
+
for (const next of edges.get(name2) ?? []) {
|
|
4542
|
+
if (!edges.has(next)) continue;
|
|
4543
|
+
const found = walk(next);
|
|
4544
|
+
if (found) return found;
|
|
4545
|
+
}
|
|
4546
|
+
stack.pop();
|
|
4547
|
+
state.set(name2, 2);
|
|
4548
|
+
return null;
|
|
4549
|
+
};
|
|
4550
|
+
for (const n of nodes) {
|
|
4551
|
+
const found = walk(n.name);
|
|
4552
|
+
if (found) return found;
|
|
4553
|
+
}
|
|
4554
|
+
return null;
|
|
4555
|
+
}
|
|
4556
|
+
function effectiveAgents(globals, project) {
|
|
4557
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4558
|
+
for (const a of globals) byName.set(a.name, a);
|
|
4559
|
+
for (const a of project) byName.set(a.name, a);
|
|
4560
|
+
return [...byName.values()].filter((a) => a.enabled).sort((x, y) => x.name.localeCompare(y.name));
|
|
4561
|
+
}
|
|
4562
|
+
var AGENT_NAME_RE, DEFAULT_AGENT_TOOLS, MENTION_TOOL, MENTION_MAX_HOPS, GLOBAL_SCOPE, zCustomAgent, zCreateCustomAgent, zUpdateCustomAgent, customAgentId;
|
|
4563
|
+
var init_custom_agent = __esm({
|
|
4564
|
+
"../shared/src/custom-agent.ts"() {
|
|
4565
|
+
"use strict";
|
|
4566
|
+
init_zod();
|
|
4567
|
+
AGENT_NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
|
|
4568
|
+
DEFAULT_AGENT_TOOLS = [
|
|
4569
|
+
"Read",
|
|
4570
|
+
"Write",
|
|
4571
|
+
"Edit",
|
|
4572
|
+
"Bash",
|
|
4573
|
+
"Glob",
|
|
4574
|
+
"Grep",
|
|
4575
|
+
"WebFetch",
|
|
4576
|
+
"WebSearch"
|
|
4577
|
+
];
|
|
4578
|
+
MENTION_TOOL = "Task";
|
|
4579
|
+
MENTION_MAX_HOPS = 3;
|
|
4580
|
+
GLOBAL_SCOPE = "global";
|
|
4581
|
+
zCustomAgent = external_exports.object({
|
|
4582
|
+
id: external_exports.string(),
|
|
4583
|
+
projectId: external_exports.string().nullable(),
|
|
4584
|
+
name: external_exports.string().regex(AGENT_NAME_RE),
|
|
4585
|
+
description: external_exports.string(),
|
|
4586
|
+
instructions: external_exports.string(),
|
|
4587
|
+
tools: external_exports.array(external_exports.string()).nullable(),
|
|
4588
|
+
model: external_exports.string().nullable(),
|
|
4589
|
+
mentions: external_exports.array(external_exports.string()).nullable(),
|
|
4590
|
+
enabled: external_exports.boolean(),
|
|
4591
|
+
createdAt: external_exports.string(),
|
|
4592
|
+
updatedAt: external_exports.string()
|
|
4593
|
+
});
|
|
4594
|
+
zCreateCustomAgent = external_exports.object({
|
|
4595
|
+
projectId: external_exports.string().nullable().optional(),
|
|
4596
|
+
name: external_exports.string().regex(AGENT_NAME_RE),
|
|
4597
|
+
description: external_exports.string().trim().min(1).max(500),
|
|
4598
|
+
instructions: external_exports.string().trim().min(1).max(2e4),
|
|
4599
|
+
tools: external_exports.array(external_exports.string()).nullable().optional(),
|
|
4600
|
+
model: external_exports.string().nullable().optional(),
|
|
4601
|
+
mentions: external_exports.array(external_exports.string()).nullable().optional(),
|
|
4602
|
+
enabled: external_exports.boolean().optional()
|
|
4603
|
+
});
|
|
4604
|
+
zUpdateCustomAgent = zCreateCustomAgent.omit({ name: true, projectId: true }).partial();
|
|
4605
|
+
customAgentId = (projectId, name2) => `${projectId ?? GLOBAL_SCOPE}:${name2}`;
|
|
4606
|
+
}
|
|
4607
|
+
});
|
|
4608
|
+
|
|
4495
4609
|
// ../shared/src/lead.ts
|
|
4496
4610
|
function leadActionAllowed(action) {
|
|
4497
4611
|
return LEAD_ACTIONS.includes(action);
|
|
@@ -4650,7 +4764,9 @@ var init_dto = __esm({
|
|
|
4650
4764
|
title: external_exports.string().min(1),
|
|
4651
4765
|
priority: zPriority,
|
|
4652
4766
|
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]),
|
|
4653
|
-
branchFrom: external_exports.string().min(1).optional()
|
|
4767
|
+
branchFrom: external_exports.string().min(1).optional(),
|
|
4768
|
+
// SPEC-447 · ADR-0093 · divalidasi server (id ada / satu project / bukan diri sendiri / non-siklus).
|
|
4769
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4654
4770
|
}).superRefine((o, ctx) => {
|
|
4655
4771
|
const shape = "severity" in o.payload ? "qa" : "goal" in o.payload ? "goal" : "brief";
|
|
4656
4772
|
const want = o.source === "qa" ? "qa" : o.source === "goal" ? "goal" : "brief";
|
|
@@ -4664,8 +4780,12 @@ var init_dto = __esm({
|
|
|
4664
4780
|
// SPEC-186 · edit konten selagi item belum dimulai. Ditolak server bila sudah mulai.
|
|
4665
4781
|
title: external_exports.string().min(1).optional(),
|
|
4666
4782
|
priority: zPriority.optional(),
|
|
4667
|
-
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional()
|
|
4783
|
+
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional(),
|
|
4668
4784
|
// SPEC-407 · +goal
|
|
4785
|
+
// SPEC-447 · ADR-0093 · SENGAJA di luar gerbang `editingContent` (SPEC-186): gerbang itu
|
|
4786
|
+
// melindungi konten yang sudah jadi dasar kerja sesi berjalan, sedangkan dependsOn hanya
|
|
4787
|
+
// menggerbangi peluncuran BERIKUTNYA. `[]` = kosongkan.
|
|
4788
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4669
4789
|
});
|
|
4670
4790
|
zIntegrate = external_exports.object({
|
|
4671
4791
|
op: external_exports.enum(["merge", "rebase"]),
|
|
@@ -4861,7 +4981,10 @@ var init_dto = __esm({
|
|
|
4861
4981
|
goal: external_exports.boolean().optional(),
|
|
4862
4982
|
goalCondition: external_exports.string().max(4e3).optional(),
|
|
4863
4983
|
agent: zAgent.optional(),
|
|
4864
|
-
verifyScope: zVerifyScope.optional()
|
|
4984
|
+
verifyScope: zVerifyScope.optional(),
|
|
4985
|
+
// SPEC-447 · ADR-0093 — lewati gerbang dependency. Hanya jalur manusia; UI hanya
|
|
4986
|
+
// mengirimkannya sesudah operator melihat daftar pemblokirnya.
|
|
4987
|
+
force: external_exports.boolean().optional()
|
|
4865
4988
|
})
|
|
4866
4989
|
]);
|
|
4867
4990
|
zDocFileContent = external_exports.object({ content: external_exports.string() });
|
|
@@ -5095,6 +5218,9 @@ var init_api = __esm({
|
|
|
5095
5218
|
leadDecisions: `${API}/lead/decisions`,
|
|
5096
5219
|
leadDecisionOverride: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/override`,
|
|
5097
5220
|
leadDecisionCancel: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/cancel`,
|
|
5221
|
+
// SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
|
|
5222
|
+
customAgents: `${API}/custom-agents`,
|
|
5223
|
+
customAgent: (id) => `${API}/custom-agents/${encodeURIComponent(id)}`,
|
|
5098
5224
|
// SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
|
|
5099
5225
|
syncNow: `${API}/sync/now`,
|
|
5100
5226
|
// SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
|
|
@@ -5415,6 +5541,7 @@ var init_src = __esm({
|
|
|
5415
5541
|
init_enums();
|
|
5416
5542
|
init_entities();
|
|
5417
5543
|
init_agent();
|
|
5544
|
+
init_custom_agent();
|
|
5418
5545
|
init_lead();
|
|
5419
5546
|
init_dto();
|
|
5420
5547
|
init_api();
|
|
@@ -5989,6 +6116,17 @@ var init_git = __esm({
|
|
|
5989
6116
|
{ cwd: repo, encoding: "utf8" }
|
|
5990
6117
|
);
|
|
5991
6118
|
return r.status === 0 ? r.stdout.trim() : null;
|
|
6119
|
+
},
|
|
6120
|
+
// SPEC-447 · `git merge-base --is-ancestor A B` = exit 0 (ya) / 1 (tidak) / lainnya (error).
|
|
6121
|
+
// `--end-of-options` menjaga ADR-0032: sha & ref datang dari DB/kolom, jangan sampai terbaca
|
|
6122
|
+
// sebagai flag. Ref yang tak resolve membuat git exit 128 → dibaca sebagai "belum" (fail-closed).
|
|
6123
|
+
isAncestor: (repo, sha, ref) => {
|
|
6124
|
+
const r = spawnSync(
|
|
6125
|
+
"git",
|
|
6126
|
+
["merge-base", "--is-ancestor", "--end-of-options", sha, ref],
|
|
6127
|
+
{ cwd: repo, encoding: "utf8" }
|
|
6128
|
+
);
|
|
6129
|
+
return r.status === 0;
|
|
5992
6130
|
}
|
|
5993
6131
|
};
|
|
5994
6132
|
}
|
|
@@ -6172,6 +6310,74 @@ var init_agent_cli = __esm({
|
|
|
6172
6310
|
}
|
|
6173
6311
|
});
|
|
6174
6312
|
|
|
6313
|
+
// ../runner/src/custom-agents.ts
|
|
6314
|
+
function agentPromptOf(def2, roster) {
|
|
6315
|
+
const can = liveMentions(def2, roster);
|
|
6316
|
+
if (can.length === 0) {
|
|
6317
|
+
return `${def2.instructions}
|
|
6318
|
+
|
|
6319
|
+
---
|
|
6320
|
+
Kamu TIDAK boleh mendelegasikan ke agen lain. Selesaikan sendiri lalu laporkan hasilnya.`;
|
|
6321
|
+
}
|
|
6322
|
+
const list2 = can.map((m) => `@${m}`).join(", ");
|
|
6323
|
+
return [
|
|
6324
|
+
def2.instructions,
|
|
6325
|
+
"",
|
|
6326
|
+
"---",
|
|
6327
|
+
`Kamu boleh mendelegasikan HANYA ke: ${list2}. Panggil lewat ${MENTION_TOOL} dengan nama agennya.`,
|
|
6328
|
+
`Anggaran rantai delegasi seluruh sesi ini ${MENTION_MAX_HOPS} hop. Bila kamu sudah berada di hop ke-${MENTION_MAX_HOPS}, JANGAN mendelegasikan lagi \u2014 selesaikan sendiri lalu laporkan.`,
|
|
6329
|
+
"Sebutkan hop keberapa kamu berada saat mendelegasikan, dan jangan pernah memanggil agen yang sudah ada di rantai yang membawamu ke sini."
|
|
6330
|
+
].join("\n");
|
|
6331
|
+
}
|
|
6332
|
+
function renderAgentsJson(defs) {
|
|
6333
|
+
if (defs.length === 0) return "";
|
|
6334
|
+
const out3 = {};
|
|
6335
|
+
for (const d of defs) {
|
|
6336
|
+
out3[d.name] = {
|
|
6337
|
+
description: d.description,
|
|
6338
|
+
prompt: agentPromptOf(d, defs),
|
|
6339
|
+
tools: resolveTools({ tools: d.tools, mentions: d.mentions }),
|
|
6340
|
+
...d.model ? { model: d.model } : {}
|
|
6341
|
+
};
|
|
6342
|
+
}
|
|
6343
|
+
return JSON.stringify(out3);
|
|
6344
|
+
}
|
|
6345
|
+
function agentRosterBlock(defs) {
|
|
6346
|
+
if (defs.length === 0) return "";
|
|
6347
|
+
const lines2 = [
|
|
6348
|
+
"",
|
|
6349
|
+
"## Custom agent hanoman",
|
|
6350
|
+
"",
|
|
6351
|
+
"Peran berikut tersedia untuk sesi ini. Saat sebuah tugas cocok dengan salah satunya, ADOPSI",
|
|
6352
|
+
"perannya (baca instruksinya, kerjakan dengan sudut pandang itu) lalu kembali ke peranmu sendiri.",
|
|
6353
|
+
"Jangan melahirkan proses agen baru.",
|
|
6354
|
+
""
|
|
6355
|
+
];
|
|
6356
|
+
for (const d of defs) {
|
|
6357
|
+
const can = liveMentions(d, defs);
|
|
6358
|
+
lines2.push(`### @${d.name} \u2014 ${d.description}`);
|
|
6359
|
+
lines2.push("");
|
|
6360
|
+
lines2.push(d.instructions);
|
|
6361
|
+
lines2.push("");
|
|
6362
|
+
lines2.push(
|
|
6363
|
+
can.length ? `Boleh berkonsultasi ke: ${can.map((m) => `@${m}`).join(", ")} (maks ${MENTION_MAX_HOPS} hop berantai).` : "Tidak boleh berkonsultasi ke peran lain."
|
|
6364
|
+
);
|
|
6365
|
+
lines2.push("");
|
|
6366
|
+
}
|
|
6367
|
+
return lines2.join("\n");
|
|
6368
|
+
}
|
|
6369
|
+
var liveMentions;
|
|
6370
|
+
var init_custom_agents = __esm({
|
|
6371
|
+
"../runner/src/custom-agents.ts"() {
|
|
6372
|
+
"use strict";
|
|
6373
|
+
init_src();
|
|
6374
|
+
liveMentions = (def2, roster) => {
|
|
6375
|
+
const names = new Set(roster.map((r) => r.name));
|
|
6376
|
+
return def2.mentions.filter((m) => names.has(m) && m !== def2.name);
|
|
6377
|
+
};
|
|
6378
|
+
}
|
|
6379
|
+
});
|
|
6380
|
+
|
|
6175
6381
|
// ../runner/src/paths.ts
|
|
6176
6382
|
import { homedir } from "node:os";
|
|
6177
6383
|
import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve4 } from "node:path";
|
|
@@ -6232,6 +6438,7 @@ var init_src2 = __esm({
|
|
|
6232
6438
|
init_goal_spec();
|
|
6233
6439
|
init_codex_settings();
|
|
6234
6440
|
init_agent_cli();
|
|
6441
|
+
init_custom_agents();
|
|
6235
6442
|
init_verify_scope();
|
|
6236
6443
|
init_paths();
|
|
6237
6444
|
}
|
|
@@ -6361,6 +6568,74 @@ var init_session_phases = __esm({
|
|
|
6361
6568
|
}
|
|
6362
6569
|
});
|
|
6363
6570
|
|
|
6571
|
+
// src/services/tui-dialog.ts
|
|
6572
|
+
function readChoiceDialog(paneText2) {
|
|
6573
|
+
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
6574
|
+
let footer = -1;
|
|
6575
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
6576
|
+
if (FOOTER.test(lines2[i] ?? "")) {
|
|
6577
|
+
footer = i;
|
|
6578
|
+
break;
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
if (footer < 0) return null;
|
|
6582
|
+
const found = [];
|
|
6583
|
+
for (const line of lines2.slice(0, footer)) {
|
|
6584
|
+
const m = ROW.exec(line);
|
|
6585
|
+
if (m) found.push({ n: Number(m[1]), label: (m[2] ?? "").trim() });
|
|
6586
|
+
}
|
|
6587
|
+
if (found.length < 2) return null;
|
|
6588
|
+
const run2 = [];
|
|
6589
|
+
let expected = found[found.length - 1].n;
|
|
6590
|
+
for (let i = found.length - 1; i >= 0 && expected >= 1; i--) {
|
|
6591
|
+
const row = found[i];
|
|
6592
|
+
if (row.n !== expected) break;
|
|
6593
|
+
run2.unshift(row);
|
|
6594
|
+
expected -= 1;
|
|
6595
|
+
}
|
|
6596
|
+
if (run2.length < 2 || run2[0].n !== 1) return null;
|
|
6597
|
+
const rows = run2.map((r) => ({
|
|
6598
|
+
n: r.n,
|
|
6599
|
+
label: r.label,
|
|
6600
|
+
free: PLACEHOLDER.test(r.label),
|
|
6601
|
+
chat: CHAT_ROW.test(r.label)
|
|
6602
|
+
}));
|
|
6603
|
+
return {
|
|
6604
|
+
rows,
|
|
6605
|
+
freeIndex: rows.find((r) => r.free)?.n ?? null,
|
|
6606
|
+
options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
|
|
6607
|
+
};
|
|
6608
|
+
}
|
|
6609
|
+
function freeTextFilled(paneText2, n) {
|
|
6610
|
+
const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n);
|
|
6611
|
+
if (!row) return false;
|
|
6612
|
+
return row.label.length > 0 && !PLACEHOLDER.test(row.label);
|
|
6613
|
+
}
|
|
6614
|
+
async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
|
|
6615
|
+
io.literal(String(freeIndex));
|
|
6616
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
6617
|
+
for (const chunk of goalChunks(line)) {
|
|
6618
|
+
io.literal(chunk);
|
|
6619
|
+
await io.sleep(chunkMs);
|
|
6620
|
+
}
|
|
6621
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
6622
|
+
if (!freeTextFilled(io.capture(), freeIndex)) return false;
|
|
6623
|
+
io.enter();
|
|
6624
|
+
return true;
|
|
6625
|
+
}
|
|
6626
|
+
var FOOTER, ROW, PLACEHOLDER, CHAT_ROW, DIALOG_SETTLE_MS;
|
|
6627
|
+
var init_tui_dialog = __esm({
|
|
6628
|
+
"src/services/tui-dialog.ts"() {
|
|
6629
|
+
"use strict";
|
|
6630
|
+
init_src2();
|
|
6631
|
+
FOOTER = /enter to (?:select|confirm)\b/i;
|
|
6632
|
+
ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+(\S.*)$/;
|
|
6633
|
+
PLACEHOLDER = /^(?:type something\.?|other)$/i;
|
|
6634
|
+
CHAT_ROW = /^chat about this$/i;
|
|
6635
|
+
DIALOG_SETTLE_MS = 250;
|
|
6636
|
+
}
|
|
6637
|
+
});
|
|
6638
|
+
|
|
6364
6639
|
// src/config.ts
|
|
6365
6640
|
var config_exports = {};
|
|
6366
6641
|
__export(config_exports, {
|
|
@@ -6470,6 +6745,9 @@ function listPanes() {
|
|
|
6470
6745
|
function registerSessionHooks(h) {
|
|
6471
6746
|
hooks = h;
|
|
6472
6747
|
}
|
|
6748
|
+
function registerCustomAgentSource(fn) {
|
|
6749
|
+
customAgentSource = fn;
|
|
6750
|
+
}
|
|
6473
6751
|
function sessionKind(o, projectId, cwd) {
|
|
6474
6752
|
if (o.specId) return "spec";
|
|
6475
6753
|
if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
|
|
@@ -6491,11 +6769,14 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6491
6769
|
const existing = getSession(id);
|
|
6492
6770
|
if (existing && !existing.exited) return existing;
|
|
6493
6771
|
if (existing) killSession(id);
|
|
6772
|
+
const agentForDefs = opts.agent ?? "claude";
|
|
6773
|
+
const customDefs = opts.command ? [] : customAgentsFor(projectId);
|
|
6774
|
+
const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : "";
|
|
6494
6775
|
let promptArg = "";
|
|
6495
6776
|
if (!opts.command && opts.prompt) {
|
|
6496
6777
|
const promptFile = promptFilePath(id);
|
|
6497
6778
|
mkdirSync3(dirname4(promptFile), { recursive: true });
|
|
6498
|
-
writeFileSync(promptFile, opts.prompt);
|
|
6779
|
+
writeFileSync(promptFile, opts.prompt + rosterBlock);
|
|
6499
6780
|
promptArg = `"$(cat ${sq(promptFile)})"`;
|
|
6500
6781
|
}
|
|
6501
6782
|
const agent = opts.agent ?? "claude";
|
|
@@ -6517,6 +6798,15 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6517
6798
|
}), { mode: 493 });
|
|
6518
6799
|
}
|
|
6519
6800
|
const effort = agent === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort;
|
|
6801
|
+
let agentsFile;
|
|
6802
|
+
if (agent === "claude") {
|
|
6803
|
+
const json = renderAgentsJson(customDefs);
|
|
6804
|
+
if (json) {
|
|
6805
|
+
agentsFile = agentsFilePath(id);
|
|
6806
|
+
mkdirSync3(dirname4(agentsFile), { recursive: true });
|
|
6807
|
+
writeFileSync(agentsFile, json);
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6520
6810
|
const flags = agentFlags({
|
|
6521
6811
|
agent,
|
|
6522
6812
|
model: opts.model,
|
|
@@ -6525,7 +6815,8 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6525
6815
|
goal: opts.goal,
|
|
6526
6816
|
goalGate
|
|
6527
6817
|
}).map(sq).join(" ");
|
|
6528
|
-
|
|
6818
|
+
const agentsArg = agentsFile ? `--agents "$(cat ${sq(agentsFile)})"` : "";
|
|
6819
|
+
argv = [sq(agentBin(agent)), promptArg, flags, agentsArg].filter(Boolean).join(" ");
|
|
6529
6820
|
}
|
|
6530
6821
|
const envPairs = [];
|
|
6531
6822
|
if (!opts.command && agent === "claude") {
|
|
@@ -6630,11 +6921,23 @@ async function sendToPane(id, text, chunkMs = 50) {
|
|
|
6630
6921
|
const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
6631
6922
|
if (!line) return false;
|
|
6632
6923
|
try {
|
|
6924
|
+
const io = {
|
|
6925
|
+
capture: () => capturePane(id, DIALOG_CAPTURE_LINES),
|
|
6926
|
+
literal: (s) => {
|
|
6927
|
+
tmux("send-keys", "-t", name(id), "-l", s);
|
|
6928
|
+
},
|
|
6929
|
+
enter: () => {
|
|
6930
|
+
tmux("send-keys", "-t", name(id), "Enter");
|
|
6931
|
+
},
|
|
6932
|
+
sleep
|
|
6933
|
+
};
|
|
6934
|
+
const free = readChoiceDialog(io.capture())?.freeIndex ?? null;
|
|
6935
|
+
if (free !== null) return await answerChoiceDialog(io, free, line, chunkMs);
|
|
6633
6936
|
for (const chunk of goalChunks(line)) {
|
|
6634
|
-
|
|
6937
|
+
io.literal(chunk);
|
|
6635
6938
|
await sleep(chunkMs);
|
|
6636
6939
|
}
|
|
6637
|
-
|
|
6940
|
+
io.enter();
|
|
6638
6941
|
return true;
|
|
6639
6942
|
} catch {
|
|
6640
6943
|
return false;
|
|
@@ -6728,7 +7031,7 @@ function end(id, code) {
|
|
|
6728
7031
|
function pollPhases(p, a) {
|
|
6729
7032
|
if (!p.flow || !p.phaseFile) return;
|
|
6730
7033
|
const phases = readPhases(p.phaseFile, p.flow);
|
|
6731
|
-
const complete =
|
|
7034
|
+
const complete = paneComplete(p);
|
|
6732
7035
|
const json = phaseKey(phases, complete);
|
|
6733
7036
|
if (json === a.lastPhases) return;
|
|
6734
7037
|
a.lastPhases = json;
|
|
@@ -6775,7 +7078,7 @@ function attach(id, c) {
|
|
|
6775
7078
|
if (a.scrollback) c.send(frame({ t: "data", d: a.scrollback }));
|
|
6776
7079
|
if (p.flow && p.phaseFile) {
|
|
6777
7080
|
const phases = readPhases(p.phaseFile, p.flow);
|
|
6778
|
-
const complete =
|
|
7081
|
+
const complete = paneComplete(p);
|
|
6779
7082
|
a.lastPhases = phaseKey(phases, complete);
|
|
6780
7083
|
c.send(frame({ t: "phase", phases, complete }));
|
|
6781
7084
|
}
|
|
@@ -6814,13 +7117,14 @@ function spawnPty(...args) {
|
|
|
6814
7117
|
);
|
|
6815
7118
|
}
|
|
6816
7119
|
}
|
|
6817
|
-
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, sleep, paneText, GOAL_ARMED_MARKERS, goalArmed, phaseKey, poll, detach;
|
|
7120
|
+
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
|
|
6818
7121
|
var init_pty = __esm({
|
|
6819
7122
|
"src/services/pty.ts"() {
|
|
6820
7123
|
"use strict";
|
|
6821
7124
|
init_src2();
|
|
6822
7125
|
init_src();
|
|
6823
7126
|
init_session_phases();
|
|
7127
|
+
init_tui_dialog();
|
|
6824
7128
|
init_config2();
|
|
6825
7129
|
socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
|
|
6826
7130
|
PREFIX = "hanoman-";
|
|
@@ -6844,6 +7148,7 @@ var init_pty = __esm({
|
|
|
6844
7148
|
promptFilePath = (id) => `${tmpdir()}/hanoman-prompts/${id}`;
|
|
6845
7149
|
goalGatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.sh`;
|
|
6846
7150
|
goalStatePath = (id) => `${tmpdir()}/hanoman-goal-gates/${id}.count`;
|
|
7151
|
+
agentsFilePath = (id) => `${tmpdir()}/hanoman-agents/${id}.json`;
|
|
6847
7152
|
NO_SERVER = /no server running|error connecting to/i;
|
|
6848
7153
|
TmuxError = class extends Error {
|
|
6849
7154
|
constructor(message, noServer) {
|
|
@@ -6896,7 +7201,17 @@ var init_pty = __esm({
|
|
|
6896
7201
|
} catch {
|
|
6897
7202
|
}
|
|
6898
7203
|
};
|
|
6899
|
-
|
|
7204
|
+
customAgentSource = () => [];
|
|
7205
|
+
customAgentsFor = (projectId) => {
|
|
7206
|
+
try {
|
|
7207
|
+
return customAgentSource(projectId);
|
|
7208
|
+
} catch {
|
|
7209
|
+
return [];
|
|
7210
|
+
}
|
|
7211
|
+
};
|
|
7212
|
+
sleep = (ms) => new Promise((r) => {
|
|
7213
|
+
setTimeout(r, ms);
|
|
7214
|
+
});
|
|
6900
7215
|
paneText = (id) => {
|
|
6901
7216
|
try {
|
|
6902
7217
|
return tmux("capture-pane", "-p", "-t", name(id));
|
|
@@ -6904,6 +7219,7 @@ var init_pty = __esm({
|
|
|
6904
7219
|
return "";
|
|
6905
7220
|
}
|
|
6906
7221
|
};
|
|
7222
|
+
DIALOG_CAPTURE_LINES = 60;
|
|
6907
7223
|
GOAL_ARMED_MARKERS = {
|
|
6908
7224
|
claude: ["/goal"],
|
|
6909
7225
|
codex: ["Goal active", "Pursuing goal", "Goal achieved"]
|
|
@@ -6913,6 +7229,11 @@ var init_pty = __esm({
|
|
|
6913
7229
|
return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
|
|
6914
7230
|
};
|
|
6915
7231
|
phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
|
|
7232
|
+
paneComplete = (p) => !!p.flow && !!p.phaseFile && sessionComplete(readPhases(p.phaseFile, p.flow), p.cwd, p.specId);
|
|
7233
|
+
sessionFinished = (id) => {
|
|
7234
|
+
const p = getSession(id);
|
|
7235
|
+
return !!p && paneComplete(p);
|
|
7236
|
+
};
|
|
6916
7237
|
detach = (id, c) => {
|
|
6917
7238
|
attached.get(id)?.clients.delete(c);
|
|
6918
7239
|
};
|
|
@@ -7000,6 +7321,7 @@ var init_rename_project = __esm({
|
|
|
7000
7321
|
var sync_exports = {};
|
|
7001
7322
|
__export(sync_exports, {
|
|
7002
7323
|
SYNCED: () => SYNCED,
|
|
7324
|
+
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
7003
7325
|
applyPush: () => applyPush,
|
|
7004
7326
|
backfillFeed: () => backfillFeed,
|
|
7005
7327
|
isEntity: () => isEntity,
|
|
@@ -7137,33 +7459,41 @@ async function upsertLocal(entity, id, version, data) {
|
|
|
7137
7459
|
function setAcceptedHook(hook) {
|
|
7138
7460
|
onAccepted = hook;
|
|
7139
7461
|
}
|
|
7140
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted;
|
|
7462
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
7141
7463
|
var init_sync = __esm({
|
|
7142
7464
|
"src/services/sync.ts"() {
|
|
7143
7465
|
"use strict";
|
|
7144
7466
|
init_db();
|
|
7145
7467
|
init_rename_project();
|
|
7146
|
-
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment"];
|
|
7468
|
+
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent"];
|
|
7147
7469
|
DELEGATE = {
|
|
7148
7470
|
project: prisma.project,
|
|
7149
7471
|
spec: prisma.spec,
|
|
7150
7472
|
vps: prisma.vps,
|
|
7151
7473
|
sessionResult: prisma.sessionResult,
|
|
7152
7474
|
ticket: prisma.ticket,
|
|
7153
|
-
ticketAttachment: prisma.ticketAttachment
|
|
7475
|
+
ticketAttachment: prisma.ticketAttachment,
|
|
7476
|
+
customAgent: prisma.customAgent
|
|
7154
7477
|
};
|
|
7155
7478
|
FIELDS = {
|
|
7156
7479
|
project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
|
|
7157
7480
|
// SPEC-408 · ADR-0090 · createdAt/startedAt ikut menyeberang — sejajar baseSha/headSha. Tanpa
|
|
7158
7481
|
// ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
|
|
7159
|
-
|
|
7482
|
+
// SPEC-447 · ADR-0093 · dependsOn ikut juga: tanpa itu client tak tahu urutannya dan akan
|
|
7483
|
+
// meluncurkan pekerjaan yang di hub terblokir. Bukan DATE_FIELDS — nilainya array string.
|
|
7484
|
+
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "createdAt", "startedAt", "updatedAt"],
|
|
7160
7485
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7161
7486
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
7162
7487
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
7163
7488
|
// (kolom required @unique tanpa default); kunci plaintext tak pernah menyeberang.
|
|
7164
7489
|
ticket: ["projectId", "number", "category", "title", "detail", "reporterEmail", "status", "accessKeyHash", "specId", "createdAt", "updatedAt"],
|
|
7165
7490
|
// SPEC-272 · ADR-0068 · metadata lampiran (byte tak disync; ditarik lazy dari hub saat dibuka).
|
|
7166
|
-
ticketAttachment: ["ticketId", "projectId", "filename", "mimeType", "size", "storageKey", "createdAt", "updatedAt"]
|
|
7491
|
+
ticketAttachment: ["ticketId", "projectId", "filename", "mimeType", "size", "storageKey", "createdAt", "updatedAt"],
|
|
7492
|
+
// SPEC-450 · ADR-0094 · SELURUH kolom bermakna ikut menyeberang. `enabled` & `mentions` wajib
|
|
7493
|
+
// ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
|
|
7494
|
+
// mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
|
|
7495
|
+
// `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
|
|
7496
|
+
customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"]
|
|
7167
7497
|
};
|
|
7168
7498
|
DATE_FIELDS = {
|
|
7169
7499
|
project: ["updatedAt"],
|
|
@@ -7171,8 +7501,10 @@ var init_sync = __esm({
|
|
|
7171
7501
|
vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7172
7502
|
sessionResult: ["createdAt", "updatedAt"],
|
|
7173
7503
|
ticket: ["createdAt", "updatedAt"],
|
|
7174
|
-
ticketAttachment: ["createdAt", "updatedAt"]
|
|
7504
|
+
ticketAttachment: ["createdAt", "updatedAt"],
|
|
7505
|
+
customAgent: ["createdAt", "updatedAt"]
|
|
7175
7506
|
};
|
|
7507
|
+
__FIELDS_FOR_TEST = FIELDS;
|
|
7176
7508
|
}
|
|
7177
7509
|
});
|
|
7178
7510
|
|
|
@@ -15360,6 +15692,128 @@ async function readEscalation(specId, sessions = listSessions()) {
|
|
|
15360
15692
|
};
|
|
15361
15693
|
}
|
|
15362
15694
|
|
|
15695
|
+
// src/services/spec-deps.ts
|
|
15696
|
+
init_db();
|
|
15697
|
+
init_src2();
|
|
15698
|
+
var REASON_LABEL = {
|
|
15699
|
+
missing: "tak ditemukan",
|
|
15700
|
+
unfinished: "belum selesai",
|
|
15701
|
+
unmerged: "belum ter-merge"
|
|
15702
|
+
};
|
|
15703
|
+
function dependsOnOf(spec) {
|
|
15704
|
+
const v = spec.dependsOn;
|
|
15705
|
+
if (!Array.isArray(v)) return [];
|
|
15706
|
+
const out3 = [];
|
|
15707
|
+
for (const x of v) if (typeof x === "string" && x !== "" && !out3.includes(x)) out3.push(x);
|
|
15708
|
+
return out3;
|
|
15709
|
+
}
|
|
15710
|
+
function blockersFor(spec, deps, isMerged) {
|
|
15711
|
+
const ids = dependsOnOf(spec);
|
|
15712
|
+
if (ids.length === 0) return [];
|
|
15713
|
+
const base = spec.branchFrom ?? "HEAD";
|
|
15714
|
+
const out3 = [];
|
|
15715
|
+
for (const id of ids) {
|
|
15716
|
+
const d = deps.get(id);
|
|
15717
|
+
if (!d) {
|
|
15718
|
+
out3.push({ id, reason: "missing" });
|
|
15719
|
+
continue;
|
|
15720
|
+
}
|
|
15721
|
+
if (d.stage !== "done") {
|
|
15722
|
+
out3.push({ id, reason: "unfinished" });
|
|
15723
|
+
continue;
|
|
15724
|
+
}
|
|
15725
|
+
if (d.headSha && !isMerged(d.headSha, base)) out3.push({ id, reason: "unmerged" });
|
|
15726
|
+
}
|
|
15727
|
+
return out3;
|
|
15728
|
+
}
|
|
15729
|
+
function blockedNote(bl) {
|
|
15730
|
+
return `menunggu ${bl.map((b) => `${b.id} (${REASON_LABEL[b.reason]})`).join(", ")}`;
|
|
15731
|
+
}
|
|
15732
|
+
function reaches(edges, from, target2) {
|
|
15733
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15734
|
+
const stack = [...from];
|
|
15735
|
+
while (stack.length) {
|
|
15736
|
+
const cur = stack.pop();
|
|
15737
|
+
if (cur === target2) return true;
|
|
15738
|
+
if (seen.has(cur)) continue;
|
|
15739
|
+
seen.add(cur);
|
|
15740
|
+
for (const n of edges.get(cur) ?? []) stack.push(n);
|
|
15741
|
+
}
|
|
15742
|
+
return false;
|
|
15743
|
+
}
|
|
15744
|
+
var TTL_MS = 15e3;
|
|
15745
|
+
var mergeCache = /* @__PURE__ */ new Map();
|
|
15746
|
+
function mergedInto(repoDir, sha, baseRef) {
|
|
15747
|
+
const key = `${repoDir} ${sha} ${baseRef}`;
|
|
15748
|
+
const hit = mergeCache.get(key);
|
|
15749
|
+
const now = Date.now();
|
|
15750
|
+
if (hit && now - hit.at < TTL_MS) return hit.v;
|
|
15751
|
+
let v = false;
|
|
15752
|
+
try {
|
|
15753
|
+
v = realGit.isAncestor(repoDir, sha, baseRef);
|
|
15754
|
+
} catch {
|
|
15755
|
+
v = false;
|
|
15756
|
+
}
|
|
15757
|
+
mergeCache.set(key, { at: now, v });
|
|
15758
|
+
return v;
|
|
15759
|
+
}
|
|
15760
|
+
var merger = (repoDir) => (sha, base) => repoDir ? mergedInto(repoDir, sha, base) : false;
|
|
15761
|
+
var depRows = (ids) => prisma.spec.findMany({
|
|
15762
|
+
where: { id: { in: ids } },
|
|
15763
|
+
select: { id: true, stage: true, headSha: true }
|
|
15764
|
+
});
|
|
15765
|
+
async function blockersForSpec(spec, repoDir) {
|
|
15766
|
+
const ids = dependsOnOf(spec);
|
|
15767
|
+
if (ids.length === 0) return [];
|
|
15768
|
+
const rows = await depRows(ids);
|
|
15769
|
+
return blockersFor(spec, new Map(rows.map((r) => [r.id, r])), merger(repoDir));
|
|
15770
|
+
}
|
|
15771
|
+
async function decorateBlocked(specs) {
|
|
15772
|
+
const ids = [...new Set(specs.flatMap(dependsOnOf))];
|
|
15773
|
+
if (ids.length === 0) return specs.map((s) => ({ ...s, dependsOn: [], blockedBy: [] }));
|
|
15774
|
+
const rows = await depRows(ids);
|
|
15775
|
+
const deps = new Map(rows.map((r) => [r.id, r]));
|
|
15776
|
+
const repos = /* @__PURE__ */ new Map();
|
|
15777
|
+
const out3 = [];
|
|
15778
|
+
for (const s of specs) {
|
|
15779
|
+
const own = dependsOnOf(s);
|
|
15780
|
+
if (own.length === 0) {
|
|
15781
|
+
out3.push({ ...s, dependsOn: [], blockedBy: [] });
|
|
15782
|
+
continue;
|
|
15783
|
+
}
|
|
15784
|
+
if (!repos.has(s.projectId)) repos.set(s.projectId, await resolveRepoDir(s.projectId));
|
|
15785
|
+
out3.push({ ...s, dependsOn: own, blockedBy: blockersFor(s, deps, merger(repos.get(s.projectId))) });
|
|
15786
|
+
}
|
|
15787
|
+
return out3;
|
|
15788
|
+
}
|
|
15789
|
+
async function validateDependsOn(specId, projectId, raw) {
|
|
15790
|
+
const ids = dependsOnOf({ dependsOn: raw });
|
|
15791
|
+
if (ids.length === 0) return { ok: true, ids: [] };
|
|
15792
|
+
if (specId && ids.includes(specId))
|
|
15793
|
+
return { ok: false, error: "backlog tak bisa bergantung pada dirinya sendiri" };
|
|
15794
|
+
const rows = await prisma.spec.findMany({
|
|
15795
|
+
where: { id: { in: ids } },
|
|
15796
|
+
select: { id: true, projectId: true }
|
|
15797
|
+
});
|
|
15798
|
+
const found = new Map(rows.map((r) => [r.id, r.projectId]));
|
|
15799
|
+
const missing = ids.filter((i) => !found.has(i));
|
|
15800
|
+
if (missing.length) return { ok: false, error: `backlog tak ditemukan: ${missing.join(", ")}` };
|
|
15801
|
+
const foreign = ids.filter((i) => found.get(i) !== projectId);
|
|
15802
|
+
if (foreign.length)
|
|
15803
|
+
return { ok: false, error: `dependency harus di project yang sama: ${foreign.join(", ")}` };
|
|
15804
|
+
if (specId) {
|
|
15805
|
+
const all = await prisma.spec.findMany({
|
|
15806
|
+
where: { projectId },
|
|
15807
|
+
select: { id: true, dependsOn: true }
|
|
15808
|
+
});
|
|
15809
|
+
const edges = new Map(all.map((r) => [r.id, dependsOnOf(r)]));
|
|
15810
|
+
edges.set(specId, ids);
|
|
15811
|
+
if (reaches(edges, ids, specId))
|
|
15812
|
+
return { ok: false, error: "dependency membentuk siklus" };
|
|
15813
|
+
}
|
|
15814
|
+
return { ok: true, ids };
|
|
15815
|
+
}
|
|
15816
|
+
|
|
15363
15817
|
// src/services/doc-export.ts
|
|
15364
15818
|
import PDFDocument from "pdfkit";
|
|
15365
15819
|
|
|
@@ -17829,7 +18283,7 @@ async function liveSpecs(filter = {}) {
|
|
|
17829
18283
|
orderBy: { id: "desc" }
|
|
17830
18284
|
});
|
|
17831
18285
|
const live = sessionPhasesBySpec();
|
|
17832
|
-
if (live.size === 0) return specs;
|
|
18286
|
+
if (live.size === 0) return decorateBlocked(specs);
|
|
17833
18287
|
const advanced = [];
|
|
17834
18288
|
const doneNow = [];
|
|
17835
18289
|
const out3 = specs.map((s) => {
|
|
@@ -17847,7 +18301,7 @@ async function liveSpecs(filter = {}) {
|
|
|
17847
18301
|
if (res.count > 0) await notifySynced("spec", a.id);
|
|
17848
18302
|
}));
|
|
17849
18303
|
await Promise.all(doneNow.map((d) => recordCompletion(d.specId, d.title, d.projectId)));
|
|
17850
|
-
return out3;
|
|
18304
|
+
return decorateBlocked(out3);
|
|
17851
18305
|
}
|
|
17852
18306
|
|
|
17853
18307
|
// src/routes/specs.ts
|
|
@@ -17887,6 +18341,8 @@ async function specs_default(app2) {
|
|
|
17887
18341
|
const repoDir = await resolveRepoDir(b.project);
|
|
17888
18342
|
if (b.branchFrom && await branchUnknown(repoDir, b.branchFrom))
|
|
17889
18343
|
return reply.code(400).send({ error: `branch "${b.branchFrom}" tidak ada di repo project` });
|
|
18344
|
+
const dep = await validateDependsOn(null, b.project, b.dependsOn ?? []);
|
|
18345
|
+
if (!dep.ok) return reply.code(400).send({ error: dep.error });
|
|
17890
18346
|
const isQa = b.source === "qa";
|
|
17891
18347
|
const { priority, objective } = deriveSpecFields(b.source, b.payload, b.priority);
|
|
17892
18348
|
const author = req.user?.email ?? "system";
|
|
@@ -17905,7 +18361,9 @@ async function specs_default(app2) {
|
|
|
17905
18361
|
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
|
|
17906
18362
|
objective,
|
|
17907
18363
|
payload: b.payload,
|
|
17908
|
-
branchFrom: b.branchFrom ?? null
|
|
18364
|
+
branchFrom: b.branchFrom ?? null,
|
|
18365
|
+
dependsOn: dep.ids
|
|
18366
|
+
// SPEC-447 · ADR-0093
|
|
17909
18367
|
}
|
|
17910
18368
|
});
|
|
17911
18369
|
} catch (e) {
|
|
@@ -17969,7 +18427,7 @@ ${item.context}` : item.context;
|
|
|
17969
18427
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
17970
18428
|
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
17971
18429
|
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
17972
|
-
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload } = parsed.data;
|
|
18430
|
+
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload, dependsOn } = parsed.data;
|
|
17973
18431
|
const editingContent = title !== void 0 || newPriority !== void 0 || payload !== void 0;
|
|
17974
18432
|
if (editingContent && (spec.stage !== "brainstorming" || spec.baseSha !== null))
|
|
17975
18433
|
return reply.code(409).send({ error: "backlog item sudah dimulai \u2014 tak bisa diedit" });
|
|
@@ -17977,6 +18435,12 @@ ${item.context}` : item.context;
|
|
|
17977
18435
|
if (await branchUnknown(await resolveRepoDir(spec.projectId), branchFrom))
|
|
17978
18436
|
return reply.code(400).send({ error: `branch "${branchFrom}" tidak ada di repo project` });
|
|
17979
18437
|
}
|
|
18438
|
+
let depIds;
|
|
18439
|
+
if (dependsOn !== void 0) {
|
|
18440
|
+
const d = await validateDependsOn(spec.id, spec.projectId, dependsOn);
|
|
18441
|
+
if (!d.ok) return reply.code(400).send({ error: d.error });
|
|
18442
|
+
depIds = d.ids;
|
|
18443
|
+
}
|
|
17980
18444
|
if (stage !== void 0) {
|
|
17981
18445
|
if (STAGES.indexOf(stage) >= STAGES.indexOf(spec.stage))
|
|
17982
18446
|
return reply.code(422).send({ error: "stage hanya boleh dikembalikan mundur" });
|
|
@@ -17988,6 +18452,7 @@ ${item.context}` : item.context;
|
|
|
17988
18452
|
}
|
|
17989
18453
|
const data = {};
|
|
17990
18454
|
if (branchFrom !== void 0) data.branchFrom = branchFrom;
|
|
18455
|
+
if (depIds !== void 0) data.dependsOn = depIds;
|
|
17991
18456
|
if (stage !== void 0) data.stage = stage;
|
|
17992
18457
|
if (editingContent) {
|
|
17993
18458
|
const effPayload = payload ?? spec.payload;
|
|
@@ -18020,8 +18485,21 @@ ${item.context}` : item.context;
|
|
|
18020
18485
|
});
|
|
18021
18486
|
app2.delete("/specs/:id", async (req, reply) => {
|
|
18022
18487
|
const { id } = req.params;
|
|
18488
|
+
const gone = await prisma.spec.findUnique({ where: { id }, select: { projectId: true } });
|
|
18023
18489
|
await prisma.spec.delete({ where: { id } }).catch(() => {
|
|
18024
18490
|
});
|
|
18491
|
+
if (gone) {
|
|
18492
|
+
const rows = await prisma.spec.findMany({
|
|
18493
|
+
where: { projectId: gone.projectId },
|
|
18494
|
+
select: { id: true, dependsOn: true }
|
|
18495
|
+
});
|
|
18496
|
+
for (const r of rows) {
|
|
18497
|
+
const ids = dependsOnOf(r);
|
|
18498
|
+
if (!ids.includes(id)) continue;
|
|
18499
|
+
await prisma.spec.update({ where: { id: r.id }, data: { dependsOn: ids.filter((x) => x !== id) } });
|
|
18500
|
+
await notifySynced("spec", r.id);
|
|
18501
|
+
}
|
|
18502
|
+
}
|
|
18025
18503
|
return reply.code(204).send();
|
|
18026
18504
|
});
|
|
18027
18505
|
app2.post("/specs/:id/integrate", async (req, reply) => {
|
|
@@ -19251,9 +19729,11 @@ init_pty();
|
|
|
19251
19729
|
init_session_phases();
|
|
19252
19730
|
init_pty();
|
|
19253
19731
|
var LaunchError = class extends Error {
|
|
19254
|
-
|
|
19732
|
+
// SPEC-447 · `blockers` hanya terisi untuk kind "blocked"; route memetakannya ke body 409.
|
|
19733
|
+
constructor(message, kind, blockers = []) {
|
|
19255
19734
|
super(message);
|
|
19256
19735
|
this.kind = kind;
|
|
19736
|
+
this.blockers = blockers;
|
|
19257
19737
|
}
|
|
19258
19738
|
};
|
|
19259
19739
|
function resumeState(repoDir, worktree, branchTo, headSha) {
|
|
@@ -19275,6 +19755,11 @@ async function startSpecSession(spec, opts) {
|
|
|
19275
19755
|
const id = sessionIdForSpec(spec.id);
|
|
19276
19756
|
const pane = getSession(id);
|
|
19277
19757
|
if (pane && !pane.exited) return { id: pane.id, reused: true };
|
|
19758
|
+
if (!opts.force) {
|
|
19759
|
+
const blockers = await blockersForSpec(spec, repoDir);
|
|
19760
|
+
if (blockers.length)
|
|
19761
|
+
throw new LaunchError(`${spec.id} ${blockedNote(blockers)}`, "blocked", blockers);
|
|
19762
|
+
}
|
|
19278
19763
|
if (pane) killSession(id);
|
|
19279
19764
|
const setting = await getSetting();
|
|
19280
19765
|
const agent = opts.agent ?? setting.agent;
|
|
@@ -19437,12 +19922,16 @@ async function terminal_default(app2) {
|
|
|
19437
19922
|
// SPEC-332 · ADR-0073
|
|
19438
19923
|
agent: parsed.data.agent,
|
|
19439
19924
|
// SPEC-338 · ADR-0074
|
|
19440
|
-
verifyScope: parsed.data.verifyScope
|
|
19925
|
+
verifyScope: parsed.data.verifyScope,
|
|
19441
19926
|
// SPEC-376 · ADR-0080
|
|
19927
|
+
force: parsed.data.force
|
|
19928
|
+
// SPEC-447 · ADR-0093
|
|
19442
19929
|
});
|
|
19443
19930
|
return reply.code(201).send(r.resumed ? { id: r.id, resumed: true } : { id: r.id });
|
|
19444
19931
|
} catch (e) {
|
|
19445
19932
|
if (e instanceof LaunchError) {
|
|
19933
|
+
if (e.kind === "blocked")
|
|
19934
|
+
return reply.code(409).send({ error: e.message, blocked: true, blockers: e.blockers });
|
|
19446
19935
|
return e.kind === "needs-bind" ? reply.code(400).send({ error: e.message, needsBind: true }) : reply.code(422).send({ error: e.message });
|
|
19447
19936
|
}
|
|
19448
19937
|
throw e;
|
|
@@ -20378,7 +20867,7 @@ import { readFileSync as readFileSync9 } from "node:fs";
|
|
|
20378
20867
|
import { homedir as homedir6 } from "node:os";
|
|
20379
20868
|
import { join as join9 } from "node:path";
|
|
20380
20869
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
20381
|
-
var
|
|
20870
|
+
var TTL_MS2 = 3e4;
|
|
20382
20871
|
var lastOk = null;
|
|
20383
20872
|
var freshUntil = 0;
|
|
20384
20873
|
var LABELS = {
|
|
@@ -20456,7 +20945,7 @@ async function fetchUsage() {
|
|
|
20456
20945
|
async function getLimits() {
|
|
20457
20946
|
if (lastOk && Date.now() < freshUntil) return lastOk;
|
|
20458
20947
|
const dto = await fetchUsage();
|
|
20459
|
-
if (dto.status === "ok") freshUntil = Date.now() +
|
|
20948
|
+
if (dto.status === "ok") freshUntil = Date.now() + TTL_MS2;
|
|
20460
20949
|
return dto;
|
|
20461
20950
|
}
|
|
20462
20951
|
|
|
@@ -20464,7 +20953,7 @@ async function getLimits() {
|
|
|
20464
20953
|
import { open as open2, readdir as readdir2, stat } from "node:fs/promises";
|
|
20465
20954
|
import { homedir as homedir7 } from "node:os";
|
|
20466
20955
|
import { join as join10 } from "node:path";
|
|
20467
|
-
var
|
|
20956
|
+
var TTL_MS3 = 3e4;
|
|
20468
20957
|
var STALE_AFTER_MS = 12 * 36e5;
|
|
20469
20958
|
var TAIL_BYTES = 512 * 1024;
|
|
20470
20959
|
var MAX_FILES = 8;
|
|
@@ -20569,7 +21058,7 @@ async function getCodexLimits(dir2) {
|
|
|
20569
21058
|
const dto = await readLimits(codexSessionsDir());
|
|
20570
21059
|
if (dto.status !== "unavailable") {
|
|
20571
21060
|
cache2 = dto;
|
|
20572
|
-
freshUntil2 = Date.now() +
|
|
21061
|
+
freshUntil2 = Date.now() + TTL_MS3;
|
|
20573
21062
|
}
|
|
20574
21063
|
return dto;
|
|
20575
21064
|
}
|
|
@@ -20587,7 +21076,7 @@ import { execFile as execFile8 } from "node:child_process";
|
|
|
20587
21076
|
import { promisify as promisify8 } from "node:util";
|
|
20588
21077
|
var run = promisify8(execFile8);
|
|
20589
21078
|
var CODEX_MIN_CLIENT = "0.144.0";
|
|
20590
|
-
var
|
|
21079
|
+
var TTL_MS4 = 5 * 6e4;
|
|
20591
21080
|
var cache3 = null;
|
|
20592
21081
|
var codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
|
|
20593
21082
|
function parseCodexVersion(out3) {
|
|
@@ -20595,7 +21084,7 @@ function parseCodexVersion(out3) {
|
|
|
20595
21084
|
return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
|
|
20596
21085
|
}
|
|
20597
21086
|
async function getCodexVersion(now = Date.now()) {
|
|
20598
|
-
if (cache3 && now - cache3.at <
|
|
21087
|
+
if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
|
|
20599
21088
|
let version = null;
|
|
20600
21089
|
try {
|
|
20601
21090
|
const { stdout } = await run(codexBin2(), ["--version"], { timeout: 5e3 });
|
|
@@ -21825,6 +22314,11 @@ async function markDone(id, note) {
|
|
|
21825
22314
|
data: { status: "done", ...note ? { note } : {} }
|
|
21826
22315
|
});
|
|
21827
22316
|
}
|
|
22317
|
+
async function noteQueued(id, note) {
|
|
22318
|
+
const row = await prisma.schedulerQueueItem.findUnique({ where: { id }, select: { note: true } });
|
|
22319
|
+
if (row?.note === note) return;
|
|
22320
|
+
await prisma.schedulerQueueItem.update({ where: { id }, data: { note } });
|
|
22321
|
+
}
|
|
21828
22322
|
|
|
21829
22323
|
// src/services/scheduler/registry.ts
|
|
21830
22324
|
var sources = /* @__PURE__ */ new Map();
|
|
@@ -22113,6 +22607,7 @@ function toDecisionView(r) {
|
|
|
22113
22607
|
|
|
22114
22608
|
// src/services/lead/brain.ts
|
|
22115
22609
|
init_config2();
|
|
22610
|
+
init_pty();
|
|
22116
22611
|
import { execFile as execFile9 } from "node:child_process";
|
|
22117
22612
|
var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
22118
22613
|
function leadArgv(o) {
|
|
@@ -22133,14 +22628,16 @@ function leadArgv(o) {
|
|
|
22133
22628
|
o.prompt
|
|
22134
22629
|
];
|
|
22135
22630
|
}
|
|
22631
|
+
var leadEnv = (agent, base = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base } : { ...base };
|
|
22136
22632
|
function think(prompt, o) {
|
|
22137
22633
|
const bin = binFor(o.agent);
|
|
22138
22634
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
22139
22635
|
return new Promise((resolve14, reject) => {
|
|
22140
|
-
execFile9(bin, args, {
|
|
22636
|
+
const child = execFile9(bin, args, {
|
|
22141
22637
|
cwd: o.cwd,
|
|
22142
22638
|
timeout: o.timeoutMs,
|
|
22143
22639
|
maxBuffer: 16 * 1024 * 1024,
|
|
22640
|
+
env: leadEnv(o.agent),
|
|
22144
22641
|
encoding: "utf8",
|
|
22145
22642
|
killSignal: "SIGTERM"
|
|
22146
22643
|
}, (err, stdout, stderr) => {
|
|
@@ -22151,6 +22648,7 @@ function think(prompt, o) {
|
|
|
22151
22648
|
}
|
|
22152
22649
|
resolve14(stdout);
|
|
22153
22650
|
});
|
|
22651
|
+
child.stdin?.end();
|
|
22154
22652
|
});
|
|
22155
22653
|
}
|
|
22156
22654
|
|
|
@@ -22373,12 +22871,11 @@ async function integrateMain(row, deps) {
|
|
|
22373
22871
|
if (!repoDir) return { ok: false, detail: `project ${row.projectId} belum di-bind ke checkout lokal` };
|
|
22374
22872
|
const spec = await prisma.spec.findUnique({ where: { id: row.specId } });
|
|
22375
22873
|
if (!spec) return { ok: false, detail: `backlog ${row.specId} tak ada` };
|
|
22874
|
+
const sid = sessionIdForSpec(spec.id);
|
|
22875
|
+
const done = deps.planDone(worktreeDir(repoDir, spec.id), spec.id);
|
|
22376
22876
|
const evidence = [];
|
|
22377
22877
|
if (cfg.requireGreenBeforeIntegrate) {
|
|
22378
|
-
const wt = worktreeDir(repoDir, spec.id);
|
|
22379
|
-
const done = deps.planDone(wt, spec.id);
|
|
22380
22878
|
evidence.push(done ? "plan tak menyisakan `- [ ]`" : "plan MASIH menyisakan `- [ ]`");
|
|
22381
|
-
const sid = sessionIdForSpec(spec.id);
|
|
22382
22879
|
const pane = deps.sessionExists(sid);
|
|
22383
22880
|
evidence.push(pane ? `pane ${sid} masih ada` : `pane ${sid} sudah tak ada`);
|
|
22384
22881
|
if (!done) {
|
|
@@ -22397,6 +22894,9 @@ async function integrateMain(row, deps) {
|
|
|
22397
22894
|
}
|
|
22398
22895
|
const res = await deps.integrate(repoDir, spec.id, "merge", "local:main");
|
|
22399
22896
|
evidence.push(`integrate \u2192 ${res.status}`);
|
|
22897
|
+
if (res.status === "clean" && done && deps.sessionExists(sid)) {
|
|
22898
|
+
evidence.push(deps.killSession(sid) ? `pane ${sid} dilepas (worktree utuh)` : `pane ${sid} gagal dilepas`);
|
|
22899
|
+
}
|
|
22400
22900
|
await recordEvidence(row, evidence, res.status === "clean" ? "integrasi bersih" : `integrasi tak bersih: ${res.status}`);
|
|
22401
22901
|
if (res.status !== "clean") {
|
|
22402
22902
|
await deps.notify(
|
|
@@ -22421,8 +22921,10 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
|
|
|
22421
22921
|
|
|
22422
22922
|
// src/services/lead/detect.ts
|
|
22423
22923
|
init_pty();
|
|
22924
|
+
import { writeFileSync as writeFileSync4 } from "node:fs";
|
|
22424
22925
|
|
|
22425
22926
|
// src/services/lead/pane.ts
|
|
22927
|
+
init_tui_dialog();
|
|
22426
22928
|
var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
|
|
22427
22929
|
var tail = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
|
|
22428
22930
|
var CODEX_FINISHED = [
|
|
@@ -22439,15 +22941,16 @@ var ASK_SIGNALS = [
|
|
|
22439
22941
|
function readPaneQuestion(text, agent) {
|
|
22440
22942
|
const lines2 = tail(text, 40);
|
|
22441
22943
|
const body = lines2.join("\n").trim();
|
|
22442
|
-
|
|
22944
|
+
const choices = readChoiceDialog(text)?.options ?? [];
|
|
22945
|
+
if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
|
|
22443
22946
|
const question = tail(text, 25).join("\n").trim();
|
|
22444
22947
|
if (agent === "codex") {
|
|
22445
22948
|
const finished = CODEX_FINISHED.find((re) => re.test(body));
|
|
22446
|
-
if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)" };
|
|
22949
|
+
if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
|
|
22447
22950
|
if (!ASK_SIGNALS.some((re) => re.test(body)))
|
|
22448
|
-
return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex" };
|
|
22951
|
+
return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex", choices };
|
|
22449
22952
|
}
|
|
22450
|
-
return { asking: true, question, reason: "" };
|
|
22953
|
+
return { asking: true, question, reason: "", choices };
|
|
22451
22954
|
}
|
|
22452
22955
|
|
|
22453
22956
|
// src/services/lead/detect.ts
|
|
@@ -22485,6 +22988,12 @@ var prodDetectDeps = {
|
|
|
22485
22988
|
}
|
|
22486
22989
|
},
|
|
22487
22990
|
send: (id, text) => sendToPane(id, text),
|
|
22991
|
+
clearMarker: (file) => {
|
|
22992
|
+
try {
|
|
22993
|
+
writeFileSync4(file, "");
|
|
22994
|
+
} catch {
|
|
22995
|
+
}
|
|
22996
|
+
},
|
|
22488
22997
|
decide,
|
|
22489
22998
|
decideDeps: prodDecideDeps,
|
|
22490
22999
|
optIn: leadProjects,
|
|
@@ -22547,6 +23056,10 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
22547
23056
|
skip(read.reason);
|
|
22548
23057
|
continue;
|
|
22549
23058
|
}
|
|
23059
|
+
const notes = [`Sesi ini menunggu di terminal; teks di bawah adalah layar terakhirnya. Jawablah sebagai masukan yang bisa langsung diketik ke terminal itu (isi \`reply\`).`];
|
|
23060
|
+
if (read.choices.length) {
|
|
23061
|
+
notes.push("Layarnya adalah dialog pilihan. `reply` akan dimasukkan sebagai JAWABAN BEBAS ke dialog itu, jadi tulislah jawaban yang berdiri sendiri \u2014 sebut opsi yang kamu pilih beserta alasan singkatnya, bukan nomornya saja.");
|
|
23062
|
+
}
|
|
22550
23063
|
const row = await deps.decide({
|
|
22551
23064
|
projectId: s.projectId,
|
|
22552
23065
|
specId: s.specId,
|
|
@@ -22554,7 +23067,8 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
22554
23067
|
gate: "detected",
|
|
22555
23068
|
kind: "answer",
|
|
22556
23069
|
question: read.question,
|
|
22557
|
-
|
|
23070
|
+
options: read.choices.length ? read.choices : void 0,
|
|
23071
|
+
notes
|
|
22558
23072
|
}, deps.decideDeps);
|
|
22559
23073
|
if (!row || row.status !== "berlaku") {
|
|
22560
23074
|
skip("lead tak menghasilkan keputusan yang berlaku");
|
|
@@ -22566,6 +23080,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
22566
23080
|
skip("gagal mengetik ke pane");
|
|
22567
23081
|
continue;
|
|
22568
23082
|
}
|
|
23083
|
+
deps.clearMarker(s.decisionFile);
|
|
22569
23084
|
answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
|
|
22570
23085
|
out3.answered.push(s.id);
|
|
22571
23086
|
}
|
|
@@ -22618,6 +23133,13 @@ var prodPulseDeps = {
|
|
|
22618
23133
|
}
|
|
22619
23134
|
},
|
|
22620
23135
|
planDone: planComplete,
|
|
23136
|
+
finished: (id) => {
|
|
23137
|
+
try {
|
|
23138
|
+
return sessionFinished(id);
|
|
23139
|
+
} catch {
|
|
23140
|
+
return false;
|
|
23141
|
+
}
|
|
23142
|
+
},
|
|
22621
23143
|
decide,
|
|
22622
23144
|
decideDeps: prodDecideDeps,
|
|
22623
23145
|
apply: applyAction,
|
|
@@ -22638,6 +23160,10 @@ async function pulse(deps = prodPulseDeps) {
|
|
|
22638
23160
|
res.quality = await followUpFinished(cfg, optIn, deps);
|
|
22639
23161
|
} catch {
|
|
22640
23162
|
}
|
|
23163
|
+
try {
|
|
23164
|
+
res.quality += await followUpComplete(optIn, deps);
|
|
23165
|
+
} catch {
|
|
23166
|
+
}
|
|
22641
23167
|
try {
|
|
22642
23168
|
res.collisions = await detectCollisions(optIn, deps);
|
|
22643
23169
|
} catch {
|
|
@@ -22690,6 +23216,49 @@ async function followUpFinished(cfg, optIn, deps) {
|
|
|
22690
23216
|
}
|
|
22691
23217
|
return n;
|
|
22692
23218
|
}
|
|
23219
|
+
async function followUpComplete(optIn, deps) {
|
|
23220
|
+
let n = 0;
|
|
23221
|
+
const opt = new Set(optIn);
|
|
23222
|
+
for (const s of deps.sessions()) {
|
|
23223
|
+
if (!s.specId || !opt.has(s.projectId)) continue;
|
|
23224
|
+
if ((s.exitCode ?? 0) !== 0) continue;
|
|
23225
|
+
if (!deps.finished(s.id)) continue;
|
|
23226
|
+
const mark = `Backlog ${s.specId} sudah selesai di sesi ${s.id}`;
|
|
23227
|
+
const seen = await prisma.leadDecision.findFirst({
|
|
23228
|
+
where: { sessionId: s.id, gate: "pulse", question: { startsWith: mark } }
|
|
23229
|
+
});
|
|
23230
|
+
if (seen) continue;
|
|
23231
|
+
const row = await deps.decide({
|
|
23232
|
+
projectId: s.projectId,
|
|
23233
|
+
specId: s.specId,
|
|
23234
|
+
sessionId: s.id,
|
|
23235
|
+
gate: "pulse",
|
|
23236
|
+
kind: "quality",
|
|
23237
|
+
question: `${mark}: seluruh fasenya tercatat dan plan-nya tak menyisakan kotak \`- [ ]\`. Selama sesinya belum dilepas, ia memegang satu slot concurrency scheduler sehingga backlog lain tak bisa mulai. Integrasikan hasilnya ke main, hentikan sesinya saja, atau biarkan?`,
|
|
23238
|
+
options: [
|
|
23239
|
+
"integrate-main \u2014 merge branch sesi ini ke main; panenya ikut dilepas, worktree tetap utuh",
|
|
23240
|
+
"stop-session \u2014 lepas panenya tanpa mengintegrasikan (worktree tetap utuh, ADR-0084 masih bisa melanjutkan)",
|
|
23241
|
+
"none \u2014 biarkan sesinya berdiri, sertakan alasan kenapa slot itu layak ditahan"
|
|
23242
|
+
],
|
|
23243
|
+
notes: [
|
|
23244
|
+
`Worktree sesi: ${s.cwd}`,
|
|
23245
|
+
// Rebase sengaja tak ditawarkan: `LEAD_ACTIONS` adalah konstanta tertutup (AC-31), dan
|
|
23246
|
+
// merge adalah yang PALING MUDAH DIBATALKAN dari keduanya — kriteria yang diperintahkan
|
|
23247
|
+
// prompt lead sendiri. Rebase tetap tindakan operator lewat POST /specs/:id/integrate.
|
|
23248
|
+
"Rebase tidak tersedia untukmu; bila hasilnya menuntut rebase, pilih `none` dan katakan begitu."
|
|
23249
|
+
]
|
|
23250
|
+
}, deps.decideDeps);
|
|
23251
|
+
if (!row) continue;
|
|
23252
|
+
n++;
|
|
23253
|
+
if (row.status === "berlaku" && row.action !== "none") {
|
|
23254
|
+
try {
|
|
23255
|
+
await deps.apply(row);
|
|
23256
|
+
} catch {
|
|
23257
|
+
}
|
|
23258
|
+
}
|
|
23259
|
+
}
|
|
23260
|
+
return n;
|
|
23261
|
+
}
|
|
22693
23262
|
async function detectCollisions(optIn, deps) {
|
|
22694
23263
|
const opt = new Set(optIn);
|
|
22695
23264
|
const live = deps.sessions().filter((s) => !s.exited && s.specId && opt.has(s.projectId));
|
|
@@ -22736,11 +23305,22 @@ async function orderReadyWork(optIn, deps) {
|
|
|
22736
23305
|
async function orderProject(projectId, deps) {
|
|
22737
23306
|
const project = await prisma.project.findUnique({ where: { id: projectId }, select: { schedulerOptIn: true } });
|
|
22738
23307
|
if (!project?.schedulerOptIn) return 0;
|
|
22739
|
-
const
|
|
23308
|
+
const readyRaw = await prisma.spec.findMany({
|
|
22740
23309
|
where: { ...UNSTARTED_SPEC_WHERE, projectId },
|
|
22741
|
-
select: {
|
|
23310
|
+
select: {
|
|
23311
|
+
id: true,
|
|
23312
|
+
projectId: true,
|
|
23313
|
+
title: true,
|
|
23314
|
+
priority: true,
|
|
23315
|
+
objective: true,
|
|
23316
|
+
branchFrom: true,
|
|
23317
|
+
dependsOn: true
|
|
23318
|
+
},
|
|
22742
23319
|
orderBy: { id: "asc" }
|
|
22743
23320
|
});
|
|
23321
|
+
const repoDir = await resolveRepoDir(projectId);
|
|
23322
|
+
const ready = [];
|
|
23323
|
+
for (const r of readyRaw) if ((await blockersForSpec(r, repoDir)).length === 0) ready.push(r);
|
|
22744
23324
|
if (ready.length < 2) return 0;
|
|
22745
23325
|
const already = new Set((await prisma.schedulerQueueItem.findMany({
|
|
22746
23326
|
where: { specId: { in: ready.map((r) => r.id) } },
|
|
@@ -22951,6 +23531,190 @@ async function lead_default(app2) {
|
|
|
22951
23531
|
});
|
|
22952
23532
|
}
|
|
22953
23533
|
|
|
23534
|
+
// src/routes/custom-agents.ts
|
|
23535
|
+
init_src();
|
|
23536
|
+
init_db();
|
|
23537
|
+
|
|
23538
|
+
// src/services/custom-agents.ts
|
|
23539
|
+
init_db();
|
|
23540
|
+
init_src();
|
|
23541
|
+
init_pty();
|
|
23542
|
+
var cache4 = [];
|
|
23543
|
+
var asCustomAgent = (r) => ({
|
|
23544
|
+
id: r.id,
|
|
23545
|
+
projectId: r.projectId,
|
|
23546
|
+
name: r.name,
|
|
23547
|
+
description: r.description,
|
|
23548
|
+
instructions: r.instructions,
|
|
23549
|
+
tools: toolsOf(r.tools),
|
|
23550
|
+
model: r.model,
|
|
23551
|
+
mentions: mentionsOf(r.mentions),
|
|
23552
|
+
enabled: r.enabled,
|
|
23553
|
+
createdAt: "",
|
|
23554
|
+
updatedAt: ""
|
|
23555
|
+
// tak dipakai lapis ini
|
|
23556
|
+
});
|
|
23557
|
+
async function loadCustomAgents() {
|
|
23558
|
+
try {
|
|
23559
|
+
cache4 = await prisma.customAgent.findMany();
|
|
23560
|
+
} catch {
|
|
23561
|
+
cache4 = [];
|
|
23562
|
+
}
|
|
23563
|
+
}
|
|
23564
|
+
function agentDefsFor(projectId) {
|
|
23565
|
+
const globals = cache4.filter((r) => r.projectId === null).map(asCustomAgent);
|
|
23566
|
+
const project = cache4.filter((r) => r.projectId === projectId).map(asCustomAgent);
|
|
23567
|
+
return effectiveAgents(globals, project).map((a) => ({
|
|
23568
|
+
name: a.name,
|
|
23569
|
+
description: a.description,
|
|
23570
|
+
instructions: a.instructions,
|
|
23571
|
+
tools: a.tools,
|
|
23572
|
+
model: a.model,
|
|
23573
|
+
mentions: a.mentions ?? []
|
|
23574
|
+
}));
|
|
23575
|
+
}
|
|
23576
|
+
function validateGraph(rows) {
|
|
23577
|
+
const projectScopes = [...new Set(rows.map((r) => r.projectId).filter((p) => p !== null))];
|
|
23578
|
+
const globals = rows.filter((r) => r.projectId === null).map(asCustomAgent);
|
|
23579
|
+
for (const scope of [null, ...projectScopes]) {
|
|
23580
|
+
const project = scope === null ? [] : rows.filter((r) => r.projectId === scope).map(asCustomAgent);
|
|
23581
|
+
const nodes = effectiveAgents(globals, project).map((a) => ({ name: a.name, mentions: a.mentions ?? [] }));
|
|
23582
|
+
const cycle = detectCycle(nodes);
|
|
23583
|
+
if (cycle) return { scope: scope ?? GLOBAL_SCOPE, cycle };
|
|
23584
|
+
}
|
|
23585
|
+
return null;
|
|
23586
|
+
}
|
|
23587
|
+
function unknownMentions(row, all) {
|
|
23588
|
+
const visible = new Set(
|
|
23589
|
+
all.filter((r) => r.projectId === null || row.projectId !== null && r.projectId === row.projectId).map((r) => r.name)
|
|
23590
|
+
);
|
|
23591
|
+
return mentionsOf(row.mentions).filter((m) => !visible.has(m));
|
|
23592
|
+
}
|
|
23593
|
+
async function installCustomAgents() {
|
|
23594
|
+
await loadCustomAgents();
|
|
23595
|
+
registerCustomAgentSource((projectId) => agentDefsFor(projectId));
|
|
23596
|
+
}
|
|
23597
|
+
|
|
23598
|
+
// src/routes/custom-agents.ts
|
|
23599
|
+
var rowsOf = async () => await prisma.customAgent.findMany();
|
|
23600
|
+
var view6 = (r, projectId) => ({
|
|
23601
|
+
id: r.id,
|
|
23602
|
+
projectId: r.projectId,
|
|
23603
|
+
name: r.name,
|
|
23604
|
+
description: r.description,
|
|
23605
|
+
instructions: r.instructions,
|
|
23606
|
+
tools: toolsOf(r.tools),
|
|
23607
|
+
model: r.model,
|
|
23608
|
+
mentions: mentionsOf(r.mentions),
|
|
23609
|
+
enabled: r.enabled,
|
|
23610
|
+
...projectId ? { inherited: r.projectId === null } : {}
|
|
23611
|
+
});
|
|
23612
|
+
async function custom_agents_default(app2) {
|
|
23613
|
+
app2.get("/custom-agents", async (req) => {
|
|
23614
|
+
const projectId = req.query.projectId;
|
|
23615
|
+
const rows = await prisma.customAgent.findMany({
|
|
23616
|
+
where: projectId ? { OR: [{ projectId: null }, { projectId }] } : { projectId: null },
|
|
23617
|
+
orderBy: { name: "asc" }
|
|
23618
|
+
});
|
|
23619
|
+
const byName = /* @__PURE__ */ new Map();
|
|
23620
|
+
for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
|
|
23621
|
+
for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
|
|
23622
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view6(r, projectId));
|
|
23623
|
+
});
|
|
23624
|
+
app2.post("/custom-agents", async (req, reply) => {
|
|
23625
|
+
const parsed = zCreateCustomAgent.safeParse(req.body);
|
|
23626
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
23627
|
+
const p = parsed.data;
|
|
23628
|
+
const projectId = p.projectId ?? null;
|
|
23629
|
+
if (projectId && !await prisma.project.findUnique({ where: { id: projectId } }))
|
|
23630
|
+
return reply.code(400).send({ error: "project tak ditemukan", projectId });
|
|
23631
|
+
const id = customAgentId(projectId, p.name);
|
|
23632
|
+
if (await prisma.customAgent.findUnique({ where: { id } }))
|
|
23633
|
+
return reply.code(409).send({ error: "nama sudah dipakai di scope ini", id });
|
|
23634
|
+
const candidate = {
|
|
23635
|
+
id,
|
|
23636
|
+
projectId,
|
|
23637
|
+
name: p.name,
|
|
23638
|
+
description: p.description,
|
|
23639
|
+
instructions: p.instructions,
|
|
23640
|
+
tools: p.tools ?? null,
|
|
23641
|
+
model: p.model ?? null,
|
|
23642
|
+
mentions: p.mentions ?? [],
|
|
23643
|
+
enabled: p.enabled ?? true
|
|
23644
|
+
};
|
|
23645
|
+
const all = [...await rowsOf(), candidate];
|
|
23646
|
+
const unknown = unknownMentions(candidate, all);
|
|
23647
|
+
if (unknown.length) return reply.code(400).send({ error: "mention tak dikenal", unknown });
|
|
23648
|
+
const cycle = validateGraph(all);
|
|
23649
|
+
if (cycle) return reply.code(409).send({ error: "mention membentuk siklus", ...cycle });
|
|
23650
|
+
const row = await prisma.customAgent.create({ data: {
|
|
23651
|
+
id,
|
|
23652
|
+
projectId,
|
|
23653
|
+
name: p.name,
|
|
23654
|
+
description: p.description,
|
|
23655
|
+
instructions: p.instructions,
|
|
23656
|
+
tools: candidate.tools,
|
|
23657
|
+
model: candidate.model,
|
|
23658
|
+
mentions: candidate.mentions,
|
|
23659
|
+
enabled: candidate.enabled
|
|
23660
|
+
} });
|
|
23661
|
+
await loadCustomAgents();
|
|
23662
|
+
await notifySynced("customAgent", id);
|
|
23663
|
+
return reply.code(201).send(view6(row));
|
|
23664
|
+
});
|
|
23665
|
+
app2.patch("/custom-agents/:id", async (req, reply) => {
|
|
23666
|
+
const { id } = req.params;
|
|
23667
|
+
const body = req.body ?? {};
|
|
23668
|
+
if ("name" in body || "projectId" in body)
|
|
23669
|
+
return reply.code(400).send({ error: "name & projectId tak bisa diubah \u2014 hapus lalu buat baru" });
|
|
23670
|
+
const parsed = zUpdateCustomAgent.safeParse(body);
|
|
23671
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
23672
|
+
const existing = await prisma.customAgent.findUnique({ where: { id } });
|
|
23673
|
+
if (!existing) return reply.code(404).send({ error: "not found" });
|
|
23674
|
+
const before = existing;
|
|
23675
|
+
const candidate = {
|
|
23676
|
+
...before,
|
|
23677
|
+
...parsed.data,
|
|
23678
|
+
mentions: parsed.data.mentions ?? mentionsOf(before.mentions),
|
|
23679
|
+
tools: parsed.data.tools !== void 0 ? parsed.data.tools : toolsOf(before.tools)
|
|
23680
|
+
};
|
|
23681
|
+
const all = (await rowsOf()).map((r) => r.id === id ? candidate : r);
|
|
23682
|
+
const unknown = unknownMentions(candidate, all);
|
|
23683
|
+
if (unknown.length) return reply.code(400).send({ error: "mention tak dikenal", unknown });
|
|
23684
|
+
const cycle = validateGraph(all);
|
|
23685
|
+
if (cycle) return reply.code(409).send({ error: "mention membentuk siklus", ...cycle });
|
|
23686
|
+
const row = await prisma.customAgent.update({ where: { id }, data: {
|
|
23687
|
+
description: candidate.description,
|
|
23688
|
+
instructions: candidate.instructions,
|
|
23689
|
+
tools: candidate.tools,
|
|
23690
|
+
model: candidate.model,
|
|
23691
|
+
mentions: candidate.mentions,
|
|
23692
|
+
enabled: candidate.enabled
|
|
23693
|
+
} });
|
|
23694
|
+
await loadCustomAgents();
|
|
23695
|
+
await notifySynced("customAgent", id);
|
|
23696
|
+
return view6(row);
|
|
23697
|
+
});
|
|
23698
|
+
app2.delete("/custom-agents/:id", async (req, reply) => {
|
|
23699
|
+
const { id } = req.params;
|
|
23700
|
+
const existing = await prisma.customAgent.findUnique({ where: { id } });
|
|
23701
|
+
if (!existing) return reply.code(404).send({ error: "not found" });
|
|
23702
|
+
await prisma.customAgent.delete({ where: { id } });
|
|
23703
|
+
const name2 = existing.name;
|
|
23704
|
+
for (const r of await rowsOf()) {
|
|
23705
|
+
const m = mentionsOf(r.mentions);
|
|
23706
|
+
if (!m.includes(name2)) continue;
|
|
23707
|
+
await prisma.customAgent.update({
|
|
23708
|
+
where: { id: r.id },
|
|
23709
|
+
data: { mentions: m.filter((x) => x !== name2) }
|
|
23710
|
+
});
|
|
23711
|
+
await notifySynced("customAgent", r.id);
|
|
23712
|
+
}
|
|
23713
|
+
await loadCustomAgents();
|
|
23714
|
+
return reply.code(204).send();
|
|
23715
|
+
});
|
|
23716
|
+
}
|
|
23717
|
+
|
|
22954
23718
|
// src/app.ts
|
|
22955
23719
|
var import_multipart = __toESM(require_multipart2(), 1);
|
|
22956
23720
|
|
|
@@ -23032,7 +23796,7 @@ function cookieOpts() {
|
|
|
23032
23796
|
}
|
|
23033
23797
|
|
|
23034
23798
|
// src/routes/auth.ts
|
|
23035
|
-
var
|
|
23799
|
+
var view7 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
|
|
23036
23800
|
async function issue(reply, userId) {
|
|
23037
23801
|
const token = await createSession2(userId);
|
|
23038
23802
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -23050,7 +23814,7 @@ async function auth_default(app2) {
|
|
|
23050
23814
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
23051
23815
|
});
|
|
23052
23816
|
await issue(reply, user.id);
|
|
23053
|
-
return { user:
|
|
23817
|
+
return { user: view7(user) };
|
|
23054
23818
|
});
|
|
23055
23819
|
app2.post("/auth/login", async (req, reply) => {
|
|
23056
23820
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
@@ -23063,7 +23827,7 @@ async function auth_default(app2) {
|
|
|
23063
23827
|
}
|
|
23064
23828
|
clearLoginFails(req.ip);
|
|
23065
23829
|
await issue(reply, user.id);
|
|
23066
|
-
return { user:
|
|
23830
|
+
return { user: view7(user) };
|
|
23067
23831
|
});
|
|
23068
23832
|
app2.post("/auth/logout", async (req, reply) => {
|
|
23069
23833
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -23071,7 +23835,7 @@ async function auth_default(app2) {
|
|
|
23071
23835
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
23072
23836
|
return reply.code(204).send();
|
|
23073
23837
|
});
|
|
23074
|
-
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(
|
|
23838
|
+
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view7));
|
|
23075
23839
|
app2.post("/auth/users", async (req, reply) => {
|
|
23076
23840
|
const p = zSignup.safeParse(req.body);
|
|
23077
23841
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -23080,7 +23844,7 @@ async function auth_default(app2) {
|
|
|
23080
23844
|
const user = await prisma.user.create({
|
|
23081
23845
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
23082
23846
|
});
|
|
23083
|
-
return
|
|
23847
|
+
return view7(user);
|
|
23084
23848
|
});
|
|
23085
23849
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
23086
23850
|
if (await prisma.user.count() <= 1) return reply.code(400).send({ error: "tak bisa hapus user terakhir" });
|
|
@@ -23100,7 +23864,7 @@ async function auth_default(app2) {
|
|
|
23100
23864
|
});
|
|
23101
23865
|
await deleteUserSessions(user.id);
|
|
23102
23866
|
await issue(reply, user.id);
|
|
23103
|
-
return { user:
|
|
23867
|
+
return { user: view7(user) };
|
|
23104
23868
|
});
|
|
23105
23869
|
}
|
|
23106
23870
|
|
|
@@ -23179,15 +23943,15 @@ async function agent_tokens_default(app2) {
|
|
|
23179
23943
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
23180
23944
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
23181
23945
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
23182
|
-
const { view:
|
|
23183
|
-
return reply.code(201).send({ ...
|
|
23946
|
+
const { view: view8, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
|
|
23947
|
+
return reply.code(201).send({ ...view8, token });
|
|
23184
23948
|
});
|
|
23185
23949
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
23186
23950
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
23187
23951
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
23188
23952
|
const { id } = req.params;
|
|
23189
|
-
const
|
|
23190
|
-
return
|
|
23953
|
+
const view8 = await patchAgentToken(id, p.data);
|
|
23954
|
+
return view8 ? reply.send(view8) : reply.code(404).send({ error: "not found" });
|
|
23191
23955
|
});
|
|
23192
23956
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
23193
23957
|
const { id } = req.params;
|
|
@@ -23235,6 +23999,7 @@ function capabilityForRoute(method, path) {
|
|
|
23235
23999
|
return read ? "GLOBAL_READ" : "COOKIE_ONLY";
|
|
23236
24000
|
if (top === "scheduler") return rw("settings");
|
|
23237
24001
|
if (top === "lead") return rw("lead");
|
|
24002
|
+
if (top === "custom-agents") return rw("agents");
|
|
23238
24003
|
if (top === "settings" || top === "config") return rw("settings");
|
|
23239
24004
|
if (top === "specs") return rw("backlog");
|
|
23240
24005
|
if (top === "notifications") return rw("notifications");
|
|
@@ -23341,6 +24106,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
23341
24106
|
await api.register(scheduler_default);
|
|
23342
24107
|
await api.register(codex);
|
|
23343
24108
|
await api.register(lead_default);
|
|
24109
|
+
await api.register(custom_agents_default);
|
|
23344
24110
|
}, { prefix: "/api" });
|
|
23345
24111
|
if (process.env.NODE_ENV === "production") {
|
|
23346
24112
|
const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
|
|
@@ -23407,6 +24173,11 @@ async function drain(cfg, deps) {
|
|
|
23407
24173
|
await markDone(item.id, ALREADY_DONE_NOTE);
|
|
23408
24174
|
continue;
|
|
23409
24175
|
}
|
|
24176
|
+
const blocked = await deps.blockers(item.specId);
|
|
24177
|
+
if (blocked.length) {
|
|
24178
|
+
await noteQueued(item.id, blockedNote(blocked));
|
|
24179
|
+
continue;
|
|
24180
|
+
}
|
|
23410
24181
|
const liveId = deps.isLive(item.specId);
|
|
23411
24182
|
if (liveId) {
|
|
23412
24183
|
await markLaunched(item.id, liveId);
|
|
@@ -23536,6 +24307,13 @@ var prodDeps = {
|
|
|
23536
24307
|
const s = await prisma.spec.findUnique({ where: { id: specId }, select: { stage: true } });
|
|
23537
24308
|
return s?.stage === "done";
|
|
23538
24309
|
},
|
|
24310
|
+
// SPEC-447 · ADR-0093 · dibaca ULANG dari DB tepat sebelum launch, seperti `isDone`: `dependsOn`
|
|
24311
|
+
// bisa ditulis operator selagi item mengantre, dan merged-ness bergerak sendiri saat ada integrate.
|
|
24312
|
+
blockers: async (specId) => {
|
|
24313
|
+
const spec = await prisma.spec.findUnique({ where: { id: specId } });
|
|
24314
|
+
if (!spec) return [];
|
|
24315
|
+
return blockersForSpec(spec, await resolveRepoDir(spec.projectId));
|
|
24316
|
+
},
|
|
23539
24317
|
launch: async (item, autonomy) => {
|
|
23540
24318
|
const spec = await prisma.spec.findUnique({ where: { id: item.specId } });
|
|
23541
24319
|
if (!spec) throw new Error(`spec ${item.specId} tak ada`);
|
|
@@ -23614,8 +24392,9 @@ async function shutdown(sig) {
|
|
|
23614
24392
|
}
|
|
23615
24393
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
23616
24394
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
23617
|
-
app.listen({ port, host }).then(() => {
|
|
24395
|
+
app.listen({ port, host }).then(async () => {
|
|
23618
24396
|
console.log(`hanoman api ${host}:${port}`);
|
|
24397
|
+
await installCustomAgents();
|
|
23619
24398
|
installSessionHistory();
|
|
23620
24399
|
try {
|
|
23621
24400
|
const liveIds = listSessions().map((s) => s.id);
|