hanoman 0.1.5 → 0.1.7
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 +177 -117
- package/dist/server.js +1520 -1636
- package/package.json +1 -1
- package/prisma/migrations/20260731000000_spec_created_started_at/migration.sql +43 -0
- package/prisma/migrations/20260731120000_lead_decision/migration.sql +33 -0
- package/prisma/migrations/20260731180000_drop_errors_sdk_crossaudit/migration.sql +58 -0
- package/prisma/schema.prisma +44 -87
- package/web/assets/{index-CeJYUp36.js → index-BWyQwoAk.js} +1530 -1531
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -4145,22 +4145,20 @@ var init_zod = __esm({
|
|
|
4145
4145
|
});
|
|
4146
4146
|
|
|
4147
4147
|
// ../shared/src/enums.ts
|
|
4148
|
-
var zStage, zSpecSource, zDocStatus, zPriority, zProjectKind, zAgent, zSeverity,
|
|
4148
|
+
var zStage, zSpecSource, zDocStatus, zPriority, zProjectKind, zAgent, zSeverity, zTicketCategory, zTicketStatus, VERIFY_SCOPES, zVerifyScope;
|
|
4149
4149
|
var init_enums = __esm({
|
|
4150
4150
|
"../shared/src/enums.ts"() {
|
|
4151
4151
|
"use strict";
|
|
4152
4152
|
init_zod();
|
|
4153
4153
|
zStage = external_exports.enum(["brainstorming", "objective", "spec-ready", "planned", "executing", "done"]);
|
|
4154
|
-
zSpecSource = external_exports.enum(["brief", "qa", "audit", "
|
|
4154
|
+
zSpecSource = external_exports.enum(["brief", "qa", "audit", "help", "goal"]);
|
|
4155
4155
|
zDocStatus = external_exports.enum(["ok", "drift", "broken"]);
|
|
4156
4156
|
zPriority = external_exports.enum(["tinggi", "sedang", "rendah"]);
|
|
4157
4157
|
zProjectKind = external_exports.enum(["from-scratch", "existing"]);
|
|
4158
4158
|
zAgent = external_exports.enum(["claude", "codex"]);
|
|
4159
4159
|
zSeverity = external_exports.enum(["critical", "major", "minor"]);
|
|
4160
|
-
zErrorStatus = external_exports.enum(["new", "escalated", "resolved"]);
|
|
4161
4160
|
zTicketCategory = external_exports.enum(["bug", "fitur", "pertanyaan", "lainnya"]);
|
|
4162
4161
|
zTicketStatus = external_exports.enum(["new", "accepted", "rejected"]);
|
|
4163
|
-
zLinkKind = external_exports.enum(["api", "sdk", "data", "event", "lainnya"]);
|
|
4164
4162
|
VERIFY_SCOPES = ["changed", "full"];
|
|
4165
4163
|
zVerifyScope = external_exports.enum(VERIFY_SCOPES);
|
|
4166
4164
|
}
|
|
@@ -4184,7 +4182,7 @@ function cmpVersion(a, b) {
|
|
|
4184
4182
|
}
|
|
4185
4183
|
return 0;
|
|
4186
4184
|
}
|
|
4187
|
-
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, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
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;
|
|
4188
4186
|
var init_entities = __esm({
|
|
4189
4187
|
"../shared/src/entities.ts"() {
|
|
4190
4188
|
"use strict";
|
|
@@ -4219,9 +4217,7 @@ var init_entities = __esm({
|
|
|
4219
4217
|
expected: external_exports.string(),
|
|
4220
4218
|
actual: external_exports.string(),
|
|
4221
4219
|
env: external_exports.string(),
|
|
4222
|
-
fromAudit: external_exports.string().optional()
|
|
4223
|
-
// SPEC-244 · qa dinaikkan dari audit → sinyal skip fase Audit (ADR-0059)
|
|
4224
|
-
fromErrorGroup: external_exports.string().optional()
|
|
4220
|
+
fromAudit: external_exports.string().optional()
|
|
4225
4221
|
});
|
|
4226
4222
|
zGoalPayload = external_exports.object({
|
|
4227
4223
|
goal: external_exports.string(),
|
|
@@ -4242,8 +4238,12 @@ var init_entities = __esm({
|
|
|
4242
4238
|
// SPEC-407 · +goal
|
|
4243
4239
|
branchFrom: external_exports.string().nullable(),
|
|
4244
4240
|
// SPEC-143 · null = default project (main)
|
|
4245
|
-
baseSha: external_exports.string().nullable()
|
|
4241
|
+
baseSha: external_exports.string().nullable(),
|
|
4246
4242
|
// SPEC-186 · null = belum pernah ada sesi (belum dimulai)
|
|
4243
|
+
// SPEC-408 · ADR-0090 · stempel waktu backlog (ISO string di wire — kolom DateTime di DB).
|
|
4244
|
+
// `startedAt` null = belum pernah dikerjakan; ia tak pernah ditulis ulang saat sesi dilanjutkan.
|
|
4245
|
+
createdAt: external_exports.string(),
|
|
4246
|
+
startedAt: external_exports.string().nullable()
|
|
4247
4247
|
});
|
|
4248
4248
|
NOTIFY_SOUNDS = [
|
|
4249
4249
|
"off",
|
|
@@ -4292,7 +4292,6 @@ var init_entities = __esm({
|
|
|
4292
4292
|
// dikonsumsi daun #5
|
|
4293
4293
|
sources: external_exports.object({
|
|
4294
4294
|
backlog: external_exports.object({ ...zSourceCommon, everyMin: external_exports.number().int().min(1).default(15) }).default({}),
|
|
4295
|
-
errors: external_exports.object({ ...zSourceCommon, everyMin: external_exports.number().int().min(1).default(15), minCount: external_exports.number().int().min(1).default(5) }).default({}),
|
|
4296
4295
|
triase: external_exports.object({ ...zSourceCommon, everyMin: external_exports.number().int().min(1).default(30) }).default({})
|
|
4297
4296
|
}).default({})
|
|
4298
4297
|
});
|
|
@@ -4310,6 +4309,35 @@ var init_entities = __esm({
|
|
|
4310
4309
|
effort: external_exports.string().default("xhigh")
|
|
4311
4310
|
});
|
|
4312
4311
|
CONFLICT_DEFAULTS = zConflict.parse({});
|
|
4312
|
+
zLeadEngine = external_exports.object({
|
|
4313
|
+
enabled: external_exports.boolean().default(false),
|
|
4314
|
+
agent: zAgent.default("claude"),
|
|
4315
|
+
model: external_exports.string().default("claude-opus-5"),
|
|
4316
|
+
effort: external_exports.string().default("xhigh")
|
|
4317
|
+
});
|
|
4318
|
+
zLead = external_exports.object({
|
|
4319
|
+
enabled: external_exports.boolean().default(false),
|
|
4320
|
+
// master switch (AC-30)
|
|
4321
|
+
paused: external_exports.boolean().default(false),
|
|
4322
|
+
// rem darurat global (AC-27)
|
|
4323
|
+
pausedProjects: external_exports.array(external_exports.string()).default([]),
|
|
4324
|
+
// rem per project (AC-15/US-4)
|
|
4325
|
+
everyMin: external_exports.number().int().min(1).max(1440).default(5),
|
|
4326
|
+
// denyut proaktif (OQ-2)
|
|
4327
|
+
// SPEC-432 · batas waktu satu putusan (AC-4/35). Default 120 dtk MELAWAN prompt lead sendiri
|
|
4328
|
+
// ("kumpulkan bukti dulu: SoT, ADR, plan, kode, riwayat git") dan kalah: satu keputusan `order`
|
|
4329
|
+
// nyata terukur 306 dtk pada claude-opus-5 · xhigh, jadi 7/7 baris jejak operator berstatus
|
|
4330
|
+
// `gagal`. 600 dtk memberi kelonggaran di atas ongkos terukur itu; yang memangkas ongkosnya
|
|
4331
|
+
// sendiri adalah paragraf anggaran waktu di `leadPrompt` (306 dtk → 101 dtk).
|
|
4332
|
+
timeoutSec: external_exports.number().int().min(10).max(900).default(600),
|
|
4333
|
+
// AC-11 / OQ-10 · berapa jawaban otomatis berturut-turut untuk SATU sesi sebelum lead berhenti.
|
|
4334
|
+
maxAutoAnswers: external_exports.number().int().min(1).max(20).default(3),
|
|
4335
|
+
// OQ-3 · syarat objektif sebelum lead boleh mengintegrasikan ke branch utama. Default MENYALA:
|
|
4336
|
+
// risiko "kode masuk main tanpa mata manusia" diterima sadar, tapi syaratnya tetap terukur.
|
|
4337
|
+
requireGreenBeforeIntegrate: external_exports.boolean().default(true),
|
|
4338
|
+
engine: zLeadEngine.default({})
|
|
4339
|
+
});
|
|
4340
|
+
LEAD_DEFAULTS = zLead.parse({});
|
|
4313
4341
|
zSetting = external_exports.object({
|
|
4314
4342
|
model: external_exports.string().default("claude-opus-5"),
|
|
4315
4343
|
effort: external_exports.string().default("xhigh"),
|
|
@@ -4336,13 +4364,18 @@ var init_entities = __esm({
|
|
|
4336
4364
|
// SPEC-338 · ADR-0074 · model/effort codex
|
|
4337
4365
|
verifyScope: zVerifyScope.default("changed"),
|
|
4338
4366
|
// SPEC-376 · ADR-0080 · scope verifikasi sesi
|
|
4339
|
-
conflict: zConflict.default(CONFLICT_DEFAULTS)
|
|
4367
|
+
conflict: zConflict.default(CONFLICT_DEFAULTS),
|
|
4340
4368
|
// SPEC-383 · ADR-0081 · default sesi konflik rebase/merge
|
|
4369
|
+
lead: zLead.default(LEAD_DEFAULTS)
|
|
4370
|
+
// SPEC-409 · ADR-0091 · hanoman-lead (default mati)
|
|
4341
4371
|
});
|
|
4342
4372
|
zNotification = external_exports.object({
|
|
4343
4373
|
id: external_exports.string(),
|
|
4344
|
-
|
|
4345
|
-
// SPEC-
|
|
4374
|
+
// SPEC-253 · +ticket; SPEC-298 · +fail (sesi scheduler gagal/limit)
|
|
4375
|
+
// SPEC-409 · +lead (ADR-0091): keputusan berbobot / ragu / tindakan terkunci ditolak. MEMBERI
|
|
4376
|
+
// TAHU, bukan meminta izin — tak ada pekerjaan yang menunggu notifikasi ini dibaca (AC-25).
|
|
4377
|
+
// SPEC-384 · −error (ADR-0092) · dicabut bersama error monitoring.
|
|
4378
|
+
type: external_exports.enum(["done", "decision", "ticket", "fail", "lead"]).default("done"),
|
|
4346
4379
|
specId: external_exports.string().nullable(),
|
|
4347
4380
|
sessionId: external_exports.string().nullable(),
|
|
4348
4381
|
title: external_exports.string(),
|
|
@@ -4397,7 +4430,13 @@ var init_agent = __esm({
|
|
|
4397
4430
|
"support:read",
|
|
4398
4431
|
"support:write",
|
|
4399
4432
|
"notifications:read",
|
|
4400
|
-
"notifications:write"
|
|
4433
|
+
"notifications:write",
|
|
4434
|
+
// SPEC-409 · ADR-0091 · minta putusan ke hanoman-lead + baca jejak keputusan. Domain TERSENDIRI,
|
|
4435
|
+
// bukan menumpang prefix baca yang sudah ada: meminta putusan adalah operasi TULIS (ia melahirkan
|
|
4436
|
+
// baris jejak & bisa menggerakkan sesi), dan SPEC-405 sudah membuktikan apa yang terjadi saat
|
|
4437
|
+
// endpoint tulis menumpang prefix status yang dipetakan tanpa melihat method (AC-5).
|
|
4438
|
+
"lead:read",
|
|
4439
|
+
"lead:write"
|
|
4401
4440
|
];
|
|
4402
4441
|
zCapability = external_exports.enum(CAPABILITY_IDS);
|
|
4403
4442
|
zCapabilityInfo = external_exports.object({
|
|
@@ -4410,7 +4449,7 @@ var init_agent = __esm({
|
|
|
4410
4449
|
});
|
|
4411
4450
|
CAPABILITIES = [
|
|
4412
4451
|
{ id: "projects:read", domain: "projects", access: "read", label: "Projects \u2014 baca", desc: "Lihat daftar & detail project, branch, binding." },
|
|
4413
|
-
{ id: "projects:write", domain: "projects", access: "write", label: "Projects \u2014 tulis", desc: "Buat/ubah/hapus project, rename, clone,
|
|
4452
|
+
{ id: "projects:write", domain: "projects", access: "write", label: "Projects \u2014 tulis", desc: "Buat/ubah/hapus project, rename, clone, Help Center." },
|
|
4414
4453
|
{ id: "backlog:read", domain: "backlog", access: "read", label: "Backlog \u2014 baca", desc: "Lihat spec/backlog, dokumen, review diff." },
|
|
4415
4454
|
{ id: "backlog:write", domain: "backlog", access: "write", label: "Backlog \u2014 tulis", desc: "Buat/ubah/hapus spec, integrate branch." },
|
|
4416
4455
|
{ id: "sessions:read", domain: "sessions", access: "read", label: "Sesi \u2014 baca", desc: "Lihat sesi terminal, fase, review." },
|
|
@@ -4423,10 +4462,12 @@ var init_agent = __esm({
|
|
|
4423
4462
|
{ id: "vps:write", domain: "vps", access: "write", label: "VPS \u2014 tulis", desc: "Kelola VPS, audit, harden, remediasi, konsol (remote exec).", risk: "exec" },
|
|
4424
4463
|
{ id: "settings:read", domain: "settings", access: "read", label: "Settings \u2014 baca", desc: "Baca setelan & config runtime." },
|
|
4425
4464
|
{ id: "settings:write", domain: "settings", access: "write", label: "Settings \u2014 tulis", desc: "Ubah setelan & config runtime." },
|
|
4426
|
-
{ id: "support:read", domain: "support", access: "read", label: "
|
|
4427
|
-
{ id: "support:write", domain: "support", access: "write", label: "
|
|
4465
|
+
{ id: "support:read", domain: "support", access: "read", label: "Tiket \u2014 baca", desc: "Lihat tiket Help Center." },
|
|
4466
|
+
{ id: "support:write", domain: "support", access: "write", label: "Tiket \u2014 tulis", desc: "Terima/tolak tiket, ubah status, promosikan ke backlog." },
|
|
4428
4467
|
{ id: "notifications:read", domain: "notifications", access: "read", label: "Notifikasi \u2014 baca", desc: "Lihat notifikasi." },
|
|
4429
|
-
{ id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." }
|
|
4468
|
+
{ id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." },
|
|
4469
|
+
{ 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" }
|
|
4430
4471
|
];
|
|
4431
4472
|
zAgentTokenView = external_exports.object({
|
|
4432
4473
|
id: external_exports.string(),
|
|
@@ -4451,16 +4492,103 @@ var init_agent = __esm({
|
|
|
4451
4492
|
}
|
|
4452
4493
|
});
|
|
4453
4494
|
|
|
4495
|
+
// ../shared/src/lead.ts
|
|
4496
|
+
function leadActionAllowed(action) {
|
|
4497
|
+
return LEAD_ACTIONS.includes(action);
|
|
4498
|
+
}
|
|
4499
|
+
function leadRefusalReason(action) {
|
|
4500
|
+
return LEAD_FORBIDDEN[action] ?? `tindakan "${action}" di luar permukaan tindakan lead`;
|
|
4501
|
+
}
|
|
4502
|
+
function isWeightyDecision(o) {
|
|
4503
|
+
if (o.confidence === "ragu") return true;
|
|
4504
|
+
if (o.kind === "collision" || o.kind === "refusal") return true;
|
|
4505
|
+
return o.action === "integrate-main" || o.action === "run-migration" || o.action === "stop-session" || o.action === "restart-session";
|
|
4506
|
+
}
|
|
4507
|
+
var zLeadGate, zLeadKind, zLeadConfidence, zLeadStatus, LEAD_ACTIONS, zLeadAction, LEAD_FORBIDDEN, zLeadVerdict, zLeadAsk, zLeadOverride;
|
|
4508
|
+
var init_lead = __esm({
|
|
4509
|
+
"../shared/src/lead.ts"() {
|
|
4510
|
+
"use strict";
|
|
4511
|
+
init_zod();
|
|
4512
|
+
zLeadGate = external_exports.enum(["contract", "detected", "pulse"]);
|
|
4513
|
+
zLeadKind = external_exports.enum(["answer", "order", "collision", "quality", "refusal"]);
|
|
4514
|
+
zLeadConfidence = external_exports.enum(["tinggi", "sedang", "ragu"]);
|
|
4515
|
+
zLeadStatus = external_exports.enum(["berlaku", "ditimpa", "dibatalkan", "gagal"]);
|
|
4516
|
+
LEAD_ACTIONS = [
|
|
4517
|
+
"none",
|
|
4518
|
+
// keputusan murni jawaban; tak ada tindakan menyusul
|
|
4519
|
+
"answer-session",
|
|
4520
|
+
// ketik jawaban ke pane sesi (pintu deteksi otomatis)
|
|
4521
|
+
"start-session",
|
|
4522
|
+
// mulai sesi untuk sebuah backlog (lewat antrean scheduler)
|
|
4523
|
+
"stop-session",
|
|
4524
|
+
// hentikan sesi — worktree DIBIARKAN utuh (AC-32a)
|
|
4525
|
+
"resume-session",
|
|
4526
|
+
// lanjutkan sesi terputus (jalur ADR-0084)
|
|
4527
|
+
"restart-session",
|
|
4528
|
+
// ulangi pekerjaan dari awal
|
|
4529
|
+
"order-queue",
|
|
4530
|
+
// tata urutan antrean yang sudah ada (ADR-0072) — bukan antrean kedua
|
|
4531
|
+
"hold-work",
|
|
4532
|
+
// tunda satu pekerjaan karena tabrakan area kerja
|
|
4533
|
+
"push-branch",
|
|
4534
|
+
// dorong perubahan ke branch
|
|
4535
|
+
"integrate-main",
|
|
4536
|
+
// integrasikan ke branch utama (risiko yang diterima sadar, PRD §Risiko)
|
|
4537
|
+
"run-migration"
|
|
4538
|
+
// jalankan migration pada basis data LOKAL operator
|
|
4539
|
+
];
|
|
4540
|
+
zLeadAction = external_exports.enum(LEAD_ACTIONS);
|
|
4541
|
+
LEAD_FORBIDDEN = {
|
|
4542
|
+
deploy: "deploy ke produksi",
|
|
4543
|
+
"vps-exec": "menjalankan perintah di VPS",
|
|
4544
|
+
"vps-console": "membuka konsol VPS",
|
|
4545
|
+
"prod-data": "menyentuh data produksi",
|
|
4546
|
+
"delete-project": "menghapus project",
|
|
4547
|
+
"delete-spec": "menghapus backlog",
|
|
4548
|
+
"delete-branch": "menghapus branch",
|
|
4549
|
+
"delete-worktree": "menghapus worktree",
|
|
4550
|
+
"delete-notification": "menghapus notifikasi",
|
|
4551
|
+
"delete-decision": "menghapus jejak keputusan"
|
|
4552
|
+
};
|
|
4553
|
+
zLeadVerdict = external_exports.object({
|
|
4554
|
+
decision: external_exports.string().min(1),
|
|
4555
|
+
reason: external_exports.string().min(1),
|
|
4556
|
+
refs: external_exports.array(external_exports.string()).default([]),
|
|
4557
|
+
confidence: zLeadConfidence.default("sedang"),
|
|
4558
|
+
// Sengaja `string`, BUKAN `zLeadAction`: tindakan di luar allowlist harus bisa MASUK supaya
|
|
4559
|
+
// server menolaknya secara sadar — mencatat penolakan & menotifikasi operator (AC-33). Kalau
|
|
4560
|
+
// enum yang menyaring di sini, permintaan "deploy ke produksi" hanya akan tampak sebagai
|
|
4561
|
+
// keluaran rusak, dan justru peristiwa paling layak dilaporkan itulah yang hilang dari jejak.
|
|
4562
|
+
action: external_exports.string().default("none"),
|
|
4563
|
+
/** Teks yang benar-benar diketik ke pane sesi (pintu deteksi otomatis). */
|
|
4564
|
+
reply: external_exports.string().default("")
|
|
4565
|
+
});
|
|
4566
|
+
zLeadAsk = external_exports.object({
|
|
4567
|
+
projectId: external_exports.string().min(1),
|
|
4568
|
+
specId: external_exports.string().nullish(),
|
|
4569
|
+
sessionId: external_exports.string().nullish(),
|
|
4570
|
+
question: external_exports.string().min(1).max(8e3),
|
|
4571
|
+
options: external_exports.array(external_exports.string().max(2e3)).max(20).default([]),
|
|
4572
|
+
context: external_exports.string().max(2e4).default("")
|
|
4573
|
+
});
|
|
4574
|
+
zLeadOverride = external_exports.object({
|
|
4575
|
+
answer: external_exports.string().min(1).max(8e3),
|
|
4576
|
+
reason: external_exports.string().max(8e3).default("")
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
});
|
|
4580
|
+
|
|
4454
4581
|
// ../shared/src/dto.ts
|
|
4455
4582
|
function flowForSource(source) {
|
|
4456
|
-
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "
|
|
4583
|
+
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
4457
4584
|
}
|
|
4458
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject,
|
|
4585
|
+
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;
|
|
4459
4586
|
var init_dto = __esm({
|
|
4460
4587
|
"../shared/src/dto.ts"() {
|
|
4461
4588
|
"use strict";
|
|
4462
4589
|
init_zod();
|
|
4463
4590
|
init_entities();
|
|
4591
|
+
init_lead();
|
|
4464
4592
|
init_enums();
|
|
4465
4593
|
zIssueDeviceToken = external_exports.object({ name: external_exports.string().min(1) });
|
|
4466
4594
|
zSessionResult = external_exports.object({
|
|
@@ -4504,11 +4632,6 @@ var init_dto = __esm({
|
|
|
4504
4632
|
});
|
|
4505
4633
|
zProjectId = external_exports.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "slug tak sah");
|
|
4506
4634
|
zRenameProject = external_exports.object({ newId: zProjectId });
|
|
4507
|
-
zCreateLink = external_exports.object({
|
|
4508
|
-
to: external_exports.string().min(1),
|
|
4509
|
-
kind: zLinkKind,
|
|
4510
|
-
note: external_exports.string().max(2e3).optional()
|
|
4511
|
-
});
|
|
4512
4635
|
zUpdateProject = external_exports.object({
|
|
4513
4636
|
name: external_exports.string().min(1).optional(),
|
|
4514
4637
|
desc: external_exports.string().optional(),
|
|
@@ -4516,8 +4639,10 @@ var init_dto = __esm({
|
|
|
4516
4639
|
// SPEC-213 · set git remote resmi project
|
|
4517
4640
|
repoDir: external_exports.string().nullable().optional(),
|
|
4518
4641
|
// SPEC-217 · path default/server editable (null = kosongkan)
|
|
4519
|
-
schedulerOptIn: external_exports.boolean().optional()
|
|
4642
|
+
schedulerOptIn: external_exports.boolean().optional(),
|
|
4520
4643
|
// SPEC-294 · opt-in scheduler otonom (lokal, tak disync)
|
|
4644
|
+
leadOptIn: external_exports.boolean().optional()
|
|
4645
|
+
// SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal, tak disync)
|
|
4521
4646
|
});
|
|
4522
4647
|
zCreateSpec = external_exports.object({
|
|
4523
4648
|
project: external_exports.string(),
|
|
@@ -4559,13 +4684,11 @@ var init_dto = __esm({
|
|
|
4559
4684
|
session: zSessionSummary,
|
|
4560
4685
|
activity: external_exports.string(),
|
|
4561
4686
|
commit: external_exports.string(),
|
|
4562
|
-
monitoringEnabled: external_exports.boolean().default(false),
|
|
4563
|
-
// SPEC-249 · error monitoring aktif (ingest key ada)
|
|
4564
|
-
ingestKeyPrefix: external_exports.string().nullable().default(null),
|
|
4565
|
-
// SPEC-249 · hint prefix DSN (bukan hash/rahasia)
|
|
4566
4687
|
helpEnabled: external_exports.boolean().default(false),
|
|
4567
4688
|
// SPEC-253 · Help Center publik aktif
|
|
4568
|
-
schedulerOptIn: external_exports.boolean().default(false)
|
|
4689
|
+
schedulerOptIn: external_exports.boolean().default(false),
|
|
4690
|
+
// SPEC-294 · opt-in scheduler otonom
|
|
4691
|
+
leadOptIn: external_exports.boolean().default(false)
|
|
4569
4692
|
});
|
|
4570
4693
|
zSchedulerQueueItem = external_exports.object({
|
|
4571
4694
|
id: external_exports.string(),
|
|
@@ -4583,8 +4706,6 @@ var init_dto = __esm({
|
|
|
4583
4706
|
id: external_exports.string(),
|
|
4584
4707
|
enabled: external_exports.boolean(),
|
|
4585
4708
|
everyMin: external_exports.number(),
|
|
4586
|
-
minCount: external_exports.number().optional(),
|
|
4587
|
-
// hanya errors
|
|
4588
4709
|
lastRunAt: external_exports.string().nullable(),
|
|
4589
4710
|
nextRunAt: external_exports.string().nullable()
|
|
4590
4711
|
});
|
|
@@ -4605,7 +4726,51 @@ var init_dto = __esm({
|
|
|
4605
4726
|
queue: external_exports.array(zSchedulerQueueItem),
|
|
4606
4727
|
sessions: external_exports.array(zSchedulerSessionView)
|
|
4607
4728
|
});
|
|
4608
|
-
|
|
4729
|
+
zLeadDecisionView = external_exports.object({
|
|
4730
|
+
id: external_exports.string(),
|
|
4731
|
+
projectId: external_exports.string(),
|
|
4732
|
+
specId: external_exports.string().nullable(),
|
|
4733
|
+
sessionId: external_exports.string().nullable(),
|
|
4734
|
+
gate: zLeadGate,
|
|
4735
|
+
kind: zLeadKind,
|
|
4736
|
+
question: external_exports.string(),
|
|
4737
|
+
answer: external_exports.string(),
|
|
4738
|
+
reason: external_exports.string(),
|
|
4739
|
+
refs: external_exports.array(external_exports.string()),
|
|
4740
|
+
confidence: zLeadConfidence,
|
|
4741
|
+
action: zLeadAction,
|
|
4742
|
+
status: zLeadStatus,
|
|
4743
|
+
weighty: external_exports.boolean(),
|
|
4744
|
+
supersededById: external_exports.string().nullable(),
|
|
4745
|
+
createdAt: external_exports.string()
|
|
4746
|
+
});
|
|
4747
|
+
zLeadAnswer = external_exports.object({
|
|
4748
|
+
id: external_exports.string(),
|
|
4749
|
+
decision: external_exports.string(),
|
|
4750
|
+
reason: external_exports.string(),
|
|
4751
|
+
refs: external_exports.array(external_exports.string()),
|
|
4752
|
+
confidence: zLeadConfidence,
|
|
4753
|
+
action: zLeadAction
|
|
4754
|
+
});
|
|
4755
|
+
zLeadProjectStatus = external_exports.object({
|
|
4756
|
+
projectId: external_exports.string(),
|
|
4757
|
+
name: external_exports.string(),
|
|
4758
|
+
optIn: external_exports.boolean(),
|
|
4759
|
+
paused: external_exports.boolean(),
|
|
4760
|
+
decisions24h: external_exports.number(),
|
|
4761
|
+
openSessions: external_exports.number()
|
|
4762
|
+
});
|
|
4763
|
+
zLeadStatusView = external_exports.object({
|
|
4764
|
+
config: zLead,
|
|
4765
|
+
projects: external_exports.array(zLeadProjectStatus),
|
|
4766
|
+
queue: external_exports.array(zSchedulerQueueItem),
|
|
4767
|
+
deciding: external_exports.array(external_exports.string()),
|
|
4768
|
+
// id sesi yang sedang disusun keputusannya (AC-3)
|
|
4769
|
+
waiting: external_exports.array(external_exports.string()),
|
|
4770
|
+
// id sesi ber-marker keputusan terisi
|
|
4771
|
+
lastPulseAt: external_exports.string().nullable()
|
|
4772
|
+
});
|
|
4773
|
+
zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "goal"]);
|
|
4609
4774
|
zPrdBrief = external_exports.object({
|
|
4610
4775
|
title: external_exports.string().min(1),
|
|
4611
4776
|
context: external_exports.string(),
|
|
@@ -4680,8 +4845,6 @@ var init_dto = __esm({
|
|
|
4680
4845
|
}),
|
|
4681
4846
|
// SPEC-273 · sesi breakdown project-level: pecah SATU PRD (prdPath) → manifest N backlog.
|
|
4682
4847
|
external_exports.object({ project: external_exports.string(), flow: external_exports.literal("breakdown"), prdPath: external_exports.string().min(1) }),
|
|
4683
|
-
// SPEC-337 · ADR-0075 · sesi audit lintas project LEPAS (tanya-jawab): tanpa Spec, tanpa fase.
|
|
4684
|
-
external_exports.object({ project: external_exports.string(), flow: external_exports.literal("cross-audit") }),
|
|
4685
4848
|
// SPEC-222 · scaffold: sesi project-level from-scratch, menyusun SoT dari ide. Tanpa brief
|
|
4686
4849
|
// (diseed dari Project.desc), tanpa Spec — cermin reverse.
|
|
4687
4850
|
external_exports.object({ project: external_exports.string(), flow: external_exports.literal("scaffold") }),
|
|
@@ -4753,75 +4916,6 @@ var init_dto = __esm({
|
|
|
4753
4916
|
zRemediate = external_exports.object({ items: external_exports.array(external_exports.string()).min(1).max(64) });
|
|
4754
4917
|
UPDATE_RESTART_EXIT = 75;
|
|
4755
4918
|
zUpdateApplyBody = external_exports.object({ confirm: external_exports.boolean().optional() });
|
|
4756
|
-
zStackFrame = external_exports.object({
|
|
4757
|
-
function: external_exports.string().max(500).optional(),
|
|
4758
|
-
filename: external_exports.string().max(2e3).optional(),
|
|
4759
|
-
lineno: external_exports.number().int().optional(),
|
|
4760
|
-
colno: external_exports.number().int().optional(),
|
|
4761
|
-
in_app: external_exports.boolean().optional()
|
|
4762
|
-
});
|
|
4763
|
-
zSymbolicatedFrame = zStackFrame.extend({
|
|
4764
|
-
source: external_exports.string().optional(),
|
|
4765
|
-
sourceLine: external_exports.number().int().optional(),
|
|
4766
|
-
sourceColumn: external_exports.number().int().optional(),
|
|
4767
|
-
contextLine: external_exports.string().optional(),
|
|
4768
|
-
preContext: external_exports.array(external_exports.string()).optional(),
|
|
4769
|
-
postContext: external_exports.array(external_exports.string()).optional(),
|
|
4770
|
-
symbolicated: external_exports.boolean()
|
|
4771
|
-
});
|
|
4772
|
-
zSourceMapUpload = external_exports.object({
|
|
4773
|
-
release: external_exports.string().min(1).max(200),
|
|
4774
|
-
artifacts: external_exports.array(external_exports.object({
|
|
4775
|
-
filename: external_exports.string().min(1).max(2e3),
|
|
4776
|
-
map: external_exports.string().min(1),
|
|
4777
|
-
debugId: external_exports.string().max(200).optional()
|
|
4778
|
-
})).min(1).max(200)
|
|
4779
|
-
});
|
|
4780
|
-
zIngestPayload = external_exports.object({
|
|
4781
|
-
type: external_exports.string().min(1).max(500),
|
|
4782
|
-
message: external_exports.string().min(1),
|
|
4783
|
-
stack: external_exports.string().optional(),
|
|
4784
|
-
frames: external_exports.array(zStackFrame).max(200).optional(),
|
|
4785
|
-
// SPEC-276 · opsional → kompatibel mundur
|
|
4786
|
-
environment: external_exports.string().max(120).optional(),
|
|
4787
|
-
release: external_exports.string().max(200).optional(),
|
|
4788
|
-
context: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
4789
|
-
});
|
|
4790
|
-
zErrorGroupView = external_exports.object({
|
|
4791
|
-
id: external_exports.string(),
|
|
4792
|
-
projectId: external_exports.string(),
|
|
4793
|
-
type: external_exports.string(),
|
|
4794
|
-
message: external_exports.string(),
|
|
4795
|
-
environment: external_exports.string(),
|
|
4796
|
-
status: zErrorStatus,
|
|
4797
|
-
count: external_exports.number().int(),
|
|
4798
|
-
firstSeenAt: external_exports.string(),
|
|
4799
|
-
lastSeenAt: external_exports.string(),
|
|
4800
|
-
specId: external_exports.string().nullable(),
|
|
4801
|
-
release: external_exports.string().nullable()
|
|
4802
|
-
// SPEC-276 · release terakhir grup (korelasi build)
|
|
4803
|
-
});
|
|
4804
|
-
zErrorEventView = external_exports.object({
|
|
4805
|
-
id: external_exports.string(),
|
|
4806
|
-
type: external_exports.string(),
|
|
4807
|
-
message: external_exports.string(),
|
|
4808
|
-
stack: external_exports.string().nullable(),
|
|
4809
|
-
environment: external_exports.string(),
|
|
4810
|
-
release: external_exports.string().nullable(),
|
|
4811
|
-
receivedAt: external_exports.string()
|
|
4812
|
-
});
|
|
4813
|
-
zErrorGroupDetail = zErrorGroupView.extend({
|
|
4814
|
-
sampleStack: external_exports.string().nullable(),
|
|
4815
|
-
sampleFrames: external_exports.array(zSymbolicatedFrame).nullable(),
|
|
4816
|
-
// SPEC-276 · frame tersimbolikasi (bila map ada)
|
|
4817
|
-
events: external_exports.array(zErrorEventView)
|
|
4818
|
-
});
|
|
4819
|
-
zIngestKeyView = external_exports.object({
|
|
4820
|
-
enabled: external_exports.boolean(),
|
|
4821
|
-
prefix: external_exports.string().nullable(),
|
|
4822
|
-
key: external_exports.string().optional(),
|
|
4823
|
-
dsnUrl: external_exports.string().optional()
|
|
4824
|
-
});
|
|
4825
4919
|
zTicketView = external_exports.object({
|
|
4826
4920
|
id: external_exports.string(),
|
|
4827
4921
|
projectId: external_exports.string(),
|
|
@@ -4995,6 +5089,12 @@ var init_api = __esm({
|
|
|
4995
5089
|
// SPEC-299 · panel scheduler (daun #6) — konsumen read-only fondasi SPEC-294/ADR-0072.
|
|
4996
5090
|
schedulerConfig: `${API}/scheduler/config`,
|
|
4997
5091
|
schedulerState: `${API}/scheduler/state`,
|
|
5092
|
+
// SPEC-409 · ADR-0091 · hanoman-lead. Semua HTTP (polling) — tak ada kanal WS baru (AC-26).
|
|
5093
|
+
leadConfig: `${API}/lead/config`,
|
|
5094
|
+
leadStatus: `${API}/lead/status`,
|
|
5095
|
+
leadDecisions: `${API}/lead/decisions`,
|
|
5096
|
+
leadDecisionOverride: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/override`,
|
|
5097
|
+
leadDecisionCancel: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/cancel`,
|
|
4998
5098
|
// SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
|
|
4999
5099
|
syncNow: `${API}/sync/now`,
|
|
5000
5100
|
// SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
|
|
@@ -5004,15 +5104,6 @@ var init_api = __esm({
|
|
|
5004
5104
|
agentTokens: `${API}/agent-tokens`,
|
|
5005
5105
|
agentToken: (id) => `${API}/agent-tokens/${id}`,
|
|
5006
5106
|
agentCapabilities: `${API}/agent-tokens/capabilities`,
|
|
5007
|
-
// SPEC-249 · error monitoring. ingest publik ber-DSN (bypass gate cookie, ADR-0060).
|
|
5008
|
-
ingest: (slug) => `${API}/ingest/${encodeURIComponent(slug)}`,
|
|
5009
|
-
errors: `${API}/errors`,
|
|
5010
|
-
errorsGuide: `${API}/errors/integration-guide`,
|
|
5011
|
-
error: (id) => `${API}/errors/${id}`,
|
|
5012
|
-
errorEscalate: (id) => `${API}/errors/${id}/escalate`,
|
|
5013
|
-
errorUnlink: (id) => `${API}/errors/${id}/unlink`,
|
|
5014
|
-
// SPEC-271 · lepas tautan backlog
|
|
5015
|
-
projectIngestKey: (id) => `${API}/projects/${encodeURIComponent(id)}/ingest-key`,
|
|
5016
5107
|
// SPEC-253 · Help Center publik (bypass gate cookie; otorisasi helpEnabled + kunci opaque tiket).
|
|
5017
5108
|
help: (slug) => `${API}/help/${encodeURIComponent(slug)}`,
|
|
5018
5109
|
helpTickets: (slug) => `${API}/help/${encodeURIComponent(slug)}/tickets`,
|
|
@@ -5026,9 +5117,6 @@ var init_api = __esm({
|
|
|
5026
5117
|
ticketUnlink: (id) => `${API}/tickets/${id}/unlink`,
|
|
5027
5118
|
// SPEC-271 · lepas tautan backlog
|
|
5028
5119
|
ticketReject: (id) => `${API}/tickets/${id}/reject`,
|
|
5029
|
-
// SPEC-337 · ADR-0075 · relasi integrasi antar project (LOCAL-only) + log lintas project.
|
|
5030
|
-
projectLinks: (id) => `${API}/projects/${encodeURIComponent(id)}/links`,
|
|
5031
|
-
projectLink: (id, linkId) => `${API}/projects/${encodeURIComponent(id)}/links/${encodeURIComponent(linkId)}`,
|
|
5032
5120
|
// SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
|
|
5033
5121
|
// (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
|
|
5034
5122
|
download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
|
|
@@ -5118,7 +5206,6 @@ var init_session_kind = __esm({
|
|
|
5118
5206
|
"prd",
|
|
5119
5207
|
"scaffold",
|
|
5120
5208
|
"breakdown",
|
|
5121
|
-
"cross-audit",
|
|
5122
5209
|
"vps",
|
|
5123
5210
|
"shell",
|
|
5124
5211
|
"worktree",
|
|
@@ -5328,6 +5415,7 @@ var init_src = __esm({
|
|
|
5328
5415
|
init_enums();
|
|
5329
5416
|
init_entities();
|
|
5330
5417
|
init_agent();
|
|
5418
|
+
init_lead();
|
|
5331
5419
|
init_dto();
|
|
5332
5420
|
init_api();
|
|
5333
5421
|
init_coverage();
|
|
@@ -5686,45 +5774,7 @@ Stack: ${project.stack || "\u2014"}`,
|
|
|
5686
5774
|
${REVERSE_STANDARD}`
|
|
5687
5775
|
].filter(Boolean).join("\n\n");
|
|
5688
5776
|
}
|
|
5689
|
-
|
|
5690
|
-
const map = [projectLine(ctx.primary, true), ...ctx.neighbors.map((n) => projectLine(n, false))].join("\n");
|
|
5691
|
-
const scopeNote = ctx.neighbors.length ? "" : 'CATATAN: project ini belum punya relasi integrasi terdaftar, jadi scope-nya hanya dirinya sendiri. Katakan itu di awal jawabanmu \u2014 operator mungkin lupa mendaftarkan relasinya di kartu "Integrasi antar project".';
|
|
5692
|
-
const head = [
|
|
5693
|
-
`hanoman cross-audit. Kamu mengaudit INTEGRASI ANTAR PROJECT \u2014 bukan satu project saja. Semua project di bawah ini berada dalam scope-mu.`,
|
|
5694
|
-
`Project dalam scope:
|
|
5695
|
-
${map}`,
|
|
5696
|
-
scopeNote,
|
|
5697
|
-
// ADR-0002 · yang boleh ditulis HANYA worktree sesi — termasuk checkout utama project sendiri
|
|
5698
|
-
// pun read-only. Menyebut repoDir di sini akan mengundang agen menyentuh working tree utama.
|
|
5699
|
-
`Aturan tulis: kamu HANYA boleh menulis di ${ctx.worktree ? `worktree sesi ini (\`${ctx.worktree}\`)` : "direktori kerja sesi ini (worktree-mu)"}. SEMUA checkout project di atas \u2014 termasuk milik project utama \u2014 bersifat READ-ONLY: baca sepuasnya, JANGAN menulis, JANGAN commit di sana, JANGAN menjalankan perintah yang mengubah isinya.`,
|
|
5700
|
-
crossAuditLogGuide(ctx.apiUrl),
|
|
5701
|
-
CROSS_AUDIT_FOCUS
|
|
5702
|
-
].filter(Boolean);
|
|
5703
|
-
if (mode === "live") {
|
|
5704
|
-
return [
|
|
5705
|
-
...head,
|
|
5706
|
-
"Ini sesi TANYA-JAWAB: manusia menonton terminal ini dan akan bertanya. Jawab dengan bukti (kutipan kode + baris log beserta waktunya), ringkas dan langsung. Tak ada fase, tak ada dokumen, tak ada commit \u2014 kalau temuannya layak ditindaklanjuti, sarankan membuat backlog audit lintas."
|
|
5707
|
-
].join("\n\n");
|
|
5708
|
-
}
|
|
5709
|
-
const spec = ctx.spec;
|
|
5710
|
-
const slug = spec.title.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
5711
|
-
const detail = spec.payload ? `
|
|
5712
|
-
Detail: ${JSON.stringify(spec.payload)}` : "";
|
|
5713
|
-
return [
|
|
5714
|
-
...head,
|
|
5715
|
-
phaseInstruction(PIPELINES["cross-audit"]),
|
|
5716
|
-
`Fase Audit: telusuri akar masalah lintas project (log + kode kedua sisi). Fase Laporan: tulis DOKUMEN AUDIT ke Source of Truth project utama \`internal/docs/research/audit-${spec.id.toLowerCase()}-${slug}.md\` (ikuti konvensi audit yang ada), tautkan di \`internal/docs/README.md\`, memuat: keluhan/pertanyaan, peta integrasi yang diaudit, temuan dengan BUKTI dari tiap project (kutipan kode + baris timeline beserta waktunya), apakah issue terdefinisi baik, dan rekomendasi tindak lanjut \u2014 sebut project mana yang harus ditindaklanjuti. JANGAN menulis perbaikan kode.`,
|
|
5717
|
-
// SPEC-340 · ADR-0076 · cross-audit berdokumen memakai kontrak rekomendasi yang sama dgn audit-only.
|
|
5718
|
-
ESCALATION_CONTRACT,
|
|
5719
|
-
AUTONOMY_CLAUSE,
|
|
5720
|
-
skillInstruction(PIPELINES["cross-audit"]),
|
|
5721
|
-
`Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${ctx.branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
|
|
5722
|
-
`Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
|
|
5723
|
-
Judul: ${spec.title}
|
|
5724
|
-
Objective: ${spec.objective}${detail}`
|
|
5725
|
-
].filter(Boolean).join("\n\n");
|
|
5726
|
-
}
|
|
5727
|
-
var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, PHASE_SKILLS, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE, projectLine, crossAuditLogGuide, CROSS_AUDIT_FOCUS;
|
|
5777
|
+
var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, PHASE_SKILLS, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
|
|
5728
5778
|
var init_prompt = __esm({
|
|
5729
5779
|
"../runner/src/prompt.ts"() {
|
|
5730
5780
|
"use strict";
|
|
@@ -5741,7 +5791,6 @@ var init_prompt = __esm({
|
|
|
5741
5791
|
breakdown: ["Analisis", "Breakdown"],
|
|
5742
5792
|
// SPEC-337 · ADR-0075 · audit lintas project: fase & stage-map identik audit-only, scope-nya
|
|
5743
5793
|
// yang berbeda (project utama + tetangga ProjectLink).
|
|
5744
|
-
"cross-audit": ["Audit", "Laporan"],
|
|
5745
5794
|
// SPEC-407 · ADR-0089 · backlog goal: tak ada fase perencanaan sama sekali. `Goal` = kerjakan,
|
|
5746
5795
|
// `Verifikasi` = buktikan. Kedua nama unik lintas PIPELINES — syarat peta REACHED di server,
|
|
5747
5796
|
// yang berkunci nama fase saja.
|
|
@@ -5836,27 +5885,6 @@ ${lines2.join("\n")}` : "";
|
|
|
5836
5885
|
"- Objective: kunci SATU MVP objective yang terukur dari hasil brainstorm, tulis ringkas di docs.",
|
|
5837
5886
|
"- Doc index: tulis SELURUH internal/docs/** dari ide+objective+jawaban, mengikuti STANDAR DOCS di bawah \u2014 entrypoints, product, business, requirements (+EARS dari perilaku yang diinginkan), research, architecture (stack/data-model/api-contract/nfr), adr awal (Status accepted), design-system/frontend bila ada UI, operations, security, plus README index bernomor + CLAUDE.md + AGENTS.md + Stop hook. Lengkap dan spesifik terhadap ide ini, BUKAN kerangka."
|
|
5838
5887
|
].join("\n");
|
|
5839
|
-
projectLine = (p, primary) => {
|
|
5840
|
-
const path = p.repoDir ?? "(tak ada checkout lokal di mesin ini \u2014 audit project ini dari log & docs saja)";
|
|
5841
|
-
const head = primary ? `- ${p.id} \xB7 ${p.name} \xB7 PROJECT UTAMA (worktree kamu)` : `- ${p.id} \xB7 ${p.name}`;
|
|
5842
|
-
return [
|
|
5843
|
-
head,
|
|
5844
|
-
` stack: ${p.stack || "\u2014"}`,
|
|
5845
|
-
` path: ${path}`,
|
|
5846
|
-
...p.relation ? [` relasi: ${p.relation}`] : [],
|
|
5847
|
-
...p.note ? [` catatan integrasi: ${p.note}`] : []
|
|
5848
|
-
].join("\n");
|
|
5849
|
-
};
|
|
5850
|
-
crossAuditLogGuide = (apiUrl) => [
|
|
5851
|
-
`Menarik log: hanoman memberi sesi ini kunci baca ber-scope di env \`$HANOMAN_AUDIT_KEY\` (URL di \`$HANOMAN_AUDIT_URL\`, yaitu ${apiUrl}). Kunci ini HANYA membaca error project di atas, dan mati saat sesi ini berakhir. Panggil berkali-kali sesuai kebutuhan \u2014 jangan puas dengan sekali tarik:`,
|
|
5852
|
-
"```bash",
|
|
5853
|
-
`curl -s -H "X-Hanoman-Audit-Key: $HANOMAN_AUDIT_KEY" "$HANOMAN_AUDIT_URL/logs?since=24h"`,
|
|
5854
|
-
`curl -s -H "X-Hanoman-Audit-Key: $HANOMAN_AUDIT_KEY" "$HANOMAN_AUDIT_URL/logs?since=7d&environment=production&q=timeout"`,
|
|
5855
|
-
`curl -s -H "X-Hanoman-Audit-Key: $HANOMAN_AUDIT_KEY" "$HANOMAN_AUDIT_URL/logs/<groupId>" # detail + stack`,
|
|
5856
|
-
"```",
|
|
5857
|
-
`Bentuk jawabannya: \`timeline\` = error SEMUA project di atas, TERCAMPUR & terurut waktu \u2014 di situlah korelasi lintas project terlihat (error di satu sisi tepat sesudah kegagalan di sisi lain). \`groups\` = agregat berulang. Filter: \`since\`/\`until\` (\`24h\`|\`7d\`|ISO), \`environment\`, \`q\`, \`projects\`, \`limit\`. Project yang tak punya data error: katakan itu terang-terangan lalu bandingkan kontraknya di level kode \u2014 JANGAN mengarang log.`
|
|
5858
|
-
].join("\n");
|
|
5859
|
-
CROSS_AUDIT_FOCUS = "Fokus audit lintas: (1) kontrak API yang bergeser antara pemanggil & penyedia (path, bentuk payload, kode status, header auth); (2) versi paket/SDK yang tertinggal di satu sisi; (3) error yang BERKORELASI WAKTU di dua project; (4) environment/release yang tak sejalan antar sisi; (5) asumsi auth, format tanggal/uang, retry & timeout yang berbeda. Setiap temuan harus bersandar pada bukti dari KEDUA sisi \u2014 kutipan kode/kontrak dan/atau baris timeline, lengkap dengan waktunya.";
|
|
5860
5888
|
}
|
|
5861
5889
|
});
|
|
5862
5890
|
|
|
@@ -6300,12 +6328,16 @@ function planComplete(worktree, specId) {
|
|
|
6300
6328
|
}
|
|
6301
6329
|
return true;
|
|
6302
6330
|
}
|
|
6331
|
+
function sessionComplete(phases, worktree, specId) {
|
|
6332
|
+
if (!phasesComplete(phases)) return false;
|
|
6333
|
+
return specId ? planComplete(worktree, specId) : true;
|
|
6334
|
+
}
|
|
6303
6335
|
function stageForRun(phases, worktree, specId) {
|
|
6304
6336
|
const s = stageFor(phases);
|
|
6305
6337
|
if (s === "done" && !planComplete(worktree, specId)) return "executing";
|
|
6306
6338
|
return s;
|
|
6307
6339
|
}
|
|
6308
|
-
var phaseFilePath, decisionFilePath, REACHED;
|
|
6340
|
+
var phaseFilePath, decisionFilePath, REACHED, phasesComplete;
|
|
6309
6341
|
var init_session_phases = __esm({
|
|
6310
6342
|
"src/services/session-phases.ts"() {
|
|
6311
6343
|
"use strict";
|
|
@@ -6325,6 +6357,7 @@ var init_session_phases = __esm({
|
|
|
6325
6357
|
Goal: "executing",
|
|
6326
6358
|
Verifikasi: "done"
|
|
6327
6359
|
};
|
|
6360
|
+
phasesComplete = (phases) => phases.length > 0 && phases.every((p) => p.state === "done" || p.state === "skipped");
|
|
6328
6361
|
}
|
|
6329
6362
|
});
|
|
6330
6363
|
|
|
@@ -6412,21 +6445,7 @@ function listPanes() {
|
|
|
6412
6445
|
throw e;
|
|
6413
6446
|
}
|
|
6414
6447
|
return out3.split("\n").filter(Boolean).flatMap((line) => {
|
|
6415
|
-
const [
|
|
6416
|
-
n,
|
|
6417
|
-
projectId,
|
|
6418
|
-
specId,
|
|
6419
|
-
flow,
|
|
6420
|
-
phaseFile,
|
|
6421
|
-
cwd,
|
|
6422
|
-
dead,
|
|
6423
|
-
code,
|
|
6424
|
-
decisionFile,
|
|
6425
|
-
branch,
|
|
6426
|
-
agent,
|
|
6427
|
-
auditKey,
|
|
6428
|
-
auditProjects
|
|
6429
|
-
] = line.split(" ");
|
|
6448
|
+
const [n, projectId, specId, flow, phaseFile, cwd, dead, code, decisionFile, branch, agent] = line.split(" ");
|
|
6430
6449
|
if (!n?.startsWith(PREFIX)) return [];
|
|
6431
6450
|
const exited = dead === "1";
|
|
6432
6451
|
return [{
|
|
@@ -6439,9 +6458,6 @@ function listPanes() {
|
|
|
6439
6458
|
exited,
|
|
6440
6459
|
code: Number(code) || 0,
|
|
6441
6460
|
decisionFile: decisionFile || void 0,
|
|
6442
|
-
// SPEC-337 · kunci audit + scope-nya (internal; tak pernah keluar lewat listSessions).
|
|
6443
|
-
auditKey: auditKey || void 0,
|
|
6444
|
-
auditProjects: auditProjects || void 0,
|
|
6445
6461
|
// SPEC-230 · branch integrasi sesi project-level (PRD: prd/<slug>). Kosong = tak ada.
|
|
6446
6462
|
branch: branch || void 0,
|
|
6447
6463
|
// SPEC-196 · sesi hidup dengan marker keputusan terisi = menunggu manusia.
|
|
@@ -6457,7 +6473,6 @@ function registerSessionHooks(h) {
|
|
|
6457
6473
|
function sessionKind(o, projectId, cwd) {
|
|
6458
6474
|
if (o.specId) return "spec";
|
|
6459
6475
|
if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
|
|
6460
|
-
if (o.id.startsWith("xaudit-")) return "cross-audit";
|
|
6461
6476
|
if (projectId.startsWith("vps")) return "vps";
|
|
6462
6477
|
if (o.command) return "shell";
|
|
6463
6478
|
if (cwd.includes("/.worktrees/")) return "worktree";
|
|
@@ -6471,13 +6486,6 @@ function captureTranscript(id) {
|
|
|
6471
6486
|
return null;
|
|
6472
6487
|
}
|
|
6473
6488
|
}
|
|
6474
|
-
function auditSessionScope(key) {
|
|
6475
|
-
if (!key) return null;
|
|
6476
|
-
const p = listPanes().find((x) => x.auditKey === key && !x.exited);
|
|
6477
|
-
if (!p) return null;
|
|
6478
|
-
const scope = (p.auditProjects ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
6479
|
-
return scope.length ? scope : null;
|
|
6480
|
-
}
|
|
6481
6489
|
function createSession(projectId, cwd, opts = {}) {
|
|
6482
6490
|
const id = opts.id ?? idFor(opts.specId);
|
|
6483
6491
|
const existing = getSession(id);
|
|
@@ -6520,6 +6528,9 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6520
6528
|
argv = [sq(agentBin(agent)), promptArg, flags].filter(Boolean).join(" ");
|
|
6521
6529
|
}
|
|
6522
6530
|
const envPairs = [];
|
|
6531
|
+
if (!opts.command && agent === "claude") {
|
|
6532
|
+
for (const [k, v] of Object.entries(rootBypassEnv())) envPairs.push(`${k}=${sq(v)}`);
|
|
6533
|
+
}
|
|
6523
6534
|
if (opts.phaseFile) {
|
|
6524
6535
|
mkdirSync3(dirname4(opts.phaseFile), { recursive: true });
|
|
6525
6536
|
envPairs.push(`HANOMAN_PHASE_FILE=${sq(opts.phaseFile)}`);
|
|
@@ -6590,10 +6601,6 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6590
6601
|
tmux("set-option", "-t", name(id), "@hanoman_agent", agent);
|
|
6591
6602
|
if (opts.phaseFile) tmux("set-option", "-t", name(id), "@hanoman_phase_file", opts.phaseFile);
|
|
6592
6603
|
if (opts.decisionFile) tmux("set-option", "-t", name(id), "@hanoman_decision_file", opts.decisionFile);
|
|
6593
|
-
if (opts.audit) {
|
|
6594
|
-
tmux("set-option", "-t", name(id), "@hanoman_audit_key", opts.audit.key);
|
|
6595
|
-
tmux("set-option", "-t", name(id), "@hanoman_audit_projects", opts.audit.projects.join(","));
|
|
6596
|
-
}
|
|
6597
6604
|
if (opts.goal && !opts.command) void armGoalInTui(id, opts.goal, { agent }).catch(() => {
|
|
6598
6605
|
});
|
|
6599
6606
|
emitBirth({
|
|
@@ -6610,6 +6617,29 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6610
6617
|
});
|
|
6611
6618
|
return { id, projectId, specId: opts.specId, flow: opts.flow, cwd, branch: opts.branch, exited: false, decision: false, agent };
|
|
6612
6619
|
}
|
|
6620
|
+
function capturePane(id, lines2 = 200) {
|
|
6621
|
+
try {
|
|
6622
|
+
return tmux("capture-pane", "-p", "-J", "-S", `-${lines2}`, "-t", name(id));
|
|
6623
|
+
} catch {
|
|
6624
|
+
return "";
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
async function sendToPane(id, text, chunkMs = 50) {
|
|
6628
|
+
const p = getSession(id);
|
|
6629
|
+
if (!p || p.exited) return false;
|
|
6630
|
+
const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
6631
|
+
if (!line) return false;
|
|
6632
|
+
try {
|
|
6633
|
+
for (const chunk of goalChunks(line)) {
|
|
6634
|
+
tmux("send-keys", "-t", name(id), "-l", chunk);
|
|
6635
|
+
await sleep(chunkMs);
|
|
6636
|
+
}
|
|
6637
|
+
tmux("send-keys", "-t", name(id), "Enter");
|
|
6638
|
+
return true;
|
|
6639
|
+
} catch {
|
|
6640
|
+
return false;
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6613
6643
|
async function armGoalInTui(id, condition, o = {}) {
|
|
6614
6644
|
const pollMs = o.pollMs ?? 500, readyTries = o.readyTries ?? 20;
|
|
6615
6645
|
const settleMs = o.settleMs ?? 1200, verifyTries = o.verifyTries ?? 12;
|
|
@@ -6698,10 +6728,11 @@ function end(id, code) {
|
|
|
6698
6728
|
function pollPhases(p, a) {
|
|
6699
6729
|
if (!p.flow || !p.phaseFile) return;
|
|
6700
6730
|
const phases = readPhases(p.phaseFile, p.flow);
|
|
6701
|
-
const
|
|
6731
|
+
const complete = sessionComplete(phases, p.cwd, p.specId);
|
|
6732
|
+
const json = phaseKey(phases, complete);
|
|
6702
6733
|
if (json === a.lastPhases) return;
|
|
6703
6734
|
a.lastPhases = json;
|
|
6704
|
-
broadcast(a, { t: "phase", phases });
|
|
6735
|
+
broadcast(a, { t: "phase", phases, complete });
|
|
6705
6736
|
}
|
|
6706
6737
|
function startPoll() {
|
|
6707
6738
|
if (poll) return;
|
|
@@ -6742,10 +6773,11 @@ function attach(id, c) {
|
|
|
6742
6773
|
const a = attached.get(id) ?? open(id);
|
|
6743
6774
|
a.clients.add(c);
|
|
6744
6775
|
if (a.scrollback) c.send(frame({ t: "data", d: a.scrollback }));
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6776
|
+
if (p.flow && p.phaseFile) {
|
|
6777
|
+
const phases = readPhases(p.phaseFile, p.flow);
|
|
6778
|
+
const complete = sessionComplete(phases, p.cwd, p.specId);
|
|
6779
|
+
a.lastPhases = phaseKey(phases, complete);
|
|
6780
|
+
c.send(frame({ t: "phase", phases, complete }));
|
|
6749
6781
|
}
|
|
6750
6782
|
}
|
|
6751
6783
|
function writeTo(id, d) {
|
|
@@ -6782,7 +6814,7 @@ function spawnPty(...args) {
|
|
|
6782
6814
|
);
|
|
6783
6815
|
}
|
|
6784
6816
|
}
|
|
6785
|
-
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, frame, name, promptFilePath, goalGatePath, goalStatePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, sleep, paneText, GOAL_ARMED_MARKERS, goalArmed, poll, detach;
|
|
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;
|
|
6786
6818
|
var init_pty = __esm({
|
|
6787
6819
|
"src/services/pty.ts"() {
|
|
6788
6820
|
"use strict";
|
|
@@ -6806,6 +6838,7 @@ var init_pty = __esm({
|
|
|
6806
6838
|
shellBin = () => effectiveStr("HANOMAN_SHELL") ?? process.env.SHELL ?? "/bin/bash";
|
|
6807
6839
|
codexBin = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
|
|
6808
6840
|
agentBin = (agent) => agent === "codex" ? codexBin() : claudeBin();
|
|
6841
|
+
rootBypassEnv = (uid = process.getuid?.()) => uid === 0 ? { IS_SANDBOX: "1" } : {};
|
|
6809
6842
|
frame = (f) => JSON.stringify(f);
|
|
6810
6843
|
name = (id) => PREFIX + id;
|
|
6811
6844
|
promptFilePath = (id) => `${tmpdir()}/hanoman-prompts/${id}`;
|
|
@@ -6832,11 +6865,7 @@ var init_pty = __esm({
|
|
|
6832
6865
|
"#{pane_dead_status}",
|
|
6833
6866
|
"#{@hanoman_decision_file}",
|
|
6834
6867
|
"#{@hanoman_branch}",
|
|
6835
|
-
"#{@hanoman_agent}"
|
|
6836
|
-
// SPEC-337 · ADR-0075 · kunci audit lintas project + scope-nya. Hidup di tmux (bukan DB): selamat
|
|
6837
|
-
// dari restart API, mati bersama pane. TAK PERNAH ikut ke SessionInfo/API — lihat listSessions.
|
|
6838
|
-
"#{@hanoman_audit_key}",
|
|
6839
|
-
"#{@hanoman_audit_projects}"
|
|
6868
|
+
"#{@hanoman_agent}"
|
|
6840
6869
|
].join(" ");
|
|
6841
6870
|
listSessions = () => listPanes().map(({ id, projectId, specId, flow, cwd, exited, code, branch, decision, agent }) => ({
|
|
6842
6871
|
id,
|
|
@@ -6883,6 +6912,7 @@ var init_pty = __esm({
|
|
|
6883
6912
|
const text = paneText(id);
|
|
6884
6913
|
return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
|
|
6885
6914
|
};
|
|
6915
|
+
phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
|
|
6886
6916
|
detach = (id, c) => {
|
|
6887
6917
|
attached.get(id)?.clients.delete(c);
|
|
6888
6918
|
};
|
|
@@ -6917,11 +6947,9 @@ var init_outbox = __esm({
|
|
|
6917
6947
|
async function renameProjectCore(tx, oldId, newId) {
|
|
6918
6948
|
await tx.project.update({ where: { id: oldId }, data: { id: newId, updatedAt: /* @__PURE__ */ new Date() } });
|
|
6919
6949
|
const spec = await tx.spec.count({ where: { projectId: newId } });
|
|
6920
|
-
const errorGroup = await tx.errorGroup.count({ where: { projectId: newId } });
|
|
6921
6950
|
const ticket = await tx.ticket.count({ where: { projectId: newId } });
|
|
6922
6951
|
const notification = (await tx.notification.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
6923
6952
|
const sessionResult = (await tx.sessionResult.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
6924
|
-
const errorEvent = (await tx.errorEvent.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
6925
6953
|
const ticketAttachment = (await tx.ticketAttachment.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
6926
6954
|
const binding = await tx.localBinding.findUnique({ where: { projectId: oldId } });
|
|
6927
6955
|
let localBinding = 0;
|
|
@@ -6930,7 +6958,7 @@ async function renameProjectCore(tx, oldId, newId) {
|
|
|
6930
6958
|
await tx.localBinding.create({ data: { projectId: newId, repoDir: binding.repoDir, createdAt: binding.createdAt } });
|
|
6931
6959
|
localBinding = 1;
|
|
6932
6960
|
}
|
|
6933
|
-
return { spec,
|
|
6961
|
+
return { spec, ticket, notification, sessionResult, ticketAttachment, localBinding };
|
|
6934
6962
|
}
|
|
6935
6963
|
async function renameProject(oldId, newId) {
|
|
6936
6964
|
if (!zProjectId.safeParse(newId).success) return { ok: false, code: 400, error: "id baru tak sah (slug)" };
|
|
@@ -6959,11 +6987,9 @@ var init_rename_project = __esm({
|
|
|
6959
6987
|
RENAME_SEP = " ";
|
|
6960
6988
|
ZERO_AFFECTED = {
|
|
6961
6989
|
spec: 0,
|
|
6962
|
-
errorGroup: 0,
|
|
6963
6990
|
ticket: 0,
|
|
6964
6991
|
notification: 0,
|
|
6965
6992
|
sessionResult: 0,
|
|
6966
|
-
errorEvent: 0,
|
|
6967
6993
|
ticketAttachment: 0,
|
|
6968
6994
|
localBinding: 0
|
|
6969
6995
|
};
|
|
@@ -7117,23 +7143,22 @@ var init_sync = __esm({
|
|
|
7117
7143
|
"use strict";
|
|
7118
7144
|
init_db();
|
|
7119
7145
|
init_rename_project();
|
|
7120
|
-
SYNCED = ["project", "spec", "vps", "sessionResult", "
|
|
7146
|
+
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment"];
|
|
7121
7147
|
DELEGATE = {
|
|
7122
7148
|
project: prisma.project,
|
|
7123
7149
|
spec: prisma.spec,
|
|
7124
7150
|
vps: prisma.vps,
|
|
7125
7151
|
sessionResult: prisma.sessionResult,
|
|
7126
|
-
errorGroup: prisma.errorGroup,
|
|
7127
7152
|
ticket: prisma.ticket,
|
|
7128
7153
|
ticketAttachment: prisma.ticketAttachment
|
|
7129
7154
|
};
|
|
7130
7155
|
FIELDS = {
|
|
7131
7156
|
project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
|
|
7132
|
-
|
|
7157
|
+
// SPEC-408 · ADR-0090 · createdAt/startedAt ikut menyeberang — sejajar baseSha/headSha. Tanpa
|
|
7158
|
+
// ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
|
|
7159
|
+
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "createdAt", "startedAt", "updatedAt"],
|
|
7133
7160
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7134
7161
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
7135
|
-
// SPEC-268 · ADR-0066 · agregat grup error (ErrorEvent mentah tak disync).
|
|
7136
|
-
errorGroup: ["projectId", "fingerprint", "type", "message", "sampleStack", "environment", "status", "count", "firstSeenAt", "lastSeenAt", "specId", "updatedAt"],
|
|
7137
7162
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
7138
7163
|
// (kolom required @unique tanpa default); kunci plaintext tak pernah menyeberang.
|
|
7139
7164
|
ticket: ["projectId", "number", "category", "title", "detail", "reporterEmail", "status", "accessKeyHash", "specId", "createdAt", "updatedAt"],
|
|
@@ -7142,10 +7167,9 @@ var init_sync = __esm({
|
|
|
7142
7167
|
};
|
|
7143
7168
|
DATE_FIELDS = {
|
|
7144
7169
|
project: ["updatedAt"],
|
|
7145
|
-
spec: ["updatedAt"],
|
|
7170
|
+
spec: ["createdAt", "startedAt", "updatedAt"],
|
|
7146
7171
|
vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7147
7172
|
sessionResult: ["createdAt", "updatedAt"],
|
|
7148
|
-
errorGroup: ["firstSeenAt", "lastSeenAt", "updatedAt"],
|
|
7149
7173
|
ticket: ["createdAt", "updatedAt"],
|
|
7150
7174
|
ticketAttachment: ["createdAt", "updatedAt"]
|
|
7151
7175
|
};
|
|
@@ -9314,7 +9338,7 @@ var require_extension = __commonJS({
|
|
|
9314
9338
|
if (dest[name2] === void 0) dest[name2] = [elem];
|
|
9315
9339
|
else dest[name2].push(elem);
|
|
9316
9340
|
}
|
|
9317
|
-
function
|
|
9341
|
+
function parse2(header) {
|
|
9318
9342
|
const offers = /* @__PURE__ */ Object.create(null);
|
|
9319
9343
|
let params = /* @__PURE__ */ Object.create(null);
|
|
9320
9344
|
let mustUnescape = false;
|
|
@@ -9454,7 +9478,7 @@ var require_extension = __commonJS({
|
|
|
9454
9478
|
}).join(", ");
|
|
9455
9479
|
}).join(", ");
|
|
9456
9480
|
}
|
|
9457
|
-
module.exports = { format, parse:
|
|
9481
|
+
module.exports = { format, parse: parse2 };
|
|
9458
9482
|
}
|
|
9459
9483
|
});
|
|
9460
9484
|
|
|
@@ -9467,7 +9491,7 @@ var require_websocket = __commonJS({
|
|
|
9467
9491
|
var http = __require("http");
|
|
9468
9492
|
var net = __require("net");
|
|
9469
9493
|
var tls = __require("tls");
|
|
9470
|
-
var { randomBytes:
|
|
9494
|
+
var { randomBytes: randomBytes5, createHash: createHash5 } = __require("crypto");
|
|
9471
9495
|
var { Duplex, Readable } = __require("stream");
|
|
9472
9496
|
var { URL: URL2 } = __require("url");
|
|
9473
9497
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -9488,7 +9512,7 @@ var require_websocket = __commonJS({
|
|
|
9488
9512
|
var {
|
|
9489
9513
|
EventTarget: { addEventListener, removeEventListener }
|
|
9490
9514
|
} = require_event_target();
|
|
9491
|
-
var { format, parse:
|
|
9515
|
+
var { format, parse: parse2 } = require_extension();
|
|
9492
9516
|
var { toBuffer } = require_buffer_util();
|
|
9493
9517
|
var kAborted = Symbol("kAborted");
|
|
9494
9518
|
var protocolVersions = [8, 13];
|
|
@@ -10005,7 +10029,7 @@ var require_websocket = __commonJS({
|
|
|
10005
10029
|
}
|
|
10006
10030
|
}
|
|
10007
10031
|
const defaultPort = isSecure ? 443 : 80;
|
|
10008
|
-
const key =
|
|
10032
|
+
const key = randomBytes5(16).toString("base64");
|
|
10009
10033
|
const request = isSecure ? https.request : http.request;
|
|
10010
10034
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
10011
10035
|
let perMessageDeflate;
|
|
@@ -10135,7 +10159,7 @@ var require_websocket = __commonJS({
|
|
|
10135
10159
|
abortHandshake(websocket2, socket2, "Invalid Upgrade header");
|
|
10136
10160
|
return;
|
|
10137
10161
|
}
|
|
10138
|
-
const digest =
|
|
10162
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
10139
10163
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
10140
10164
|
abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
|
|
10141
10165
|
return;
|
|
@@ -10165,7 +10189,7 @@ var require_websocket = __commonJS({
|
|
|
10165
10189
|
}
|
|
10166
10190
|
let extensions;
|
|
10167
10191
|
try {
|
|
10168
|
-
extensions =
|
|
10192
|
+
extensions = parse2(secWebSocketExtensions);
|
|
10169
10193
|
} catch (err) {
|
|
10170
10194
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
10171
10195
|
abortHandshake(websocket2, socket2, message);
|
|
@@ -10457,7 +10481,7 @@ var require_subprotocol = __commonJS({
|
|
|
10457
10481
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
|
|
10458
10482
|
"use strict";
|
|
10459
10483
|
var { tokenChars } = require_validation();
|
|
10460
|
-
function
|
|
10484
|
+
function parse2(header) {
|
|
10461
10485
|
const protocols = /* @__PURE__ */ new Set();
|
|
10462
10486
|
let start = -1;
|
|
10463
10487
|
let end2 = -1;
|
|
@@ -10493,7 +10517,7 @@ var require_subprotocol = __commonJS({
|
|
|
10493
10517
|
protocols.add(protocol);
|
|
10494
10518
|
return protocols;
|
|
10495
10519
|
}
|
|
10496
|
-
module.exports = { parse:
|
|
10520
|
+
module.exports = { parse: parse2 };
|
|
10497
10521
|
}
|
|
10498
10522
|
});
|
|
10499
10523
|
|
|
@@ -10504,7 +10528,7 @@ var require_websocket_server = __commonJS({
|
|
|
10504
10528
|
var EventEmitter = __require("events");
|
|
10505
10529
|
var http = __require("http");
|
|
10506
10530
|
var { Duplex } = __require("stream");
|
|
10507
|
-
var { createHash:
|
|
10531
|
+
var { createHash: createHash5 } = __require("crypto");
|
|
10508
10532
|
var extension2 = require_extension();
|
|
10509
10533
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
10510
10534
|
var subprotocol2 = require_subprotocol();
|
|
@@ -10811,7 +10835,7 @@ var require_websocket_server = __commonJS({
|
|
|
10811
10835
|
);
|
|
10812
10836
|
}
|
|
10813
10837
|
if (this._state > RUNNING) return abortHandshake(socket2, 503);
|
|
10814
|
-
const digest =
|
|
10838
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
10815
10839
|
const headers = [
|
|
10816
10840
|
"HTTP/1.1 101 Switching Protocols",
|
|
10817
10841
|
"Upgrade: websocket",
|
|
@@ -11111,7 +11135,7 @@ function syncStatus() {
|
|
|
11111
11135
|
async function startSyncClient(base, token, tickMs2) {
|
|
11112
11136
|
started = true;
|
|
11113
11137
|
const transport = fetchTransport(base, token);
|
|
11114
|
-
const
|
|
11138
|
+
const tick4 = async () => {
|
|
11115
11139
|
try {
|
|
11116
11140
|
await syncOnce(transport);
|
|
11117
11141
|
} catch {
|
|
@@ -11122,13 +11146,13 @@ async function startSyncClient(base, token, tickMs2) {
|
|
|
11122
11146
|
const wsUrl = base.replace(/^http/, "ws").replace(/\/$/, "") + `/api/sync/ws?token=${encodeURIComponent(token)}`;
|
|
11123
11147
|
ws = new WebSocket2(wsUrl);
|
|
11124
11148
|
ws.on("open", () => {
|
|
11125
|
-
void
|
|
11149
|
+
void tick4();
|
|
11126
11150
|
});
|
|
11127
11151
|
ws.on("message", async (raw) => {
|
|
11128
11152
|
try {
|
|
11129
11153
|
const msg = JSON.parse(raw.toString());
|
|
11130
11154
|
if (msg.t !== "sync") return;
|
|
11131
|
-
if (!await applyFeedFrame(msg)) void
|
|
11155
|
+
if (!await applyFeedFrame(msg)) void tick4();
|
|
11132
11156
|
} catch {
|
|
11133
11157
|
}
|
|
11134
11158
|
});
|
|
@@ -11145,11 +11169,11 @@ async function startSyncClient(base, token, tickMs2) {
|
|
|
11145
11169
|
}
|
|
11146
11170
|
});
|
|
11147
11171
|
};
|
|
11148
|
-
await
|
|
11172
|
+
await tick4();
|
|
11149
11173
|
void connectWs();
|
|
11150
11174
|
const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
|
|
11151
11175
|
timer2 = setInterval(() => {
|
|
11152
|
-
void
|
|
11176
|
+
void tick4();
|
|
11153
11177
|
}, ms);
|
|
11154
11178
|
timer2.unref?.();
|
|
11155
11179
|
}
|
|
@@ -13682,17 +13706,17 @@ var require_stream_wormhole = __commonJS({
|
|
|
13682
13706
|
"../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
|
|
13683
13707
|
"use strict";
|
|
13684
13708
|
module.exports = (stream, throwError) => {
|
|
13685
|
-
return new Promise((
|
|
13709
|
+
return new Promise((resolve14, reject) => {
|
|
13686
13710
|
if (typeof stream.resume !== "function") {
|
|
13687
|
-
return
|
|
13711
|
+
return resolve14();
|
|
13688
13712
|
}
|
|
13689
13713
|
stream.unpipe && stream.unpipe();
|
|
13690
13714
|
stream.resume();
|
|
13691
13715
|
if (stream._readableState && stream._readableState.ended) {
|
|
13692
|
-
return
|
|
13716
|
+
return resolve14();
|
|
13693
13717
|
}
|
|
13694
13718
|
if (!stream.readable || stream.destroyed) {
|
|
13695
|
-
return
|
|
13719
|
+
return resolve14();
|
|
13696
13720
|
}
|
|
13697
13721
|
function cleanup() {
|
|
13698
13722
|
stream.removeListener("end", onEnd);
|
|
@@ -13701,14 +13725,14 @@ var require_stream_wormhole = __commonJS({
|
|
|
13701
13725
|
}
|
|
13702
13726
|
function onEnd() {
|
|
13703
13727
|
cleanup();
|
|
13704
|
-
|
|
13728
|
+
resolve14();
|
|
13705
13729
|
}
|
|
13706
13730
|
function onError(err) {
|
|
13707
13731
|
cleanup();
|
|
13708
13732
|
if (throwError) {
|
|
13709
13733
|
reject(err);
|
|
13710
13734
|
} else {
|
|
13711
|
-
|
|
13735
|
+
resolve14();
|
|
13712
13736
|
}
|
|
13713
13737
|
}
|
|
13714
13738
|
stream.on("end", onEnd);
|
|
@@ -13924,7 +13948,7 @@ var require_secure_json_parse = __commonJS({
|
|
|
13924
13948
|
}
|
|
13925
13949
|
return obj;
|
|
13926
13950
|
}
|
|
13927
|
-
function
|
|
13951
|
+
function parse2(text, reviver, options2) {
|
|
13928
13952
|
const stackTraceLimit = Error.stackTraceLimit;
|
|
13929
13953
|
Error.stackTraceLimit = 0;
|
|
13930
13954
|
try {
|
|
@@ -13944,9 +13968,9 @@ var require_secure_json_parse = __commonJS({
|
|
|
13944
13968
|
Error.stackTraceLimit = stackTraceLimit;
|
|
13945
13969
|
}
|
|
13946
13970
|
}
|
|
13947
|
-
module.exports =
|
|
13948
|
-
module.exports.default =
|
|
13949
|
-
module.exports.parse =
|
|
13971
|
+
module.exports = parse2;
|
|
13972
|
+
module.exports.default = parse2;
|
|
13973
|
+
module.exports.parse = parse2;
|
|
13950
13974
|
module.exports.safeParse = safeParse;
|
|
13951
13975
|
module.exports.scan = filter;
|
|
13952
13976
|
}
|
|
@@ -13960,7 +13984,7 @@ var require_multipart2 = __commonJS({
|
|
|
13960
13984
|
var os = __require("node:os");
|
|
13961
13985
|
var fp = require_plugin();
|
|
13962
13986
|
var { createWriteStream } = __require("node:fs");
|
|
13963
|
-
var { unlink:
|
|
13987
|
+
var { unlink: unlink3 } = __require("node:fs/promises");
|
|
13964
13988
|
var path = __require("node:path");
|
|
13965
13989
|
var { generateId } = require_generateId();
|
|
13966
13990
|
var util2 = __require("node:util");
|
|
@@ -14143,16 +14167,16 @@ var require_multipart2 = __commonJS({
|
|
|
14143
14167
|
}
|
|
14144
14168
|
};
|
|
14145
14169
|
const parts = () => {
|
|
14146
|
-
return new Promise((
|
|
14170
|
+
return new Promise((resolve14, reject) => {
|
|
14147
14171
|
handle((val) => {
|
|
14148
14172
|
if (val instanceof Error) {
|
|
14149
14173
|
if (val.message === "Unexpected end of multipart data") {
|
|
14150
|
-
|
|
14174
|
+
resolve14(null);
|
|
14151
14175
|
} else {
|
|
14152
14176
|
reject(val);
|
|
14153
14177
|
}
|
|
14154
14178
|
} else {
|
|
14155
|
-
|
|
14179
|
+
resolve14(val);
|
|
14156
14180
|
}
|
|
14157
14181
|
});
|
|
14158
14182
|
});
|
|
@@ -14355,7 +14379,7 @@ var require_multipart2 = __commonJS({
|
|
|
14355
14379
|
for (let i = 0; i < this.tmpUploads.length; ++i) {
|
|
14356
14380
|
const filepath = this.tmpUploads[i];
|
|
14357
14381
|
try {
|
|
14358
|
-
await
|
|
14382
|
+
await unlink3(filepath);
|
|
14359
14383
|
} catch (error) {
|
|
14360
14384
|
this.log.error(error, "Could not delete file");
|
|
14361
14385
|
}
|
|
@@ -14419,7 +14443,7 @@ import websocket from "@fastify/websocket";
|
|
|
14419
14443
|
import cookie from "@fastify/cookie";
|
|
14420
14444
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
14421
14445
|
import { dirname as dirname12 } from "node:path";
|
|
14422
|
-
import { existsSync as
|
|
14446
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
14423
14447
|
|
|
14424
14448
|
// src/web-dir.ts
|
|
14425
14449
|
import { resolve } from "node:path";
|
|
@@ -14442,41 +14466,6 @@ async function health_default(app2) {
|
|
|
14442
14466
|
|
|
14443
14467
|
// src/routes/projects.ts
|
|
14444
14468
|
init_src();
|
|
14445
|
-
|
|
14446
|
-
// src/services/project-links.ts
|
|
14447
|
-
init_db();
|
|
14448
|
-
var linksOf = (projectId) => prisma.projectLink.findMany({
|
|
14449
|
-
where: { OR: [{ fromProjectId: projectId }, { toProjectId: projectId }] },
|
|
14450
|
-
orderBy: { createdAt: "asc" }
|
|
14451
|
-
});
|
|
14452
|
-
function neighborIds(projectId, links) {
|
|
14453
|
-
const out3 = /* @__PURE__ */ new Set();
|
|
14454
|
-
for (const l of links) {
|
|
14455
|
-
const other = l.fromProjectId === projectId ? l.toProjectId : l.fromProjectId;
|
|
14456
|
-
if (other !== projectId) out3.add(other);
|
|
14457
|
-
}
|
|
14458
|
-
return [...out3];
|
|
14459
|
-
}
|
|
14460
|
-
async function linkViews(projectId, links) {
|
|
14461
|
-
const ids = neighborIds(projectId, links);
|
|
14462
|
-
const rows = await prisma.project.findMany({ where: { id: { in: ids } }, select: { id: true, name: true } });
|
|
14463
|
-
const names = new Map(rows.map((p) => [p.id, p.name]));
|
|
14464
|
-
return links.map((l) => {
|
|
14465
|
-
const direction = l.fromProjectId === projectId ? "keluar" : "masuk";
|
|
14466
|
-
const otherId = direction === "keluar" ? l.toProjectId : l.fromProjectId;
|
|
14467
|
-
return {
|
|
14468
|
-
id: l.id,
|
|
14469
|
-
fromProjectId: l.fromProjectId,
|
|
14470
|
-
toProjectId: l.toProjectId,
|
|
14471
|
-
kind: l.kind,
|
|
14472
|
-
note: l.note,
|
|
14473
|
-
direction,
|
|
14474
|
-
other: { id: otherId, name: names.get(otherId) ?? otherId }
|
|
14475
|
-
};
|
|
14476
|
-
});
|
|
14477
|
-
}
|
|
14478
|
-
|
|
14479
|
-
// src/routes/projects.ts
|
|
14480
14469
|
init_db();
|
|
14481
14470
|
init_rename_project();
|
|
14482
14471
|
|
|
@@ -14632,13 +14621,12 @@ async function toProjectView(p, sessions) {
|
|
|
14632
14621
|
session,
|
|
14633
14622
|
activity: session.status === "running" ? `running \xB7 ${session.flow ?? "sesi"}` : "idle",
|
|
14634
14623
|
commit,
|
|
14635
|
-
// SPEC-249 · error monitoring: ekspos status + hint prefix. ingestKeyHash TAK PERNAH ke client.
|
|
14636
|
-
monitoringEnabled: !!p.ingestKeyHash,
|
|
14637
|
-
ingestKeyPrefix: p.ingestKeyPrefix ?? null,
|
|
14638
14624
|
// SPEC-253 · Help Center publik aktif.
|
|
14639
14625
|
helpEnabled: p.helpEnabled,
|
|
14640
14626
|
// SPEC-294 · opt-in scheduler otonom (lokal per-instance).
|
|
14641
|
-
schedulerOptIn: p.schedulerOptIn
|
|
14627
|
+
schedulerOptIn: p.schedulerOptIn,
|
|
14628
|
+
// SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal per-instance, cermin schedulerOptIn).
|
|
14629
|
+
leadOptIn: p.leadOptIn
|
|
14642
14630
|
};
|
|
14643
14631
|
}
|
|
14644
14632
|
|
|
@@ -14692,25 +14680,6 @@ function paginate(items, page, limit) {
|
|
|
14692
14680
|
return { items: pageSize ? items.slice(start, start + pageSize) : items, total, page: p, pageSize };
|
|
14693
14681
|
}
|
|
14694
14682
|
|
|
14695
|
-
// src/services/ingest-key.ts
|
|
14696
|
-
import { randomBytes, createHash, timingSafeEqual } from "node:crypto";
|
|
14697
|
-
function hashKey(key) {
|
|
14698
|
-
return createHash("sha256").update(key).digest("hex");
|
|
14699
|
-
}
|
|
14700
|
-
function generateIngestKey() {
|
|
14701
|
-
const key = "hnm_ing_" + randomBytes(24).toString("hex");
|
|
14702
|
-
return { key, hash: hashKey(key), prefix: key.slice(0, 16) };
|
|
14703
|
-
}
|
|
14704
|
-
function verifyKey(key, hash2) {
|
|
14705
|
-
if (!key || !hash2) return false;
|
|
14706
|
-
const a = Buffer.from(hashKey(key), "hex");
|
|
14707
|
-
const b = Buffer.from(hash2, "hex");
|
|
14708
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
14709
|
-
}
|
|
14710
|
-
function dsnUrl(slug, key, base) {
|
|
14711
|
-
return `${base.replace(/\/$/, "")}/api/ingest/${encodeURIComponent(slug)}?key=${key}`;
|
|
14712
|
-
}
|
|
14713
|
-
|
|
14714
14683
|
// src/routes/projects.ts
|
|
14715
14684
|
init_src2();
|
|
14716
14685
|
async function projects_default(app2) {
|
|
@@ -14777,8 +14746,6 @@ async function projects_default(app2) {
|
|
|
14777
14746
|
const base = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
14778
14747
|
return {
|
|
14779
14748
|
id: newId,
|
|
14780
|
-
// Plaintext key DSN tak tersimpan; kembalikan path DSN baru (tanpa key) sbg hint bila monitoring aktif.
|
|
14781
|
-
dsnUrl: p?.ingestKeyHash ? `${base.replace(/\/$/, "")}/api/ingest/${encodeURIComponent(newId)}` : void 0,
|
|
14782
14749
|
helpUrl: p?.helpEnabled ? `${base}/help/${encodeURIComponent(newId)}` : void 0,
|
|
14783
14750
|
affected: r.affected
|
|
14784
14751
|
};
|
|
@@ -14798,30 +14765,6 @@ async function projects_default(app2) {
|
|
|
14798
14765
|
const repoDir = await resolveRepoDir(id);
|
|
14799
14766
|
return { branches: await listRepoBranches(repoDir), remotes: await listRepoRemoteBranches(repoDir) };
|
|
14800
14767
|
});
|
|
14801
|
-
app2.get("/projects/:id/ingest-key", async (req, reply) => {
|
|
14802
|
-
const { id } = req.params;
|
|
14803
|
-
const p = await prisma.project.findUnique({ where: { id } });
|
|
14804
|
-
if (!p) return reply.code(404).send({ error: "not found" });
|
|
14805
|
-
return { enabled: !!p.ingestKeyHash, prefix: p.ingestKeyPrefix ?? null };
|
|
14806
|
-
});
|
|
14807
|
-
app2.post("/projects/:id/ingest-key", async (req, reply) => {
|
|
14808
|
-
const { id } = req.params;
|
|
14809
|
-
const p = await prisma.project.findUnique({ where: { id } });
|
|
14810
|
-
if (!p) return reply.code(404).send({ error: "not found" });
|
|
14811
|
-
const { key, hash: hash2, prefix } = generateIngestKey();
|
|
14812
|
-
await prisma.project.update({ where: { id }, data: { ingestKeyHash: hash2, ingestKeyPrefix: prefix } });
|
|
14813
|
-
await notifySynced("project", id);
|
|
14814
|
-
const base = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
14815
|
-
return reply.code(201).send({ enabled: true, prefix, key, dsnUrl: dsnUrl(id, key, base) });
|
|
14816
|
-
});
|
|
14817
|
-
app2.delete("/projects/:id/ingest-key", async (req, reply) => {
|
|
14818
|
-
const { id } = req.params;
|
|
14819
|
-
const p = await prisma.project.findUnique({ where: { id } });
|
|
14820
|
-
if (!p) return reply.code(404).send({ error: "not found" });
|
|
14821
|
-
await prisma.project.update({ where: { id }, data: { ingestKeyHash: null, ingestKeyPrefix: null } });
|
|
14822
|
-
await notifySynced("project", id);
|
|
14823
|
-
return reply.code(204).send();
|
|
14824
|
-
});
|
|
14825
14768
|
app2.get("/projects/:id/help-center", async (req, reply) => {
|
|
14826
14769
|
const { id } = req.params;
|
|
14827
14770
|
const p = await prisma.project.findUnique({ where: { id } });
|
|
@@ -14846,35 +14789,6 @@ async function projects_default(app2) {
|
|
|
14846
14789
|
await notifySynced("project", id);
|
|
14847
14790
|
return reply.code(204).send();
|
|
14848
14791
|
});
|
|
14849
|
-
app2.get("/projects/:id/links", async (req, reply) => {
|
|
14850
|
-
const { id } = req.params;
|
|
14851
|
-
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
14852
|
-
return { links: await linkViews(id, await linksOf(id)) };
|
|
14853
|
-
});
|
|
14854
|
-
app2.post("/projects/:id/links", async (req, reply) => {
|
|
14855
|
-
const { id } = req.params;
|
|
14856
|
-
const parsed = zCreateLink.safeParse(req.body);
|
|
14857
|
-
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
14858
|
-
const { to, kind, note } = parsed.data;
|
|
14859
|
-
if (to === id) return reply.code(400).send({ error: "project tak bisa bergantung pada dirinya sendiri" });
|
|
14860
|
-
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
14861
|
-
if (!await prisma.project.findUnique({ where: { id: to } })) return reply.code(404).send({ error: `project "${to}" tak ada` });
|
|
14862
|
-
const existing = await prisma.projectLink.findUnique({
|
|
14863
|
-
where: { fromProjectId_toProjectId: { fromProjectId: id, toProjectId: to } }
|
|
14864
|
-
});
|
|
14865
|
-
if (existing) return reply.code(409).send({ error: `relasi ${id} \u2192 ${to} sudah ada` });
|
|
14866
|
-
const created = await prisma.projectLink.create({ data: { fromProjectId: id, toProjectId: to, kind, note: note ?? "" } });
|
|
14867
|
-
const [view7] = await linkViews(id, [created]);
|
|
14868
|
-
return reply.code(201).send(view7);
|
|
14869
|
-
});
|
|
14870
|
-
app2.delete("/projects/:id/links/:linkId", async (req, reply) => {
|
|
14871
|
-
const { id, linkId } = req.params;
|
|
14872
|
-
const link2 = await prisma.projectLink.findUnique({ where: { id: linkId } });
|
|
14873
|
-
if (!link2 || link2.fromProjectId !== id && link2.toProjectId !== id)
|
|
14874
|
-
return reply.code(404).send({ error: "not found" });
|
|
14875
|
-
await prisma.projectLink.delete({ where: { id: linkId } });
|
|
14876
|
-
return reply.code(204).send();
|
|
14877
|
-
});
|
|
14878
14792
|
}
|
|
14879
14793
|
|
|
14880
14794
|
// src/routes/specs.ts
|
|
@@ -15091,8 +15005,10 @@ var DEFAULT_SETTING = {
|
|
|
15091
15005
|
// SPEC-338 · ADR-0074 · model/effort codex
|
|
15092
15006
|
verifyScope: "changed",
|
|
15093
15007
|
// SPEC-376 · ADR-0080 · uji hanya yang berubah
|
|
15094
|
-
conflict: CONFLICT_DEFAULTS
|
|
15008
|
+
conflict: CONFLICT_DEFAULTS,
|
|
15095
15009
|
// SPEC-383 · ADR-0081 · default sesi konflik (opt-in, mati)
|
|
15010
|
+
lead: LEAD_DEFAULTS
|
|
15011
|
+
// SPEC-409 · ADR-0091 · hanoman-lead (master switch mati)
|
|
15096
15012
|
};
|
|
15097
15013
|
async function getSetting() {
|
|
15098
15014
|
const raw = (await prisma.setting.findUnique({ where: { id: 1 } }))?.data;
|
|
@@ -15425,22 +15341,22 @@ async function findAuditDoc(specId, sessions) {
|
|
|
15425
15341
|
return doc ? { dir: dir2, path: doc.path, live } : null;
|
|
15426
15342
|
}
|
|
15427
15343
|
async function readAuditDoc(specId, sessions = listSessions()) {
|
|
15428
|
-
const
|
|
15429
|
-
if (!
|
|
15430
|
-
const content = readDocFile(
|
|
15431
|
-
return content === null ? null : { path:
|
|
15344
|
+
const found = await findAuditDoc(specId, sessions);
|
|
15345
|
+
if (!found) return null;
|
|
15346
|
+
const content = readDocFile(found.dir, found.path);
|
|
15347
|
+
return content === null ? null : { path: found.path, content };
|
|
15432
15348
|
}
|
|
15433
15349
|
async function readEscalation(specId, sessions = listSessions()) {
|
|
15434
|
-
const
|
|
15435
|
-
if (!
|
|
15350
|
+
const found = await findAuditDoc(specId, sessions);
|
|
15351
|
+
if (!found) {
|
|
15436
15352
|
const live = sessions.some((s) => s.specId === specId && !s.exited && s.cwd);
|
|
15437
15353
|
return { escalation: null, docPath: null, live };
|
|
15438
15354
|
}
|
|
15439
|
-
const content = readDocFile(
|
|
15355
|
+
const content = readDocFile(found.dir, found.path);
|
|
15440
15356
|
return {
|
|
15441
15357
|
escalation: content === null ? null : parseEscalation(content),
|
|
15442
|
-
docPath:
|
|
15443
|
-
live:
|
|
15358
|
+
docPath: found.path,
|
|
15359
|
+
live: found.live
|
|
15444
15360
|
};
|
|
15445
15361
|
}
|
|
15446
15362
|
|
|
@@ -15478,8 +15394,8 @@ var escapeReplacements = {
|
|
|
15478
15394
|
"'": "'"
|
|
15479
15395
|
};
|
|
15480
15396
|
var getEscapeReplacement = (ch) => escapeReplacements[ch];
|
|
15481
|
-
function escape$1(html2,
|
|
15482
|
-
if (
|
|
15397
|
+
function escape$1(html2, encode) {
|
|
15398
|
+
if (encode) {
|
|
15483
15399
|
if (escapeTest.test(html2)) {
|
|
15484
15400
|
return html2.replace(escapeReplace, getEscapeReplacement);
|
|
15485
15401
|
}
|
|
@@ -17766,7 +17682,7 @@ function renderDocPdf(text, name2, meta) {
|
|
|
17766
17682
|
});
|
|
17767
17683
|
const chunks = [];
|
|
17768
17684
|
doc.on("data", (c) => chunks.push(c));
|
|
17769
|
-
const done = new Promise((
|
|
17685
|
+
const done = new Promise((resolve14) => doc.on("end", () => resolve14(Buffer.concat(chunks))));
|
|
17770
17686
|
doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
|
|
17771
17687
|
doc.moveDown(0.25);
|
|
17772
17688
|
doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
|
|
@@ -17814,6 +17730,27 @@ async function sendReviewDownload(reply, fmt, rf, a) {
|
|
|
17814
17730
|
});
|
|
17815
17731
|
}
|
|
17816
17732
|
|
|
17733
|
+
// src/services/date-range.ts
|
|
17734
|
+
var DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
17735
|
+
function dayStart(s) {
|
|
17736
|
+
if (!s || !DAY.test(s)) return null;
|
|
17737
|
+
const [y, m, d] = s.split("-").map(Number);
|
|
17738
|
+
const dt = new Date(y, m - 1, d, 0, 0, 0, 0);
|
|
17739
|
+
if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;
|
|
17740
|
+
return dt;
|
|
17741
|
+
}
|
|
17742
|
+
function dayEnd(s) {
|
|
17743
|
+
const start = dayStart(s);
|
|
17744
|
+
if (!start) return null;
|
|
17745
|
+
return new Date(start.getFullYear(), start.getMonth(), start.getDate(), 23, 59, 59, 999);
|
|
17746
|
+
}
|
|
17747
|
+
function inDayRange(at, from, to) {
|
|
17748
|
+
if (!from && !to) return true;
|
|
17749
|
+
if (!at) return false;
|
|
17750
|
+
const t = at.getTime();
|
|
17751
|
+
return (!from || t >= from.getTime()) && (!to || t <= to.getTime());
|
|
17752
|
+
}
|
|
17753
|
+
|
|
17817
17754
|
// src/services/live-specs.ts
|
|
17818
17755
|
init_db();
|
|
17819
17756
|
init_pty();
|
|
@@ -17847,14 +17784,6 @@ async function recordFailure(specId, title, projectId, reason) {
|
|
|
17847
17784
|
}).catch(() => {
|
|
17848
17785
|
});
|
|
17849
17786
|
}
|
|
17850
|
-
async function recordNewErrorGroup(groupId, projectId, projectName, type, message) {
|
|
17851
|
-
const short = message.length > 80 ? message.slice(0, 77) + "\u2026" : message;
|
|
17852
|
-
const title = `Error baru di "${projectName}": ${type}: ${short}`;
|
|
17853
|
-
await prisma.notification.create({
|
|
17854
|
-
data: { type: "error", key: `error:${groupId}`, projectId, title }
|
|
17855
|
-
}).catch(() => {
|
|
17856
|
-
});
|
|
17857
|
-
}
|
|
17858
17787
|
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
17859
17788
|
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
17860
17789
|
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
@@ -17863,6 +17792,12 @@ async function recordNewTicket(ticketId, projectId, projectName, category, title
|
|
|
17863
17792
|
}).catch(() => {
|
|
17864
17793
|
});
|
|
17865
17794
|
}
|
|
17795
|
+
async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
|
|
17796
|
+
await prisma.notification.create({
|
|
17797
|
+
data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
|
|
17798
|
+
}).catch(() => {
|
|
17799
|
+
});
|
|
17800
|
+
}
|
|
17866
17801
|
var awaiting = /* @__PURE__ */ new Set();
|
|
17867
17802
|
async function scanDecisions(read = liveDecisions) {
|
|
17868
17803
|
const next = /* @__PURE__ */ new Set();
|
|
@@ -17932,13 +17867,16 @@ function deriveSpecFields(source, payload, manualPriority) {
|
|
|
17932
17867
|
}
|
|
17933
17868
|
function filterSpecs(specs, f) {
|
|
17934
17869
|
const needle = (f.q ?? "").trim().toLowerCase();
|
|
17935
|
-
|
|
17870
|
+
const from = dayStart(f.from);
|
|
17871
|
+
const to = dayEnd(f.to);
|
|
17872
|
+
const byStarted = f.dateField === "started";
|
|
17873
|
+
return specs.filter((s) => (!f.stage || s.stage === f.stage) && (!f.priority || s.priority === f.priority) && (f.startable !== "true" || s.stage !== "done") && inDayRange(byStarted ? s.startedAt : s.createdAt, from, to) && (needle === "" || `${s.id} ${s.title} ${s.objective}`.toLowerCase().includes(needle)));
|
|
17936
17874
|
}
|
|
17937
17875
|
async function specs_default(app2) {
|
|
17938
17876
|
app2.get("/specs", async (req) => {
|
|
17939
|
-
const { project, source, q, stage, priority, startable, page, limit } = req.query;
|
|
17877
|
+
const { project, source, q, stage, priority, startable, dateField, from, to, page, limit } = req.query;
|
|
17940
17878
|
const overlaid = await liveSpecs({ project, source });
|
|
17941
|
-
return paginate(filterSpecs(overlaid, { q, stage, priority, startable }), page, limit);
|
|
17879
|
+
return paginate(filterSpecs(overlaid, { q, stage, priority, startable, dateField, from, to }), page, limit);
|
|
17942
17880
|
});
|
|
17943
17881
|
app2.post("/specs", async (req, reply) => {
|
|
17944
17882
|
const parsed = zCreateSpec.safeParse(req.body);
|
|
@@ -17964,7 +17902,7 @@ async function specs_default(app2) {
|
|
|
17964
17902
|
source: b.source,
|
|
17965
17903
|
stage: "brainstorming",
|
|
17966
17904
|
priority,
|
|
17967
|
-
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "
|
|
17905
|
+
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
|
|
17968
17906
|
objective,
|
|
17969
17907
|
payload: b.payload,
|
|
17970
17908
|
branchFrom: b.branchFrom ?? null
|
|
@@ -19303,70 +19241,8 @@ async function fs_default(app2) {
|
|
|
19303
19241
|
init_db();
|
|
19304
19242
|
init_src();
|
|
19305
19243
|
init_src2();
|
|
19306
|
-
import { existsSync as existsSync6 } from "node:fs";
|
|
19307
|
-
|
|
19308
|
-
// src/services/cross-audit.ts
|
|
19309
|
-
init_db();
|
|
19310
|
-
|
|
19311
|
-
// src/services/audit-scope.ts
|
|
19312
|
-
init_pty();
|
|
19313
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
19314
|
-
var AUDIT_KEY_HEADER = "x-hanoman-audit-key";
|
|
19315
|
-
var newAuditKey = () => `hnm_xa_${randomBytes2(16).toString("hex")}`;
|
|
19316
|
-
var auditApiUrl = () => `http://127.0.0.1:${process.env.PORT ?? 8787}/api/audit`;
|
|
19317
|
-
function auditScopeFromReq(req) {
|
|
19318
|
-
const raw = req.headers[AUDIT_KEY_HEADER];
|
|
19319
|
-
const key = Array.isArray(raw) ? raw[0] : raw;
|
|
19320
|
-
if (typeof key !== "string" || !key) return null;
|
|
19321
|
-
return auditSessionScope(key);
|
|
19322
|
-
}
|
|
19323
|
-
|
|
19324
|
-
// src/services/cross-audit.ts
|
|
19325
|
-
async function buildCrossAuditCtx(primaryId) {
|
|
19326
|
-
const primary = await prisma.project.findUnique({ where: { id: primaryId } });
|
|
19327
|
-
if (!primary) return null;
|
|
19328
|
-
const views = await linkViews(primaryId, await linksOf(primaryId));
|
|
19329
|
-
const rows = await prisma.project.findMany({ where: { id: { in: views.map((v) => v.other.id) } } });
|
|
19330
|
-
const byId2 = new Map(rows.map((p) => [p.id, p]));
|
|
19331
|
-
const neighbors = [];
|
|
19332
|
-
for (const v of views) {
|
|
19333
|
-
const p = byId2.get(v.other.id);
|
|
19334
|
-
if (!p) continue;
|
|
19335
|
-
const relation = v.direction === "keluar" ? `${primary.name} bergantung pada ${p.name} (${v.kind})` : `${p.name} bergantung pada ${primary.name} (${v.kind})`;
|
|
19336
|
-
neighbors.push({
|
|
19337
|
-
id: p.id,
|
|
19338
|
-
name: p.name,
|
|
19339
|
-
stack: p.stack,
|
|
19340
|
-
repoDir: await resolveRepoDir(p.id),
|
|
19341
|
-
// null = belum di-bind di mesin ini; prompt menandainya
|
|
19342
|
-
relation,
|
|
19343
|
-
note: v.note
|
|
19344
|
-
});
|
|
19345
|
-
}
|
|
19346
|
-
return {
|
|
19347
|
-
ctx: {
|
|
19348
|
-
primary: {
|
|
19349
|
-
id: primary.id,
|
|
19350
|
-
name: primary.name,
|
|
19351
|
-
stack: primary.stack,
|
|
19352
|
-
repoDir: await resolveRepoDir(primary.id)
|
|
19353
|
-
},
|
|
19354
|
-
neighbors,
|
|
19355
|
-
apiUrl: auditApiUrl()
|
|
19356
|
-
},
|
|
19357
|
-
scope: [primary.id, ...neighbors.map((n) => n.id)]
|
|
19358
|
-
};
|
|
19359
|
-
}
|
|
19360
|
-
function crossAuditSessionOpts(scope) {
|
|
19361
|
-
const key = newAuditKey();
|
|
19362
|
-
return {
|
|
19363
|
-
audit: { key, projects: scope },
|
|
19364
|
-
env: { HANOMAN_AUDIT_KEY: key, HANOMAN_AUDIT_URL: auditApiUrl() }
|
|
19365
|
-
};
|
|
19366
|
-
}
|
|
19367
|
-
|
|
19368
|
-
// src/routes/terminal.ts
|
|
19369
19244
|
init_session_phases();
|
|
19245
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
19370
19246
|
|
|
19371
19247
|
// src/services/session-launch.ts
|
|
19372
19248
|
init_db();
|
|
@@ -19429,7 +19305,10 @@ async function startSpecSession(spec, opts) {
|
|
|
19429
19305
|
} catch (e) {
|
|
19430
19306
|
throw new LaunchError(`gagal membuat worktree: ${e.message}`, "worktree");
|
|
19431
19307
|
}
|
|
19432
|
-
if (!resume) await prisma.spec.update({
|
|
19308
|
+
if (!resume) await prisma.spec.update({
|
|
19309
|
+
where: { id: spec.id },
|
|
19310
|
+
data: { baseSha, headSha: null, startedAt: /* @__PURE__ */ new Date() }
|
|
19311
|
+
});
|
|
19433
19312
|
}
|
|
19434
19313
|
const brief = {
|
|
19435
19314
|
id: spec.id,
|
|
@@ -19455,17 +19334,6 @@ async function startSpecSession(spec, opts) {
|
|
|
19455
19334
|
prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope);
|
|
19456
19335
|
}
|
|
19457
19336
|
const scopeEnv = { HANOMAN_BASE_SHA: baseSha, HANOMAN_VERIFY_SCOPE: verifyScope };
|
|
19458
|
-
let extra = {};
|
|
19459
|
-
if (opts.flow === "cross-audit") {
|
|
19460
|
-
const built = await buildCrossAuditCtx(spec.projectId);
|
|
19461
|
-
if (built) {
|
|
19462
|
-
prompt = startCrossAuditPrompt(
|
|
19463
|
-
{ ...built.ctx, worktree, spec: brief, branchTo },
|
|
19464
|
-
"backlog"
|
|
19465
|
-
);
|
|
19466
|
-
extra = crossAuditSessionOpts(built.scope);
|
|
19467
|
-
}
|
|
19468
|
-
}
|
|
19469
19337
|
const s = createSession(spec.projectId, worktree, {
|
|
19470
19338
|
specId: spec.id,
|
|
19471
19339
|
flow: opts.flow,
|
|
@@ -19476,9 +19344,7 @@ async function startSpecSession(spec, opts) {
|
|
|
19476
19344
|
phaseFile: phaseFilePath(repoDir, id),
|
|
19477
19345
|
decisionFile: decisionFilePath(repoDir, id),
|
|
19478
19346
|
prompt,
|
|
19479
|
-
|
|
19480
|
-
// Digabung SESUDAH `extra` supaya env audit lintas (SPEC-337) tak terhapus dan sebaliknya.
|
|
19481
|
-
env: { ...scopeEnv, ...extra.env ?? {} }
|
|
19347
|
+
env: scopeEnv
|
|
19482
19348
|
});
|
|
19483
19349
|
return resume ? { id: s.id, resumed: true } : { id: s.id };
|
|
19484
19350
|
}
|
|
@@ -19726,32 +19592,6 @@ async function terminal_default(app2) {
|
|
|
19726
19592
|
});
|
|
19727
19593
|
return reply.code(201).send({ id: s2.id });
|
|
19728
19594
|
}
|
|
19729
|
-
if (parsed.data.flow === "cross-audit") {
|
|
19730
|
-
const id = `xaudit-${project.id.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}`;
|
|
19731
|
-
const live = getSession(id);
|
|
19732
|
-
if (live && !live.exited) return reply.code(201).send({ id: live.id });
|
|
19733
|
-
const built = await buildCrossAuditCtx(project.id);
|
|
19734
|
-
if (!built) return reply.code(404).send({ error: "project not found" });
|
|
19735
|
-
const { agent: agent2, model: model2, effort: effort2 } = await sessionAgentDefaults();
|
|
19736
|
-
if (agent2 === "codex") ensureCodexTrust(repoDir);
|
|
19737
|
-
const wt = `${repoDir}/.worktrees/${id}`;
|
|
19738
|
-
let reused;
|
|
19739
|
-
try {
|
|
19740
|
-
reused = ensureWorktree(repoDir, wt, "HEAD");
|
|
19741
|
-
} catch (e) {
|
|
19742
|
-
return reply.code(422).send({ error: `gagal membuat worktree: ${e.message}` });
|
|
19743
|
-
}
|
|
19744
|
-
const s2 = createSession(project.id, wt, {
|
|
19745
|
-
id,
|
|
19746
|
-
model: model2,
|
|
19747
|
-
effort: effort2,
|
|
19748
|
-
agent: agent2,
|
|
19749
|
-
decisionFile: decisionFilePath(repoDir, id),
|
|
19750
|
-
prompt: startCrossAuditPrompt({ ...built.ctx, worktree: wt }, "live") + resumeNote(reused),
|
|
19751
|
-
...crossAuditSessionOpts(built.scope)
|
|
19752
|
-
});
|
|
19753
|
-
return reply.code(201).send({ id: s2.id });
|
|
19754
|
-
}
|
|
19755
19595
|
const { agent, model, effort } = await sessionAgentDefaults();
|
|
19756
19596
|
if (agent === "codex") ensureCodexTrust(repoDir);
|
|
19757
19597
|
const s = createSession(project.id, repoDir, { agent, model, effort });
|
|
@@ -20003,14 +19843,14 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
20003
19843
|
SSH_ASKPASS_REQUIRE: "force",
|
|
20004
19844
|
HANOMAN_SSH_PASSWORD: opts.password
|
|
20005
19845
|
} : process.env;
|
|
20006
|
-
return new Promise((
|
|
19846
|
+
return new Promise((resolve14) => {
|
|
20007
19847
|
const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
20008
19848
|
let out3 = "";
|
|
20009
|
-
const
|
|
19849
|
+
const timer5 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
20010
19850
|
const done = (r) => {
|
|
20011
|
-
clearTimeout(
|
|
19851
|
+
clearTimeout(timer5);
|
|
20012
19852
|
if (askpass) rmSync4(dirname9(askpass), { recursive: true, force: true });
|
|
20013
|
-
|
|
19853
|
+
resolve14(r);
|
|
20014
19854
|
};
|
|
20015
19855
|
p.stdout.on("data", (d) => {
|
|
20016
19856
|
out3 += d;
|
|
@@ -20890,12 +20730,36 @@ async function update(app2) {
|
|
|
20890
20730
|
|
|
20891
20731
|
// src/services/events.ts
|
|
20892
20732
|
init_pty();
|
|
20733
|
+
|
|
20734
|
+
// src/services/lead/deciding.ts
|
|
20735
|
+
var deciding = /* @__PURE__ */ new Set();
|
|
20736
|
+
function markDeciding(sessionId2) {
|
|
20737
|
+
deciding.add(sessionId2);
|
|
20738
|
+
}
|
|
20739
|
+
function clearDeciding(sessionId2) {
|
|
20740
|
+
deciding.delete(sessionId2);
|
|
20741
|
+
}
|
|
20742
|
+
function isDeciding(sessionId2) {
|
|
20743
|
+
return deciding.has(sessionId2);
|
|
20744
|
+
}
|
|
20745
|
+
function decidingIds() {
|
|
20746
|
+
return [...deciding];
|
|
20747
|
+
}
|
|
20748
|
+
|
|
20749
|
+
// src/services/events.ts
|
|
20893
20750
|
init_db();
|
|
20894
20751
|
init_config2();
|
|
20895
20752
|
var clients = /* @__PURE__ */ new Set();
|
|
20896
20753
|
var tickMs = () => effectiveInt("HANOMAN_EVENTS_TICK_MS") ?? 1e3;
|
|
20897
20754
|
var GROUPS = [
|
|
20898
|
-
|
|
20755
|
+
// SPEC-409 · ADR-0091 · AC-3 · `deciding` menandai sesi yang sedang DISUSUN keputusannya oleh
|
|
20756
|
+
// hanoman-lead. Tanpa penanda ini sesi yang justru sedang dilayani terbaca persis seperti sesi
|
|
20757
|
+
// yang mandek — bentuknya sama: diam, marker keputusan terisi. Dihias DI SINI, bukan di pty.ts:
|
|
20758
|
+
// service itu sengaja tak tahu apa-apa soal lead maupun DB.
|
|
20759
|
+
{ everyTicks: 1, last: "", build: async () => ({
|
|
20760
|
+
t: "sessions",
|
|
20761
|
+
sessions: listSessions().map((s) => isDeciding(s.id) ? { ...s, deciding: true } : s)
|
|
20762
|
+
}) },
|
|
20899
20763
|
{ everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
|
|
20900
20764
|
{ everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
|
|
20901
20765
|
// ponytail: cermin GET /vps (orderBy createdAt asc). Query sepele — tak diekstrak.
|
|
@@ -20996,9 +20860,9 @@ init_db();
|
|
|
20996
20860
|
|
|
20997
20861
|
// src/services/device-token.ts
|
|
20998
20862
|
init_db();
|
|
20999
|
-
import { randomBytes
|
|
21000
|
-
var newDeviceToken = () =>
|
|
21001
|
-
var tokenHash = (token) =>
|
|
20863
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
20864
|
+
var newDeviceToken = () => randomBytes(32).toString("base64url");
|
|
20865
|
+
var tokenHash = (token) => createHash("sha256").update(token).digest("hex");
|
|
21002
20866
|
async function issueDeviceToken(userId, name2) {
|
|
21003
20867
|
const token = newDeviceToken();
|
|
21004
20868
|
const row = await prisma.deviceToken.create({ data: { userId, name: name2, tokenHash: tokenHash(token) } });
|
|
@@ -21312,11 +21176,11 @@ function clamp(text) {
|
|
|
21312
21176
|
const buf = Buffer.from(text, "utf8");
|
|
21313
21177
|
if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
|
|
21314
21178
|
const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
|
|
21315
|
-
let
|
|
21316
|
-
const nl =
|
|
21317
|
-
if (nl >= 0)
|
|
21179
|
+
let tail2 = buf.subarray(cut).toString("utf8");
|
|
21180
|
+
const nl = tail2.indexOf("\n");
|
|
21181
|
+
if (nl >= 0) tail2 = tail2.slice(nl + 1);
|
|
21318
21182
|
return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
|
|
21319
|
-
${
|
|
21183
|
+
${tail2}`, truncated: true };
|
|
21320
21184
|
}
|
|
21321
21185
|
async function saveTranscript(text) {
|
|
21322
21186
|
if (!text.trim()) return { key: "", bytes: 0, truncated: false };
|
|
@@ -21555,979 +21419,54 @@ async function config_default(app2) {
|
|
|
21555
21419
|
});
|
|
21556
21420
|
}
|
|
21557
21421
|
|
|
21558
|
-
// src/routes/
|
|
21422
|
+
// src/routes/help.ts
|
|
21423
|
+
init_zod();
|
|
21559
21424
|
init_src();
|
|
21560
21425
|
init_db();
|
|
21561
21426
|
|
|
21562
|
-
// src/services/
|
|
21427
|
+
// src/services/ticket.ts
|
|
21563
21428
|
init_db();
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21567
|
-
|
|
21568
|
-
return (msg || "").replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<uuid>").replace(/0x[0-9a-f]+/gi, "<hex>").replace(/"[^"]*"|'[^']*'/g, "<str>").replace(/\b\d+\b/g, "<n>").replace(/\s+/g, " ").trim().slice(0, 500);
|
|
21569
|
-
}
|
|
21570
|
-
function normalizeBundleName(name2) {
|
|
21571
|
-
return name2.replace(/([-.])(?=[a-z0-9_]*\d)[a-z0-9_]{6,}(\.[a-z0-9]+)$/i, "$2");
|
|
21572
|
-
}
|
|
21573
|
-
function baseOf(s) {
|
|
21574
|
-
const clean = s.split(/[?#]/)[0] ?? "";
|
|
21575
|
-
const parts = clean.split(/[/\\]/);
|
|
21576
|
-
return parts[parts.length - 1] || clean;
|
|
21577
|
-
}
|
|
21578
|
-
function topFrame(stack) {
|
|
21579
|
-
if (!stack) return "";
|
|
21580
|
-
for (const raw of stack.split("\n")) {
|
|
21581
|
-
const line = raw.trim();
|
|
21582
|
-
if (!line.startsWith("at ")) continue;
|
|
21583
|
-
return line.replace(/:\d+:\d+/g, "").replace(/\(([^)]*)\)/g, (_m, inner) => `(${normalizeBundleName(baseOf(inner))})`).replace(/\b(?:[a-z][a-z0-9+.-]*:\/\/|\/)[^\s()]+/gi, (m) => normalizeBundleName(baseOf(m))).replace(/\s+/g, " ").trim();
|
|
21584
|
-
}
|
|
21585
|
-
return "";
|
|
21586
|
-
}
|
|
21587
|
-
function topFrameFromFrames(frames) {
|
|
21588
|
-
const f = frames.find((x) => x.in_app) ?? frames[0];
|
|
21589
|
-
if (!f) return "";
|
|
21590
|
-
return `at ${f.function ?? "?"} (${normalizeBundleName(baseOf(f.filename ?? ""))})`;
|
|
21591
|
-
}
|
|
21592
|
-
function fingerprint(type, message, stack, frames) {
|
|
21593
|
-
const top = frames && frames.length ? topFrameFromFrames(frames) : topFrame(stack);
|
|
21594
|
-
const basis = `${type}
|
|
21595
|
-
${normalizeMessage(message)}
|
|
21596
|
-
${top}`;
|
|
21597
|
-
return createHash3("sha256").update(basis).digest("hex").slice(0, 32);
|
|
21429
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
|
|
21430
|
+
init_src();
|
|
21431
|
+
function hashAccessKey(key) {
|
|
21432
|
+
return createHash2("sha256").update(key).digest("hex");
|
|
21598
21433
|
}
|
|
21599
|
-
|
|
21600
|
-
|
|
21601
|
-
|
|
21602
|
-
var MSG_CAP = 2e3;
|
|
21603
|
-
var STACK_CAP = 16e3;
|
|
21604
|
-
var EVENTS_PER_GROUP = () => effectiveInt("HANOMAN_ERROR_EVENTS_PER_GROUP") ?? 50;
|
|
21605
|
-
var RETENTION_DAYS = () => effectiveInt("HANOMAN_ERROR_RETENTION_DAYS") ?? 30;
|
|
21606
|
-
var RATE_PER_MIN = () => effectiveInt("HANOMAN_INGEST_RATE_PER_MIN") ?? 120;
|
|
21607
|
-
var buckets = /* @__PURE__ */ new Map();
|
|
21608
|
-
function rateLimitOk(projectId, now = Date.now()) {
|
|
21609
|
-
const cap = RATE_PER_MIN();
|
|
21610
|
-
const refillPerMs = cap / 6e4;
|
|
21611
|
-
const b = buckets.get(projectId) ?? { tokens: cap, ts: now };
|
|
21612
|
-
b.tokens = Math.min(cap, b.tokens + (now - b.ts) * refillPerMs);
|
|
21613
|
-
b.ts = now;
|
|
21614
|
-
buckets.set(projectId, b);
|
|
21615
|
-
if (b.tokens < 1) return false;
|
|
21616
|
-
b.tokens -= 1;
|
|
21617
|
-
return true;
|
|
21434
|
+
function generateAccessKey() {
|
|
21435
|
+
const key = "hnm_tkt_" + randomBytes2(24).toString("hex");
|
|
21436
|
+
return { key, hash: hashAccessKey(key) };
|
|
21618
21437
|
}
|
|
21619
|
-
|
|
21620
|
-
|
|
21621
|
-
|
|
21622
|
-
|
|
21623
|
-
const
|
|
21624
|
-
|
|
21625
|
-
|
|
21626
|
-
|
|
21627
|
-
let groupId;
|
|
21628
|
-
let isNew = false;
|
|
21629
|
-
const existing = await prisma.errorGroup.findUnique({ where: key });
|
|
21630
|
-
if (existing) {
|
|
21631
|
-
await prisma.errorGroup.update({
|
|
21632
|
-
where: { id: existing.id },
|
|
21633
|
-
data: { count: { increment: 1 }, lastSeenAt: /* @__PURE__ */ new Date(), environment, updatedAt: /* @__PURE__ */ new Date(), release: payload.release ?? void 0 }
|
|
21634
|
-
});
|
|
21635
|
-
groupId = existing.id;
|
|
21636
|
-
} else {
|
|
21438
|
+
function generateShareToken() {
|
|
21439
|
+
return "hnm_shr_" + randomBytes2(24).toString("hex");
|
|
21440
|
+
}
|
|
21441
|
+
async function createTicket(input) {
|
|
21442
|
+
const { key, hash: hash2 } = generateAccessKey();
|
|
21443
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
21444
|
+
const max = await prisma.ticket.aggregate({ where: { projectId: input.projectId }, _max: { number: true } });
|
|
21445
|
+
const number = (max._max.number ?? 0) + 1;
|
|
21637
21446
|
try {
|
|
21638
|
-
const
|
|
21639
|
-
data: {
|
|
21640
|
-
projectId,
|
|
21641
|
-
fingerprint: fp,
|
|
21642
|
-
type,
|
|
21643
|
-
message,
|
|
21644
|
-
sampleStack: stack,
|
|
21645
|
-
environment,
|
|
21646
|
-
count: 1,
|
|
21647
|
-
sampleFrames: frames ?? void 0,
|
|
21648
|
-
// SPEC-276 · frame sample (disymbolikasi saat display)
|
|
21649
|
-
release: payload.release ?? null
|
|
21650
|
-
// SPEC-276 · korelasi build
|
|
21651
|
-
}
|
|
21447
|
+
const ticket = await prisma.ticket.create({
|
|
21448
|
+
data: { ...input, number, accessKeyHash: hash2, shareToken: generateShareToken(), status: "new" }
|
|
21652
21449
|
});
|
|
21653
|
-
|
|
21654
|
-
isNew = true;
|
|
21450
|
+
return { ticket, key };
|
|
21655
21451
|
} catch (e) {
|
|
21656
|
-
if (e.code
|
|
21657
|
-
|
|
21658
|
-
where: key,
|
|
21659
|
-
data: { count: { increment: 1 }, lastSeenAt: /* @__PURE__ */ new Date(), environment, updatedAt: /* @__PURE__ */ new Date() }
|
|
21660
|
-
});
|
|
21661
|
-
groupId = g.id;
|
|
21452
|
+
if (e.code === "P2002" && attempt < 2) continue;
|
|
21453
|
+
throw e;
|
|
21662
21454
|
}
|
|
21663
21455
|
}
|
|
21664
|
-
|
|
21665
|
-
await recordNewErrorGroup(groupId, projectId, projectName, type, message);
|
|
21666
|
-
await prisma.errorEvent.create({
|
|
21667
|
-
data: {
|
|
21668
|
-
groupId,
|
|
21669
|
-
projectId,
|
|
21670
|
-
type,
|
|
21671
|
-
message,
|
|
21672
|
-
stack,
|
|
21673
|
-
environment,
|
|
21674
|
-
frames: frames ?? void 0,
|
|
21675
|
-
// SPEC-276 · frame terstruktur mentah
|
|
21676
|
-
release: payload.release ?? null,
|
|
21677
|
-
context: payload.context ?? void 0
|
|
21678
|
-
}
|
|
21679
|
-
});
|
|
21680
|
-
await pruneGroup(groupId);
|
|
21681
|
-
if (isNew) await notifySynced("errorGroup", groupId);
|
|
21682
|
-
return { groupId, new: isNew };
|
|
21683
|
-
}
|
|
21684
|
-
async function pruneGroup(groupId) {
|
|
21685
|
-
const cutoff = new Date(Date.now() - RETENTION_DAYS() * 864e5);
|
|
21686
|
-
await prisma.errorEvent.deleteMany({ where: { groupId, receivedAt: { lt: cutoff } } });
|
|
21687
|
-
const cap = EVENTS_PER_GROUP();
|
|
21688
|
-
const keep = await prisma.errorEvent.findMany({
|
|
21689
|
-
where: { groupId },
|
|
21690
|
-
orderBy: { receivedAt: "desc" },
|
|
21691
|
-
take: cap,
|
|
21692
|
-
select: { id: true }
|
|
21693
|
-
});
|
|
21694
|
-
if (keep.length >= cap)
|
|
21695
|
-
await prisma.errorEvent.deleteMany({ where: { groupId, id: { notIn: keep.map((k) => k.id) } } });
|
|
21696
|
-
}
|
|
21697
|
-
|
|
21698
|
-
// src/services/sourcemap-store.ts
|
|
21699
|
-
init_db();
|
|
21700
|
-
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
21701
|
-
import { mkdir as mkdir4, writeFile as writeFile4, readFile as readFile5, unlink as unlink3 } from "node:fs/promises";
|
|
21702
|
-
import { join as join13 } from "node:path";
|
|
21703
|
-
function basenameOf(ref) {
|
|
21704
|
-
const clean = ref.split(/[?#]/)[0] ?? "";
|
|
21705
|
-
const parts = clean.split(/[/\\]/);
|
|
21706
|
-
return parts[parts.length - 1] || clean;
|
|
21456
|
+
throw new Error("gagal membuat tiket (nomor bentrok)");
|
|
21707
21457
|
}
|
|
21708
|
-
async function
|
|
21709
|
-
const
|
|
21710
|
-
|
|
21711
|
-
const
|
|
21712
|
-
|
|
21713
|
-
|
|
21714
|
-
|
|
21715
|
-
const prev = await prisma.sourceMapArtifact.findUnique({
|
|
21716
|
-
where: { projectId_release_filename: { projectId, release, filename: base } }
|
|
21717
|
-
});
|
|
21718
|
-
const row = await prisma.sourceMapArtifact.upsert({
|
|
21719
|
-
where: { projectId_release_filename: { projectId, release, filename: base } },
|
|
21720
|
-
update: { storageKey, size: buf.length, debugId: debugId ?? null },
|
|
21721
|
-
create: { projectId, release, filename: base, storageKey, size: buf.length, debugId: debugId ?? null }
|
|
21458
|
+
async function pruneOldTickets(now = Date.now()) {
|
|
21459
|
+
const days = Number(process.env.HANOMAN_TICKET_RETENTION_DAYS ?? 90);
|
|
21460
|
+
const cutoff = new Date(now - days * 864e5);
|
|
21461
|
+
const stale = await prisma.ticket.findMany({
|
|
21462
|
+
where: { status: "rejected", specId: null, createdAt: { lt: cutoff } },
|
|
21463
|
+
include: { attachments: true },
|
|
21464
|
+
take: 50
|
|
21722
21465
|
});
|
|
21723
|
-
|
|
21724
|
-
|
|
21725
|
-
});
|
|
21726
|
-
|
|
21727
|
-
}
|
|
21728
|
-
async function findSourceMap(projectId, release, frameFilename) {
|
|
21729
|
-
const base = basenameOf(frameFilename);
|
|
21730
|
-
const row = await prisma.sourceMapArtifact.findUnique({
|
|
21731
|
-
where: { projectId_release_filename: { projectId, release, filename: base } }
|
|
21732
|
-
});
|
|
21733
|
-
if (!row) return null;
|
|
21734
|
-
try {
|
|
21735
|
-
const buf = await readFile5(join13(uploadDir(), row.storageKey.replace(/[/\\]/g, "")));
|
|
21736
|
-
return buf.toString("utf8");
|
|
21737
|
-
} catch {
|
|
21738
|
-
return null;
|
|
21739
|
-
}
|
|
21740
|
-
}
|
|
21741
|
-
async function pruneReleases(projectId, keep = 10) {
|
|
21742
|
-
const rows = await prisma.sourceMapArtifact.findMany({
|
|
21743
|
-
where: { projectId },
|
|
21744
|
-
orderBy: [{ createdAt: "desc" }, { id: "desc" }]
|
|
21745
|
-
});
|
|
21746
|
-
const order = [];
|
|
21747
|
-
for (const r of rows) if (!order.includes(r.release)) order.push(r.release);
|
|
21748
|
-
const keepSet = new Set(order.slice(0, keep));
|
|
21749
|
-
const dir2 = uploadDir();
|
|
21750
|
-
for (const r of rows) if (!keepSet.has(r.release)) {
|
|
21751
|
-
await unlink3(join13(dir2, r.storageKey.replace(/[/\\]/g, ""))).catch(() => {
|
|
21752
|
-
});
|
|
21753
|
-
await prisma.sourceMapArtifact.delete({ where: { id: r.id } }).catch(() => {
|
|
21754
|
-
});
|
|
21755
|
-
}
|
|
21756
|
-
}
|
|
21757
|
-
|
|
21758
|
-
// src/routes/ingest.ts
|
|
21759
|
-
var BODY_CAP = 64e3;
|
|
21760
|
-
var SOURCEMAP_CAP = 30 * 1024 * 1024;
|
|
21761
|
-
function cors(reply) {
|
|
21762
|
-
reply.header("access-control-allow-origin", "*");
|
|
21763
|
-
reply.header("access-control-allow-methods", "POST,OPTIONS");
|
|
21764
|
-
reply.header("access-control-allow-headers", "content-type,x-hanoman-dsn");
|
|
21765
|
-
}
|
|
21766
|
-
async function ingest_default(app2) {
|
|
21767
|
-
app2.options("/ingest/:slug", async (_req, reply) => {
|
|
21768
|
-
cors(reply);
|
|
21769
|
-
return reply.code(204).send();
|
|
21770
|
-
});
|
|
21771
|
-
app2.post("/ingest/:slug", async (req, reply) => {
|
|
21772
|
-
cors(reply);
|
|
21773
|
-
const { slug } = req.params;
|
|
21774
|
-
const key = req.query.key ?? req.headers["x-hanoman-dsn"] ?? "";
|
|
21775
|
-
const raw = JSON.stringify(req.body ?? {});
|
|
21776
|
-
if (raw.length > BODY_CAP) return reply.code(413).send({ error: "payload too large" });
|
|
21777
|
-
const project = await prisma.project.findUnique({ where: { id: slug } });
|
|
21778
|
-
if (!project || !verifyKey(key, project.ingestKeyHash)) return reply.code(401).send({ error: "unauthorized" });
|
|
21779
|
-
if (!rateLimitOk(project.id)) return reply.code(429).send({ error: "rate limited" });
|
|
21780
|
-
const parsed = zIngestPayload.safeParse(req.body);
|
|
21781
|
-
if (!parsed.success) return reply.code(400).send({ error: "invalid payload" });
|
|
21782
|
-
const r = await ingestError(project.id, project.name, parsed.data);
|
|
21783
|
-
return reply.code(202).send({ ok: true, groupId: r.groupId, new: r.new });
|
|
21784
|
-
});
|
|
21785
|
-
app2.options("/ingest/:slug/sourcemaps", async (_req, reply) => {
|
|
21786
|
-
cors(reply);
|
|
21787
|
-
return reply.code(204).send();
|
|
21788
|
-
});
|
|
21789
|
-
app2.post("/ingest/:slug/sourcemaps", { bodyLimit: SOURCEMAP_CAP }, async (req, reply) => {
|
|
21790
|
-
cors(reply);
|
|
21791
|
-
const { slug } = req.params;
|
|
21792
|
-
const key = req.query.key ?? req.headers["x-hanoman-dsn"] ?? "";
|
|
21793
|
-
const project = await prisma.project.findUnique({ where: { id: slug } });
|
|
21794
|
-
if (!project || !verifyKey(key, project.ingestKeyHash)) return reply.code(401).send({ error: "unauthorized" });
|
|
21795
|
-
const parsed = zSourceMapUpload.safeParse(req.body);
|
|
21796
|
-
if (!parsed.success) return reply.code(400).send({ error: "invalid payload" });
|
|
21797
|
-
const total = parsed.data.artifacts.reduce((n, a) => n + a.map.length, 0);
|
|
21798
|
-
if (total > SOURCEMAP_CAP) return reply.code(413).send({ error: "payload too large" });
|
|
21799
|
-
for (const a of parsed.data.artifacts)
|
|
21800
|
-
await saveSourceMap(project.id, parsed.data.release, a.filename, a.map, a.debugId);
|
|
21801
|
-
await pruneReleases(project.id);
|
|
21802
|
-
return reply.code(202).send({ ok: true, stored: parsed.data.artifacts.length });
|
|
21803
|
-
});
|
|
21804
|
-
}
|
|
21805
|
-
|
|
21806
|
-
// src/routes/errors.ts
|
|
21807
|
-
init_src();
|
|
21808
|
-
init_db();
|
|
21809
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
21810
|
-
import { join as join14 } from "node:path";
|
|
21811
|
-
|
|
21812
|
-
// src/services/error-escalate.ts
|
|
21813
|
-
init_db();
|
|
21814
|
-
async function escalateErrorGroup(group2, opts) {
|
|
21815
|
-
if (group2.specId) {
|
|
21816
|
-
const spec2 = await prisma.spec.findUnique({ where: { id: group2.specId } });
|
|
21817
|
-
return { spec: spec2, created: false };
|
|
21818
|
-
}
|
|
21819
|
-
const short = group2.message.length > 80 ? group2.message.slice(0, 77) + "\u2026" : group2.message;
|
|
21820
|
-
const topStack = (group2.sampleStack ?? "").split("\n").slice(0, 12).join("\n");
|
|
21821
|
-
const backlink = `Dari Error monitoring: grup ${group2.id} (${group2.count}\xD7, ${group2.environment}).`;
|
|
21822
|
-
const payload = {
|
|
21823
|
-
severity: "major",
|
|
21824
|
-
steps: "Otomatis dari Error monitoring \u2014 reproduksi dari stack sampel.",
|
|
21825
|
-
expected: "Tidak ada error.",
|
|
21826
|
-
actual: `${group2.type}: ${group2.message}
|
|
21827
|
-
|
|
21828
|
-
${topStack}
|
|
21829
|
-
|
|
21830
|
-
${backlink}`,
|
|
21831
|
-
env: group2.environment,
|
|
21832
|
-
fromErrorGroup: group2.id
|
|
21833
|
-
};
|
|
21834
|
-
const repoDir = await resolveRepoDir(group2.projectId);
|
|
21835
|
-
let spec = null;
|
|
21836
|
-
for (let attempt = 0; attempt < 3 && !spec; attempt++) {
|
|
21837
|
-
const sid = await nextSpecId(repoDir);
|
|
21838
|
-
try {
|
|
21839
|
-
spec = await prisma.spec.create({
|
|
21840
|
-
data: {
|
|
21841
|
-
id: sid,
|
|
21842
|
-
projectId: group2.projectId,
|
|
21843
|
-
title: `${group2.type}: ${short}`,
|
|
21844
|
-
source: "qa",
|
|
21845
|
-
stage: "brainstorming",
|
|
21846
|
-
priority: "tinggi",
|
|
21847
|
-
author: `QA \xB7 ${opts.author}`,
|
|
21848
|
-
objective: `${group2.type}: ${group2.message}. ${backlink}`,
|
|
21849
|
-
payload
|
|
21850
|
-
}
|
|
21851
|
-
});
|
|
21852
|
-
} catch (e) {
|
|
21853
|
-
if (e.code === "P2002" && attempt < 2) continue;
|
|
21854
|
-
throw e;
|
|
21855
|
-
}
|
|
21856
|
-
}
|
|
21857
|
-
await prisma.errorGroup.update({ where: { id: group2.id }, data: { status: "escalated", specId: spec.id } });
|
|
21858
|
-
await notifySynced("spec", spec.id);
|
|
21859
|
-
await notifySynced("errorGroup", group2.id);
|
|
21860
|
-
return { spec, created: true };
|
|
21861
|
-
}
|
|
21862
|
-
|
|
21863
|
-
// ../node_modules/.pnpm/@jridgewell+sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs
|
|
21864
|
-
var comma = ",".charCodeAt(0);
|
|
21865
|
-
var semicolon = ";".charCodeAt(0);
|
|
21866
|
-
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
21867
|
-
var intToChar = new Uint8Array(64);
|
|
21868
|
-
var charToInt = new Uint8Array(128);
|
|
21869
|
-
for (let i = 0; i < chars.length; i++) {
|
|
21870
|
-
const c = chars.charCodeAt(i);
|
|
21871
|
-
intToChar[i] = c;
|
|
21872
|
-
charToInt[c] = i;
|
|
21873
|
-
}
|
|
21874
|
-
function decodeInteger(reader, relative) {
|
|
21875
|
-
let value = 0;
|
|
21876
|
-
let shift = 0;
|
|
21877
|
-
let integer = 0;
|
|
21878
|
-
do {
|
|
21879
|
-
const c = reader.next();
|
|
21880
|
-
integer = charToInt[c];
|
|
21881
|
-
value |= (integer & 31) << shift;
|
|
21882
|
-
shift += 5;
|
|
21883
|
-
} while (integer & 32);
|
|
21884
|
-
const shouldNegate = value & 1;
|
|
21885
|
-
value >>>= 1;
|
|
21886
|
-
if (shouldNegate) {
|
|
21887
|
-
value = -2147483648 | -value;
|
|
21888
|
-
}
|
|
21889
|
-
return relative + value;
|
|
21890
|
-
}
|
|
21891
|
-
function hasMoreVlq(reader, max) {
|
|
21892
|
-
if (reader.pos >= max) return false;
|
|
21893
|
-
return reader.peek() !== comma;
|
|
21894
|
-
}
|
|
21895
|
-
var bufLength = 1024 * 16;
|
|
21896
|
-
var StringReader = class {
|
|
21897
|
-
constructor(buffer) {
|
|
21898
|
-
this.pos = 0;
|
|
21899
|
-
this.buffer = buffer;
|
|
21900
|
-
}
|
|
21901
|
-
next() {
|
|
21902
|
-
return this.buffer.charCodeAt(this.pos++);
|
|
21903
|
-
}
|
|
21904
|
-
peek() {
|
|
21905
|
-
return this.buffer.charCodeAt(this.pos);
|
|
21906
|
-
}
|
|
21907
|
-
indexOf(char) {
|
|
21908
|
-
const { buffer, pos } = this;
|
|
21909
|
-
const idx = buffer.indexOf(char, pos);
|
|
21910
|
-
return idx === -1 ? buffer.length : idx;
|
|
21911
|
-
}
|
|
21912
|
-
};
|
|
21913
|
-
function decode(mappings) {
|
|
21914
|
-
const { length } = mappings;
|
|
21915
|
-
const reader = new StringReader(mappings);
|
|
21916
|
-
const decoded = [];
|
|
21917
|
-
let genColumn = 0;
|
|
21918
|
-
let sourcesIndex = 0;
|
|
21919
|
-
let sourceLine = 0;
|
|
21920
|
-
let sourceColumn = 0;
|
|
21921
|
-
let namesIndex = 0;
|
|
21922
|
-
do {
|
|
21923
|
-
const semi = reader.indexOf(";");
|
|
21924
|
-
const line = [];
|
|
21925
|
-
let sorted = true;
|
|
21926
|
-
let lastCol = 0;
|
|
21927
|
-
genColumn = 0;
|
|
21928
|
-
while (reader.pos < semi) {
|
|
21929
|
-
let seg;
|
|
21930
|
-
genColumn = decodeInteger(reader, genColumn);
|
|
21931
|
-
if (genColumn < lastCol) sorted = false;
|
|
21932
|
-
lastCol = genColumn;
|
|
21933
|
-
if (hasMoreVlq(reader, semi)) {
|
|
21934
|
-
sourcesIndex = decodeInteger(reader, sourcesIndex);
|
|
21935
|
-
sourceLine = decodeInteger(reader, sourceLine);
|
|
21936
|
-
sourceColumn = decodeInteger(reader, sourceColumn);
|
|
21937
|
-
if (hasMoreVlq(reader, semi)) {
|
|
21938
|
-
namesIndex = decodeInteger(reader, namesIndex);
|
|
21939
|
-
seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex];
|
|
21940
|
-
} else {
|
|
21941
|
-
seg = [genColumn, sourcesIndex, sourceLine, sourceColumn];
|
|
21942
|
-
}
|
|
21943
|
-
} else {
|
|
21944
|
-
seg = [genColumn];
|
|
21945
|
-
}
|
|
21946
|
-
line.push(seg);
|
|
21947
|
-
reader.pos++;
|
|
21948
|
-
}
|
|
21949
|
-
if (!sorted) sort(line);
|
|
21950
|
-
decoded.push(line);
|
|
21951
|
-
reader.pos = semi + 1;
|
|
21952
|
-
} while (reader.pos <= length);
|
|
21953
|
-
return decoded;
|
|
21954
|
-
}
|
|
21955
|
-
function sort(line) {
|
|
21956
|
-
line.sort(sortComparator);
|
|
21957
|
-
}
|
|
21958
|
-
function sortComparator(a, b) {
|
|
21959
|
-
return a[0] - b[0];
|
|
21960
|
-
}
|
|
21961
|
-
|
|
21962
|
-
// ../node_modules/.pnpm/@jridgewell+resolve-uri@3.1.2/node_modules/@jridgewell/resolve-uri/dist/resolve-uri.mjs
|
|
21963
|
-
var schemeRegex = /^[\w+.-]+:\/\//;
|
|
21964
|
-
var urlRegex = /^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/;
|
|
21965
|
-
var fileRegex = /^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i;
|
|
21966
|
-
function isAbsoluteUrl(input) {
|
|
21967
|
-
return schemeRegex.test(input);
|
|
21968
|
-
}
|
|
21969
|
-
function isSchemeRelativeUrl(input) {
|
|
21970
|
-
return input.startsWith("//");
|
|
21971
|
-
}
|
|
21972
|
-
function isAbsolutePath(input) {
|
|
21973
|
-
return input.startsWith("/");
|
|
21974
|
-
}
|
|
21975
|
-
function isFileUrl(input) {
|
|
21976
|
-
return input.startsWith("file:");
|
|
21977
|
-
}
|
|
21978
|
-
function isRelative(input) {
|
|
21979
|
-
return /^[.?#]/.test(input);
|
|
21980
|
-
}
|
|
21981
|
-
function parseAbsoluteUrl(input) {
|
|
21982
|
-
const match = urlRegex.exec(input);
|
|
21983
|
-
return makeUrl(match[1], match[2] || "", match[3], match[4] || "", match[5] || "/", match[6] || "", match[7] || "");
|
|
21984
|
-
}
|
|
21985
|
-
function parseFileUrl(input) {
|
|
21986
|
-
const match = fileRegex.exec(input);
|
|
21987
|
-
const path = match[2];
|
|
21988
|
-
return makeUrl("file:", "", match[1] || "", "", isAbsolutePath(path) ? path : "/" + path, match[3] || "", match[4] || "");
|
|
21989
|
-
}
|
|
21990
|
-
function makeUrl(scheme, user, host2, port2, path, query, hash2) {
|
|
21991
|
-
return {
|
|
21992
|
-
scheme,
|
|
21993
|
-
user,
|
|
21994
|
-
host: host2,
|
|
21995
|
-
port: port2,
|
|
21996
|
-
path,
|
|
21997
|
-
query,
|
|
21998
|
-
hash: hash2,
|
|
21999
|
-
type: 7
|
|
22000
|
-
};
|
|
22001
|
-
}
|
|
22002
|
-
function parseUrl(input) {
|
|
22003
|
-
if (isSchemeRelativeUrl(input)) {
|
|
22004
|
-
const url3 = parseAbsoluteUrl("http:" + input);
|
|
22005
|
-
url3.scheme = "";
|
|
22006
|
-
url3.type = 6;
|
|
22007
|
-
return url3;
|
|
22008
|
-
}
|
|
22009
|
-
if (isAbsolutePath(input)) {
|
|
22010
|
-
const url3 = parseAbsoluteUrl("http://foo.com" + input);
|
|
22011
|
-
url3.scheme = "";
|
|
22012
|
-
url3.host = "";
|
|
22013
|
-
url3.type = 5;
|
|
22014
|
-
return url3;
|
|
22015
|
-
}
|
|
22016
|
-
if (isFileUrl(input))
|
|
22017
|
-
return parseFileUrl(input);
|
|
22018
|
-
if (isAbsoluteUrl(input))
|
|
22019
|
-
return parseAbsoluteUrl(input);
|
|
22020
|
-
const url2 = parseAbsoluteUrl("http://foo.com/" + input);
|
|
22021
|
-
url2.scheme = "";
|
|
22022
|
-
url2.host = "";
|
|
22023
|
-
url2.type = input ? input.startsWith("?") ? 3 : input.startsWith("#") ? 2 : 4 : 1;
|
|
22024
|
-
return url2;
|
|
22025
|
-
}
|
|
22026
|
-
function stripPathFilename(path) {
|
|
22027
|
-
if (path.endsWith("/.."))
|
|
22028
|
-
return path;
|
|
22029
|
-
const index = path.lastIndexOf("/");
|
|
22030
|
-
return path.slice(0, index + 1);
|
|
22031
|
-
}
|
|
22032
|
-
function mergePaths(url2, base) {
|
|
22033
|
-
normalizePath(base, base.type);
|
|
22034
|
-
if (url2.path === "/") {
|
|
22035
|
-
url2.path = base.path;
|
|
22036
|
-
} else {
|
|
22037
|
-
url2.path = stripPathFilename(base.path) + url2.path;
|
|
22038
|
-
}
|
|
22039
|
-
}
|
|
22040
|
-
function normalizePath(url2, type) {
|
|
22041
|
-
const rel = type <= 4;
|
|
22042
|
-
const pieces = url2.path.split("/");
|
|
22043
|
-
let pointer = 1;
|
|
22044
|
-
let positive = 0;
|
|
22045
|
-
let addTrailingSlash = false;
|
|
22046
|
-
for (let i = 1; i < pieces.length; i++) {
|
|
22047
|
-
const piece = pieces[i];
|
|
22048
|
-
if (!piece) {
|
|
22049
|
-
addTrailingSlash = true;
|
|
22050
|
-
continue;
|
|
22051
|
-
}
|
|
22052
|
-
addTrailingSlash = false;
|
|
22053
|
-
if (piece === ".")
|
|
22054
|
-
continue;
|
|
22055
|
-
if (piece === "..") {
|
|
22056
|
-
if (positive) {
|
|
22057
|
-
addTrailingSlash = true;
|
|
22058
|
-
positive--;
|
|
22059
|
-
pointer--;
|
|
22060
|
-
} else if (rel) {
|
|
22061
|
-
pieces[pointer++] = piece;
|
|
22062
|
-
}
|
|
22063
|
-
continue;
|
|
22064
|
-
}
|
|
22065
|
-
pieces[pointer++] = piece;
|
|
22066
|
-
positive++;
|
|
22067
|
-
}
|
|
22068
|
-
let path = "";
|
|
22069
|
-
for (let i = 1; i < pointer; i++) {
|
|
22070
|
-
path += "/" + pieces[i];
|
|
22071
|
-
}
|
|
22072
|
-
if (!path || addTrailingSlash && !path.endsWith("/..")) {
|
|
22073
|
-
path += "/";
|
|
22074
|
-
}
|
|
22075
|
-
url2.path = path;
|
|
22076
|
-
}
|
|
22077
|
-
function resolve14(input, base) {
|
|
22078
|
-
if (!input && !base)
|
|
22079
|
-
return "";
|
|
22080
|
-
const url2 = parseUrl(input);
|
|
22081
|
-
let inputType = url2.type;
|
|
22082
|
-
if (base && inputType !== 7) {
|
|
22083
|
-
const baseUrl = parseUrl(base);
|
|
22084
|
-
const baseType = baseUrl.type;
|
|
22085
|
-
switch (inputType) {
|
|
22086
|
-
case 1:
|
|
22087
|
-
url2.hash = baseUrl.hash;
|
|
22088
|
-
// fall through
|
|
22089
|
-
case 2:
|
|
22090
|
-
url2.query = baseUrl.query;
|
|
22091
|
-
// fall through
|
|
22092
|
-
case 3:
|
|
22093
|
-
case 4:
|
|
22094
|
-
mergePaths(url2, baseUrl);
|
|
22095
|
-
// fall through
|
|
22096
|
-
case 5:
|
|
22097
|
-
url2.user = baseUrl.user;
|
|
22098
|
-
url2.host = baseUrl.host;
|
|
22099
|
-
url2.port = baseUrl.port;
|
|
22100
|
-
// fall through
|
|
22101
|
-
case 6:
|
|
22102
|
-
url2.scheme = baseUrl.scheme;
|
|
22103
|
-
}
|
|
22104
|
-
if (baseType > inputType)
|
|
22105
|
-
inputType = baseType;
|
|
22106
|
-
}
|
|
22107
|
-
normalizePath(url2, inputType);
|
|
22108
|
-
const queryHash = url2.query + url2.hash;
|
|
22109
|
-
switch (inputType) {
|
|
22110
|
-
// This is impossible, because of the empty checks at the start of the function.
|
|
22111
|
-
// case UrlType.Empty:
|
|
22112
|
-
case 2:
|
|
22113
|
-
case 3:
|
|
22114
|
-
return queryHash;
|
|
22115
|
-
case 4: {
|
|
22116
|
-
const path = url2.path.slice(1);
|
|
22117
|
-
if (!path)
|
|
22118
|
-
return queryHash || ".";
|
|
22119
|
-
if (isRelative(base || input) && !isRelative(path)) {
|
|
22120
|
-
return "./" + path + queryHash;
|
|
22121
|
-
}
|
|
22122
|
-
return path + queryHash;
|
|
22123
|
-
}
|
|
22124
|
-
case 5:
|
|
22125
|
-
return url2.path + queryHash;
|
|
22126
|
-
default:
|
|
22127
|
-
return url2.scheme + "//" + url2.user + url2.host + url2.port + url2.path + queryHash;
|
|
22128
|
-
}
|
|
22129
|
-
}
|
|
22130
|
-
|
|
22131
|
-
// ../node_modules/.pnpm/@jridgewell+trace-mapping@0.3.31/node_modules/@jridgewell/trace-mapping/dist/trace-mapping.mjs
|
|
22132
|
-
function stripFilename(path) {
|
|
22133
|
-
if (!path) return "";
|
|
22134
|
-
const index = path.lastIndexOf("/");
|
|
22135
|
-
return path.slice(0, index + 1);
|
|
22136
|
-
}
|
|
22137
|
-
function resolver(mapUrl, sourceRoot) {
|
|
22138
|
-
const from = stripFilename(mapUrl);
|
|
22139
|
-
const prefix = sourceRoot ? sourceRoot + "/" : "";
|
|
22140
|
-
return (source) => resolve14(prefix + (source || ""), from);
|
|
22141
|
-
}
|
|
22142
|
-
var COLUMN = 0;
|
|
22143
|
-
var SOURCES_INDEX = 1;
|
|
22144
|
-
var SOURCE_LINE = 2;
|
|
22145
|
-
var SOURCE_COLUMN = 3;
|
|
22146
|
-
var NAMES_INDEX = 4;
|
|
22147
|
-
function maybeSort(mappings, owned) {
|
|
22148
|
-
const unsortedIndex = nextUnsortedSegmentLine(mappings, 0);
|
|
22149
|
-
if (unsortedIndex === mappings.length) return mappings;
|
|
22150
|
-
if (!owned) mappings = mappings.slice();
|
|
22151
|
-
for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) {
|
|
22152
|
-
mappings[i] = sortSegments(mappings[i], owned);
|
|
22153
|
-
}
|
|
22154
|
-
return mappings;
|
|
22155
|
-
}
|
|
22156
|
-
function nextUnsortedSegmentLine(mappings, start) {
|
|
22157
|
-
for (let i = start; i < mappings.length; i++) {
|
|
22158
|
-
if (!isSorted(mappings[i])) return i;
|
|
22159
|
-
}
|
|
22160
|
-
return mappings.length;
|
|
22161
|
-
}
|
|
22162
|
-
function isSorted(line) {
|
|
22163
|
-
for (let j = 1; j < line.length; j++) {
|
|
22164
|
-
if (line[j][COLUMN] < line[j - 1][COLUMN]) {
|
|
22165
|
-
return false;
|
|
22166
|
-
}
|
|
22167
|
-
}
|
|
22168
|
-
return true;
|
|
22169
|
-
}
|
|
22170
|
-
function sortSegments(line, owned) {
|
|
22171
|
-
if (!owned) line = line.slice();
|
|
22172
|
-
return line.sort(sortComparator2);
|
|
22173
|
-
}
|
|
22174
|
-
function sortComparator2(a, b) {
|
|
22175
|
-
return a[COLUMN] - b[COLUMN];
|
|
22176
|
-
}
|
|
22177
|
-
var found = false;
|
|
22178
|
-
function binarySearch(haystack, needle, low, high) {
|
|
22179
|
-
while (low <= high) {
|
|
22180
|
-
const mid = low + (high - low >> 1);
|
|
22181
|
-
const cmp = haystack[mid][COLUMN] - needle;
|
|
22182
|
-
if (cmp === 0) {
|
|
22183
|
-
found = true;
|
|
22184
|
-
return mid;
|
|
22185
|
-
}
|
|
22186
|
-
if (cmp < 0) {
|
|
22187
|
-
low = mid + 1;
|
|
22188
|
-
} else {
|
|
22189
|
-
high = mid - 1;
|
|
22190
|
-
}
|
|
22191
|
-
}
|
|
22192
|
-
found = false;
|
|
22193
|
-
return low - 1;
|
|
22194
|
-
}
|
|
22195
|
-
function upperBound(haystack, needle, index) {
|
|
22196
|
-
for (let i = index + 1; i < haystack.length; index = i++) {
|
|
22197
|
-
if (haystack[i][COLUMN] !== needle) break;
|
|
22198
|
-
}
|
|
22199
|
-
return index;
|
|
22200
|
-
}
|
|
22201
|
-
function lowerBound(haystack, needle, index) {
|
|
22202
|
-
for (let i = index - 1; i >= 0; index = i--) {
|
|
22203
|
-
if (haystack[i][COLUMN] !== needle) break;
|
|
22204
|
-
}
|
|
22205
|
-
return index;
|
|
22206
|
-
}
|
|
22207
|
-
function memoizedState() {
|
|
22208
|
-
return {
|
|
22209
|
-
lastKey: -1,
|
|
22210
|
-
lastNeedle: -1,
|
|
22211
|
-
lastIndex: -1
|
|
22212
|
-
};
|
|
22213
|
-
}
|
|
22214
|
-
function memoizedBinarySearch(haystack, needle, state, key) {
|
|
22215
|
-
const { lastKey, lastNeedle, lastIndex } = state;
|
|
22216
|
-
let low = 0;
|
|
22217
|
-
let high = haystack.length - 1;
|
|
22218
|
-
if (key === lastKey) {
|
|
22219
|
-
if (needle === lastNeedle) {
|
|
22220
|
-
found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle;
|
|
22221
|
-
return lastIndex;
|
|
22222
|
-
}
|
|
22223
|
-
if (needle >= lastNeedle) {
|
|
22224
|
-
low = lastIndex === -1 ? 0 : lastIndex;
|
|
22225
|
-
} else {
|
|
22226
|
-
high = lastIndex;
|
|
22227
|
-
}
|
|
22228
|
-
}
|
|
22229
|
-
state.lastKey = key;
|
|
22230
|
-
state.lastNeedle = needle;
|
|
22231
|
-
return state.lastIndex = binarySearch(haystack, needle, low, high);
|
|
22232
|
-
}
|
|
22233
|
-
function parse2(map) {
|
|
22234
|
-
return typeof map === "string" ? JSON.parse(map) : map;
|
|
22235
|
-
}
|
|
22236
|
-
var LINE_GTR_ZERO = "`line` must be greater than 0 (lines start at line 1)";
|
|
22237
|
-
var COL_GTR_EQ_ZERO = "`column` must be greater than or equal to 0 (columns start at column 0)";
|
|
22238
|
-
var LEAST_UPPER_BOUND = -1;
|
|
22239
|
-
var GREATEST_LOWER_BOUND = 1;
|
|
22240
|
-
var TraceMap = class {
|
|
22241
|
-
constructor(map, mapUrl) {
|
|
22242
|
-
const isString = typeof map === "string";
|
|
22243
|
-
if (!isString && map._decodedMemo) return map;
|
|
22244
|
-
const parsed = parse2(map);
|
|
22245
|
-
const { version, file, names, sourceRoot, sources: sources2, sourcesContent } = parsed;
|
|
22246
|
-
this.version = version;
|
|
22247
|
-
this.file = file;
|
|
22248
|
-
this.names = names || [];
|
|
22249
|
-
this.sourceRoot = sourceRoot;
|
|
22250
|
-
this.sources = sources2;
|
|
22251
|
-
this.sourcesContent = sourcesContent;
|
|
22252
|
-
this.ignoreList = parsed.ignoreList || parsed.x_google_ignoreList || void 0;
|
|
22253
|
-
const resolve15 = resolver(mapUrl, sourceRoot);
|
|
22254
|
-
this.resolvedSources = sources2.map(resolve15);
|
|
22255
|
-
const { mappings } = parsed;
|
|
22256
|
-
if (typeof mappings === "string") {
|
|
22257
|
-
this._encoded = mappings;
|
|
22258
|
-
this._decoded = void 0;
|
|
22259
|
-
} else if (Array.isArray(mappings)) {
|
|
22260
|
-
this._encoded = void 0;
|
|
22261
|
-
this._decoded = maybeSort(mappings, isString);
|
|
22262
|
-
} else if (parsed.sections) {
|
|
22263
|
-
throw new Error(`TraceMap passed sectioned source map, please use FlattenMap export instead`);
|
|
22264
|
-
} else {
|
|
22265
|
-
throw new Error(`invalid source map: ${JSON.stringify(parsed)}`);
|
|
22266
|
-
}
|
|
22267
|
-
this._decodedMemo = memoizedState();
|
|
22268
|
-
this._bySources = void 0;
|
|
22269
|
-
this._bySourceMemos = void 0;
|
|
22270
|
-
}
|
|
22271
|
-
};
|
|
22272
|
-
function cast(map) {
|
|
22273
|
-
return map;
|
|
22274
|
-
}
|
|
22275
|
-
function decodedMappings(map) {
|
|
22276
|
-
var _a;
|
|
22277
|
-
return (_a = cast(map))._decoded || (_a._decoded = decode(cast(map)._encoded));
|
|
22278
|
-
}
|
|
22279
|
-
function originalPositionFor(map, needle) {
|
|
22280
|
-
let { line, column, bias } = needle;
|
|
22281
|
-
line--;
|
|
22282
|
-
if (line < 0) throw new Error(LINE_GTR_ZERO);
|
|
22283
|
-
if (column < 0) throw new Error(COL_GTR_EQ_ZERO);
|
|
22284
|
-
const decoded = decodedMappings(map);
|
|
22285
|
-
if (line >= decoded.length) return OMapping(null, null, null, null);
|
|
22286
|
-
const segments = decoded[line];
|
|
22287
|
-
const index = traceSegmentInternal(
|
|
22288
|
-
segments,
|
|
22289
|
-
cast(map)._decodedMemo,
|
|
22290
|
-
line,
|
|
22291
|
-
column,
|
|
22292
|
-
bias || GREATEST_LOWER_BOUND
|
|
22293
|
-
);
|
|
22294
|
-
if (index === -1) return OMapping(null, null, null, null);
|
|
22295
|
-
const segment = segments[index];
|
|
22296
|
-
if (segment.length === 1) return OMapping(null, null, null, null);
|
|
22297
|
-
const { names, resolvedSources } = map;
|
|
22298
|
-
return OMapping(
|
|
22299
|
-
resolvedSources[segment[SOURCES_INDEX]],
|
|
22300
|
-
segment[SOURCE_LINE] + 1,
|
|
22301
|
-
segment[SOURCE_COLUMN],
|
|
22302
|
-
segment.length === 5 ? names[segment[NAMES_INDEX]] : null
|
|
22303
|
-
);
|
|
22304
|
-
}
|
|
22305
|
-
function sourceIndex(map, source) {
|
|
22306
|
-
const { sources: sources2, resolvedSources } = map;
|
|
22307
|
-
let index = sources2.indexOf(source);
|
|
22308
|
-
if (index === -1) index = resolvedSources.indexOf(source);
|
|
22309
|
-
return index;
|
|
22310
|
-
}
|
|
22311
|
-
function sourceContentFor(map, source) {
|
|
22312
|
-
const { sourcesContent } = map;
|
|
22313
|
-
if (sourcesContent == null) return null;
|
|
22314
|
-
const index = sourceIndex(map, source);
|
|
22315
|
-
return index === -1 ? null : sourcesContent[index];
|
|
22316
|
-
}
|
|
22317
|
-
function OMapping(source, line, column, name2) {
|
|
22318
|
-
return { source, line, column, name: name2 };
|
|
22319
|
-
}
|
|
22320
|
-
function traceSegmentInternal(segments, memo, line, column, bias) {
|
|
22321
|
-
let index = memoizedBinarySearch(segments, column, memo, line);
|
|
22322
|
-
if (found) {
|
|
22323
|
-
index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index);
|
|
22324
|
-
} else if (bias === LEAST_UPPER_BOUND) index++;
|
|
22325
|
-
if (index === -1 || index === segments.length) return -1;
|
|
22326
|
-
return index;
|
|
22327
|
-
}
|
|
22328
|
-
|
|
22329
|
-
// src/services/symbolicate.ts
|
|
22330
|
-
var CONTEXT = 3;
|
|
22331
|
-
async function symbolicateFrames(frames, lookup) {
|
|
22332
|
-
const cache4 = /* @__PURE__ */ new Map();
|
|
22333
|
-
const out3 = [];
|
|
22334
|
-
for (const f of frames) {
|
|
22335
|
-
const base = { ...f, symbolicated: false };
|
|
22336
|
-
if (!f.filename || !f.lineno) {
|
|
22337
|
-
out3.push(base);
|
|
22338
|
-
continue;
|
|
22339
|
-
}
|
|
22340
|
-
let tracer = cache4.get(f.filename);
|
|
22341
|
-
if (tracer === void 0) {
|
|
22342
|
-
const text = await lookup(f.filename);
|
|
22343
|
-
try {
|
|
22344
|
-
tracer = text ? new TraceMap(text) : null;
|
|
22345
|
-
} catch {
|
|
22346
|
-
tracer = null;
|
|
22347
|
-
}
|
|
22348
|
-
cache4.set(f.filename, tracer);
|
|
22349
|
-
}
|
|
22350
|
-
if (!tracer) {
|
|
22351
|
-
out3.push(base);
|
|
22352
|
-
continue;
|
|
22353
|
-
}
|
|
22354
|
-
try {
|
|
22355
|
-
const pos = originalPositionFor(tracer, { line: f.lineno, column: (f.colno ?? 1) - 1 });
|
|
22356
|
-
if (pos.source == null || pos.line == null) {
|
|
22357
|
-
out3.push(base);
|
|
22358
|
-
continue;
|
|
22359
|
-
}
|
|
22360
|
-
const content = sourceContentFor(tracer, pos.source);
|
|
22361
|
-
let contextLine, preContext, postContext;
|
|
22362
|
-
if (content != null) {
|
|
22363
|
-
const lines2 = content.split("\n");
|
|
22364
|
-
const idx = pos.line - 1;
|
|
22365
|
-
contextLine = lines2[idx];
|
|
22366
|
-
preContext = lines2.slice(Math.max(0, idx - CONTEXT), idx);
|
|
22367
|
-
postContext = lines2.slice(idx + 1, idx + 1 + CONTEXT);
|
|
22368
|
-
}
|
|
22369
|
-
out3.push({
|
|
22370
|
-
...f,
|
|
22371
|
-
function: pos.name ?? f.function,
|
|
22372
|
-
source: pos.source,
|
|
22373
|
-
sourceLine: pos.line,
|
|
22374
|
-
sourceColumn: pos.column ?? void 0,
|
|
22375
|
-
in_app: !/node_modules/.test(pos.source),
|
|
22376
|
-
contextLine,
|
|
22377
|
-
preContext,
|
|
22378
|
-
postContext,
|
|
22379
|
-
symbolicated: true
|
|
22380
|
-
});
|
|
22381
|
-
} catch {
|
|
22382
|
-
out3.push(base);
|
|
22383
|
-
}
|
|
22384
|
-
}
|
|
22385
|
-
return out3;
|
|
22386
|
-
}
|
|
22387
|
-
|
|
22388
|
-
// src/routes/errors.ts
|
|
22389
|
-
var groupView = (g) => ({
|
|
22390
|
-
id: g.id,
|
|
22391
|
-
projectId: g.projectId,
|
|
22392
|
-
type: g.type,
|
|
22393
|
-
message: g.message,
|
|
22394
|
-
environment: g.environment,
|
|
22395
|
-
status: g.status,
|
|
22396
|
-
count: g.count,
|
|
22397
|
-
firstSeenAt: g.firstSeenAt,
|
|
22398
|
-
lastSeenAt: g.lastSeenAt,
|
|
22399
|
-
specId: g.specId,
|
|
22400
|
-
release: g.release
|
|
22401
|
-
// SPEC-276 · korelasi build
|
|
22402
|
-
});
|
|
22403
|
-
var eventView = (e) => ({
|
|
22404
|
-
id: e.id,
|
|
22405
|
-
type: e.type,
|
|
22406
|
-
message: e.message,
|
|
22407
|
-
stack: e.stack,
|
|
22408
|
-
environment: e.environment,
|
|
22409
|
-
release: e.release,
|
|
22410
|
-
receivedAt: e.receivedAt
|
|
22411
|
-
});
|
|
22412
|
-
async function errors_default(app2) {
|
|
22413
|
-
app2.get("/errors/integration-guide", async (_req, reply) => {
|
|
22414
|
-
try {
|
|
22415
|
-
const text = await readFile6(join14(repoRoot(), "sdk", "README.md"), "utf8");
|
|
22416
|
-
return { text };
|
|
22417
|
-
} catch {
|
|
22418
|
-
return reply.code(404).send({ error: "panduan integrasi tidak ditemukan" });
|
|
22419
|
-
}
|
|
22420
|
-
});
|
|
22421
|
-
app2.get("/errors", async (req) => {
|
|
22422
|
-
const { project, environment, status, q, page, limit } = req.query;
|
|
22423
|
-
const where = {};
|
|
22424
|
-
if (project) where.projectId = project;
|
|
22425
|
-
if (environment) where.environment = environment;
|
|
22426
|
-
if (status) where.status = status;
|
|
22427
|
-
let groups = await prisma.errorGroup.findMany({ where, orderBy: { lastSeenAt: "desc" } });
|
|
22428
|
-
if (q) {
|
|
22429
|
-
const n = q.toLowerCase();
|
|
22430
|
-
groups = groups.filter((g) => `${g.type} ${g.message}`.toLowerCase().includes(n));
|
|
22431
|
-
}
|
|
22432
|
-
return paginate(groups.map(groupView), page, limit);
|
|
22433
|
-
});
|
|
22434
|
-
app2.get("/errors/:id", async (req, reply) => {
|
|
22435
|
-
const { id } = req.params;
|
|
22436
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22437
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22438
|
-
const events = await prisma.errorEvent.findMany({
|
|
22439
|
-
where: { groupId: id },
|
|
22440
|
-
orderBy: { receivedAt: "desc" },
|
|
22441
|
-
take: 50
|
|
22442
|
-
});
|
|
22443
|
-
const sampleFrames = Array.isArray(g.sampleFrames) ? await symbolicateFrames(
|
|
22444
|
-
g.sampleFrames,
|
|
22445
|
-
(fn) => findSourceMap(g.projectId, g.release ?? "", fn)
|
|
22446
|
-
) : null;
|
|
22447
|
-
return { ...groupView(g), sampleStack: g.sampleStack, sampleFrames, events: events.map(eventView) };
|
|
22448
|
-
});
|
|
22449
|
-
app2.post("/errors/:id/escalate", async (req, reply) => {
|
|
22450
|
-
const { id } = req.params;
|
|
22451
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22452
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22453
|
-
const { spec, created } = await escalateErrorGroup(g, { author: req.user?.email ?? "system" });
|
|
22454
|
-
return created ? reply.code(201).send({ spec }) : reply.code(200).send({ alreadyEscalated: true, spec });
|
|
22455
|
-
});
|
|
22456
|
-
app2.post("/errors/:id/unlink", async (req, reply) => {
|
|
22457
|
-
const { id } = req.params;
|
|
22458
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22459
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22460
|
-
const updated = await prisma.errorGroup.update({ where: { id }, data: { status: "new", specId: null } });
|
|
22461
|
-
await notifySynced("errorGroup", id);
|
|
22462
|
-
return { id: updated.id, status: updated.status, specId: updated.specId };
|
|
22463
|
-
});
|
|
22464
|
-
app2.patch("/errors/:id", async (req, reply) => {
|
|
22465
|
-
const { id } = req.params;
|
|
22466
|
-
const parsed = zErrorStatus.safeParse(req.body?.status);
|
|
22467
|
-
if (!parsed.success) return reply.code(400).send({ error: "status invalid" });
|
|
22468
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22469
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22470
|
-
const updated = await prisma.errorGroup.update({ where: { id }, data: { status: parsed.data } });
|
|
22471
|
-
await notifySynced("errorGroup", id);
|
|
22472
|
-
return { id: updated.id, status: updated.status };
|
|
22473
|
-
});
|
|
22474
|
-
app2.delete("/errors/:id", async (req, reply) => {
|
|
22475
|
-
const { id } = req.params;
|
|
22476
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22477
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22478
|
-
await prisma.errorGroup.delete({ where: { id } });
|
|
22479
|
-
return { ok: true };
|
|
22480
|
-
});
|
|
22481
|
-
}
|
|
22482
|
-
|
|
22483
|
-
// src/routes/help.ts
|
|
22484
|
-
init_zod();
|
|
22485
|
-
init_src();
|
|
22486
|
-
init_db();
|
|
22487
|
-
|
|
22488
|
-
// src/services/ticket.ts
|
|
22489
|
-
init_db();
|
|
22490
|
-
import { createHash as createHash4, randomBytes as randomBytes4 } from "node:crypto";
|
|
22491
|
-
init_src();
|
|
22492
|
-
function hashAccessKey(key) {
|
|
22493
|
-
return createHash4("sha256").update(key).digest("hex");
|
|
22494
|
-
}
|
|
22495
|
-
function generateAccessKey() {
|
|
22496
|
-
const key = "hnm_tkt_" + randomBytes4(24).toString("hex");
|
|
22497
|
-
return { key, hash: hashAccessKey(key) };
|
|
22498
|
-
}
|
|
22499
|
-
function generateShareToken() {
|
|
22500
|
-
return "hnm_shr_" + randomBytes4(24).toString("hex");
|
|
22501
|
-
}
|
|
22502
|
-
async function createTicket(input) {
|
|
22503
|
-
const { key, hash: hash2 } = generateAccessKey();
|
|
22504
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
22505
|
-
const max = await prisma.ticket.aggregate({ where: { projectId: input.projectId }, _max: { number: true } });
|
|
22506
|
-
const number = (max._max.number ?? 0) + 1;
|
|
22507
|
-
try {
|
|
22508
|
-
const ticket = await prisma.ticket.create({
|
|
22509
|
-
data: { ...input, number, accessKeyHash: hash2, shareToken: generateShareToken(), status: "new" }
|
|
22510
|
-
});
|
|
22511
|
-
return { ticket, key };
|
|
22512
|
-
} catch (e) {
|
|
22513
|
-
if (e.code === "P2002" && attempt < 2) continue;
|
|
22514
|
-
throw e;
|
|
22515
|
-
}
|
|
22516
|
-
}
|
|
22517
|
-
throw new Error("gagal membuat tiket (nomor bentrok)");
|
|
22518
|
-
}
|
|
22519
|
-
async function pruneOldTickets(now = Date.now()) {
|
|
22520
|
-
const days = Number(process.env.HANOMAN_TICKET_RETENTION_DAYS ?? 90);
|
|
22521
|
-
const cutoff = new Date(now - days * 864e5);
|
|
22522
|
-
const stale = await prisma.ticket.findMany({
|
|
22523
|
-
where: { status: "rejected", specId: null, createdAt: { lt: cutoff } },
|
|
22524
|
-
include: { attachments: true },
|
|
22525
|
-
take: 50
|
|
22526
|
-
});
|
|
22527
|
-
for (const t of stale) {
|
|
22528
|
-
for (const a of t.attachments) await deleteUpload(a.storageKey);
|
|
22529
|
-
await prisma.ticket.delete({ where: { id: t.id } });
|
|
22530
|
-
}
|
|
21466
|
+
for (const t of stale) {
|
|
21467
|
+
for (const a of t.attachments) await deleteUpload(a.storageKey);
|
|
21468
|
+
await prisma.ticket.delete({ where: { id: t.id } });
|
|
21469
|
+
}
|
|
22531
21470
|
}
|
|
22532
21471
|
|
|
22533
21472
|
// src/services/help-ratelimit.ts
|
|
@@ -22646,7 +21585,7 @@ init_db();
|
|
|
22646
21585
|
|
|
22647
21586
|
// src/services/ticket-accept.ts
|
|
22648
21587
|
init_db();
|
|
22649
|
-
import { join as
|
|
21588
|
+
import { join as join13 } from "node:path";
|
|
22650
21589
|
async function materializeAttachments(atts) {
|
|
22651
21590
|
const ready = /* @__PURE__ */ new Set();
|
|
22652
21591
|
for (const a of atts) {
|
|
@@ -22661,7 +21600,7 @@ async function materializeAttachments(atts) {
|
|
|
22661
21600
|
var attachmentInstruction = (t, atts, ready) => {
|
|
22662
21601
|
if (atts.length === 0) return "Tanpa lampiran.";
|
|
22663
21602
|
const list2 = atts.map(
|
|
22664
|
-
(a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${
|
|
21603
|
+
(a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${join13(uploadDir(), a.storageKey)}` : `- ${a.filename} (${a.mimeType}) \u2192 BELUM TERUNDUH, ambil via GET /api/tickets/${t.id}/attachments/${a.id}`
|
|
22665
21604
|
).join("\n");
|
|
22666
21605
|
return `LAMPIRAN (${atts.length}) dari pelapor \u2014 biasanya screenshot yang menunjukkan masalah. PERIKSA setiap lampiran untuk memahami konteks keluhan sebelum bekerja; jangan berasumsi dari teks saja. Berkas ada di direktori upload server (baca langsung dengan tool Read):
|
|
22667
21606
|
${list2}
|
|
@@ -22859,6 +21798,7 @@ async function setScheduler(next) {
|
|
|
22859
21798
|
// src/services/scheduler/queue.ts
|
|
22860
21799
|
init_db();
|
|
22861
21800
|
var RANK = { tinggi: 0, sedang: 1, rendah: 2 };
|
|
21801
|
+
var UNSTARTED_SPEC_WHERE = { baseSha: null, stage: { not: "done" } };
|
|
22862
21802
|
async function enqueue(i) {
|
|
22863
21803
|
await prisma.schedulerQueueItem.upsert({
|
|
22864
21804
|
where: { specId: i.specId },
|
|
@@ -22879,8 +21819,11 @@ async function markLaunched(id, sessionId2) {
|
|
|
22879
21819
|
async function markFailed(id, note) {
|
|
22880
21820
|
await prisma.schedulerQueueItem.update({ where: { id }, data: { status: "failed", note: note ?? null } });
|
|
22881
21821
|
}
|
|
22882
|
-
async function markDone(id) {
|
|
22883
|
-
await prisma.schedulerQueueItem.update({
|
|
21822
|
+
async function markDone(id, note) {
|
|
21823
|
+
await prisma.schedulerQueueItem.update({
|
|
21824
|
+
where: { id },
|
|
21825
|
+
data: { status: "done", ...note ? { note } : {} }
|
|
21826
|
+
});
|
|
22884
21827
|
}
|
|
22885
21828
|
|
|
22886
21829
|
// src/services/scheduler/registry.ts
|
|
@@ -22929,7 +21872,6 @@ async function scheduler_default(app2) {
|
|
|
22929
21872
|
};
|
|
22930
21873
|
const sources2 = [
|
|
22931
21874
|
srcView("backlog", cfg.sources.backlog),
|
|
22932
|
-
srcView("errors", cfg.sources.errors, cfg.sources.errors.minCount),
|
|
22933
21875
|
srcView("triase", cfg.sources.triase)
|
|
22934
21876
|
];
|
|
22935
21877
|
const launchedSpecs = new Set(queue.filter((q) => q.status === "launched").map((q) => q.specId));
|
|
@@ -22938,111 +21880,1074 @@ async function scheduler_default(app2) {
|
|
|
22938
21880
|
});
|
|
22939
21881
|
}
|
|
22940
21882
|
|
|
22941
|
-
// src/routes/
|
|
21883
|
+
// src/routes/lead.ts
|
|
21884
|
+
init_src();
|
|
22942
21885
|
init_db();
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
22948
|
-
|
|
22949
|
-
|
|
22950
|
-
return isNaN(d.getTime()) ? null : d;
|
|
22951
|
-
}
|
|
22952
|
-
var groupView2 = (g) => ({
|
|
22953
|
-
id: g.id,
|
|
22954
|
-
projectId: g.projectId,
|
|
22955
|
-
type: g.type,
|
|
22956
|
-
message: g.message,
|
|
22957
|
-
environment: g.environment,
|
|
22958
|
-
release: g.release,
|
|
22959
|
-
status: g.status,
|
|
22960
|
-
count: g.count,
|
|
22961
|
-
firstSeenAt: g.firstSeenAt,
|
|
22962
|
-
lastSeenAt: g.lastSeenAt,
|
|
22963
|
-
specId: g.specId
|
|
22964
|
-
});
|
|
22965
|
-
var timelineView = (e) => ({
|
|
22966
|
-
at: e.receivedAt,
|
|
22967
|
-
projectId: e.projectId,
|
|
22968
|
-
groupId: e.groupId,
|
|
22969
|
-
type: e.type,
|
|
22970
|
-
message: e.message,
|
|
22971
|
-
environment: e.environment,
|
|
22972
|
-
release: e.release
|
|
22973
|
-
});
|
|
22974
|
-
async function scopeFor(req) {
|
|
22975
|
-
const s = auditScopeFromReq(req);
|
|
22976
|
-
if (s) return s;
|
|
22977
|
-
return (await prisma.project.findMany({ select: { id: true } })).map((p) => p.id);
|
|
21886
|
+
init_pty();
|
|
21887
|
+
|
|
21888
|
+
// src/services/lead/config.ts
|
|
21889
|
+
init_db();
|
|
21890
|
+
init_src();
|
|
21891
|
+
async function getLead() {
|
|
21892
|
+
return (await getSetting()).lead;
|
|
22978
21893
|
}
|
|
22979
|
-
async function
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
|
|
22991
|
-
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
21894
|
+
async function setLead(next) {
|
|
21895
|
+
const cur = await getSetting();
|
|
21896
|
+
const data = { ...cur, lead: next };
|
|
21897
|
+
await prisma.setting.upsert({ where: { id: 1 }, update: { data }, create: { id: 1, data } });
|
|
21898
|
+
return next;
|
|
21899
|
+
}
|
|
21900
|
+
async function leadAgentDefaults() {
|
|
21901
|
+
const s = await getSetting();
|
|
21902
|
+
const e = s.lead.engine;
|
|
21903
|
+
if (!e.enabled) return sessionAgentDefaults();
|
|
21904
|
+
return e.agent === "codex" ? { agent: "codex", model: e.model, effort: coerceCodexEffort(e.model, e.effort) } : { agent: "claude", model: e.model, effort: e.effort };
|
|
21905
|
+
}
|
|
21906
|
+
function leadActive(cfg, projectId) {
|
|
21907
|
+
if (!cfg.enabled || cfg.paused) return false;
|
|
21908
|
+
if (projectId && cfg.pausedProjects.includes(projectId)) return false;
|
|
21909
|
+
return true;
|
|
21910
|
+
}
|
|
21911
|
+
async function leadProjects() {
|
|
21912
|
+
const rows = await prisma.project.findMany({ where: { leadOptIn: true }, select: { id: true } });
|
|
21913
|
+
return rows.map((r) => r.id);
|
|
21914
|
+
}
|
|
21915
|
+
|
|
21916
|
+
// src/services/lead/decide.ts
|
|
21917
|
+
init_src();
|
|
21918
|
+
init_db();
|
|
21919
|
+
init_pty();
|
|
21920
|
+
|
|
21921
|
+
// src/services/lead/prompt.ts
|
|
21922
|
+
init_src();
|
|
21923
|
+
var bullet2 = (s) => `- ${s}`;
|
|
21924
|
+
function leadPrompt(q, c) {
|
|
21925
|
+
const lines2 = [];
|
|
21926
|
+
lines2.push("Kamu adalah **hanoman-lead**: tech lead mesin di atas semua agen yang bekerja di workspace ini.");
|
|
21927
|
+
lines2.push("Kamu MEMUTUSKAN, lalu melapor. Tidak ada manusia yang menunggu untuk menyetujui jawabanmu.");
|
|
21928
|
+
lines2.push("");
|
|
21929
|
+
lines2.push("## Konteks");
|
|
21930
|
+
lines2.push(bullet2(`Project: ${c.projectName} (${c.projectId})`));
|
|
21931
|
+
if (c.repoDir) lines2.push(bullet2(`Checkout: ${c.repoDir}`));
|
|
21932
|
+
if (c.spec) {
|
|
21933
|
+
lines2.push(bullet2(`Backlog item: ${c.spec.id} \u2014 ${c.spec.title} \xB7 stage ${c.spec.stage} \xB7 prioritas ${c.spec.priority}`));
|
|
21934
|
+
if (c.spec.objective) lines2.push(bullet2(`Objective: ${c.spec.objective}`));
|
|
21935
|
+
}
|
|
21936
|
+
for (const s of c.liveSessions ?? []) {
|
|
21937
|
+
lines2.push(bullet2(`Sesi berjalan: ${s.id}${s.specId ? ` (${s.specId})` : ""}${s.flow ? ` \xB7 flow ${s.flow}` : ""}${s.branch ? ` \xB7 branch ${s.branch}` : ""}`));
|
|
21938
|
+
}
|
|
21939
|
+
for (const n of c.notes ?? []) lines2.push(bullet2(n));
|
|
21940
|
+
lines2.push("");
|
|
21941
|
+
if (c.priorDecisions?.length) {
|
|
21942
|
+
lines2.push("## Keputusan yang sudah kamu ambil sebelumnya (jangan bertentangan tanpa alasan)");
|
|
21943
|
+
for (const d of c.priorDecisions.slice(0, 10)) {
|
|
21944
|
+
lines2.push(`- [${d.createdAt}] "${d.question.slice(0, 200)}" \u2192 ${d.answer.slice(0, 300)} (${d.reason.slice(0, 200)})`);
|
|
21945
|
+
}
|
|
21946
|
+
lines2.push("");
|
|
21947
|
+
}
|
|
21948
|
+
lines2.push("## Yang harus kamu putuskan");
|
|
21949
|
+
lines2.push(q.question.trim());
|
|
21950
|
+
if (q.options?.length) {
|
|
21951
|
+
lines2.push("");
|
|
21952
|
+
lines2.push("Opsi yang dilihat peminta:");
|
|
21953
|
+
for (const [i, o] of q.options.entries()) lines2.push(`${i + 1}. ${o}`);
|
|
21954
|
+
}
|
|
21955
|
+
lines2.push("");
|
|
21956
|
+
lines2.push("## Batas waktu (BACA INI DULU)");
|
|
21957
|
+
lines2.push(`Kamu punya **${c.timeoutSec} detik** sejak sekarang. Lewat dari itu prosesmu dihentikan paksa, keluaranmu DIBUANG, dan permintaan ini dicatat sebagai kegagalan \u2014 peminta kembali mandek menunggu manusia. Jadi keputusan tepat waktu di atas bukti secukupnya jauh lebih berguna daripada pembacaan lengkap yang tak pernah sampai.`);
|
|
21958
|
+
lines2.push(`Anggarkan begini: pakai paling banyak separuh waktu untuk mengumpulkan bukti, lalu **berhenti membaca** dan tulis jawabannya dengan apa yang sudah kamu punya. Kalau buktinya jadi tipis karena itu, turunkan \`confidence\`-nya \u2014 jangan menambah waktu baca.`);
|
|
21959
|
+
lines2.push("");
|
|
21960
|
+
lines2.push("## Cara kerja");
|
|
21961
|
+
lines2.push("1. KUMPULKAN BUKTI DULU sebelum memutuskan, DI DALAM anggaran waktu di atas: `internal/docs/**` (Source of Truth) dan index-nya, ADR yang relevan, plan `docs/superpowers/plans/**`, kode yang bersangkutan, dan riwayat git. Baca, jangan mengingat \u2014 tapi baca seperlunya, bukan sehabisnya.");
|
|
21962
|
+
lines2.push('2. Putuskan. Kalau setelah membaca kamu masih ragu, TETAP putuskan: pilih opsi yang PALING MUDAH DIBATALKAN, lalu tandai `confidence: "ragu"`. Jangan pernah menjawab "tidak tahu" atau meminta manusia memutuskan \u2014 itu persis keadaan yang kamu ada untuk menghapusnya.');
|
|
21963
|
+
lines2.push("3. Rujuk bukti yang BENAR-BENAR kamu baca. Rujukan berupa path berkas relatif terhadap checkout, nomor ADR (`ADR-0091`), atau sha commit. Rujukan yang tak ada di repo akan dibuang server dan membuat jawabanmu tampak tanpa dasar.");
|
|
21964
|
+
lines2.push("4. JANGAN membaca atau mengutip kredensial (isi `.env*`, token, kunci privat). Jejak keputusan disimpan di basis data; rahasia tak boleh mendarat di sana.");
|
|
21965
|
+
lines2.push("5. Kamu TIDAK mengeksekusi apa pun sendiri. Kamu mengusulkan satu `action`; server yang menjalankannya, dan hanya bila ia ada di daftar tertutup ini:");
|
|
21966
|
+
lines2.push(` ${LEAD_ACTIONS.join(" \xB7 ")}`);
|
|
21967
|
+
lines2.push(" Deploy, perintah/konsol VPS, data produksi, dan penghapusan apa pun (project, backlog, branch, worktree, notifikasi, jejak) TERKUNCI dan tidak akan pernah dijalankan.");
|
|
21968
|
+
lines2.push("");
|
|
21969
|
+
lines2.push("## Bentuk jawaban (WAJIB)");
|
|
21970
|
+
lines2.push("Akhiri jawabanmu dengan TEPAT SATU blok berikut, tanpa teks sesudahnya:");
|
|
21971
|
+
lines2.push("");
|
|
21972
|
+
lines2.push("```json");
|
|
21973
|
+
lines2.push(JSON.stringify({
|
|
21974
|
+
decision: "keputusan yang dipilih, satu kalimat",
|
|
21975
|
+
reason: "alasannya, menyebut bukti",
|
|
21976
|
+
refs: ["internal/docs/\u2026", "ADR-00xx"],
|
|
21977
|
+
confidence: "tinggi | sedang | ragu",
|
|
21978
|
+
action: "none",
|
|
21979
|
+
reply: "teks yang akan diketikkan ke terminal agen peminta (kosongkan bila tak relevan)"
|
|
21980
|
+
}, null, 2));
|
|
21981
|
+
lines2.push("```");
|
|
21982
|
+
return lines2.join("\n");
|
|
21983
|
+
}
|
|
21984
|
+
|
|
21985
|
+
// src/services/lead/verdict.ts
|
|
21986
|
+
init_src();
|
|
21987
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
21988
|
+
import { isAbsolute as isAbsolute3, resolve as resolvePath, relative } from "node:path";
|
|
21989
|
+
function extractJsonBlock(raw) {
|
|
21990
|
+
const re = /```json\s*\n([\s\S]*?)```/g;
|
|
21991
|
+
let last = null;
|
|
21992
|
+
for (const m of raw.matchAll(re)) if (m[1]) last = m[1];
|
|
21993
|
+
if (last) return last;
|
|
21994
|
+
const t = raw.trim();
|
|
21995
|
+
return t.startsWith("{") && t.endsWith("}") ? t : null;
|
|
21996
|
+
}
|
|
21997
|
+
function parseLeadVerdict(raw) {
|
|
21998
|
+
const block2 = extractJsonBlock(raw);
|
|
21999
|
+
if (!block2) return null;
|
|
22000
|
+
let data;
|
|
22001
|
+
try {
|
|
22002
|
+
data = JSON.parse(block2);
|
|
22003
|
+
} catch {
|
|
22004
|
+
return null;
|
|
22005
|
+
}
|
|
22006
|
+
const parsed = zLeadVerdict.safeParse(data);
|
|
22007
|
+
return parsed.success ? parsed.data : null;
|
|
22008
|
+
}
|
|
22009
|
+
var SHA = /^[0-9a-f]{7,40}$/i;
|
|
22010
|
+
var isNonFileRef = (ref) => SHA.test(ref) || /^ADR-\d{4}$/i.test(ref);
|
|
22011
|
+
function keepExistingRefs(refs2, repoDir) {
|
|
22012
|
+
const out3 = [];
|
|
22013
|
+
for (const raw of refs2) {
|
|
22014
|
+
const ref = raw.trim();
|
|
22015
|
+
if (!ref) continue;
|
|
22016
|
+
if (isNonFileRef(ref)) {
|
|
22017
|
+
out3.push(ref);
|
|
22018
|
+
continue;
|
|
22019
|
+
}
|
|
22020
|
+
if (!repoDir) continue;
|
|
22021
|
+
if (isAbsolute3(ref)) continue;
|
|
22022
|
+
const full = resolvePath(repoDir, ref);
|
|
22023
|
+
const rel = relative(repoDir, full);
|
|
22024
|
+
if (rel.startsWith("..")) continue;
|
|
22025
|
+
if (existsSync10(full)) out3.push(ref);
|
|
22026
|
+
}
|
|
22027
|
+
return [...new Set(out3)];
|
|
22028
|
+
}
|
|
22029
|
+
|
|
22030
|
+
// src/services/lead/trail.ts
|
|
22031
|
+
init_db();
|
|
22032
|
+
async function recordDecision(i) {
|
|
22033
|
+
return prisma.leadDecision.create({
|
|
22034
|
+
data: {
|
|
22035
|
+
projectId: i.projectId,
|
|
22036
|
+
specId: i.specId ?? null,
|
|
22037
|
+
sessionId: i.sessionId ?? null,
|
|
22038
|
+
gate: i.gate,
|
|
22039
|
+
kind: i.kind,
|
|
22040
|
+
question: i.question,
|
|
22041
|
+
answer: i.answer,
|
|
22042
|
+
reason: i.reason,
|
|
22043
|
+
refs: i.refs,
|
|
22044
|
+
confidence: i.confidence,
|
|
22045
|
+
action: i.action,
|
|
22046
|
+
status: i.status ?? "berlaku",
|
|
22047
|
+
weighty: i.weighty ?? false,
|
|
22048
|
+
actor: i.actor ?? "lead"
|
|
22049
|
+
}
|
|
22050
|
+
});
|
|
22051
|
+
}
|
|
22052
|
+
async function listDecisions(f = {}) {
|
|
22053
|
+
return prisma.leadDecision.findMany({
|
|
22054
|
+
where: {
|
|
22055
|
+
...f.projectId ? { projectId: f.projectId } : {},
|
|
22056
|
+
...f.specId ? { specId: f.specId } : {},
|
|
22057
|
+
...f.sessionId ? { sessionId: f.sessionId } : {},
|
|
22058
|
+
...f.status ? { status: f.status } : {}
|
|
22059
|
+
},
|
|
22060
|
+
orderBy: { createdAt: "desc" },
|
|
22061
|
+
take: Math.min(f.take ?? 50, 200),
|
|
22062
|
+
skip: f.skip ?? 0
|
|
22063
|
+
});
|
|
22064
|
+
}
|
|
22065
|
+
async function overrideDecision(id, answer, reason) {
|
|
22066
|
+
const old = await prisma.leadDecision.findUnique({ where: { id } });
|
|
22067
|
+
if (!old || old.status !== "berlaku") return null;
|
|
22068
|
+
const next = await recordDecision({
|
|
22069
|
+
projectId: old.projectId,
|
|
22070
|
+
specId: old.specId,
|
|
22071
|
+
sessionId: old.sessionId,
|
|
22072
|
+
gate: old.gate,
|
|
22073
|
+
kind: old.kind,
|
|
22074
|
+
question: old.question,
|
|
22075
|
+
answer,
|
|
22076
|
+
reason: reason || "ditimpa operator",
|
|
22077
|
+
refs: [],
|
|
22078
|
+
confidence: "tinggi",
|
|
22079
|
+
action: "none",
|
|
22080
|
+
actor: "operator"
|
|
22081
|
+
});
|
|
22082
|
+
const updated = await prisma.leadDecision.update({
|
|
22083
|
+
where: { id },
|
|
22084
|
+
data: { status: "ditimpa", supersededById: next.id }
|
|
22085
|
+
});
|
|
22086
|
+
return { old: updated, next };
|
|
22087
|
+
}
|
|
22088
|
+
async function cancelDecision(id) {
|
|
22089
|
+
const row = await prisma.leadDecision.findUnique({ where: { id } });
|
|
22090
|
+
if (!row || row.status !== "berlaku") return null;
|
|
22091
|
+
return prisma.leadDecision.update({ where: { id }, data: { status: "dibatalkan" } });
|
|
22092
|
+
}
|
|
22093
|
+
function toDecisionView(r) {
|
|
22094
|
+
return {
|
|
22095
|
+
id: r.id,
|
|
22096
|
+
projectId: r.projectId,
|
|
22097
|
+
specId: r.specId,
|
|
22098
|
+
sessionId: r.sessionId,
|
|
22099
|
+
gate: r.gate,
|
|
22100
|
+
kind: r.kind,
|
|
22101
|
+
question: r.question,
|
|
22102
|
+
answer: r.answer,
|
|
22103
|
+
reason: r.reason,
|
|
22104
|
+
refs: Array.isArray(r.refs) ? r.refs.map(String) : [],
|
|
22105
|
+
confidence: r.confidence,
|
|
22106
|
+
action: r.action,
|
|
22107
|
+
status: r.status,
|
|
22108
|
+
weighty: r.weighty,
|
|
22109
|
+
supersededById: r.supersededById,
|
|
22110
|
+
createdAt: r.createdAt.toISOString()
|
|
22111
|
+
};
|
|
22112
|
+
}
|
|
22113
|
+
|
|
22114
|
+
// src/services/lead/brain.ts
|
|
22115
|
+
init_config2();
|
|
22116
|
+
import { execFile as execFile9 } from "node:child_process";
|
|
22117
|
+
var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
22118
|
+
function leadArgv(o) {
|
|
22119
|
+
if (o.agent === "codex") {
|
|
22120
|
+
return [
|
|
22121
|
+
"exec",
|
|
22122
|
+
...o.model ? ["-m", o.model] : [],
|
|
22123
|
+
...o.effort ? ["-c", `model_reasoning_effort="${o.effort}"`] : [],
|
|
22124
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
22125
|
+
o.prompt
|
|
22126
|
+
];
|
|
22127
|
+
}
|
|
22128
|
+
return [
|
|
22129
|
+
"-p",
|
|
22130
|
+
...o.model ? ["--model", o.model] : [],
|
|
22131
|
+
...o.effort ? ["--effort", o.effort] : [],
|
|
22132
|
+
"--dangerously-skip-permissions",
|
|
22133
|
+
o.prompt
|
|
22134
|
+
];
|
|
22135
|
+
}
|
|
22136
|
+
function think(prompt, o) {
|
|
22137
|
+
const bin = binFor(o.agent);
|
|
22138
|
+
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
22139
|
+
return new Promise((resolve14, reject) => {
|
|
22140
|
+
execFile9(bin, args, {
|
|
22141
|
+
cwd: o.cwd,
|
|
22142
|
+
timeout: o.timeoutMs,
|
|
22143
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
22144
|
+
encoding: "utf8",
|
|
22145
|
+
killSignal: "SIGTERM"
|
|
22146
|
+
}, (err, stdout, stderr) => {
|
|
22147
|
+
if (err) {
|
|
22148
|
+
const killed = err.killed;
|
|
22149
|
+
reject(new Error(killed ? `lead ${o.agent} kehabisan waktu ${o.timeoutMs} ms` : `lead ${o.agent} gagal: ${(stderr || err.message).trim().slice(0, 500)}`));
|
|
22150
|
+
return;
|
|
22151
|
+
}
|
|
22152
|
+
resolve14(stdout);
|
|
23001
22153
|
});
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
22154
|
+
});
|
|
22155
|
+
}
|
|
22156
|
+
|
|
22157
|
+
// src/services/lead/decide.ts
|
|
22158
|
+
var prodDecideDeps = {
|
|
22159
|
+
think,
|
|
22160
|
+
defaults: leadAgentDefaults,
|
|
22161
|
+
repoDir: resolveRepoDir,
|
|
22162
|
+
liveSessions: () => {
|
|
22163
|
+
try {
|
|
22164
|
+
return listSessions();
|
|
22165
|
+
} catch {
|
|
22166
|
+
return [];
|
|
22167
|
+
}
|
|
22168
|
+
},
|
|
22169
|
+
notify: recordLeadDecision
|
|
22170
|
+
};
|
|
22171
|
+
var notifTitle = (kind, question, answer, confidence) => {
|
|
22172
|
+
const q = question.replace(/\s+/g, " ").trim().slice(0, 70);
|
|
22173
|
+
const a = answer.replace(/\s+/g, " ").trim().slice(0, 90);
|
|
22174
|
+
const tag2 = confidence === "ragu" ? "Lead (ragu)" : "Lead";
|
|
22175
|
+
return `${tag2} \xB7 ${kind}: ${q ? `"${q}" \u2192 ` : ""}${a}`;
|
|
22176
|
+
};
|
|
22177
|
+
async function decide(req, deps = prodDecideDeps) {
|
|
22178
|
+
const cfg = await getLead();
|
|
22179
|
+
if (!leadActive(cfg, req.projectId)) return null;
|
|
22180
|
+
const project = await prisma.project.findUnique({ where: { id: req.projectId }, select: { name: true } });
|
|
22181
|
+
const spec = req.specId ? await prisma.spec.findUnique({
|
|
22182
|
+
where: { id: req.specId },
|
|
22183
|
+
select: { id: true, title: true, objective: true, stage: true, priority: true }
|
|
22184
|
+
}) : null;
|
|
22185
|
+
const repoDir = await deps.repoDir(req.projectId).catch(() => null);
|
|
22186
|
+
const prior = await prisma.leadDecision.findMany({
|
|
22187
|
+
where: { projectId: req.projectId, status: "berlaku" },
|
|
22188
|
+
orderBy: { createdAt: "desc" },
|
|
22189
|
+
take: 10
|
|
22190
|
+
});
|
|
22191
|
+
const ctx = {
|
|
22192
|
+
projectId: req.projectId,
|
|
22193
|
+
projectName: project?.name ?? req.projectId,
|
|
22194
|
+
repoDir,
|
|
22195
|
+
// SPEC-432 · anggaran yang disebut prompt WAJIB berasal dari cfg yang sama yang dipakai
|
|
22196
|
+
// `think()` di bawah. Dua sumber angka akan berselisih diam-diam begitu operator menggeser
|
|
22197
|
+
// knob-nya, dan agen yang dianggarkan salah gagal persis seperti agen yang tak dianggarkan.
|
|
22198
|
+
timeoutSec: cfg.timeoutSec,
|
|
22199
|
+
spec,
|
|
22200
|
+
liveSessions: deps.liveSessions().filter((s) => !s.exited && s.projectId === req.projectId && s.id !== req.sessionId).map((s) => ({ id: s.id, specId: s.specId, flow: s.flow, branch: s.branch })),
|
|
22201
|
+
priorDecisions: prior.map((d) => ({
|
|
22202
|
+
question: d.question,
|
|
22203
|
+
answer: d.answer,
|
|
22204
|
+
reason: d.reason,
|
|
22205
|
+
createdAt: d.createdAt.toISOString()
|
|
22206
|
+
})),
|
|
22207
|
+
notes: req.notes
|
|
22208
|
+
};
|
|
22209
|
+
const { agent, model, effort } = await deps.defaults();
|
|
22210
|
+
const prompt = leadPrompt({ kind: req.kind, question: req.question, options: req.options }, ctx);
|
|
22211
|
+
if (req.sessionId) markDeciding(req.sessionId);
|
|
22212
|
+
let raw;
|
|
22213
|
+
try {
|
|
22214
|
+
raw = await deps.think(prompt, { agent, model, effort, cwd: repoDir ?? void 0, timeoutMs: cfg.timeoutSec * 1e3 });
|
|
22215
|
+
} catch (e) {
|
|
22216
|
+
return fail(req, deps, `lead tak menghasilkan keputusan: ${e.message}`);
|
|
22217
|
+
} finally {
|
|
22218
|
+
if (req.sessionId) clearDeciding(req.sessionId);
|
|
22219
|
+
}
|
|
22220
|
+
const verdict = parseLeadVerdict(raw);
|
|
22221
|
+
if (!verdict) return fail(req, deps, "keluaran lead tak memuat blok json keputusan yang sah");
|
|
22222
|
+
const refs2 = keepExistingRefs(verdict.refs, repoDir);
|
|
22223
|
+
const allowed = leadActionAllowed(verdict.action);
|
|
22224
|
+
const action = allowed ? verdict.action : "none";
|
|
22225
|
+
const kind = allowed ? req.kind : "refusal";
|
|
22226
|
+
const reason = allowed ? verdict.reason : `${verdict.reason}
|
|
22227
|
+
|
|
22228
|
+
DITOLAK: ${leadRefusalReason(verdict.action)} berada di luar permukaan tindakan lead (ADR-0091 \xB7 AC-31/32).`;
|
|
22229
|
+
const weighty = isWeightyDecision({ kind, action, confidence: verdict.confidence });
|
|
22230
|
+
const row = await recordDecision({
|
|
22231
|
+
projectId: req.projectId,
|
|
22232
|
+
specId: req.specId,
|
|
22233
|
+
sessionId: req.sessionId,
|
|
22234
|
+
gate: req.gate,
|
|
22235
|
+
kind,
|
|
22236
|
+
question: req.question,
|
|
22237
|
+
answer: verdict.decision,
|
|
22238
|
+
reason,
|
|
22239
|
+
refs: refs2,
|
|
22240
|
+
confidence: verdict.confidence,
|
|
22241
|
+
action,
|
|
22242
|
+
weighty
|
|
22243
|
+
});
|
|
22244
|
+
if (weighty) {
|
|
22245
|
+
await deps.notify(
|
|
22246
|
+
row.id,
|
|
22247
|
+
notifTitle(kind, req.question, verdict.decision, verdict.confidence),
|
|
22248
|
+
req.projectId,
|
|
22249
|
+
req.specId ?? null,
|
|
22250
|
+
req.sessionId ?? null
|
|
22251
|
+
);
|
|
22252
|
+
}
|
|
22253
|
+
lastReply.set(row.id, verdict.reply || verdict.decision);
|
|
22254
|
+
return row;
|
|
22255
|
+
}
|
|
22256
|
+
var lastReply = /* @__PURE__ */ new Map();
|
|
22257
|
+
function takeReply(decisionId) {
|
|
22258
|
+
const v = lastReply.get(decisionId) ?? "";
|
|
22259
|
+
lastReply.delete(decisionId);
|
|
22260
|
+
return v;
|
|
22261
|
+
}
|
|
22262
|
+
async function fail(req, deps, reason) {
|
|
22263
|
+
const prev = await prisma.leadDecision.findFirst({
|
|
22264
|
+
where: { projectId: req.projectId, gate: req.gate, kind: req.kind },
|
|
22265
|
+
orderBy: { createdAt: "desc" },
|
|
22266
|
+
select: { status: true }
|
|
22267
|
+
});
|
|
22268
|
+
const row = await recordDecision({
|
|
22269
|
+
projectId: req.projectId,
|
|
22270
|
+
specId: req.specId,
|
|
22271
|
+
sessionId: req.sessionId,
|
|
22272
|
+
gate: req.gate,
|
|
22273
|
+
kind: req.kind,
|
|
22274
|
+
question: req.question,
|
|
22275
|
+
answer: "",
|
|
22276
|
+
reason,
|
|
22277
|
+
refs: [],
|
|
22278
|
+
confidence: "ragu",
|
|
22279
|
+
action: "none",
|
|
22280
|
+
status: "gagal",
|
|
22281
|
+
weighty: true
|
|
22282
|
+
});
|
|
22283
|
+
if (prev?.status !== "gagal") {
|
|
22284
|
+
await deps.notify(
|
|
22285
|
+
row.id,
|
|
22286
|
+
`Lead gagal memutuskan: ${req.question.replace(/\s+/g, " ").slice(0, 90)}`,
|
|
22287
|
+
req.projectId,
|
|
22288
|
+
req.specId ?? null,
|
|
22289
|
+
req.sessionId ?? null
|
|
22290
|
+
);
|
|
22291
|
+
}
|
|
22292
|
+
return row;
|
|
22293
|
+
}
|
|
22294
|
+
|
|
22295
|
+
// src/services/lead/apply.ts
|
|
22296
|
+
init_src();
|
|
22297
|
+
init_db();
|
|
22298
|
+
init_pty();
|
|
22299
|
+
init_session_phases();
|
|
22300
|
+
var prodApplyDeps = {
|
|
22301
|
+
killSession: (id) => {
|
|
22302
|
+
try {
|
|
22303
|
+
return killSession(id);
|
|
22304
|
+
} catch {
|
|
22305
|
+
return false;
|
|
22306
|
+
}
|
|
22307
|
+
},
|
|
22308
|
+
sessionExists: (id) => {
|
|
22309
|
+
try {
|
|
22310
|
+
return !!getSession(id);
|
|
22311
|
+
} catch {
|
|
22312
|
+
return false;
|
|
22313
|
+
}
|
|
22314
|
+
},
|
|
22315
|
+
startSpec: startSpecSession,
|
|
22316
|
+
repoDir: resolveRepoDir,
|
|
22317
|
+
planDone: planComplete,
|
|
22318
|
+
integrate,
|
|
22319
|
+
notify: recordLeadDecision
|
|
22320
|
+
};
|
|
22321
|
+
async function applyAction(row, deps = prodApplyDeps) {
|
|
22322
|
+
const action = row.action;
|
|
22323
|
+
if (!leadActionAllowed(action)) {
|
|
22324
|
+
await deps.notify(
|
|
22325
|
+
row.id,
|
|
22326
|
+
`Lead menolak tindakan terkunci: ${leadRefusalReason(action)}`,
|
|
22327
|
+
row.projectId,
|
|
22328
|
+
row.specId,
|
|
22329
|
+
row.sessionId
|
|
22330
|
+
);
|
|
22331
|
+
return { ok: false, detail: `tindakan terkunci: ${leadRefusalReason(action)}` };
|
|
22332
|
+
}
|
|
22333
|
+
switch (action) {
|
|
22334
|
+
// Keputusan yang wujudnya memang hanya baris jejak.
|
|
22335
|
+
case "none":
|
|
22336
|
+
case "answer-session":
|
|
22337
|
+
// sudah diketikkan ke pane oleh pintu deteksi
|
|
22338
|
+
case "order-queue":
|
|
22339
|
+
return { ok: true, detail: "tak ada tindakan tersisa" };
|
|
22340
|
+
// Ditata sebagai keputusan, dieksekusi operator di versi ini (lihat ADR-0091 §Konsekuensi).
|
|
22341
|
+
case "hold-work":
|
|
22342
|
+
case "push-branch":
|
|
22343
|
+
case "run-migration":
|
|
22344
|
+
return { ok: true, detail: `dicatat sebagai keputusan; "${action}" dieksekusi operator di versi ini` };
|
|
22345
|
+
case "stop-session":
|
|
22346
|
+
return stopSession(row, deps);
|
|
22347
|
+
case "start-session":
|
|
22348
|
+
case "resume-session":
|
|
22349
|
+
case "restart-session":
|
|
22350
|
+
return relaunch(row, deps);
|
|
22351
|
+
case "integrate-main":
|
|
22352
|
+
return integrateMain(row, deps);
|
|
22353
|
+
}
|
|
22354
|
+
}
|
|
22355
|
+
function stopSession(row, deps) {
|
|
22356
|
+
if (!row.sessionId) return { ok: false, detail: "keputusan ini tak menunjuk sesi" };
|
|
22357
|
+
const killed = deps.killSession(row.sessionId);
|
|
22358
|
+
return killed ? { ok: true, detail: `sesi ${row.sessionId} dihentikan; worktree dibiarkan utuh (AC-32a)` } : { ok: false, detail: `sesi ${row.sessionId} tak ada` };
|
|
22359
|
+
}
|
|
22360
|
+
async function relaunch(row, deps) {
|
|
22361
|
+
if (!row.specId) return { ok: false, detail: "keputusan ini tak menunjuk backlog" };
|
|
22362
|
+
const spec = await prisma.spec.findUnique({ where: { id: row.specId } });
|
|
22363
|
+
if (!spec) return { ok: false, detail: `backlog ${row.specId} tak ada` };
|
|
22364
|
+
const sid = sessionIdForSpec(spec.id);
|
|
22365
|
+
if (deps.sessionExists(sid)) deps.killSession(sid);
|
|
22366
|
+
const r = await deps.startSpec(spec, { flow: flowForSource(spec.source) });
|
|
22367
|
+
return { ok: true, detail: `sesi ${r.id}${r.resumed ? " dilanjutkan" : " dimulai"}` };
|
|
22368
|
+
}
|
|
22369
|
+
async function integrateMain(row, deps) {
|
|
22370
|
+
if (!row.specId) return { ok: false, detail: "keputusan ini tak menunjuk backlog" };
|
|
22371
|
+
const cfg = await getLead();
|
|
22372
|
+
const repoDir = await deps.repoDir(row.projectId);
|
|
22373
|
+
if (!repoDir) return { ok: false, detail: `project ${row.projectId} belum di-bind ke checkout lokal` };
|
|
22374
|
+
const spec = await prisma.spec.findUnique({ where: { id: row.specId } });
|
|
22375
|
+
if (!spec) return { ok: false, detail: `backlog ${row.specId} tak ada` };
|
|
22376
|
+
const evidence = [];
|
|
22377
|
+
if (cfg.requireGreenBeforeIntegrate) {
|
|
22378
|
+
const wt = worktreeDir(repoDir, spec.id);
|
|
22379
|
+
const done = deps.planDone(wt, spec.id);
|
|
22380
|
+
evidence.push(done ? "plan tak menyisakan `- [ ]`" : "plan MASIH menyisakan `- [ ]`");
|
|
22381
|
+
const sid = sessionIdForSpec(spec.id);
|
|
22382
|
+
const pane = deps.sessionExists(sid);
|
|
22383
|
+
evidence.push(pane ? `pane ${sid} masih ada` : `pane ${sid} sudah tak ada`);
|
|
22384
|
+
if (!done) {
|
|
22385
|
+
await recordEvidence(row, evidence, "integrasi DIBATALKAN \u2014 syarat objektif tak terpenuhi");
|
|
22386
|
+
await deps.notify(
|
|
22387
|
+
row.id,
|
|
22388
|
+
`Lead membatalkan integrasi ${spec.id}: plan belum tuntas`,
|
|
22389
|
+
row.projectId,
|
|
22390
|
+
row.specId,
|
|
22391
|
+
row.sessionId
|
|
22392
|
+
);
|
|
22393
|
+
return { ok: false, detail: "syarat integrasi tak terpenuhi: plan masih menyisakan `- [ ]`" };
|
|
22394
|
+
}
|
|
22395
|
+
} else {
|
|
22396
|
+
evidence.push("syarat objektif dimatikan operator (lead.requireGreenBeforeIntegrate = false)");
|
|
22397
|
+
}
|
|
22398
|
+
const res = await deps.integrate(repoDir, spec.id, "merge", "local:main");
|
|
22399
|
+
evidence.push(`integrate \u2192 ${res.status}`);
|
|
22400
|
+
await recordEvidence(row, evidence, res.status === "clean" ? "integrasi bersih" : `integrasi tak bersih: ${res.status}`);
|
|
22401
|
+
if (res.status !== "clean") {
|
|
22402
|
+
await deps.notify(
|
|
22403
|
+
row.id,
|
|
22404
|
+
`Integrasi ${spec.id} oleh lead tak bersih (${res.status}) \u2014 butuh operator`,
|
|
22405
|
+
row.projectId,
|
|
22406
|
+
row.specId,
|
|
22407
|
+
row.sessionId
|
|
22408
|
+
);
|
|
22409
|
+
}
|
|
22410
|
+
return { ok: res.status === "clean", detail: evidence.join("; ") };
|
|
22411
|
+
}
|
|
22412
|
+
async function recordEvidence(row, evidence, verdict) {
|
|
22413
|
+
await prisma.leadDecision.update({
|
|
22414
|
+
where: { id: row.id },
|
|
22415
|
+
data: { reason: `${row.reason}
|
|
22416
|
+
|
|
22417
|
+
Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
|
|
22418
|
+
}).catch(() => {
|
|
22419
|
+
});
|
|
22420
|
+
}
|
|
22421
|
+
|
|
22422
|
+
// src/services/lead/detect.ts
|
|
22423
|
+
init_pty();
|
|
22424
|
+
|
|
22425
|
+
// src/services/lead/pane.ts
|
|
22426
|
+
var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
|
|
22427
|
+
var tail = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
|
|
22428
|
+
var CODEX_FINISHED = [
|
|
22429
|
+
/Goal achieved/i,
|
|
22430
|
+
/Goal unmet/i,
|
|
22431
|
+
/\btokens used\b/i,
|
|
22432
|
+
/\bTo continue this session\b/i
|
|
22433
|
+
];
|
|
22434
|
+
var ASK_SIGNALS = [
|
|
22435
|
+
/\?\s*$/m,
|
|
22436
|
+
/^\s*\[?\d+[).\]]\s+\S/m,
|
|
22437
|
+
/\b(pilih|apakah|haruskah|mana yang|opsi|which|should i|do you want|proceed\?)\b/i
|
|
22438
|
+
];
|
|
22439
|
+
function readPaneQuestion(text, agent) {
|
|
22440
|
+
const lines2 = tail(text, 40);
|
|
22441
|
+
const body = lines2.join("\n").trim();
|
|
22442
|
+
if (!body) return { asking: false, question: "", reason: "layar kosong" };
|
|
22443
|
+
const question = tail(text, 25).join("\n").trim();
|
|
22444
|
+
if (agent === "codex") {
|
|
22445
|
+
const finished = CODEX_FINISHED.find((re) => re.test(body));
|
|
22446
|
+
if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)" };
|
|
22447
|
+
if (!ASK_SIGNALS.some((re) => re.test(body)))
|
|
22448
|
+
return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex" };
|
|
22449
|
+
}
|
|
22450
|
+
return { asking: true, question, reason: "" };
|
|
22451
|
+
}
|
|
22452
|
+
|
|
22453
|
+
// src/services/lead/detect.ts
|
|
22454
|
+
var answers = /* @__PURE__ */ new Map();
|
|
22455
|
+
var capped = /* @__PURE__ */ new Set();
|
|
22456
|
+
function resetSession(sessionId2) {
|
|
22457
|
+
answers.delete(sessionId2);
|
|
22458
|
+
capped.delete(sessionId2);
|
|
22459
|
+
}
|
|
22460
|
+
var prodDetectDeps = {
|
|
22461
|
+
live: () => {
|
|
22462
|
+
try {
|
|
22463
|
+
return liveDecisions();
|
|
22464
|
+
} catch {
|
|
22465
|
+
return [];
|
|
22466
|
+
}
|
|
22467
|
+
},
|
|
22468
|
+
filled: markerFilled,
|
|
22469
|
+
pane: (id) => capturePane(id),
|
|
22470
|
+
agentOf: (id) => {
|
|
22471
|
+
try {
|
|
22472
|
+
return getSession(id)?.agent ?? null;
|
|
22473
|
+
} catch {
|
|
22474
|
+
return null;
|
|
22475
|
+
}
|
|
22476
|
+
},
|
|
22477
|
+
// SPEC-402 · tmux tak terbaca ≠ pane mati. Ragu → perlakukan sebagai mati: yang hilang cuma satu
|
|
22478
|
+
// jawaban otomatis (sesi jatuh ke perilaku hari ini), sementara salah arah membuat lead mengetik
|
|
22479
|
+
// ke pane yang sudah tak ada.
|
|
22480
|
+
exited: (id) => {
|
|
22481
|
+
try {
|
|
22482
|
+
return getSession(id)?.exited ?? true;
|
|
22483
|
+
} catch {
|
|
22484
|
+
return true;
|
|
22485
|
+
}
|
|
22486
|
+
},
|
|
22487
|
+
send: (id, text) => sendToPane(id, text),
|
|
22488
|
+
decide,
|
|
22489
|
+
decideDeps: prodDecideDeps,
|
|
22490
|
+
optIn: leadProjects,
|
|
22491
|
+
notify: recordLeadDecision,
|
|
22492
|
+
cfg: getLead
|
|
22493
|
+
};
|
|
22494
|
+
async function scanAndAnswer(deps = prodDetectDeps) {
|
|
22495
|
+
const out3 = { answered: [], skipped: [] };
|
|
22496
|
+
const cfg = await deps.cfg();
|
|
22497
|
+
if (!cfg.enabled || cfg.paused) return out3;
|
|
22498
|
+
const optIn = new Set(await deps.optIn());
|
|
22499
|
+
const sessions = deps.live();
|
|
22500
|
+
sweep(sessions.map((s) => s.id));
|
|
22501
|
+
for (const s of sessions) {
|
|
22502
|
+
const skip = (reason) => out3.skipped.push({ id: s.id, reason });
|
|
22503
|
+
if (!optIn.has(s.projectId)) {
|
|
22504
|
+
skip("project tak opt-in lead");
|
|
22505
|
+
continue;
|
|
22506
|
+
}
|
|
22507
|
+
if (!leadActive(cfg, s.projectId)) {
|
|
22508
|
+
skip("lead dijeda untuk project ini");
|
|
22509
|
+
continue;
|
|
22510
|
+
}
|
|
22511
|
+
if (!deps.filled(s.decisionFile)) continue;
|
|
22512
|
+
if (deps.exited(s.id)) {
|
|
22513
|
+
skip("pane mati");
|
|
22514
|
+
continue;
|
|
22515
|
+
}
|
|
22516
|
+
if ((answers.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
|
|
22517
|
+
if (!capped.has(s.id)) {
|
|
22518
|
+
capped.add(s.id);
|
|
22519
|
+
const row2 = await recordDecision({
|
|
22520
|
+
projectId: s.projectId,
|
|
22521
|
+
specId: s.specId,
|
|
22522
|
+
sessionId: s.id,
|
|
22523
|
+
gate: "detected",
|
|
22524
|
+
kind: "quality",
|
|
22525
|
+
question: `Sesi ${s.id} sudah dijawab otomatis ${cfg.maxAutoAnswers}\xD7 berturut-turut.`,
|
|
22526
|
+
answer: "Berhenti menjawab sesi ini; serahkan ke operator.",
|
|
22527
|
+
reason: "Batas jawaban otomatis per sesi tercapai \u2014 pengulangan menandakan lead tak benar-benar membuka jalan buntunya (AC-11).",
|
|
22528
|
+
refs: [],
|
|
22529
|
+
confidence: "tinggi",
|
|
22530
|
+
action: "none",
|
|
22531
|
+
weighty: true
|
|
22532
|
+
});
|
|
22533
|
+
await deps.notify(
|
|
22534
|
+
row2.id,
|
|
22535
|
+
`Lead berhenti menjawab sesi ${s.id} (batas ${cfg.maxAutoAnswers}\xD7 tercapai)`,
|
|
22536
|
+
s.projectId,
|
|
22537
|
+
s.specId ?? null,
|
|
22538
|
+
s.id
|
|
22539
|
+
);
|
|
22540
|
+
}
|
|
22541
|
+
skip("batas jawaban otomatis tercapai");
|
|
22542
|
+
continue;
|
|
22543
|
+
}
|
|
22544
|
+
const agent = deps.agentOf(s.id) ?? "claude";
|
|
22545
|
+
const read = readPaneQuestion(deps.pane(s.id), agent);
|
|
22546
|
+
if (!read.asking) {
|
|
22547
|
+
skip(read.reason);
|
|
22548
|
+
continue;
|
|
22549
|
+
}
|
|
22550
|
+
const row = await deps.decide({
|
|
22551
|
+
projectId: s.projectId,
|
|
22552
|
+
specId: s.specId,
|
|
22553
|
+
sessionId: s.id,
|
|
22554
|
+
gate: "detected",
|
|
22555
|
+
kind: "answer",
|
|
22556
|
+
question: read.question,
|
|
22557
|
+
notes: [`Sesi ini menunggu di terminal; teks di bawah adalah layar terakhirnya. Jawablah sebagai masukan yang bisa langsung diketik ke terminal itu (isi \`reply\`).`]
|
|
22558
|
+
}, deps.decideDeps);
|
|
22559
|
+
if (!row || row.status !== "berlaku") {
|
|
22560
|
+
skip("lead tak menghasilkan keputusan yang berlaku");
|
|
22561
|
+
continue;
|
|
22562
|
+
}
|
|
22563
|
+
const reply = takeReply(row.id) || row.answer;
|
|
22564
|
+
const sent = await deps.send(s.id, reply);
|
|
22565
|
+
if (!sent) {
|
|
22566
|
+
skip("gagal mengetik ke pane");
|
|
22567
|
+
continue;
|
|
22568
|
+
}
|
|
22569
|
+
answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
|
|
22570
|
+
out3.answered.push(s.id);
|
|
22571
|
+
}
|
|
22572
|
+
return out3;
|
|
22573
|
+
}
|
|
22574
|
+
function sweep(liveIds) {
|
|
22575
|
+
const live = new Set(liveIds);
|
|
22576
|
+
for (const id of [...answers.keys()]) if (!live.has(id)) answers.delete(id);
|
|
22577
|
+
for (const id of [...capped]) if (!live.has(id)) capped.delete(id);
|
|
22578
|
+
}
|
|
22579
|
+
|
|
22580
|
+
// src/services/lead/pulse.ts
|
|
22581
|
+
init_db();
|
|
22582
|
+
init_pty();
|
|
22583
|
+
init_session_phases();
|
|
22584
|
+
var moduleOf = (p) => p.split("/").slice(0, 2).join("/");
|
|
22585
|
+
function findCollisions(areas) {
|
|
22586
|
+
const out3 = [];
|
|
22587
|
+
for (let i = 0; i < areas.length; i++) {
|
|
22588
|
+
for (let j = i + 1; j < areas.length; j++) {
|
|
22589
|
+
const a = areas[i], b = areas[j];
|
|
22590
|
+
if (a.projectId !== b.projectId) continue;
|
|
22591
|
+
const bp = new Set(b.paths);
|
|
22592
|
+
const shared = a.paths.filter((p) => bp.has(p));
|
|
22593
|
+
const bm = new Set(b.paths.map(moduleOf));
|
|
22594
|
+
const nearby = [...new Set(a.paths.map(moduleOf))].filter((m) => bm.has(m) && !shared.some((s) => moduleOf(s) === m));
|
|
22595
|
+
if (shared.length || nearby.length) out3.push({ a, b, shared, nearby });
|
|
22596
|
+
}
|
|
22597
|
+
}
|
|
22598
|
+
return out3;
|
|
22599
|
+
}
|
|
22600
|
+
var prodPulseDeps = {
|
|
22601
|
+
sessions: () => {
|
|
22602
|
+
try {
|
|
22603
|
+
return listSessions();
|
|
22604
|
+
} catch {
|
|
22605
|
+
return [];
|
|
22606
|
+
}
|
|
22607
|
+
},
|
|
22608
|
+
areas: async (s) => {
|
|
22609
|
+
const repoDir = await resolveRepoDir(s.projectId);
|
|
22610
|
+
if (!repoDir) return [];
|
|
22611
|
+
const spec = await prisma.spec.findUnique({ where: { id: s.specId }, select: { baseSha: true, branchFrom: true } });
|
|
22612
|
+
if (!spec) return [];
|
|
22613
|
+
try {
|
|
22614
|
+
const r = await specReview(repoDir, s.specId, spec.baseSha, spec.branchFrom);
|
|
22615
|
+
return r.changed.map((c) => c.path);
|
|
22616
|
+
} catch {
|
|
22617
|
+
return [];
|
|
22618
|
+
}
|
|
22619
|
+
},
|
|
22620
|
+
planDone: planComplete,
|
|
22621
|
+
decide,
|
|
22622
|
+
decideDeps: prodDecideDeps,
|
|
22623
|
+
apply: applyAction,
|
|
22624
|
+
enqueue,
|
|
22625
|
+
notify: recordLeadDecision,
|
|
22626
|
+
optIn: leadProjects,
|
|
22627
|
+
cfg: getLead,
|
|
22628
|
+
scheduler: getScheduler
|
|
22629
|
+
};
|
|
22630
|
+
var lastReadySig = /* @__PURE__ */ new Map();
|
|
22631
|
+
async function pulse(deps = prodPulseDeps) {
|
|
22632
|
+
const res = { ordered: 0, collisions: 0, quality: 0 };
|
|
22633
|
+
const cfg = await deps.cfg();
|
|
22634
|
+
if (!cfg.enabled || cfg.paused) return res;
|
|
22635
|
+
const optIn = (await deps.optIn()).filter((p) => leadActive(cfg, p));
|
|
22636
|
+
if (!optIn.length) return res;
|
|
22637
|
+
try {
|
|
22638
|
+
res.quality = await followUpFinished(cfg, optIn, deps);
|
|
22639
|
+
} catch {
|
|
22640
|
+
}
|
|
22641
|
+
try {
|
|
22642
|
+
res.collisions = await detectCollisions(optIn, deps);
|
|
22643
|
+
} catch {
|
|
22644
|
+
}
|
|
22645
|
+
try {
|
|
22646
|
+
res.ordered = await orderReadyWork(optIn, deps);
|
|
22647
|
+
} catch {
|
|
22648
|
+
}
|
|
22649
|
+
return res;
|
|
22650
|
+
}
|
|
22651
|
+
async function followUpFinished(cfg, optIn, deps) {
|
|
22652
|
+
let n = 0;
|
|
22653
|
+
const opt = new Set(optIn);
|
|
22654
|
+
for (const s of deps.sessions()) {
|
|
22655
|
+
if (!s.exited || !s.specId || !opt.has(s.projectId)) continue;
|
|
22656
|
+
const bad = (s.exitCode ?? 0) !== 0;
|
|
22657
|
+
const unfinished = !deps.planDone(s.cwd, s.specId);
|
|
22658
|
+
if (!bad && !unfinished) continue;
|
|
22659
|
+
const mark = `Sesi ${s.id} untuk backlog ${s.specId}`;
|
|
22660
|
+
const seen = await prisma.leadDecision.findFirst({
|
|
22661
|
+
where: { sessionId: s.id, gate: "pulse", question: { startsWith: mark } }
|
|
22662
|
+
});
|
|
22663
|
+
if (seen) continue;
|
|
22664
|
+
const why = [
|
|
22665
|
+
bad ? `berakhir dengan kode keluar ${s.exitCode}` : null,
|
|
22666
|
+
unfinished ? "plan-nya masih menyisakan kotak `- [ ]`" : null
|
|
22667
|
+
].filter(Boolean).join(" dan ");
|
|
22668
|
+
const row = await deps.decide({
|
|
22669
|
+
projectId: s.projectId,
|
|
22670
|
+
specId: s.specId,
|
|
22671
|
+
sessionId: s.id,
|
|
22672
|
+
gate: "pulse",
|
|
22673
|
+
kind: "quality",
|
|
22674
|
+
question: `${mark} ${why}. Tindak lanjutnya apa: lanjutkan pekerjaan yang terputus, ulangi dari awal, atau hentikan?`,
|
|
22675
|
+
options: [
|
|
22676
|
+
"resume-session \u2014 lanjutkan dari keadaan worktree sekarang (ADR-0084)",
|
|
22677
|
+
"restart-session \u2014 ulangi dari awal",
|
|
22678
|
+
"none \u2014 terima apa adanya, sertakan alasannya"
|
|
22679
|
+
],
|
|
22680
|
+
notes: [`Worktree sesi: ${s.cwd}`]
|
|
22681
|
+
}, deps.decideDeps);
|
|
22682
|
+
if (!row) continue;
|
|
22683
|
+
n++;
|
|
22684
|
+
if (row.status === "berlaku" && row.action !== "none") {
|
|
22685
|
+
try {
|
|
22686
|
+
await deps.apply(row);
|
|
22687
|
+
} catch {
|
|
22688
|
+
}
|
|
22689
|
+
}
|
|
22690
|
+
}
|
|
22691
|
+
return n;
|
|
22692
|
+
}
|
|
22693
|
+
async function detectCollisions(optIn, deps) {
|
|
22694
|
+
const opt = new Set(optIn);
|
|
22695
|
+
const live = deps.sessions().filter((s) => !s.exited && s.specId && opt.has(s.projectId));
|
|
22696
|
+
if (live.length < 2) return 0;
|
|
22697
|
+
const areas = [];
|
|
22698
|
+
for (const s of live) {
|
|
22699
|
+
const paths2 = await deps.areas({ id: s.id, projectId: s.projectId, specId: s.specId });
|
|
22700
|
+
if (paths2.length) areas.push({ specId: s.specId, sessionId: s.id, projectId: s.projectId, paths: paths2 });
|
|
22701
|
+
}
|
|
22702
|
+
let n = 0;
|
|
22703
|
+
for (const c of findCollisions(areas)) {
|
|
22704
|
+
const key = [c.a.sessionId, c.b.sessionId].sort().join("|");
|
|
22705
|
+
const seen = await prisma.leadDecision.findFirst({
|
|
22706
|
+
where: { gate: "pulse", question: { contains: key } }
|
|
22707
|
+
});
|
|
22708
|
+
if (seen) continue;
|
|
22709
|
+
const row = await deps.decide({
|
|
22710
|
+
projectId: c.a.projectId,
|
|
22711
|
+
specId: c.a.specId,
|
|
22712
|
+
sessionId: c.a.sessionId,
|
|
22713
|
+
gate: "pulse",
|
|
22714
|
+
kind: "collision",
|
|
22715
|
+
question: `Dua pekerjaan menyentuh area yang sama [${key}]: ${c.a.specId} dan ${c.b.specId}. Tunda salah satu, gabungkan, atau biarkan?`,
|
|
22716
|
+
options: [
|
|
22717
|
+
`hold-work \u2014 tunda salah satu sampai yang lain terintegrasi`,
|
|
22718
|
+
`none \u2014 biarkan berjalan, sertakan alasan kenapa tabrakan ini aman`
|
|
22719
|
+
],
|
|
22720
|
+
notes: [
|
|
22721
|
+
c.shared.length ? `Berkas yang sama: ${c.shared.slice(0, 20).join(", ")}` : "",
|
|
22722
|
+
c.nearby.length ? `Modul yang sama: ${c.nearby.slice(0, 20).join(", ")}` : ""
|
|
22723
|
+
].filter(Boolean)
|
|
22724
|
+
}, deps.decideDeps);
|
|
22725
|
+
if (row) n++;
|
|
22726
|
+
}
|
|
22727
|
+
return n;
|
|
22728
|
+
}
|
|
22729
|
+
async function orderReadyWork(optIn, deps) {
|
|
22730
|
+
const sched = await deps.scheduler();
|
|
22731
|
+
if (!sched.enabled || sched.paused) return 0;
|
|
22732
|
+
let total = 0;
|
|
22733
|
+
for (const projectId of optIn) total += await orderProject(projectId, deps);
|
|
22734
|
+
return total;
|
|
22735
|
+
}
|
|
22736
|
+
async function orderProject(projectId, deps) {
|
|
22737
|
+
const project = await prisma.project.findUnique({ where: { id: projectId }, select: { schedulerOptIn: true } });
|
|
22738
|
+
if (!project?.schedulerOptIn) return 0;
|
|
22739
|
+
const ready = await prisma.spec.findMany({
|
|
22740
|
+
where: { ...UNSTARTED_SPEC_WHERE, projectId },
|
|
22741
|
+
select: { id: true, projectId: true, title: true, priority: true, objective: true },
|
|
22742
|
+
orderBy: { id: "asc" }
|
|
22743
|
+
});
|
|
22744
|
+
if (ready.length < 2) return 0;
|
|
22745
|
+
const already = new Set((await prisma.schedulerQueueItem.findMany({
|
|
22746
|
+
where: { specId: { in: ready.map((r) => r.id) } },
|
|
22747
|
+
select: { specId: true }
|
|
22748
|
+
})).map((q) => q.specId));
|
|
22749
|
+
const pending = ready.filter((r) => !already.has(r.id));
|
|
22750
|
+
if (pending.length < 2) return 0;
|
|
22751
|
+
const sig = pending.map((r) => r.id).join(",");
|
|
22752
|
+
if (sig === lastReadySig.get(projectId)) return 0;
|
|
22753
|
+
lastReadySig.set(projectId, sig);
|
|
22754
|
+
const row = await deps.decide({
|
|
22755
|
+
projectId,
|
|
22756
|
+
gate: "pulse",
|
|
22757
|
+
kind: "order",
|
|
22758
|
+
question: `Ada ${pending.length} backlog siap dikerjakan. Urutkan mana yang lebih dulu berdasarkan isi pekerjaannya, lalu tuliskan urutan id-nya (dipisah koma) di \`decision\`.`,
|
|
22759
|
+
options: pending.map((r) => `${r.id} \xB7 [${r.priority}] ${r.title}`),
|
|
22760
|
+
notes: pending.map((r) => `${r.id}: ${r.objective.slice(0, 200)}`)
|
|
22761
|
+
}, deps.decideDeps);
|
|
22762
|
+
if (!row || row.status !== "berlaku") return 0;
|
|
22763
|
+
const byId2 = new Map(pending.map((r) => [r.id.toLowerCase(), r]));
|
|
22764
|
+
const named = [];
|
|
22765
|
+
for (const tok of row.answer.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean)) {
|
|
22766
|
+
const hit = byId2.get(tok);
|
|
22767
|
+
if (hit && !named.includes(hit)) named.push(hit);
|
|
22768
|
+
}
|
|
22769
|
+
const ordered = [...named, ...pending.filter((r) => !named.includes(r))];
|
|
22770
|
+
for (const r of ordered) {
|
|
22771
|
+
await deps.enqueue({ specId: r.id, projectId: r.projectId, source: "lead", priority: r.priority });
|
|
22772
|
+
}
|
|
22773
|
+
await deps.notify(row.id, `Lead menata ${ordered.length} backlog siap kerja`, projectId, null, null);
|
|
22774
|
+
return ordered.length;
|
|
22775
|
+
}
|
|
22776
|
+
|
|
22777
|
+
// src/services/lead/engine.ts
|
|
22778
|
+
var TICK_MS = 5e3;
|
|
22779
|
+
var busyDetect = false;
|
|
22780
|
+
var busyPulse = false;
|
|
22781
|
+
var lastPulseAt = 0;
|
|
22782
|
+
var pulseEndedAt = 0;
|
|
22783
|
+
var timer3;
|
|
22784
|
+
function lastPulse() {
|
|
22785
|
+
return lastPulseAt;
|
|
22786
|
+
}
|
|
22787
|
+
async function tick2(now, deps = {}) {
|
|
22788
|
+
let cfg;
|
|
22789
|
+
try {
|
|
22790
|
+
cfg = await getLead();
|
|
22791
|
+
} catch (e) {
|
|
22792
|
+
console.error("lead tick:", e);
|
|
22793
|
+
return;
|
|
22794
|
+
}
|
|
22795
|
+
if (!cfg.enabled) return;
|
|
22796
|
+
const jobs = [];
|
|
22797
|
+
if (!busyDetect) {
|
|
22798
|
+
busyDetect = true;
|
|
22799
|
+
jobs.push(scanAndAnswer(deps.detect ?? prodDetectDeps).then(() => {
|
|
22800
|
+
}).catch((e) => {
|
|
22801
|
+
console.error("lead detect:", e);
|
|
22802
|
+
}).finally(() => {
|
|
22803
|
+
busyDetect = false;
|
|
22804
|
+
}));
|
|
22805
|
+
}
|
|
22806
|
+
if (!cfg.paused && !busyPulse && now - Math.max(lastPulseAt, pulseEndedAt) >= cfg.everyMin * 6e4) {
|
|
22807
|
+
busyPulse = true;
|
|
22808
|
+
lastPulseAt = now;
|
|
22809
|
+
const clock = deps.now ?? Date.now;
|
|
22810
|
+
jobs.push(pulse(deps.pulse ?? prodPulseDeps).then(() => {
|
|
22811
|
+
}).catch((e) => {
|
|
22812
|
+
console.error("lead pulse:", e);
|
|
22813
|
+
}).finally(() => {
|
|
22814
|
+
pulseEndedAt = clock();
|
|
22815
|
+
busyPulse = false;
|
|
22816
|
+
}));
|
|
22817
|
+
}
|
|
22818
|
+
await Promise.all(jobs);
|
|
22819
|
+
}
|
|
22820
|
+
function startLead(deps = {}) {
|
|
22821
|
+
if (timer3) return;
|
|
22822
|
+
timer3 = setInterval(() => void tick2(Date.now(), deps), TICK_MS);
|
|
22823
|
+
timer3.unref();
|
|
22824
|
+
void tick2(Date.now(), deps);
|
|
22825
|
+
}
|
|
22826
|
+
|
|
22827
|
+
// src/routes/lead.ts
|
|
22828
|
+
async function lead_default(app2) {
|
|
22829
|
+
app2.get("/lead/config", async () => getLead());
|
|
22830
|
+
app2.put("/lead/config", async (req, reply) => {
|
|
22831
|
+
const parsed = zLead.safeParse(req.body);
|
|
22832
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
22833
|
+
return setLead(parsed.data);
|
|
22834
|
+
});
|
|
22835
|
+
app2.get("/lead/status", async () => {
|
|
22836
|
+
const cfg = await getLead();
|
|
22837
|
+
const projects = await prisma.project.findMany({
|
|
22838
|
+
where: { leadOptIn: true },
|
|
22839
|
+
select: { id: true, name: true },
|
|
22840
|
+
orderBy: { id: "asc" }
|
|
23007
22841
|
});
|
|
23008
|
-
const
|
|
23009
|
-
|
|
22842
|
+
const since = new Date(Date.now() - 24 * 60 * 60 * 1e3);
|
|
22843
|
+
let live = [];
|
|
22844
|
+
try {
|
|
22845
|
+
live = listSessions().filter((s) => !s.exited);
|
|
22846
|
+
} catch {
|
|
22847
|
+
}
|
|
22848
|
+
let waiting = [];
|
|
22849
|
+
try {
|
|
22850
|
+
waiting = liveDecisions().filter((d) => markerFilled(d.decisionFile)).map((d) => d.id);
|
|
22851
|
+
} catch {
|
|
22852
|
+
}
|
|
22853
|
+
const rows = await Promise.all(projects.map(async (p) => ({
|
|
22854
|
+
projectId: p.id,
|
|
22855
|
+
name: p.name,
|
|
22856
|
+
optIn: true,
|
|
22857
|
+
paused: !leadActive(cfg, p.id),
|
|
22858
|
+
decisions24h: await prisma.leadDecision.count({ where: { projectId: p.id, createdAt: { gte: since } } }),
|
|
22859
|
+
openSessions: live.filter((s) => s.projectId === p.id).length
|
|
22860
|
+
})));
|
|
22861
|
+
const last = lastPulse();
|
|
23010
22862
|
return {
|
|
23011
|
-
|
|
23012
|
-
|
|
23013
|
-
|
|
23014
|
-
|
|
22863
|
+
config: cfg,
|
|
22864
|
+
projects: rows,
|
|
22865
|
+
queue: (await listQueue()).map((q) => ({
|
|
22866
|
+
id: q.id,
|
|
22867
|
+
specId: q.specId,
|
|
22868
|
+
projectId: q.projectId,
|
|
22869
|
+
source: q.source,
|
|
22870
|
+
priority: q.priority,
|
|
22871
|
+
status: q.status,
|
|
22872
|
+
sessionId: q.sessionId,
|
|
22873
|
+
note: q.note,
|
|
22874
|
+
enqueuedAt: q.enqueuedAt.toISOString(),
|
|
22875
|
+
launchedAt: q.launchedAt ? q.launchedAt.toISOString() : null
|
|
22876
|
+
})),
|
|
22877
|
+
deciding: decidingIds(),
|
|
22878
|
+
waiting,
|
|
22879
|
+
lastPulseAt: last ? new Date(last).toISOString() : null
|
|
23015
22880
|
};
|
|
23016
22881
|
});
|
|
23017
|
-
app2.get("/
|
|
23018
|
-
const
|
|
23019
|
-
const
|
|
23020
|
-
|
|
23021
|
-
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23025
|
-
|
|
22882
|
+
app2.get("/lead/decisions", async (req) => {
|
|
22883
|
+
const q = req.query;
|
|
22884
|
+
const rows = await listDecisions({
|
|
22885
|
+
projectId: q.projectId,
|
|
22886
|
+
specId: q.specId,
|
|
22887
|
+
sessionId: q.sessionId,
|
|
22888
|
+
status: q.status,
|
|
22889
|
+
take: q.take ? Number(q.take) : void 0,
|
|
22890
|
+
skip: q.skip ? Number(q.skip) : void 0
|
|
23026
22891
|
});
|
|
23027
|
-
|
|
23028
|
-
|
|
23029
|
-
|
|
23030
|
-
|
|
23031
|
-
return {
|
|
23032
|
-
|
|
23033
|
-
|
|
23034
|
-
|
|
23035
|
-
|
|
23036
|
-
|
|
23037
|
-
|
|
23038
|
-
|
|
23039
|
-
|
|
23040
|
-
|
|
23041
|
-
|
|
23042
|
-
|
|
23043
|
-
|
|
23044
|
-
|
|
22892
|
+
return { items: rows.map(toDecisionView) };
|
|
22893
|
+
});
|
|
22894
|
+
app2.post("/lead/decisions", async (req, reply) => {
|
|
22895
|
+
const parsed = zLeadAsk.safeParse(req.body);
|
|
22896
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
22897
|
+
const ask = parsed.data;
|
|
22898
|
+
const project = await prisma.project.findUnique({ where: { id: ask.projectId }, select: { leadOptIn: true } });
|
|
22899
|
+
if (!project) return reply.code(404).send({ error: "project not found" });
|
|
22900
|
+
const cfg = await getLead();
|
|
22901
|
+
if (!project.leadOptIn || !leadActive(cfg, ask.projectId))
|
|
22902
|
+
return reply.code(409).send({ error: "lead tidak aktif untuk project ini" });
|
|
22903
|
+
const row = await decide({
|
|
22904
|
+
projectId: ask.projectId,
|
|
22905
|
+
specId: ask.specId ?? null,
|
|
22906
|
+
sessionId: ask.sessionId ?? null,
|
|
22907
|
+
gate: "contract",
|
|
22908
|
+
kind: "answer",
|
|
22909
|
+
question: ask.question,
|
|
22910
|
+
options: ask.options,
|
|
22911
|
+
notes: ask.context ? [ask.context] : void 0
|
|
22912
|
+
});
|
|
22913
|
+
if (!row) return reply.code(409).send({ error: "lead tidak aktif untuk project ini" });
|
|
22914
|
+
takeReply(row.id);
|
|
22915
|
+
if (row.status === "gagal") return reply.code(504).send({ error: row.reason, id: row.id });
|
|
22916
|
+
if (row.action !== "none") {
|
|
22917
|
+
try {
|
|
22918
|
+
await applyAction(row);
|
|
22919
|
+
} catch {
|
|
22920
|
+
}
|
|
22921
|
+
}
|
|
22922
|
+
const answer = {
|
|
22923
|
+
id: row.id,
|
|
22924
|
+
decision: row.answer,
|
|
22925
|
+
reason: row.reason,
|
|
22926
|
+
refs: Array.isArray(row.refs) ? row.refs.map(String) : [],
|
|
22927
|
+
confidence: row.confidence,
|
|
22928
|
+
action: row.action
|
|
23045
22929
|
};
|
|
22930
|
+
return reply.code(201).send(answer);
|
|
22931
|
+
});
|
|
22932
|
+
app2.post("/lead/decisions/:id/override", async (req, reply) => {
|
|
22933
|
+
const { id } = req.params;
|
|
22934
|
+
const parsed = zLeadOverride.safeParse(req.body);
|
|
22935
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
22936
|
+
const r = await overrideDecision(id, parsed.data.answer, parsed.data.reason);
|
|
22937
|
+
if (!r) return reply.code(409).send({ error: "keputusan tak ada atau sudah tak berlaku" });
|
|
22938
|
+
let delivered = false;
|
|
22939
|
+
if (r.next.sessionId) {
|
|
22940
|
+
resetSession(r.next.sessionId);
|
|
22941
|
+
delivered = await sendToPane(r.next.sessionId, parsed.data.answer).catch(() => false);
|
|
22942
|
+
}
|
|
22943
|
+
return { old: toDecisionView(r.old), next: toDecisionView(r.next), delivered };
|
|
22944
|
+
});
|
|
22945
|
+
app2.post("/lead/decisions/:id/cancel", async (req, reply) => {
|
|
22946
|
+
const { id } = req.params;
|
|
22947
|
+
const row = await cancelDecision(id);
|
|
22948
|
+
if (!row) return reply.code(409).send({ error: "keputusan tak ada atau sudah tak berlaku" });
|
|
22949
|
+
if (row.sessionId) resetSession(row.sessionId);
|
|
22950
|
+
return toDecisionView(row);
|
|
23046
22951
|
});
|
|
23047
22952
|
}
|
|
23048
22953
|
|
|
@@ -23055,13 +22960,13 @@ init_db();
|
|
|
23055
22960
|
|
|
23056
22961
|
// src/services/auth.ts
|
|
23057
22962
|
init_db();
|
|
23058
|
-
import { randomBytes as
|
|
22963
|
+
import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual, createHash as createHash3 } from "node:crypto";
|
|
23059
22964
|
import { promisify as promisify9 } from "node:util";
|
|
23060
22965
|
var scrypt = promisify9(scryptCb);
|
|
23061
22966
|
var COOKIE_NAME = "hn_session";
|
|
23062
22967
|
var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
23063
22968
|
async function hashPassword(pw) {
|
|
23064
|
-
const salt =
|
|
22969
|
+
const salt = randomBytes3(16);
|
|
23065
22970
|
const key = await scrypt(pw, salt, 64);
|
|
23066
22971
|
return `${salt.toString("hex")}:${key.toString("hex")}`;
|
|
23067
22972
|
}
|
|
@@ -23070,10 +22975,10 @@ async function verifyPassword(pw, stored) {
|
|
|
23070
22975
|
if (!saltHex || !hashHex) return false;
|
|
23071
22976
|
const key = await scrypt(pw, Buffer.from(saltHex, "hex"), 64);
|
|
23072
22977
|
const want = Buffer.from(hashHex, "hex");
|
|
23073
|
-
return key.length === want.length &&
|
|
22978
|
+
return key.length === want.length && timingSafeEqual(key, want);
|
|
23074
22979
|
}
|
|
23075
|
-
var newSessionToken = () =>
|
|
23076
|
-
var sessionId = (token) =>
|
|
22980
|
+
var newSessionToken = () => randomBytes3(32).toString("base64url");
|
|
22981
|
+
var sessionId = (token) => createHash3("sha256").update(token).digest("hex");
|
|
23077
22982
|
async function createSession2(userId) {
|
|
23078
22983
|
const token = newSessionToken();
|
|
23079
22984
|
await prisma.session.create({
|
|
@@ -23204,8 +23109,8 @@ init_src();
|
|
|
23204
23109
|
|
|
23205
23110
|
// src/services/agent-token.ts
|
|
23206
23111
|
init_db();
|
|
23207
|
-
import { randomBytes as
|
|
23208
|
-
var hash = (token) =>
|
|
23112
|
+
import { randomBytes as randomBytes4, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
23113
|
+
var hash = (token) => createHash4("sha256").update(token).digest("hex");
|
|
23209
23114
|
function toAgentTokenView(t) {
|
|
23210
23115
|
return {
|
|
23211
23116
|
id: t.id,
|
|
@@ -23220,7 +23125,7 @@ function toAgentTokenView(t) {
|
|
|
23220
23125
|
};
|
|
23221
23126
|
}
|
|
23222
23127
|
async function issueAgentToken(input) {
|
|
23223
|
-
const token = "hnm_agt_" +
|
|
23128
|
+
const token = "hnm_agt_" + randomBytes4(24).toString("hex");
|
|
23224
23129
|
const row = await prisma.agentToken.create({
|
|
23225
23130
|
data: {
|
|
23226
23131
|
name: input.name,
|
|
@@ -23238,7 +23143,7 @@ async function verifyAgentToken(token) {
|
|
|
23238
23143
|
if (!row || !row.enabled || row.revokedAt) return null;
|
|
23239
23144
|
const a = Buffer.from(hash(token), "hex");
|
|
23240
23145
|
const b = Buffer.from(row.tokenHash, "hex");
|
|
23241
|
-
if (a.length !== b.length || !
|
|
23146
|
+
if (a.length !== b.length || !timingSafeEqual2(a, b)) return null;
|
|
23242
23147
|
await prisma.agentToken.update({ where: { id: row.id }, data: { lastUsedAt: /* @__PURE__ */ new Date() } }).catch(() => {
|
|
23243
23148
|
});
|
|
23244
23149
|
return { id: row.id, capabilities: Array.isArray(row.capabilities) ? row.capabilities : [] };
|
|
@@ -23329,10 +23234,11 @@ function capabilityForRoute(method, path) {
|
|
|
23329
23234
|
if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health")
|
|
23330
23235
|
return read ? "GLOBAL_READ" : "COOKIE_ONLY";
|
|
23331
23236
|
if (top === "scheduler") return rw("settings");
|
|
23237
|
+
if (top === "lead") return rw("lead");
|
|
23332
23238
|
if (top === "settings" || top === "config") return rw("settings");
|
|
23333
23239
|
if (top === "specs") return rw("backlog");
|
|
23334
23240
|
if (top === "notifications") return rw("notifications");
|
|
23335
|
-
if (top === "
|
|
23241
|
+
if (top === "tickets") return rw("support");
|
|
23336
23242
|
if (top === "vps") return rw("vps");
|
|
23337
23243
|
if (top === "prds") return rw("docs");
|
|
23338
23244
|
if (top === "terminal") {
|
|
@@ -23392,9 +23298,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
23392
23298
|
const path = req.url.split("?")[0] ?? req.url;
|
|
23393
23299
|
if (PUBLIC.has(`${req.method} ${path}`)) return;
|
|
23394
23300
|
if (path.startsWith("/api/sync") && path !== "/api/sync/now" && !path.startsWith("/api/sync/conflicts")) return;
|
|
23395
|
-
if (path.startsWith("/api/ingest")) return;
|
|
23396
23301
|
if (path.startsWith("/api/help")) return;
|
|
23397
|
-
if (path.startsWith("/api/audit/") && auditScopeFromReq(req)) return;
|
|
23398
23302
|
if (user) return;
|
|
23399
23303
|
const agentTok = agentTokenFromReq(req);
|
|
23400
23304
|
if (agentTok) {
|
|
@@ -23432,16 +23336,14 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
23432
23336
|
await api.register(session_results_default);
|
|
23433
23337
|
await api.register(session_history_default);
|
|
23434
23338
|
await api.register(config_default);
|
|
23435
|
-
await api.register(ingest_default);
|
|
23436
|
-
await api.register(errors_default);
|
|
23437
23339
|
await api.register(help_default);
|
|
23438
23340
|
await api.register(tickets_default);
|
|
23439
23341
|
await api.register(scheduler_default);
|
|
23440
|
-
await api.register(audit_default);
|
|
23441
23342
|
await api.register(codex);
|
|
23343
|
+
await api.register(lead_default);
|
|
23442
23344
|
}, { prefix: "/api" });
|
|
23443
23345
|
if (process.env.NODE_ENV === "production") {
|
|
23444
|
-
const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env,
|
|
23346
|
+
const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
|
|
23445
23347
|
if (dist) {
|
|
23446
23348
|
app2.register(fastifyStatic, { root: dist });
|
|
23447
23349
|
app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));
|
|
@@ -23491,6 +23393,7 @@ init_db();
|
|
|
23491
23393
|
init_src();
|
|
23492
23394
|
|
|
23493
23395
|
// src/services/scheduler/governor.ts
|
|
23396
|
+
var ALREADY_DONE_NOTE = "spec sudah selesai \u2014 tak diluncurkan";
|
|
23494
23397
|
var draining = false;
|
|
23495
23398
|
async function drain(cfg, deps) {
|
|
23496
23399
|
if (draining) return;
|
|
@@ -23500,6 +23403,10 @@ async function drain(cfg, deps) {
|
|
|
23500
23403
|
if (slots <= 0) return;
|
|
23501
23404
|
for (const item of await queued()) {
|
|
23502
23405
|
if (slots <= 0) break;
|
|
23406
|
+
if (await deps.isDone(item.specId)) {
|
|
23407
|
+
await markDone(item.id, ALREADY_DONE_NOTE);
|
|
23408
|
+
continue;
|
|
23409
|
+
}
|
|
23503
23410
|
const liveId = deps.isLive(item.specId);
|
|
23504
23411
|
if (liveId) {
|
|
23505
23412
|
await markLaunched(item.id, liveId);
|
|
@@ -23588,7 +23495,7 @@ var prodEnd = {
|
|
|
23588
23495
|
reconcile: () => reconcile(reconcileProdDeps),
|
|
23589
23496
|
scanDecisions: () => scanDecisions()
|
|
23590
23497
|
};
|
|
23591
|
-
async function
|
|
23498
|
+
async function tick3(now, deps, end2 = prodEnd) {
|
|
23592
23499
|
const cfg = await getScheduler();
|
|
23593
23500
|
if (!cfg.enabled) return;
|
|
23594
23501
|
for (const src of listSources()) {
|
|
@@ -23622,6 +23529,13 @@ var prodDeps = {
|
|
|
23622
23529
|
const s = getSession(sessionIdForSpec(specId));
|
|
23623
23530
|
return s && !s.exited ? s.id : null;
|
|
23624
23531
|
},
|
|
23532
|
+
// SPEC-431 · dibaca ULANG dari DB tepat sebelum launch, bukan dari baris antrean: antrean tak
|
|
23533
|
+
// menyimpan stage, dan item bisa selesai selagi mengantre. Spec yang hilang bukan urusan gerbang
|
|
23534
|
+
// ini — `launch` di bawah yang melempar "spec tak ada" → item ditandai failed dengan alasannya.
|
|
23535
|
+
isDone: async (specId) => {
|
|
23536
|
+
const s = await prisma.spec.findUnique({ where: { id: specId }, select: { stage: true } });
|
|
23537
|
+
return s?.stage === "done";
|
|
23538
|
+
},
|
|
23625
23539
|
launch: async (item, autonomy) => {
|
|
23626
23540
|
const spec = await prisma.spec.findUnique({ where: { id: item.specId } });
|
|
23627
23541
|
if (!spec) throw new Error(`spec ${item.specId} tak ada`);
|
|
@@ -23629,13 +23543,13 @@ var prodDeps = {
|
|
|
23629
23543
|
return r.id;
|
|
23630
23544
|
}
|
|
23631
23545
|
};
|
|
23632
|
-
var
|
|
23633
|
-
var
|
|
23546
|
+
var TICK_MS2 = 1e4;
|
|
23547
|
+
var timer4;
|
|
23634
23548
|
function startScheduler(deps = prodDeps) {
|
|
23635
|
-
if (
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
-
void
|
|
23549
|
+
if (timer4) return;
|
|
23550
|
+
timer4 = setInterval(() => void tick3(Date.now(), deps), TICK_MS2);
|
|
23551
|
+
timer4.unref();
|
|
23552
|
+
void tick3(Date.now(), deps);
|
|
23639
23553
|
}
|
|
23640
23554
|
|
|
23641
23555
|
// src/services/scheduler/sources/backlog.ts
|
|
@@ -23643,7 +23557,7 @@ init_db();
|
|
|
23643
23557
|
var RANK2 = { tinggi: 0, sedang: 1, rendah: 2 };
|
|
23644
23558
|
async function checkBacklog() {
|
|
23645
23559
|
const specs = await prisma.spec.findMany({
|
|
23646
|
-
where: {
|
|
23560
|
+
where: { ...UNSTARTED_SPEC_WHERE, project: { schedulerOptIn: true } }
|
|
23647
23561
|
});
|
|
23648
23562
|
specs.sort((a, b) => (RANK2[a.priority] ?? 1) - (RANK2[b.priority] ?? 1) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
23649
23563
|
for (const s of specs) {
|
|
@@ -23654,36 +23568,6 @@ function registerBacklogSource() {
|
|
|
23654
23568
|
registerSchedulerSource({ id: "backlog", check: checkBacklog });
|
|
23655
23569
|
}
|
|
23656
23570
|
|
|
23657
|
-
// src/services/scheduler/sources/errors.ts
|
|
23658
|
-
init_db();
|
|
23659
|
-
async function checkErrors() {
|
|
23660
|
-
const minCount = (await getScheduler()).sources.errors.minCount;
|
|
23661
|
-
const groups = await prisma.errorGroup.findMany({
|
|
23662
|
-
where: {
|
|
23663
|
-
status: "new",
|
|
23664
|
-
// escalated/resolved tersaring di query
|
|
23665
|
-
environment: "production",
|
|
23666
|
-
// literal, cermin services/error-ingest.ts
|
|
23667
|
-
specId: null,
|
|
23668
|
-
// grup ber-specId tersaring di query
|
|
23669
|
-
count: { gte: minCount },
|
|
23670
|
-
// ambang dari setelan
|
|
23671
|
-
project: { schedulerOptIn: true }
|
|
23672
|
-
// non-opt-in tak pernah ter-query
|
|
23673
|
-
}
|
|
23674
|
-
});
|
|
23675
|
-
for (const g of groups) {
|
|
23676
|
-
try {
|
|
23677
|
-
const { spec } = await escalateErrorGroup(g, { author: "scheduler" });
|
|
23678
|
-
await enqueue({ specId: spec.id, projectId: spec.projectId, source: "errors", priority: spec.priority });
|
|
23679
|
-
} catch {
|
|
23680
|
-
}
|
|
23681
|
-
}
|
|
23682
|
-
}
|
|
23683
|
-
function registerErrorsSource() {
|
|
23684
|
-
registerSchedulerSource({ id: "errors", check: checkErrors });
|
|
23685
|
-
}
|
|
23686
|
-
|
|
23687
23571
|
// src/services/scheduler/sources/triase.ts
|
|
23688
23572
|
init_db();
|
|
23689
23573
|
async function checkTriase() {
|
|
@@ -23743,9 +23627,9 @@ app.listen({ port, host }).then(() => {
|
|
|
23743
23627
|
}
|
|
23744
23628
|
startVpsMonitor();
|
|
23745
23629
|
registerBacklogSource();
|
|
23746
|
-
registerErrorsSource();
|
|
23747
23630
|
registerTriaseSource();
|
|
23748
23631
|
startScheduler();
|
|
23632
|
+
startLead();
|
|
23749
23633
|
void (async () => {
|
|
23750
23634
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
23751
23635
|
const { applyConfigOnBoot: applyConfigOnBoot2 } = await Promise.resolve().then(() => (init_config_apply(), config_apply_exports));
|