hanoman 0.1.6 → 0.1.8
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 +38 -118
- package/dist/server.js +444 -1645
- package/package.json +1 -1
- package/prisma/migrations/20260731180000_drop_errors_sdk_crossaudit/migration.sql +58 -0
- package/prisma/migrations/20260731210000_spec_depends_on/migration.sql +7 -0
- package/prisma/schema.prisma +10 -88
- package/web/assets/{index-DB8a8_aD.js → index-D6AOUdRy.js} +1540 -1541
- 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, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4185
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
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(),
|
|
@@ -4229,6 +4225,10 @@ var init_entities = __esm({
|
|
|
4229
4225
|
constraints: external_exports.string(),
|
|
4230
4226
|
priority: zPriority
|
|
4231
4227
|
});
|
|
4228
|
+
zSpecBlocker = external_exports.object({
|
|
4229
|
+
id: external_exports.string(),
|
|
4230
|
+
reason: external_exports.enum(["missing", "unfinished", "unmerged"])
|
|
4231
|
+
});
|
|
4232
4232
|
zSpec = external_exports.object({
|
|
4233
4233
|
id: external_exports.string(),
|
|
4234
4234
|
projectId: external_exports.string(),
|
|
@@ -4247,7 +4247,13 @@ var init_entities = __esm({
|
|
|
4247
4247
|
// SPEC-408 · ADR-0090 · stempel waktu backlog (ISO string di wire — kolom DateTime di DB).
|
|
4248
4248
|
// `startedAt` null = belum pernah dikerjakan; ia tak pernah ditulis ulang saat sesi dilanjutkan.
|
|
4249
4249
|
createdAt: external_exports.string(),
|
|
4250
|
-
startedAt: external_exports.string().nullable()
|
|
4250
|
+
startedAt: external_exports.string().nullable(),
|
|
4251
|
+
// SPEC-447 · ADR-0093 · id backlog yang harus selesai & ter-merge lebih dulu. Server selalu
|
|
4252
|
+
// menormalkannya ke array (kolom DB-nya `Json?`); `.default([])` menjaga respons lama.
|
|
4253
|
+
dependsOn: external_exports.array(external_exports.string()).default([]),
|
|
4254
|
+
// Turunan (bukan kolom): dihitung `liveSpecs` dari stage dependency + git. Klien tak pernah
|
|
4255
|
+
// mengirimkannya — `.default([])` supaya bentuk lama tetap parse.
|
|
4256
|
+
blockedBy: external_exports.array(zSpecBlocker).default([])
|
|
4251
4257
|
});
|
|
4252
4258
|
NOTIFY_SOUNDS = [
|
|
4253
4259
|
"off",
|
|
@@ -4296,7 +4302,6 @@ var init_entities = __esm({
|
|
|
4296
4302
|
// dikonsumsi daun #5
|
|
4297
4303
|
sources: external_exports.object({
|
|
4298
4304
|
backlog: external_exports.object({ ...zSourceCommon, everyMin: external_exports.number().int().min(1).default(15) }).default({}),
|
|
4299
|
-
errors: external_exports.object({ ...zSourceCommon, everyMin: external_exports.number().int().min(1).default(15), minCount: external_exports.number().int().min(1).default(5) }).default({}),
|
|
4300
4305
|
triase: external_exports.object({ ...zSourceCommon, everyMin: external_exports.number().int().min(1).default(30) }).default({})
|
|
4301
4306
|
}).default({})
|
|
4302
4307
|
});
|
|
@@ -4329,8 +4334,12 @@ var init_entities = __esm({
|
|
|
4329
4334
|
// rem per project (AC-15/US-4)
|
|
4330
4335
|
everyMin: external_exports.number().int().min(1).max(1440).default(5),
|
|
4331
4336
|
// denyut proaktif (OQ-2)
|
|
4332
|
-
|
|
4333
|
-
//
|
|
4337
|
+
// SPEC-432 · batas waktu satu putusan (AC-4/35). Default 120 dtk MELAWAN prompt lead sendiri
|
|
4338
|
+
// ("kumpulkan bukti dulu: SoT, ADR, plan, kode, riwayat git") dan kalah: satu keputusan `order`
|
|
4339
|
+
// nyata terukur 306 dtk pada claude-opus-5 · xhigh, jadi 7/7 baris jejak operator berstatus
|
|
4340
|
+
// `gagal`. 600 dtk memberi kelonggaran di atas ongkos terukur itu; yang memangkas ongkosnya
|
|
4341
|
+
// sendiri adalah paragraf anggaran waktu di `leadPrompt` (306 dtk → 101 dtk).
|
|
4342
|
+
timeoutSec: external_exports.number().int().min(10).max(900).default(600),
|
|
4334
4343
|
// AC-11 / OQ-10 · berapa jawaban otomatis berturut-turut untuk SATU sesi sebelum lead berhenti.
|
|
4335
4344
|
maxAutoAnswers: external_exports.number().int().min(1).max(20).default(3),
|
|
4336
4345
|
// OQ-3 · syarat objektif sebelum lead boleh mengintegrasikan ke branch utama. Default MENYALA:
|
|
@@ -4372,10 +4381,11 @@ var init_entities = __esm({
|
|
|
4372
4381
|
});
|
|
4373
4382
|
zNotification = external_exports.object({
|
|
4374
4383
|
id: external_exports.string(),
|
|
4375
|
-
// SPEC-
|
|
4384
|
+
// SPEC-253 · +ticket; SPEC-298 · +fail (sesi scheduler gagal/limit)
|
|
4376
4385
|
// SPEC-409 · +lead (ADR-0091): keputusan berbobot / ragu / tindakan terkunci ditolak. MEMBERI
|
|
4377
4386
|
// TAHU, bukan meminta izin — tak ada pekerjaan yang menunggu notifikasi ini dibaca (AC-25).
|
|
4378
|
-
|
|
4387
|
+
// SPEC-384 · −error (ADR-0092) · dicabut bersama error monitoring.
|
|
4388
|
+
type: external_exports.enum(["done", "decision", "ticket", "fail", "lead"]).default("done"),
|
|
4379
4389
|
specId: external_exports.string().nullable(),
|
|
4380
4390
|
sessionId: external_exports.string().nullable(),
|
|
4381
4391
|
title: external_exports.string(),
|
|
@@ -4449,7 +4459,7 @@ var init_agent = __esm({
|
|
|
4449
4459
|
});
|
|
4450
4460
|
CAPABILITIES = [
|
|
4451
4461
|
{ id: "projects:read", domain: "projects", access: "read", label: "Projects \u2014 baca", desc: "Lihat daftar & detail project, branch, binding." },
|
|
4452
|
-
{ id: "projects:write", domain: "projects", access: "write", label: "Projects \u2014 tulis", desc: "Buat/ubah/hapus project, rename, clone,
|
|
4462
|
+
{ id: "projects:write", domain: "projects", access: "write", label: "Projects \u2014 tulis", desc: "Buat/ubah/hapus project, rename, clone, Help Center." },
|
|
4453
4463
|
{ id: "backlog:read", domain: "backlog", access: "read", label: "Backlog \u2014 baca", desc: "Lihat spec/backlog, dokumen, review diff." },
|
|
4454
4464
|
{ id: "backlog:write", domain: "backlog", access: "write", label: "Backlog \u2014 tulis", desc: "Buat/ubah/hapus spec, integrate branch." },
|
|
4455
4465
|
{ id: "sessions:read", domain: "sessions", access: "read", label: "Sesi \u2014 baca", desc: "Lihat sesi terminal, fase, review." },
|
|
@@ -4462,8 +4472,8 @@ var init_agent = __esm({
|
|
|
4462
4472
|
{ id: "vps:write", domain: "vps", access: "write", label: "VPS \u2014 tulis", desc: "Kelola VPS, audit, harden, remediasi, konsol (remote exec).", risk: "exec" },
|
|
4463
4473
|
{ id: "settings:read", domain: "settings", access: "read", label: "Settings \u2014 baca", desc: "Baca setelan & config runtime." },
|
|
4464
4474
|
{ id: "settings:write", domain: "settings", access: "write", label: "Settings \u2014 tulis", desc: "Ubah setelan & config runtime." },
|
|
4465
|
-
{ id: "support:read", domain: "support", access: "read", label: "
|
|
4466
|
-
{ id: "support:write", domain: "support", access: "write", label: "
|
|
4475
|
+
{ id: "support:read", domain: "support", access: "read", label: "Tiket \u2014 baca", desc: "Lihat tiket Help Center." },
|
|
4476
|
+
{ id: "support:write", domain: "support", access: "write", label: "Tiket \u2014 tulis", desc: "Terima/tolak tiket, ubah status, promosikan ke backlog." },
|
|
4467
4477
|
{ id: "notifications:read", domain: "notifications", access: "read", label: "Notifikasi \u2014 baca", desc: "Lihat notifikasi." },
|
|
4468
4478
|
{ id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." },
|
|
4469
4479
|
{ id: "lead:read", domain: "lead", access: "read", label: "Lead \u2014 baca", desc: "Baca jejak keputusan hanoman-lead & statusnya." },
|
|
@@ -4580,9 +4590,9 @@ var init_lead = __esm({
|
|
|
4580
4590
|
|
|
4581
4591
|
// ../shared/src/dto.ts
|
|
4582
4592
|
function flowForSource(source) {
|
|
4583
|
-
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "
|
|
4593
|
+
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
4584
4594
|
}
|
|
4585
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject,
|
|
4595
|
+
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;
|
|
4586
4596
|
var init_dto = __esm({
|
|
4587
4597
|
"../shared/src/dto.ts"() {
|
|
4588
4598
|
"use strict";
|
|
@@ -4632,11 +4642,6 @@ var init_dto = __esm({
|
|
|
4632
4642
|
});
|
|
4633
4643
|
zProjectId = external_exports.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "slug tak sah");
|
|
4634
4644
|
zRenameProject = external_exports.object({ newId: zProjectId });
|
|
4635
|
-
zCreateLink = external_exports.object({
|
|
4636
|
-
to: external_exports.string().min(1),
|
|
4637
|
-
kind: zLinkKind,
|
|
4638
|
-
note: external_exports.string().max(2e3).optional()
|
|
4639
|
-
});
|
|
4640
4645
|
zUpdateProject = external_exports.object({
|
|
4641
4646
|
name: external_exports.string().min(1).optional(),
|
|
4642
4647
|
desc: external_exports.string().optional(),
|
|
@@ -4655,7 +4660,9 @@ var init_dto = __esm({
|
|
|
4655
4660
|
title: external_exports.string().min(1),
|
|
4656
4661
|
priority: zPriority,
|
|
4657
4662
|
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]),
|
|
4658
|
-
branchFrom: external_exports.string().min(1).optional()
|
|
4663
|
+
branchFrom: external_exports.string().min(1).optional(),
|
|
4664
|
+
// SPEC-447 · ADR-0093 · divalidasi server (id ada / satu project / bukan diri sendiri / non-siklus).
|
|
4665
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4659
4666
|
}).superRefine((o, ctx) => {
|
|
4660
4667
|
const shape = "severity" in o.payload ? "qa" : "goal" in o.payload ? "goal" : "brief";
|
|
4661
4668
|
const want = o.source === "qa" ? "qa" : o.source === "goal" ? "goal" : "brief";
|
|
@@ -4669,8 +4676,12 @@ var init_dto = __esm({
|
|
|
4669
4676
|
// SPEC-186 · edit konten selagi item belum dimulai. Ditolak server bila sudah mulai.
|
|
4670
4677
|
title: external_exports.string().min(1).optional(),
|
|
4671
4678
|
priority: zPriority.optional(),
|
|
4672
|
-
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional()
|
|
4679
|
+
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional(),
|
|
4673
4680
|
// SPEC-407 · +goal
|
|
4681
|
+
// SPEC-447 · ADR-0093 · SENGAJA di luar gerbang `editingContent` (SPEC-186): gerbang itu
|
|
4682
|
+
// melindungi konten yang sudah jadi dasar kerja sesi berjalan, sedangkan dependsOn hanya
|
|
4683
|
+
// menggerbangi peluncuran BERIKUTNYA. `[]` = kosongkan.
|
|
4684
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4674
4685
|
});
|
|
4675
4686
|
zIntegrate = external_exports.object({
|
|
4676
4687
|
op: external_exports.enum(["merge", "rebase"]),
|
|
@@ -4689,10 +4700,6 @@ var init_dto = __esm({
|
|
|
4689
4700
|
session: zSessionSummary,
|
|
4690
4701
|
activity: external_exports.string(),
|
|
4691
4702
|
commit: external_exports.string(),
|
|
4692
|
-
monitoringEnabled: external_exports.boolean().default(false),
|
|
4693
|
-
// SPEC-249 · error monitoring aktif (ingest key ada)
|
|
4694
|
-
ingestKeyPrefix: external_exports.string().nullable().default(null),
|
|
4695
|
-
// SPEC-249 · hint prefix DSN (bukan hash/rahasia)
|
|
4696
4703
|
helpEnabled: external_exports.boolean().default(false),
|
|
4697
4704
|
// SPEC-253 · Help Center publik aktif
|
|
4698
4705
|
schedulerOptIn: external_exports.boolean().default(false),
|
|
@@ -4715,8 +4722,6 @@ var init_dto = __esm({
|
|
|
4715
4722
|
id: external_exports.string(),
|
|
4716
4723
|
enabled: external_exports.boolean(),
|
|
4717
4724
|
everyMin: external_exports.number(),
|
|
4718
|
-
minCount: external_exports.number().optional(),
|
|
4719
|
-
// hanya errors
|
|
4720
4725
|
lastRunAt: external_exports.string().nullable(),
|
|
4721
4726
|
nextRunAt: external_exports.string().nullable()
|
|
4722
4727
|
});
|
|
@@ -4781,7 +4786,7 @@ var init_dto = __esm({
|
|
|
4781
4786
|
// id sesi ber-marker keputusan terisi
|
|
4782
4787
|
lastPulseAt: external_exports.string().nullable()
|
|
4783
4788
|
});
|
|
4784
|
-
zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "
|
|
4789
|
+
zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "goal"]);
|
|
4785
4790
|
zPrdBrief = external_exports.object({
|
|
4786
4791
|
title: external_exports.string().min(1),
|
|
4787
4792
|
context: external_exports.string(),
|
|
@@ -4856,8 +4861,6 @@ var init_dto = __esm({
|
|
|
4856
4861
|
}),
|
|
4857
4862
|
// SPEC-273 · sesi breakdown project-level: pecah SATU PRD (prdPath) → manifest N backlog.
|
|
4858
4863
|
external_exports.object({ project: external_exports.string(), flow: external_exports.literal("breakdown"), prdPath: external_exports.string().min(1) }),
|
|
4859
|
-
// SPEC-337 · ADR-0075 · sesi audit lintas project LEPAS (tanya-jawab): tanpa Spec, tanpa fase.
|
|
4860
|
-
external_exports.object({ project: external_exports.string(), flow: external_exports.literal("cross-audit") }),
|
|
4861
4864
|
// SPEC-222 · scaffold: sesi project-level from-scratch, menyusun SoT dari ide. Tanpa brief
|
|
4862
4865
|
// (diseed dari Project.desc), tanpa Spec — cermin reverse.
|
|
4863
4866
|
external_exports.object({ project: external_exports.string(), flow: external_exports.literal("scaffold") }),
|
|
@@ -4874,7 +4877,10 @@ var init_dto = __esm({
|
|
|
4874
4877
|
goal: external_exports.boolean().optional(),
|
|
4875
4878
|
goalCondition: external_exports.string().max(4e3).optional(),
|
|
4876
4879
|
agent: zAgent.optional(),
|
|
4877
|
-
verifyScope: zVerifyScope.optional()
|
|
4880
|
+
verifyScope: zVerifyScope.optional(),
|
|
4881
|
+
// SPEC-447 · ADR-0093 — lewati gerbang dependency. Hanya jalur manusia; UI hanya
|
|
4882
|
+
// mengirimkannya sesudah operator melihat daftar pemblokirnya.
|
|
4883
|
+
force: external_exports.boolean().optional()
|
|
4878
4884
|
})
|
|
4879
4885
|
]);
|
|
4880
4886
|
zDocFileContent = external_exports.object({ content: external_exports.string() });
|
|
@@ -4929,75 +4935,6 @@ var init_dto = __esm({
|
|
|
4929
4935
|
zRemediate = external_exports.object({ items: external_exports.array(external_exports.string()).min(1).max(64) });
|
|
4930
4936
|
UPDATE_RESTART_EXIT = 75;
|
|
4931
4937
|
zUpdateApplyBody = external_exports.object({ confirm: external_exports.boolean().optional() });
|
|
4932
|
-
zStackFrame = external_exports.object({
|
|
4933
|
-
function: external_exports.string().max(500).optional(),
|
|
4934
|
-
filename: external_exports.string().max(2e3).optional(),
|
|
4935
|
-
lineno: external_exports.number().int().optional(),
|
|
4936
|
-
colno: external_exports.number().int().optional(),
|
|
4937
|
-
in_app: external_exports.boolean().optional()
|
|
4938
|
-
});
|
|
4939
|
-
zSymbolicatedFrame = zStackFrame.extend({
|
|
4940
|
-
source: external_exports.string().optional(),
|
|
4941
|
-
sourceLine: external_exports.number().int().optional(),
|
|
4942
|
-
sourceColumn: external_exports.number().int().optional(),
|
|
4943
|
-
contextLine: external_exports.string().optional(),
|
|
4944
|
-
preContext: external_exports.array(external_exports.string()).optional(),
|
|
4945
|
-
postContext: external_exports.array(external_exports.string()).optional(),
|
|
4946
|
-
symbolicated: external_exports.boolean()
|
|
4947
|
-
});
|
|
4948
|
-
zSourceMapUpload = external_exports.object({
|
|
4949
|
-
release: external_exports.string().min(1).max(200),
|
|
4950
|
-
artifacts: external_exports.array(external_exports.object({
|
|
4951
|
-
filename: external_exports.string().min(1).max(2e3),
|
|
4952
|
-
map: external_exports.string().min(1),
|
|
4953
|
-
debugId: external_exports.string().max(200).optional()
|
|
4954
|
-
})).min(1).max(200)
|
|
4955
|
-
});
|
|
4956
|
-
zIngestPayload = external_exports.object({
|
|
4957
|
-
type: external_exports.string().min(1).max(500),
|
|
4958
|
-
message: external_exports.string().min(1),
|
|
4959
|
-
stack: external_exports.string().optional(),
|
|
4960
|
-
frames: external_exports.array(zStackFrame).max(200).optional(),
|
|
4961
|
-
// SPEC-276 · opsional → kompatibel mundur
|
|
4962
|
-
environment: external_exports.string().max(120).optional(),
|
|
4963
|
-
release: external_exports.string().max(200).optional(),
|
|
4964
|
-
context: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
4965
|
-
});
|
|
4966
|
-
zErrorGroupView = external_exports.object({
|
|
4967
|
-
id: external_exports.string(),
|
|
4968
|
-
projectId: external_exports.string(),
|
|
4969
|
-
type: external_exports.string(),
|
|
4970
|
-
message: external_exports.string(),
|
|
4971
|
-
environment: external_exports.string(),
|
|
4972
|
-
status: zErrorStatus,
|
|
4973
|
-
count: external_exports.number().int(),
|
|
4974
|
-
firstSeenAt: external_exports.string(),
|
|
4975
|
-
lastSeenAt: external_exports.string(),
|
|
4976
|
-
specId: external_exports.string().nullable(),
|
|
4977
|
-
release: external_exports.string().nullable()
|
|
4978
|
-
// SPEC-276 · release terakhir grup (korelasi build)
|
|
4979
|
-
});
|
|
4980
|
-
zErrorEventView = external_exports.object({
|
|
4981
|
-
id: external_exports.string(),
|
|
4982
|
-
type: external_exports.string(),
|
|
4983
|
-
message: external_exports.string(),
|
|
4984
|
-
stack: external_exports.string().nullable(),
|
|
4985
|
-
environment: external_exports.string(),
|
|
4986
|
-
release: external_exports.string().nullable(),
|
|
4987
|
-
receivedAt: external_exports.string()
|
|
4988
|
-
});
|
|
4989
|
-
zErrorGroupDetail = zErrorGroupView.extend({
|
|
4990
|
-
sampleStack: external_exports.string().nullable(),
|
|
4991
|
-
sampleFrames: external_exports.array(zSymbolicatedFrame).nullable(),
|
|
4992
|
-
// SPEC-276 · frame tersimbolikasi (bila map ada)
|
|
4993
|
-
events: external_exports.array(zErrorEventView)
|
|
4994
|
-
});
|
|
4995
|
-
zIngestKeyView = external_exports.object({
|
|
4996
|
-
enabled: external_exports.boolean(),
|
|
4997
|
-
prefix: external_exports.string().nullable(),
|
|
4998
|
-
key: external_exports.string().optional(),
|
|
4999
|
-
dsnUrl: external_exports.string().optional()
|
|
5000
|
-
});
|
|
5001
4938
|
zTicketView = external_exports.object({
|
|
5002
4939
|
id: external_exports.string(),
|
|
5003
4940
|
projectId: external_exports.string(),
|
|
@@ -5186,15 +5123,6 @@ var init_api = __esm({
|
|
|
5186
5123
|
agentTokens: `${API}/agent-tokens`,
|
|
5187
5124
|
agentToken: (id) => `${API}/agent-tokens/${id}`,
|
|
5188
5125
|
agentCapabilities: `${API}/agent-tokens/capabilities`,
|
|
5189
|
-
// SPEC-249 · error monitoring. ingest publik ber-DSN (bypass gate cookie, ADR-0060).
|
|
5190
|
-
ingest: (slug) => `${API}/ingest/${encodeURIComponent(slug)}`,
|
|
5191
|
-
errors: `${API}/errors`,
|
|
5192
|
-
errorsGuide: `${API}/errors/integration-guide`,
|
|
5193
|
-
error: (id) => `${API}/errors/${id}`,
|
|
5194
|
-
errorEscalate: (id) => `${API}/errors/${id}/escalate`,
|
|
5195
|
-
errorUnlink: (id) => `${API}/errors/${id}/unlink`,
|
|
5196
|
-
// SPEC-271 · lepas tautan backlog
|
|
5197
|
-
projectIngestKey: (id) => `${API}/projects/${encodeURIComponent(id)}/ingest-key`,
|
|
5198
5126
|
// SPEC-253 · Help Center publik (bypass gate cookie; otorisasi helpEnabled + kunci opaque tiket).
|
|
5199
5127
|
help: (slug) => `${API}/help/${encodeURIComponent(slug)}`,
|
|
5200
5128
|
helpTickets: (slug) => `${API}/help/${encodeURIComponent(slug)}/tickets`,
|
|
@@ -5208,9 +5136,6 @@ var init_api = __esm({
|
|
|
5208
5136
|
ticketUnlink: (id) => `${API}/tickets/${id}/unlink`,
|
|
5209
5137
|
// SPEC-271 · lepas tautan backlog
|
|
5210
5138
|
ticketReject: (id) => `${API}/tickets/${id}/reject`,
|
|
5211
|
-
// SPEC-337 · ADR-0075 · relasi integrasi antar project (LOCAL-only) + log lintas project.
|
|
5212
|
-
projectLinks: (id) => `${API}/projects/${encodeURIComponent(id)}/links`,
|
|
5213
|
-
projectLink: (id, linkId) => `${API}/projects/${encodeURIComponent(id)}/links/${encodeURIComponent(linkId)}`,
|
|
5214
5139
|
// SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
|
|
5215
5140
|
// (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
|
|
5216
5141
|
download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
|
|
@@ -5300,7 +5225,6 @@ var init_session_kind = __esm({
|
|
|
5300
5225
|
"prd",
|
|
5301
5226
|
"scaffold",
|
|
5302
5227
|
"breakdown",
|
|
5303
|
-
"cross-audit",
|
|
5304
5228
|
"vps",
|
|
5305
5229
|
"shell",
|
|
5306
5230
|
"worktree",
|
|
@@ -5869,45 +5793,7 @@ Stack: ${project.stack || "\u2014"}`,
|
|
|
5869
5793
|
${REVERSE_STANDARD}`
|
|
5870
5794
|
].filter(Boolean).join("\n\n");
|
|
5871
5795
|
}
|
|
5872
|
-
|
|
5873
|
-
const map = [projectLine(ctx.primary, true), ...ctx.neighbors.map((n) => projectLine(n, false))].join("\n");
|
|
5874
|
-
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".';
|
|
5875
|
-
const head = [
|
|
5876
|
-
`hanoman cross-audit. Kamu mengaudit INTEGRASI ANTAR PROJECT \u2014 bukan satu project saja. Semua project di bawah ini berada dalam scope-mu.`,
|
|
5877
|
-
`Project dalam scope:
|
|
5878
|
-
${map}`,
|
|
5879
|
-
scopeNote,
|
|
5880
|
-
// ADR-0002 · yang boleh ditulis HANYA worktree sesi — termasuk checkout utama project sendiri
|
|
5881
|
-
// pun read-only. Menyebut repoDir di sini akan mengundang agen menyentuh working tree utama.
|
|
5882
|
-
`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.`,
|
|
5883
|
-
crossAuditLogGuide(ctx.apiUrl),
|
|
5884
|
-
CROSS_AUDIT_FOCUS
|
|
5885
|
-
].filter(Boolean);
|
|
5886
|
-
if (mode === "live") {
|
|
5887
|
-
return [
|
|
5888
|
-
...head,
|
|
5889
|
-
"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."
|
|
5890
|
-
].join("\n\n");
|
|
5891
|
-
}
|
|
5892
|
-
const spec = ctx.spec;
|
|
5893
|
-
const slug = spec.title.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
5894
|
-
const detail = spec.payload ? `
|
|
5895
|
-
Detail: ${JSON.stringify(spec.payload)}` : "";
|
|
5896
|
-
return [
|
|
5897
|
-
...head,
|
|
5898
|
-
phaseInstruction(PIPELINES["cross-audit"]),
|
|
5899
|
-
`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.`,
|
|
5900
|
-
// SPEC-340 · ADR-0076 · cross-audit berdokumen memakai kontrak rekomendasi yang sama dgn audit-only.
|
|
5901
|
-
ESCALATION_CONTRACT,
|
|
5902
|
-
AUTONOMY_CLAUSE,
|
|
5903
|
-
skillInstruction(PIPELINES["cross-audit"]),
|
|
5904
|
-
`Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${ctx.branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
|
|
5905
|
-
`Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
|
|
5906
|
-
Judul: ${spec.title}
|
|
5907
|
-
Objective: ${spec.objective}${detail}`
|
|
5908
|
-
].filter(Boolean).join("\n\n");
|
|
5909
|
-
}
|
|
5910
|
-
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;
|
|
5796
|
+
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;
|
|
5911
5797
|
var init_prompt = __esm({
|
|
5912
5798
|
"../runner/src/prompt.ts"() {
|
|
5913
5799
|
"use strict";
|
|
@@ -5924,7 +5810,6 @@ var init_prompt = __esm({
|
|
|
5924
5810
|
breakdown: ["Analisis", "Breakdown"],
|
|
5925
5811
|
// SPEC-337 · ADR-0075 · audit lintas project: fase & stage-map identik audit-only, scope-nya
|
|
5926
5812
|
// yang berbeda (project utama + tetangga ProjectLink).
|
|
5927
|
-
"cross-audit": ["Audit", "Laporan"],
|
|
5928
5813
|
// SPEC-407 · ADR-0089 · backlog goal: tak ada fase perencanaan sama sekali. `Goal` = kerjakan,
|
|
5929
5814
|
// `Verifikasi` = buktikan. Kedua nama unik lintas PIPELINES — syarat peta REACHED di server,
|
|
5930
5815
|
// yang berkunci nama fase saja.
|
|
@@ -6019,27 +5904,6 @@ ${lines2.join("\n")}` : "";
|
|
|
6019
5904
|
"- Objective: kunci SATU MVP objective yang terukur dari hasil brainstorm, tulis ringkas di docs.",
|
|
6020
5905
|
"- 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."
|
|
6021
5906
|
].join("\n");
|
|
6022
|
-
projectLine = (p, primary) => {
|
|
6023
|
-
const path = p.repoDir ?? "(tak ada checkout lokal di mesin ini \u2014 audit project ini dari log & docs saja)";
|
|
6024
|
-
const head = primary ? `- ${p.id} \xB7 ${p.name} \xB7 PROJECT UTAMA (worktree kamu)` : `- ${p.id} \xB7 ${p.name}`;
|
|
6025
|
-
return [
|
|
6026
|
-
head,
|
|
6027
|
-
` stack: ${p.stack || "\u2014"}`,
|
|
6028
|
-
` path: ${path}`,
|
|
6029
|
-
...p.relation ? [` relasi: ${p.relation}`] : [],
|
|
6030
|
-
...p.note ? [` catatan integrasi: ${p.note}`] : []
|
|
6031
|
-
].join("\n");
|
|
6032
|
-
};
|
|
6033
|
-
crossAuditLogGuide = (apiUrl) => [
|
|
6034
|
-
`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:`,
|
|
6035
|
-
"```bash",
|
|
6036
|
-
`curl -s -H "X-Hanoman-Audit-Key: $HANOMAN_AUDIT_KEY" "$HANOMAN_AUDIT_URL/logs?since=24h"`,
|
|
6037
|
-
`curl -s -H "X-Hanoman-Audit-Key: $HANOMAN_AUDIT_KEY" "$HANOMAN_AUDIT_URL/logs?since=7d&environment=production&q=timeout"`,
|
|
6038
|
-
`curl -s -H "X-Hanoman-Audit-Key: $HANOMAN_AUDIT_KEY" "$HANOMAN_AUDIT_URL/logs/<groupId>" # detail + stack`,
|
|
6039
|
-
"```",
|
|
6040
|
-
`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.`
|
|
6041
|
-
].join("\n");
|
|
6042
|
-
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.";
|
|
6043
5907
|
}
|
|
6044
5908
|
});
|
|
6045
5909
|
|
|
@@ -6144,6 +6008,17 @@ var init_git = __esm({
|
|
|
6144
6008
|
{ cwd: repo, encoding: "utf8" }
|
|
6145
6009
|
);
|
|
6146
6010
|
return r.status === 0 ? r.stdout.trim() : null;
|
|
6011
|
+
},
|
|
6012
|
+
// SPEC-447 · `git merge-base --is-ancestor A B` = exit 0 (ya) / 1 (tidak) / lainnya (error).
|
|
6013
|
+
// `--end-of-options` menjaga ADR-0032: sha & ref datang dari DB/kolom, jangan sampai terbaca
|
|
6014
|
+
// sebagai flag. Ref yang tak resolve membuat git exit 128 → dibaca sebagai "belum" (fail-closed).
|
|
6015
|
+
isAncestor: (repo, sha, ref) => {
|
|
6016
|
+
const r = spawnSync(
|
|
6017
|
+
"git",
|
|
6018
|
+
["merge-base", "--is-ancestor", "--end-of-options", sha, ref],
|
|
6019
|
+
{ cwd: repo, encoding: "utf8" }
|
|
6020
|
+
);
|
|
6021
|
+
return r.status === 0;
|
|
6147
6022
|
}
|
|
6148
6023
|
};
|
|
6149
6024
|
}
|
|
@@ -6483,12 +6358,16 @@ function planComplete(worktree, specId) {
|
|
|
6483
6358
|
}
|
|
6484
6359
|
return true;
|
|
6485
6360
|
}
|
|
6361
|
+
function sessionComplete(phases, worktree, specId) {
|
|
6362
|
+
if (!phasesComplete(phases)) return false;
|
|
6363
|
+
return specId ? planComplete(worktree, specId) : true;
|
|
6364
|
+
}
|
|
6486
6365
|
function stageForRun(phases, worktree, specId) {
|
|
6487
6366
|
const s = stageFor(phases);
|
|
6488
6367
|
if (s === "done" && !planComplete(worktree, specId)) return "executing";
|
|
6489
6368
|
return s;
|
|
6490
6369
|
}
|
|
6491
|
-
var phaseFilePath, decisionFilePath, REACHED;
|
|
6370
|
+
var phaseFilePath, decisionFilePath, REACHED, phasesComplete;
|
|
6492
6371
|
var init_session_phases = __esm({
|
|
6493
6372
|
"src/services/session-phases.ts"() {
|
|
6494
6373
|
"use strict";
|
|
@@ -6508,6 +6387,7 @@ var init_session_phases = __esm({
|
|
|
6508
6387
|
Goal: "executing",
|
|
6509
6388
|
Verifikasi: "done"
|
|
6510
6389
|
};
|
|
6390
|
+
phasesComplete = (phases) => phases.length > 0 && phases.every((p) => p.state === "done" || p.state === "skipped");
|
|
6511
6391
|
}
|
|
6512
6392
|
});
|
|
6513
6393
|
|
|
@@ -6595,21 +6475,7 @@ function listPanes() {
|
|
|
6595
6475
|
throw e;
|
|
6596
6476
|
}
|
|
6597
6477
|
return out3.split("\n").filter(Boolean).flatMap((line) => {
|
|
6598
|
-
const [
|
|
6599
|
-
n,
|
|
6600
|
-
projectId,
|
|
6601
|
-
specId,
|
|
6602
|
-
flow,
|
|
6603
|
-
phaseFile,
|
|
6604
|
-
cwd,
|
|
6605
|
-
dead,
|
|
6606
|
-
code,
|
|
6607
|
-
decisionFile,
|
|
6608
|
-
branch,
|
|
6609
|
-
agent,
|
|
6610
|
-
auditKey,
|
|
6611
|
-
auditProjects
|
|
6612
|
-
] = line.split(" ");
|
|
6478
|
+
const [n, projectId, specId, flow, phaseFile, cwd, dead, code, decisionFile, branch, agent] = line.split(" ");
|
|
6613
6479
|
if (!n?.startsWith(PREFIX)) return [];
|
|
6614
6480
|
const exited = dead === "1";
|
|
6615
6481
|
return [{
|
|
@@ -6622,9 +6488,6 @@ function listPanes() {
|
|
|
6622
6488
|
exited,
|
|
6623
6489
|
code: Number(code) || 0,
|
|
6624
6490
|
decisionFile: decisionFile || void 0,
|
|
6625
|
-
// SPEC-337 · kunci audit + scope-nya (internal; tak pernah keluar lewat listSessions).
|
|
6626
|
-
auditKey: auditKey || void 0,
|
|
6627
|
-
auditProjects: auditProjects || void 0,
|
|
6628
6491
|
// SPEC-230 · branch integrasi sesi project-level (PRD: prd/<slug>). Kosong = tak ada.
|
|
6629
6492
|
branch: branch || void 0,
|
|
6630
6493
|
// SPEC-196 · sesi hidup dengan marker keputusan terisi = menunggu manusia.
|
|
@@ -6640,7 +6503,6 @@ function registerSessionHooks(h) {
|
|
|
6640
6503
|
function sessionKind(o, projectId, cwd) {
|
|
6641
6504
|
if (o.specId) return "spec";
|
|
6642
6505
|
if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
|
|
6643
|
-
if (o.id.startsWith("xaudit-")) return "cross-audit";
|
|
6644
6506
|
if (projectId.startsWith("vps")) return "vps";
|
|
6645
6507
|
if (o.command) return "shell";
|
|
6646
6508
|
if (cwd.includes("/.worktrees/")) return "worktree";
|
|
@@ -6654,13 +6516,6 @@ function captureTranscript(id) {
|
|
|
6654
6516
|
return null;
|
|
6655
6517
|
}
|
|
6656
6518
|
}
|
|
6657
|
-
function auditSessionScope(key) {
|
|
6658
|
-
if (!key) return null;
|
|
6659
|
-
const p = listPanes().find((x) => x.auditKey === key && !x.exited);
|
|
6660
|
-
if (!p) return null;
|
|
6661
|
-
const scope = (p.auditProjects ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
6662
|
-
return scope.length ? scope : null;
|
|
6663
|
-
}
|
|
6664
6519
|
function createSession(projectId, cwd, opts = {}) {
|
|
6665
6520
|
const id = opts.id ?? idFor(opts.specId);
|
|
6666
6521
|
const existing = getSession(id);
|
|
@@ -6776,10 +6631,6 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6776
6631
|
tmux("set-option", "-t", name(id), "@hanoman_agent", agent);
|
|
6777
6632
|
if (opts.phaseFile) tmux("set-option", "-t", name(id), "@hanoman_phase_file", opts.phaseFile);
|
|
6778
6633
|
if (opts.decisionFile) tmux("set-option", "-t", name(id), "@hanoman_decision_file", opts.decisionFile);
|
|
6779
|
-
if (opts.audit) {
|
|
6780
|
-
tmux("set-option", "-t", name(id), "@hanoman_audit_key", opts.audit.key);
|
|
6781
|
-
tmux("set-option", "-t", name(id), "@hanoman_audit_projects", opts.audit.projects.join(","));
|
|
6782
|
-
}
|
|
6783
6634
|
if (opts.goal && !opts.command) void armGoalInTui(id, opts.goal, { agent }).catch(() => {
|
|
6784
6635
|
});
|
|
6785
6636
|
emitBirth({
|
|
@@ -6907,10 +6758,11 @@ function end(id, code) {
|
|
|
6907
6758
|
function pollPhases(p, a) {
|
|
6908
6759
|
if (!p.flow || !p.phaseFile) return;
|
|
6909
6760
|
const phases = readPhases(p.phaseFile, p.flow);
|
|
6910
|
-
const
|
|
6761
|
+
const complete = sessionComplete(phases, p.cwd, p.specId);
|
|
6762
|
+
const json = phaseKey(phases, complete);
|
|
6911
6763
|
if (json === a.lastPhases) return;
|
|
6912
6764
|
a.lastPhases = json;
|
|
6913
|
-
broadcast(a, { t: "phase", phases });
|
|
6765
|
+
broadcast(a, { t: "phase", phases, complete });
|
|
6914
6766
|
}
|
|
6915
6767
|
function startPoll() {
|
|
6916
6768
|
if (poll) return;
|
|
@@ -6951,10 +6803,11 @@ function attach(id, c) {
|
|
|
6951
6803
|
const a = attached.get(id) ?? open(id);
|
|
6952
6804
|
a.clients.add(c);
|
|
6953
6805
|
if (a.scrollback) c.send(frame({ t: "data", d: a.scrollback }));
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6806
|
+
if (p.flow && p.phaseFile) {
|
|
6807
|
+
const phases = readPhases(p.phaseFile, p.flow);
|
|
6808
|
+
const complete = sessionComplete(phases, p.cwd, p.specId);
|
|
6809
|
+
a.lastPhases = phaseKey(phases, complete);
|
|
6810
|
+
c.send(frame({ t: "phase", phases, complete }));
|
|
6958
6811
|
}
|
|
6959
6812
|
}
|
|
6960
6813
|
function writeTo(id, d) {
|
|
@@ -6991,7 +6844,7 @@ function spawnPty(...args) {
|
|
|
6991
6844
|
);
|
|
6992
6845
|
}
|
|
6993
6846
|
}
|
|
6994
|
-
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, poll, detach;
|
|
6847
|
+
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;
|
|
6995
6848
|
var init_pty = __esm({
|
|
6996
6849
|
"src/services/pty.ts"() {
|
|
6997
6850
|
"use strict";
|
|
@@ -7042,11 +6895,7 @@ var init_pty = __esm({
|
|
|
7042
6895
|
"#{pane_dead_status}",
|
|
7043
6896
|
"#{@hanoman_decision_file}",
|
|
7044
6897
|
"#{@hanoman_branch}",
|
|
7045
|
-
"#{@hanoman_agent}"
|
|
7046
|
-
// SPEC-337 · ADR-0075 · kunci audit lintas project + scope-nya. Hidup di tmux (bukan DB): selamat
|
|
7047
|
-
// dari restart API, mati bersama pane. TAK PERNAH ikut ke SessionInfo/API — lihat listSessions.
|
|
7048
|
-
"#{@hanoman_audit_key}",
|
|
7049
|
-
"#{@hanoman_audit_projects}"
|
|
6898
|
+
"#{@hanoman_agent}"
|
|
7050
6899
|
].join(" ");
|
|
7051
6900
|
listSessions = () => listPanes().map(({ id, projectId, specId, flow, cwd, exited, code, branch, decision, agent }) => ({
|
|
7052
6901
|
id,
|
|
@@ -7093,6 +6942,7 @@ var init_pty = __esm({
|
|
|
7093
6942
|
const text = paneText(id);
|
|
7094
6943
|
return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
|
|
7095
6944
|
};
|
|
6945
|
+
phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
|
|
7096
6946
|
detach = (id, c) => {
|
|
7097
6947
|
attached.get(id)?.clients.delete(c);
|
|
7098
6948
|
};
|
|
@@ -7127,11 +6977,9 @@ var init_outbox = __esm({
|
|
|
7127
6977
|
async function renameProjectCore(tx, oldId, newId) {
|
|
7128
6978
|
await tx.project.update({ where: { id: oldId }, data: { id: newId, updatedAt: /* @__PURE__ */ new Date() } });
|
|
7129
6979
|
const spec = await tx.spec.count({ where: { projectId: newId } });
|
|
7130
|
-
const errorGroup = await tx.errorGroup.count({ where: { projectId: newId } });
|
|
7131
6980
|
const ticket = await tx.ticket.count({ where: { projectId: newId } });
|
|
7132
6981
|
const notification = (await tx.notification.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
7133
6982
|
const sessionResult = (await tx.sessionResult.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
7134
|
-
const errorEvent = (await tx.errorEvent.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
7135
6983
|
const ticketAttachment = (await tx.ticketAttachment.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
7136
6984
|
const binding = await tx.localBinding.findUnique({ where: { projectId: oldId } });
|
|
7137
6985
|
let localBinding = 0;
|
|
@@ -7140,7 +6988,7 @@ async function renameProjectCore(tx, oldId, newId) {
|
|
|
7140
6988
|
await tx.localBinding.create({ data: { projectId: newId, repoDir: binding.repoDir, createdAt: binding.createdAt } });
|
|
7141
6989
|
localBinding = 1;
|
|
7142
6990
|
}
|
|
7143
|
-
return { spec,
|
|
6991
|
+
return { spec, ticket, notification, sessionResult, ticketAttachment, localBinding };
|
|
7144
6992
|
}
|
|
7145
6993
|
async function renameProject(oldId, newId) {
|
|
7146
6994
|
if (!zProjectId.safeParse(newId).success) return { ok: false, code: 400, error: "id baru tak sah (slug)" };
|
|
@@ -7169,11 +7017,9 @@ var init_rename_project = __esm({
|
|
|
7169
7017
|
RENAME_SEP = " ";
|
|
7170
7018
|
ZERO_AFFECTED = {
|
|
7171
7019
|
spec: 0,
|
|
7172
|
-
errorGroup: 0,
|
|
7173
7020
|
ticket: 0,
|
|
7174
7021
|
notification: 0,
|
|
7175
7022
|
sessionResult: 0,
|
|
7176
|
-
errorEvent: 0,
|
|
7177
7023
|
ticketAttachment: 0,
|
|
7178
7024
|
localBinding: 0
|
|
7179
7025
|
};
|
|
@@ -7184,6 +7030,7 @@ var init_rename_project = __esm({
|
|
|
7184
7030
|
var sync_exports = {};
|
|
7185
7031
|
__export(sync_exports, {
|
|
7186
7032
|
SYNCED: () => SYNCED,
|
|
7033
|
+
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
7187
7034
|
applyPush: () => applyPush,
|
|
7188
7035
|
backfillFeed: () => backfillFeed,
|
|
7189
7036
|
isEntity: () => isEntity,
|
|
@@ -7321,19 +7168,18 @@ async function upsertLocal(entity, id, version, data) {
|
|
|
7321
7168
|
function setAcceptedHook(hook) {
|
|
7322
7169
|
onAccepted = hook;
|
|
7323
7170
|
}
|
|
7324
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted;
|
|
7171
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
7325
7172
|
var init_sync = __esm({
|
|
7326
7173
|
"src/services/sync.ts"() {
|
|
7327
7174
|
"use strict";
|
|
7328
7175
|
init_db();
|
|
7329
7176
|
init_rename_project();
|
|
7330
|
-
SYNCED = ["project", "spec", "vps", "sessionResult", "
|
|
7177
|
+
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment"];
|
|
7331
7178
|
DELEGATE = {
|
|
7332
7179
|
project: prisma.project,
|
|
7333
7180
|
spec: prisma.spec,
|
|
7334
7181
|
vps: prisma.vps,
|
|
7335
7182
|
sessionResult: prisma.sessionResult,
|
|
7336
|
-
errorGroup: prisma.errorGroup,
|
|
7337
7183
|
ticket: prisma.ticket,
|
|
7338
7184
|
ticketAttachment: prisma.ticketAttachment
|
|
7339
7185
|
};
|
|
@@ -7341,11 +7187,11 @@ var init_sync = __esm({
|
|
|
7341
7187
|
project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
|
|
7342
7188
|
// SPEC-408 · ADR-0090 · createdAt/startedAt ikut menyeberang — sejajar baseSha/headSha. Tanpa
|
|
7343
7189
|
// ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
|
|
7344
|
-
|
|
7190
|
+
// SPEC-447 · ADR-0093 · dependsOn ikut juga: tanpa itu client tak tahu urutannya dan akan
|
|
7191
|
+
// meluncurkan pekerjaan yang di hub terblokir. Bukan DATE_FIELDS — nilainya array string.
|
|
7192
|
+
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "createdAt", "startedAt", "updatedAt"],
|
|
7345
7193
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7346
7194
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
7347
|
-
// SPEC-268 · ADR-0066 · agregat grup error (ErrorEvent mentah tak disync).
|
|
7348
|
-
errorGroup: ["projectId", "fingerprint", "type", "message", "sampleStack", "environment", "status", "count", "firstSeenAt", "lastSeenAt", "specId", "updatedAt"],
|
|
7349
7195
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
7350
7196
|
// (kolom required @unique tanpa default); kunci plaintext tak pernah menyeberang.
|
|
7351
7197
|
ticket: ["projectId", "number", "category", "title", "detail", "reporterEmail", "status", "accessKeyHash", "specId", "createdAt", "updatedAt"],
|
|
@@ -7357,10 +7203,10 @@ var init_sync = __esm({
|
|
|
7357
7203
|
spec: ["createdAt", "startedAt", "updatedAt"],
|
|
7358
7204
|
vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7359
7205
|
sessionResult: ["createdAt", "updatedAt"],
|
|
7360
|
-
errorGroup: ["firstSeenAt", "lastSeenAt", "updatedAt"],
|
|
7361
7206
|
ticket: ["createdAt", "updatedAt"],
|
|
7362
7207
|
ticketAttachment: ["createdAt", "updatedAt"]
|
|
7363
7208
|
};
|
|
7209
|
+
__FIELDS_FOR_TEST = FIELDS;
|
|
7364
7210
|
}
|
|
7365
7211
|
});
|
|
7366
7212
|
|
|
@@ -9526,7 +9372,7 @@ var require_extension = __commonJS({
|
|
|
9526
9372
|
if (dest[name2] === void 0) dest[name2] = [elem];
|
|
9527
9373
|
else dest[name2].push(elem);
|
|
9528
9374
|
}
|
|
9529
|
-
function
|
|
9375
|
+
function parse2(header) {
|
|
9530
9376
|
const offers = /* @__PURE__ */ Object.create(null);
|
|
9531
9377
|
let params = /* @__PURE__ */ Object.create(null);
|
|
9532
9378
|
let mustUnescape = false;
|
|
@@ -9666,7 +9512,7 @@ var require_extension = __commonJS({
|
|
|
9666
9512
|
}).join(", ");
|
|
9667
9513
|
}).join(", ");
|
|
9668
9514
|
}
|
|
9669
|
-
module.exports = { format, parse:
|
|
9515
|
+
module.exports = { format, parse: parse2 };
|
|
9670
9516
|
}
|
|
9671
9517
|
});
|
|
9672
9518
|
|
|
@@ -9679,7 +9525,7 @@ var require_websocket = __commonJS({
|
|
|
9679
9525
|
var http = __require("http");
|
|
9680
9526
|
var net = __require("net");
|
|
9681
9527
|
var tls = __require("tls");
|
|
9682
|
-
var { randomBytes:
|
|
9528
|
+
var { randomBytes: randomBytes5, createHash: createHash5 } = __require("crypto");
|
|
9683
9529
|
var { Duplex, Readable } = __require("stream");
|
|
9684
9530
|
var { URL: URL2 } = __require("url");
|
|
9685
9531
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -9700,7 +9546,7 @@ var require_websocket = __commonJS({
|
|
|
9700
9546
|
var {
|
|
9701
9547
|
EventTarget: { addEventListener, removeEventListener }
|
|
9702
9548
|
} = require_event_target();
|
|
9703
|
-
var { format, parse:
|
|
9549
|
+
var { format, parse: parse2 } = require_extension();
|
|
9704
9550
|
var { toBuffer } = require_buffer_util();
|
|
9705
9551
|
var kAborted = Symbol("kAborted");
|
|
9706
9552
|
var protocolVersions = [8, 13];
|
|
@@ -10217,7 +10063,7 @@ var require_websocket = __commonJS({
|
|
|
10217
10063
|
}
|
|
10218
10064
|
}
|
|
10219
10065
|
const defaultPort = isSecure ? 443 : 80;
|
|
10220
|
-
const key =
|
|
10066
|
+
const key = randomBytes5(16).toString("base64");
|
|
10221
10067
|
const request = isSecure ? https.request : http.request;
|
|
10222
10068
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
10223
10069
|
let perMessageDeflate;
|
|
@@ -10347,7 +10193,7 @@ var require_websocket = __commonJS({
|
|
|
10347
10193
|
abortHandshake(websocket2, socket2, "Invalid Upgrade header");
|
|
10348
10194
|
return;
|
|
10349
10195
|
}
|
|
10350
|
-
const digest =
|
|
10196
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
10351
10197
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
10352
10198
|
abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
|
|
10353
10199
|
return;
|
|
@@ -10377,7 +10223,7 @@ var require_websocket = __commonJS({
|
|
|
10377
10223
|
}
|
|
10378
10224
|
let extensions;
|
|
10379
10225
|
try {
|
|
10380
|
-
extensions =
|
|
10226
|
+
extensions = parse2(secWebSocketExtensions);
|
|
10381
10227
|
} catch (err) {
|
|
10382
10228
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
10383
10229
|
abortHandshake(websocket2, socket2, message);
|
|
@@ -10669,7 +10515,7 @@ var require_subprotocol = __commonJS({
|
|
|
10669
10515
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
|
|
10670
10516
|
"use strict";
|
|
10671
10517
|
var { tokenChars } = require_validation();
|
|
10672
|
-
function
|
|
10518
|
+
function parse2(header) {
|
|
10673
10519
|
const protocols = /* @__PURE__ */ new Set();
|
|
10674
10520
|
let start = -1;
|
|
10675
10521
|
let end2 = -1;
|
|
@@ -10705,7 +10551,7 @@ var require_subprotocol = __commonJS({
|
|
|
10705
10551
|
protocols.add(protocol);
|
|
10706
10552
|
return protocols;
|
|
10707
10553
|
}
|
|
10708
|
-
module.exports = { parse:
|
|
10554
|
+
module.exports = { parse: parse2 };
|
|
10709
10555
|
}
|
|
10710
10556
|
});
|
|
10711
10557
|
|
|
@@ -10716,7 +10562,7 @@ var require_websocket_server = __commonJS({
|
|
|
10716
10562
|
var EventEmitter = __require("events");
|
|
10717
10563
|
var http = __require("http");
|
|
10718
10564
|
var { Duplex } = __require("stream");
|
|
10719
|
-
var { createHash:
|
|
10565
|
+
var { createHash: createHash5 } = __require("crypto");
|
|
10720
10566
|
var extension2 = require_extension();
|
|
10721
10567
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
10722
10568
|
var subprotocol2 = require_subprotocol();
|
|
@@ -11023,7 +10869,7 @@ var require_websocket_server = __commonJS({
|
|
|
11023
10869
|
);
|
|
11024
10870
|
}
|
|
11025
10871
|
if (this._state > RUNNING) return abortHandshake(socket2, 503);
|
|
11026
|
-
const digest =
|
|
10872
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
11027
10873
|
const headers = [
|
|
11028
10874
|
"HTTP/1.1 101 Switching Protocols",
|
|
11029
10875
|
"Upgrade: websocket",
|
|
@@ -13894,17 +13740,17 @@ var require_stream_wormhole = __commonJS({
|
|
|
13894
13740
|
"../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
|
|
13895
13741
|
"use strict";
|
|
13896
13742
|
module.exports = (stream, throwError) => {
|
|
13897
|
-
return new Promise((
|
|
13743
|
+
return new Promise((resolve14, reject) => {
|
|
13898
13744
|
if (typeof stream.resume !== "function") {
|
|
13899
|
-
return
|
|
13745
|
+
return resolve14();
|
|
13900
13746
|
}
|
|
13901
13747
|
stream.unpipe && stream.unpipe();
|
|
13902
13748
|
stream.resume();
|
|
13903
13749
|
if (stream._readableState && stream._readableState.ended) {
|
|
13904
|
-
return
|
|
13750
|
+
return resolve14();
|
|
13905
13751
|
}
|
|
13906
13752
|
if (!stream.readable || stream.destroyed) {
|
|
13907
|
-
return
|
|
13753
|
+
return resolve14();
|
|
13908
13754
|
}
|
|
13909
13755
|
function cleanup() {
|
|
13910
13756
|
stream.removeListener("end", onEnd);
|
|
@@ -13913,14 +13759,14 @@ var require_stream_wormhole = __commonJS({
|
|
|
13913
13759
|
}
|
|
13914
13760
|
function onEnd() {
|
|
13915
13761
|
cleanup();
|
|
13916
|
-
|
|
13762
|
+
resolve14();
|
|
13917
13763
|
}
|
|
13918
13764
|
function onError(err) {
|
|
13919
13765
|
cleanup();
|
|
13920
13766
|
if (throwError) {
|
|
13921
13767
|
reject(err);
|
|
13922
13768
|
} else {
|
|
13923
|
-
|
|
13769
|
+
resolve14();
|
|
13924
13770
|
}
|
|
13925
13771
|
}
|
|
13926
13772
|
stream.on("end", onEnd);
|
|
@@ -14136,7 +13982,7 @@ var require_secure_json_parse = __commonJS({
|
|
|
14136
13982
|
}
|
|
14137
13983
|
return obj;
|
|
14138
13984
|
}
|
|
14139
|
-
function
|
|
13985
|
+
function parse2(text, reviver, options2) {
|
|
14140
13986
|
const stackTraceLimit = Error.stackTraceLimit;
|
|
14141
13987
|
Error.stackTraceLimit = 0;
|
|
14142
13988
|
try {
|
|
@@ -14156,9 +14002,9 @@ var require_secure_json_parse = __commonJS({
|
|
|
14156
14002
|
Error.stackTraceLimit = stackTraceLimit;
|
|
14157
14003
|
}
|
|
14158
14004
|
}
|
|
14159
|
-
module.exports =
|
|
14160
|
-
module.exports.default =
|
|
14161
|
-
module.exports.parse =
|
|
14005
|
+
module.exports = parse2;
|
|
14006
|
+
module.exports.default = parse2;
|
|
14007
|
+
module.exports.parse = parse2;
|
|
14162
14008
|
module.exports.safeParse = safeParse;
|
|
14163
14009
|
module.exports.scan = filter;
|
|
14164
14010
|
}
|
|
@@ -14172,7 +14018,7 @@ var require_multipart2 = __commonJS({
|
|
|
14172
14018
|
var os = __require("node:os");
|
|
14173
14019
|
var fp = require_plugin();
|
|
14174
14020
|
var { createWriteStream } = __require("node:fs");
|
|
14175
|
-
var { unlink:
|
|
14021
|
+
var { unlink: unlink3 } = __require("node:fs/promises");
|
|
14176
14022
|
var path = __require("node:path");
|
|
14177
14023
|
var { generateId } = require_generateId();
|
|
14178
14024
|
var util2 = __require("node:util");
|
|
@@ -14355,16 +14201,16 @@ var require_multipart2 = __commonJS({
|
|
|
14355
14201
|
}
|
|
14356
14202
|
};
|
|
14357
14203
|
const parts = () => {
|
|
14358
|
-
return new Promise((
|
|
14204
|
+
return new Promise((resolve14, reject) => {
|
|
14359
14205
|
handle((val) => {
|
|
14360
14206
|
if (val instanceof Error) {
|
|
14361
14207
|
if (val.message === "Unexpected end of multipart data") {
|
|
14362
|
-
|
|
14208
|
+
resolve14(null);
|
|
14363
14209
|
} else {
|
|
14364
14210
|
reject(val);
|
|
14365
14211
|
}
|
|
14366
14212
|
} else {
|
|
14367
|
-
|
|
14213
|
+
resolve14(val);
|
|
14368
14214
|
}
|
|
14369
14215
|
});
|
|
14370
14216
|
});
|
|
@@ -14567,7 +14413,7 @@ var require_multipart2 = __commonJS({
|
|
|
14567
14413
|
for (let i = 0; i < this.tmpUploads.length; ++i) {
|
|
14568
14414
|
const filepath = this.tmpUploads[i];
|
|
14569
14415
|
try {
|
|
14570
|
-
await
|
|
14416
|
+
await unlink3(filepath);
|
|
14571
14417
|
} catch (error) {
|
|
14572
14418
|
this.log.error(error, "Could not delete file");
|
|
14573
14419
|
}
|
|
@@ -14654,41 +14500,6 @@ async function health_default(app2) {
|
|
|
14654
14500
|
|
|
14655
14501
|
// src/routes/projects.ts
|
|
14656
14502
|
init_src();
|
|
14657
|
-
|
|
14658
|
-
// src/services/project-links.ts
|
|
14659
|
-
init_db();
|
|
14660
|
-
var linksOf = (projectId) => prisma.projectLink.findMany({
|
|
14661
|
-
where: { OR: [{ fromProjectId: projectId }, { toProjectId: projectId }] },
|
|
14662
|
-
orderBy: { createdAt: "asc" }
|
|
14663
|
-
});
|
|
14664
|
-
function neighborIds(projectId, links) {
|
|
14665
|
-
const out3 = /* @__PURE__ */ new Set();
|
|
14666
|
-
for (const l of links) {
|
|
14667
|
-
const other = l.fromProjectId === projectId ? l.toProjectId : l.fromProjectId;
|
|
14668
|
-
if (other !== projectId) out3.add(other);
|
|
14669
|
-
}
|
|
14670
|
-
return [...out3];
|
|
14671
|
-
}
|
|
14672
|
-
async function linkViews(projectId, links) {
|
|
14673
|
-
const ids = neighborIds(projectId, links);
|
|
14674
|
-
const rows = await prisma.project.findMany({ where: { id: { in: ids } }, select: { id: true, name: true } });
|
|
14675
|
-
const names = new Map(rows.map((p) => [p.id, p.name]));
|
|
14676
|
-
return links.map((l) => {
|
|
14677
|
-
const direction = l.fromProjectId === projectId ? "keluar" : "masuk";
|
|
14678
|
-
const otherId = direction === "keluar" ? l.toProjectId : l.fromProjectId;
|
|
14679
|
-
return {
|
|
14680
|
-
id: l.id,
|
|
14681
|
-
fromProjectId: l.fromProjectId,
|
|
14682
|
-
toProjectId: l.toProjectId,
|
|
14683
|
-
kind: l.kind,
|
|
14684
|
-
note: l.note,
|
|
14685
|
-
direction,
|
|
14686
|
-
other: { id: otherId, name: names.get(otherId) ?? otherId }
|
|
14687
|
-
};
|
|
14688
|
-
});
|
|
14689
|
-
}
|
|
14690
|
-
|
|
14691
|
-
// src/routes/projects.ts
|
|
14692
14503
|
init_db();
|
|
14693
14504
|
init_rename_project();
|
|
14694
14505
|
|
|
@@ -14844,9 +14655,6 @@ async function toProjectView(p, sessions) {
|
|
|
14844
14655
|
session,
|
|
14845
14656
|
activity: session.status === "running" ? `running \xB7 ${session.flow ?? "sesi"}` : "idle",
|
|
14846
14657
|
commit,
|
|
14847
|
-
// SPEC-249 · error monitoring: ekspos status + hint prefix. ingestKeyHash TAK PERNAH ke client.
|
|
14848
|
-
monitoringEnabled: !!p.ingestKeyHash,
|
|
14849
|
-
ingestKeyPrefix: p.ingestKeyPrefix ?? null,
|
|
14850
14658
|
// SPEC-253 · Help Center publik aktif.
|
|
14851
14659
|
helpEnabled: p.helpEnabled,
|
|
14852
14660
|
// SPEC-294 · opt-in scheduler otonom (lokal per-instance).
|
|
@@ -14906,25 +14714,6 @@ function paginate(items, page, limit) {
|
|
|
14906
14714
|
return { items: pageSize ? items.slice(start, start + pageSize) : items, total, page: p, pageSize };
|
|
14907
14715
|
}
|
|
14908
14716
|
|
|
14909
|
-
// src/services/ingest-key.ts
|
|
14910
|
-
import { randomBytes, createHash, timingSafeEqual } from "node:crypto";
|
|
14911
|
-
function hashKey(key) {
|
|
14912
|
-
return createHash("sha256").update(key).digest("hex");
|
|
14913
|
-
}
|
|
14914
|
-
function generateIngestKey() {
|
|
14915
|
-
const key = "hnm_ing_" + randomBytes(24).toString("hex");
|
|
14916
|
-
return { key, hash: hashKey(key), prefix: key.slice(0, 16) };
|
|
14917
|
-
}
|
|
14918
|
-
function verifyKey(key, hash2) {
|
|
14919
|
-
if (!key || !hash2) return false;
|
|
14920
|
-
const a = Buffer.from(hashKey(key), "hex");
|
|
14921
|
-
const b = Buffer.from(hash2, "hex");
|
|
14922
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
14923
|
-
}
|
|
14924
|
-
function dsnUrl(slug, key, base) {
|
|
14925
|
-
return `${base.replace(/\/$/, "")}/api/ingest/${encodeURIComponent(slug)}?key=${key}`;
|
|
14926
|
-
}
|
|
14927
|
-
|
|
14928
14717
|
// src/routes/projects.ts
|
|
14929
14718
|
init_src2();
|
|
14930
14719
|
async function projects_default(app2) {
|
|
@@ -14991,8 +14780,6 @@ async function projects_default(app2) {
|
|
|
14991
14780
|
const base = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
14992
14781
|
return {
|
|
14993
14782
|
id: newId,
|
|
14994
|
-
// Plaintext key DSN tak tersimpan; kembalikan path DSN baru (tanpa key) sbg hint bila monitoring aktif.
|
|
14995
|
-
dsnUrl: p?.ingestKeyHash ? `${base.replace(/\/$/, "")}/api/ingest/${encodeURIComponent(newId)}` : void 0,
|
|
14996
14783
|
helpUrl: p?.helpEnabled ? `${base}/help/${encodeURIComponent(newId)}` : void 0,
|
|
14997
14784
|
affected: r.affected
|
|
14998
14785
|
};
|
|
@@ -15012,30 +14799,6 @@ async function projects_default(app2) {
|
|
|
15012
14799
|
const repoDir = await resolveRepoDir(id);
|
|
15013
14800
|
return { branches: await listRepoBranches(repoDir), remotes: await listRepoRemoteBranches(repoDir) };
|
|
15014
14801
|
});
|
|
15015
|
-
app2.get("/projects/:id/ingest-key", async (req, reply) => {
|
|
15016
|
-
const { id } = req.params;
|
|
15017
|
-
const p = await prisma.project.findUnique({ where: { id } });
|
|
15018
|
-
if (!p) return reply.code(404).send({ error: "not found" });
|
|
15019
|
-
return { enabled: !!p.ingestKeyHash, prefix: p.ingestKeyPrefix ?? null };
|
|
15020
|
-
});
|
|
15021
|
-
app2.post("/projects/:id/ingest-key", async (req, reply) => {
|
|
15022
|
-
const { id } = req.params;
|
|
15023
|
-
const p = await prisma.project.findUnique({ where: { id } });
|
|
15024
|
-
if (!p) return reply.code(404).send({ error: "not found" });
|
|
15025
|
-
const { key, hash: hash2, prefix } = generateIngestKey();
|
|
15026
|
-
await prisma.project.update({ where: { id }, data: { ingestKeyHash: hash2, ingestKeyPrefix: prefix } });
|
|
15027
|
-
await notifySynced("project", id);
|
|
15028
|
-
const base = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
15029
|
-
return reply.code(201).send({ enabled: true, prefix, key, dsnUrl: dsnUrl(id, key, base) });
|
|
15030
|
-
});
|
|
15031
|
-
app2.delete("/projects/:id/ingest-key", async (req, reply) => {
|
|
15032
|
-
const { id } = req.params;
|
|
15033
|
-
const p = await prisma.project.findUnique({ where: { id } });
|
|
15034
|
-
if (!p) return reply.code(404).send({ error: "not found" });
|
|
15035
|
-
await prisma.project.update({ where: { id }, data: { ingestKeyHash: null, ingestKeyPrefix: null } });
|
|
15036
|
-
await notifySynced("project", id);
|
|
15037
|
-
return reply.code(204).send();
|
|
15038
|
-
});
|
|
15039
14802
|
app2.get("/projects/:id/help-center", async (req, reply) => {
|
|
15040
14803
|
const { id } = req.params;
|
|
15041
14804
|
const p = await prisma.project.findUnique({ where: { id } });
|
|
@@ -15060,35 +14823,6 @@ async function projects_default(app2) {
|
|
|
15060
14823
|
await notifySynced("project", id);
|
|
15061
14824
|
return reply.code(204).send();
|
|
15062
14825
|
});
|
|
15063
|
-
app2.get("/projects/:id/links", async (req, reply) => {
|
|
15064
|
-
const { id } = req.params;
|
|
15065
|
-
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
15066
|
-
return { links: await linkViews(id, await linksOf(id)) };
|
|
15067
|
-
});
|
|
15068
|
-
app2.post("/projects/:id/links", async (req, reply) => {
|
|
15069
|
-
const { id } = req.params;
|
|
15070
|
-
const parsed = zCreateLink.safeParse(req.body);
|
|
15071
|
-
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
15072
|
-
const { to, kind, note } = parsed.data;
|
|
15073
|
-
if (to === id) return reply.code(400).send({ error: "project tak bisa bergantung pada dirinya sendiri" });
|
|
15074
|
-
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
15075
|
-
if (!await prisma.project.findUnique({ where: { id: to } })) return reply.code(404).send({ error: `project "${to}" tak ada` });
|
|
15076
|
-
const existing = await prisma.projectLink.findUnique({
|
|
15077
|
-
where: { fromProjectId_toProjectId: { fromProjectId: id, toProjectId: to } }
|
|
15078
|
-
});
|
|
15079
|
-
if (existing) return reply.code(409).send({ error: `relasi ${id} \u2192 ${to} sudah ada` });
|
|
15080
|
-
const created = await prisma.projectLink.create({ data: { fromProjectId: id, toProjectId: to, kind, note: note ?? "" } });
|
|
15081
|
-
const [view7] = await linkViews(id, [created]);
|
|
15082
|
-
return reply.code(201).send(view7);
|
|
15083
|
-
});
|
|
15084
|
-
app2.delete("/projects/:id/links/:linkId", async (req, reply) => {
|
|
15085
|
-
const { id, linkId } = req.params;
|
|
15086
|
-
const link2 = await prisma.projectLink.findUnique({ where: { id: linkId } });
|
|
15087
|
-
if (!link2 || link2.fromProjectId !== id && link2.toProjectId !== id)
|
|
15088
|
-
return reply.code(404).send({ error: "not found" });
|
|
15089
|
-
await prisma.projectLink.delete({ where: { id: linkId } });
|
|
15090
|
-
return reply.code(204).send();
|
|
15091
|
-
});
|
|
15092
14826
|
}
|
|
15093
14827
|
|
|
15094
14828
|
// src/routes/specs.ts
|
|
@@ -15641,25 +15375,147 @@ async function findAuditDoc(specId, sessions) {
|
|
|
15641
15375
|
return doc ? { dir: dir2, path: doc.path, live } : null;
|
|
15642
15376
|
}
|
|
15643
15377
|
async function readAuditDoc(specId, sessions = listSessions()) {
|
|
15644
|
-
const
|
|
15645
|
-
if (!
|
|
15646
|
-
const content = readDocFile(
|
|
15647
|
-
return content === null ? null : { path:
|
|
15378
|
+
const found = await findAuditDoc(specId, sessions);
|
|
15379
|
+
if (!found) return null;
|
|
15380
|
+
const content = readDocFile(found.dir, found.path);
|
|
15381
|
+
return content === null ? null : { path: found.path, content };
|
|
15648
15382
|
}
|
|
15649
15383
|
async function readEscalation(specId, sessions = listSessions()) {
|
|
15650
|
-
const
|
|
15651
|
-
if (!
|
|
15384
|
+
const found = await findAuditDoc(specId, sessions);
|
|
15385
|
+
if (!found) {
|
|
15652
15386
|
const live = sessions.some((s) => s.specId === specId && !s.exited && s.cwd);
|
|
15653
15387
|
return { escalation: null, docPath: null, live };
|
|
15654
15388
|
}
|
|
15655
|
-
const content = readDocFile(
|
|
15389
|
+
const content = readDocFile(found.dir, found.path);
|
|
15656
15390
|
return {
|
|
15657
15391
|
escalation: content === null ? null : parseEscalation(content),
|
|
15658
|
-
docPath:
|
|
15659
|
-
live:
|
|
15392
|
+
docPath: found.path,
|
|
15393
|
+
live: found.live
|
|
15660
15394
|
};
|
|
15661
15395
|
}
|
|
15662
15396
|
|
|
15397
|
+
// src/services/spec-deps.ts
|
|
15398
|
+
init_db();
|
|
15399
|
+
init_src2();
|
|
15400
|
+
var REASON_LABEL = {
|
|
15401
|
+
missing: "tak ditemukan",
|
|
15402
|
+
unfinished: "belum selesai",
|
|
15403
|
+
unmerged: "belum ter-merge"
|
|
15404
|
+
};
|
|
15405
|
+
function dependsOnOf(spec) {
|
|
15406
|
+
const v = spec.dependsOn;
|
|
15407
|
+
if (!Array.isArray(v)) return [];
|
|
15408
|
+
const out3 = [];
|
|
15409
|
+
for (const x of v) if (typeof x === "string" && x !== "" && !out3.includes(x)) out3.push(x);
|
|
15410
|
+
return out3;
|
|
15411
|
+
}
|
|
15412
|
+
function blockersFor(spec, deps, isMerged) {
|
|
15413
|
+
const ids = dependsOnOf(spec);
|
|
15414
|
+
if (ids.length === 0) return [];
|
|
15415
|
+
const base = spec.branchFrom ?? "HEAD";
|
|
15416
|
+
const out3 = [];
|
|
15417
|
+
for (const id of ids) {
|
|
15418
|
+
const d = deps.get(id);
|
|
15419
|
+
if (!d) {
|
|
15420
|
+
out3.push({ id, reason: "missing" });
|
|
15421
|
+
continue;
|
|
15422
|
+
}
|
|
15423
|
+
if (d.stage !== "done") {
|
|
15424
|
+
out3.push({ id, reason: "unfinished" });
|
|
15425
|
+
continue;
|
|
15426
|
+
}
|
|
15427
|
+
if (d.headSha && !isMerged(d.headSha, base)) out3.push({ id, reason: "unmerged" });
|
|
15428
|
+
}
|
|
15429
|
+
return out3;
|
|
15430
|
+
}
|
|
15431
|
+
function blockedNote(bl) {
|
|
15432
|
+
return `menunggu ${bl.map((b) => `${b.id} (${REASON_LABEL[b.reason]})`).join(", ")}`;
|
|
15433
|
+
}
|
|
15434
|
+
function reaches(edges, from, target2) {
|
|
15435
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15436
|
+
const stack = [...from];
|
|
15437
|
+
while (stack.length) {
|
|
15438
|
+
const cur = stack.pop();
|
|
15439
|
+
if (cur === target2) return true;
|
|
15440
|
+
if (seen.has(cur)) continue;
|
|
15441
|
+
seen.add(cur);
|
|
15442
|
+
for (const n of edges.get(cur) ?? []) stack.push(n);
|
|
15443
|
+
}
|
|
15444
|
+
return false;
|
|
15445
|
+
}
|
|
15446
|
+
var TTL_MS = 15e3;
|
|
15447
|
+
var mergeCache = /* @__PURE__ */ new Map();
|
|
15448
|
+
function mergedInto(repoDir, sha, baseRef) {
|
|
15449
|
+
const key = `${repoDir} ${sha} ${baseRef}`;
|
|
15450
|
+
const hit = mergeCache.get(key);
|
|
15451
|
+
const now = Date.now();
|
|
15452
|
+
if (hit && now - hit.at < TTL_MS) return hit.v;
|
|
15453
|
+
let v = false;
|
|
15454
|
+
try {
|
|
15455
|
+
v = realGit.isAncestor(repoDir, sha, baseRef);
|
|
15456
|
+
} catch {
|
|
15457
|
+
v = false;
|
|
15458
|
+
}
|
|
15459
|
+
mergeCache.set(key, { at: now, v });
|
|
15460
|
+
return v;
|
|
15461
|
+
}
|
|
15462
|
+
var merger = (repoDir) => (sha, base) => repoDir ? mergedInto(repoDir, sha, base) : false;
|
|
15463
|
+
var depRows = (ids) => prisma.spec.findMany({
|
|
15464
|
+
where: { id: { in: ids } },
|
|
15465
|
+
select: { id: true, stage: true, headSha: true }
|
|
15466
|
+
});
|
|
15467
|
+
async function blockersForSpec(spec, repoDir) {
|
|
15468
|
+
const ids = dependsOnOf(spec);
|
|
15469
|
+
if (ids.length === 0) return [];
|
|
15470
|
+
const rows = await depRows(ids);
|
|
15471
|
+
return blockersFor(spec, new Map(rows.map((r) => [r.id, r])), merger(repoDir));
|
|
15472
|
+
}
|
|
15473
|
+
async function decorateBlocked(specs) {
|
|
15474
|
+
const ids = [...new Set(specs.flatMap(dependsOnOf))];
|
|
15475
|
+
if (ids.length === 0) return specs.map((s) => ({ ...s, dependsOn: [], blockedBy: [] }));
|
|
15476
|
+
const rows = await depRows(ids);
|
|
15477
|
+
const deps = new Map(rows.map((r) => [r.id, r]));
|
|
15478
|
+
const repos = /* @__PURE__ */ new Map();
|
|
15479
|
+
const out3 = [];
|
|
15480
|
+
for (const s of specs) {
|
|
15481
|
+
const own = dependsOnOf(s);
|
|
15482
|
+
if (own.length === 0) {
|
|
15483
|
+
out3.push({ ...s, dependsOn: [], blockedBy: [] });
|
|
15484
|
+
continue;
|
|
15485
|
+
}
|
|
15486
|
+
if (!repos.has(s.projectId)) repos.set(s.projectId, await resolveRepoDir(s.projectId));
|
|
15487
|
+
out3.push({ ...s, dependsOn: own, blockedBy: blockersFor(s, deps, merger(repos.get(s.projectId))) });
|
|
15488
|
+
}
|
|
15489
|
+
return out3;
|
|
15490
|
+
}
|
|
15491
|
+
async function validateDependsOn(specId, projectId, raw) {
|
|
15492
|
+
const ids = dependsOnOf({ dependsOn: raw });
|
|
15493
|
+
if (ids.length === 0) return { ok: true, ids: [] };
|
|
15494
|
+
if (specId && ids.includes(specId))
|
|
15495
|
+
return { ok: false, error: "backlog tak bisa bergantung pada dirinya sendiri" };
|
|
15496
|
+
const rows = await prisma.spec.findMany({
|
|
15497
|
+
where: { id: { in: ids } },
|
|
15498
|
+
select: { id: true, projectId: true }
|
|
15499
|
+
});
|
|
15500
|
+
const found = new Map(rows.map((r) => [r.id, r.projectId]));
|
|
15501
|
+
const missing = ids.filter((i) => !found.has(i));
|
|
15502
|
+
if (missing.length) return { ok: false, error: `backlog tak ditemukan: ${missing.join(", ")}` };
|
|
15503
|
+
const foreign = ids.filter((i) => found.get(i) !== projectId);
|
|
15504
|
+
if (foreign.length)
|
|
15505
|
+
return { ok: false, error: `dependency harus di project yang sama: ${foreign.join(", ")}` };
|
|
15506
|
+
if (specId) {
|
|
15507
|
+
const all = await prisma.spec.findMany({
|
|
15508
|
+
where: { projectId },
|
|
15509
|
+
select: { id: true, dependsOn: true }
|
|
15510
|
+
});
|
|
15511
|
+
const edges = new Map(all.map((r) => [r.id, dependsOnOf(r)]));
|
|
15512
|
+
edges.set(specId, ids);
|
|
15513
|
+
if (reaches(edges, ids, specId))
|
|
15514
|
+
return { ok: false, error: "dependency membentuk siklus" };
|
|
15515
|
+
}
|
|
15516
|
+
return { ok: true, ids };
|
|
15517
|
+
}
|
|
15518
|
+
|
|
15663
15519
|
// src/services/doc-export.ts
|
|
15664
15520
|
import PDFDocument from "pdfkit";
|
|
15665
15521
|
|
|
@@ -15694,8 +15550,8 @@ var escapeReplacements = {
|
|
|
15694
15550
|
"'": "'"
|
|
15695
15551
|
};
|
|
15696
15552
|
var getEscapeReplacement = (ch) => escapeReplacements[ch];
|
|
15697
|
-
function escape$1(html2,
|
|
15698
|
-
if (
|
|
15553
|
+
function escape$1(html2, encode) {
|
|
15554
|
+
if (encode) {
|
|
15699
15555
|
if (escapeTest.test(html2)) {
|
|
15700
15556
|
return html2.replace(escapeReplace, getEscapeReplacement);
|
|
15701
15557
|
}
|
|
@@ -17982,7 +17838,7 @@ function renderDocPdf(text, name2, meta) {
|
|
|
17982
17838
|
});
|
|
17983
17839
|
const chunks = [];
|
|
17984
17840
|
doc.on("data", (c) => chunks.push(c));
|
|
17985
|
-
const done = new Promise((
|
|
17841
|
+
const done = new Promise((resolve14) => doc.on("end", () => resolve14(Buffer.concat(chunks))));
|
|
17986
17842
|
doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
|
|
17987
17843
|
doc.moveDown(0.25);
|
|
17988
17844
|
doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
|
|
@@ -18084,14 +17940,6 @@ async function recordFailure(specId, title, projectId, reason) {
|
|
|
18084
17940
|
}).catch(() => {
|
|
18085
17941
|
});
|
|
18086
17942
|
}
|
|
18087
|
-
async function recordNewErrorGroup(groupId, projectId, projectName, type, message) {
|
|
18088
|
-
const short = message.length > 80 ? message.slice(0, 77) + "\u2026" : message;
|
|
18089
|
-
const title = `Error baru di "${projectName}": ${type}: ${short}`;
|
|
18090
|
-
await prisma.notification.create({
|
|
18091
|
-
data: { type: "error", key: `error:${groupId}`, projectId, title }
|
|
18092
|
-
}).catch(() => {
|
|
18093
|
-
});
|
|
18094
|
-
}
|
|
18095
17943
|
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
18096
17944
|
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
18097
17945
|
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
@@ -18137,7 +17985,7 @@ async function liveSpecs(filter = {}) {
|
|
|
18137
17985
|
orderBy: { id: "desc" }
|
|
18138
17986
|
});
|
|
18139
17987
|
const live = sessionPhasesBySpec();
|
|
18140
|
-
if (live.size === 0) return specs;
|
|
17988
|
+
if (live.size === 0) return decorateBlocked(specs);
|
|
18141
17989
|
const advanced = [];
|
|
18142
17990
|
const doneNow = [];
|
|
18143
17991
|
const out3 = specs.map((s) => {
|
|
@@ -18155,7 +18003,7 @@ async function liveSpecs(filter = {}) {
|
|
|
18155
18003
|
if (res.count > 0) await notifySynced("spec", a.id);
|
|
18156
18004
|
}));
|
|
18157
18005
|
await Promise.all(doneNow.map((d) => recordCompletion(d.specId, d.title, d.projectId)));
|
|
18158
|
-
return out3;
|
|
18006
|
+
return decorateBlocked(out3);
|
|
18159
18007
|
}
|
|
18160
18008
|
|
|
18161
18009
|
// src/routes/specs.ts
|
|
@@ -18195,6 +18043,8 @@ async function specs_default(app2) {
|
|
|
18195
18043
|
const repoDir = await resolveRepoDir(b.project);
|
|
18196
18044
|
if (b.branchFrom && await branchUnknown(repoDir, b.branchFrom))
|
|
18197
18045
|
return reply.code(400).send({ error: `branch "${b.branchFrom}" tidak ada di repo project` });
|
|
18046
|
+
const dep = await validateDependsOn(null, b.project, b.dependsOn ?? []);
|
|
18047
|
+
if (!dep.ok) return reply.code(400).send({ error: dep.error });
|
|
18198
18048
|
const isQa = b.source === "qa";
|
|
18199
18049
|
const { priority, objective } = deriveSpecFields(b.source, b.payload, b.priority);
|
|
18200
18050
|
const author = req.user?.email ?? "system";
|
|
@@ -18210,10 +18060,12 @@ async function specs_default(app2) {
|
|
|
18210
18060
|
source: b.source,
|
|
18211
18061
|
stage: "brainstorming",
|
|
18212
18062
|
priority,
|
|
18213
|
-
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "
|
|
18063
|
+
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
|
|
18214
18064
|
objective,
|
|
18215
18065
|
payload: b.payload,
|
|
18216
|
-
branchFrom: b.branchFrom ?? null
|
|
18066
|
+
branchFrom: b.branchFrom ?? null,
|
|
18067
|
+
dependsOn: dep.ids
|
|
18068
|
+
// SPEC-447 · ADR-0093
|
|
18217
18069
|
}
|
|
18218
18070
|
});
|
|
18219
18071
|
} catch (e) {
|
|
@@ -18277,7 +18129,7 @@ ${item.context}` : item.context;
|
|
|
18277
18129
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
18278
18130
|
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
18279
18131
|
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
18280
|
-
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload } = parsed.data;
|
|
18132
|
+
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload, dependsOn } = parsed.data;
|
|
18281
18133
|
const editingContent = title !== void 0 || newPriority !== void 0 || payload !== void 0;
|
|
18282
18134
|
if (editingContent && (spec.stage !== "brainstorming" || spec.baseSha !== null))
|
|
18283
18135
|
return reply.code(409).send({ error: "backlog item sudah dimulai \u2014 tak bisa diedit" });
|
|
@@ -18285,6 +18137,12 @@ ${item.context}` : item.context;
|
|
|
18285
18137
|
if (await branchUnknown(await resolveRepoDir(spec.projectId), branchFrom))
|
|
18286
18138
|
return reply.code(400).send({ error: `branch "${branchFrom}" tidak ada di repo project` });
|
|
18287
18139
|
}
|
|
18140
|
+
let depIds;
|
|
18141
|
+
if (dependsOn !== void 0) {
|
|
18142
|
+
const d = await validateDependsOn(spec.id, spec.projectId, dependsOn);
|
|
18143
|
+
if (!d.ok) return reply.code(400).send({ error: d.error });
|
|
18144
|
+
depIds = d.ids;
|
|
18145
|
+
}
|
|
18288
18146
|
if (stage !== void 0) {
|
|
18289
18147
|
if (STAGES.indexOf(stage) >= STAGES.indexOf(spec.stage))
|
|
18290
18148
|
return reply.code(422).send({ error: "stage hanya boleh dikembalikan mundur" });
|
|
@@ -18296,6 +18154,7 @@ ${item.context}` : item.context;
|
|
|
18296
18154
|
}
|
|
18297
18155
|
const data = {};
|
|
18298
18156
|
if (branchFrom !== void 0) data.branchFrom = branchFrom;
|
|
18157
|
+
if (depIds !== void 0) data.dependsOn = depIds;
|
|
18299
18158
|
if (stage !== void 0) data.stage = stage;
|
|
18300
18159
|
if (editingContent) {
|
|
18301
18160
|
const effPayload = payload ?? spec.payload;
|
|
@@ -18328,8 +18187,21 @@ ${item.context}` : item.context;
|
|
|
18328
18187
|
});
|
|
18329
18188
|
app2.delete("/specs/:id", async (req, reply) => {
|
|
18330
18189
|
const { id } = req.params;
|
|
18190
|
+
const gone = await prisma.spec.findUnique({ where: { id }, select: { projectId: true } });
|
|
18331
18191
|
await prisma.spec.delete({ where: { id } }).catch(() => {
|
|
18332
18192
|
});
|
|
18193
|
+
if (gone) {
|
|
18194
|
+
const rows = await prisma.spec.findMany({
|
|
18195
|
+
where: { projectId: gone.projectId },
|
|
18196
|
+
select: { id: true, dependsOn: true }
|
|
18197
|
+
});
|
|
18198
|
+
for (const r of rows) {
|
|
18199
|
+
const ids = dependsOnOf(r);
|
|
18200
|
+
if (!ids.includes(id)) continue;
|
|
18201
|
+
await prisma.spec.update({ where: { id: r.id }, data: { dependsOn: ids.filter((x) => x !== id) } });
|
|
18202
|
+
await notifySynced("spec", r.id);
|
|
18203
|
+
}
|
|
18204
|
+
}
|
|
18333
18205
|
return reply.code(204).send();
|
|
18334
18206
|
});
|
|
18335
18207
|
app2.post("/specs/:id/integrate", async (req, reply) => {
|
|
@@ -19549,70 +19421,8 @@ async function fs_default(app2) {
|
|
|
19549
19421
|
init_db();
|
|
19550
19422
|
init_src();
|
|
19551
19423
|
init_src2();
|
|
19552
|
-
import { existsSync as existsSync6 } from "node:fs";
|
|
19553
|
-
|
|
19554
|
-
// src/services/cross-audit.ts
|
|
19555
|
-
init_db();
|
|
19556
|
-
|
|
19557
|
-
// src/services/audit-scope.ts
|
|
19558
|
-
init_pty();
|
|
19559
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
19560
|
-
var AUDIT_KEY_HEADER = "x-hanoman-audit-key";
|
|
19561
|
-
var newAuditKey = () => `hnm_xa_${randomBytes2(16).toString("hex")}`;
|
|
19562
|
-
var auditApiUrl = () => `http://127.0.0.1:${process.env.PORT ?? 8787}/api/audit`;
|
|
19563
|
-
function auditScopeFromReq(req) {
|
|
19564
|
-
const raw = req.headers[AUDIT_KEY_HEADER];
|
|
19565
|
-
const key = Array.isArray(raw) ? raw[0] : raw;
|
|
19566
|
-
if (typeof key !== "string" || !key) return null;
|
|
19567
|
-
return auditSessionScope(key);
|
|
19568
|
-
}
|
|
19569
|
-
|
|
19570
|
-
// src/services/cross-audit.ts
|
|
19571
|
-
async function buildCrossAuditCtx(primaryId) {
|
|
19572
|
-
const primary = await prisma.project.findUnique({ where: { id: primaryId } });
|
|
19573
|
-
if (!primary) return null;
|
|
19574
|
-
const views = await linkViews(primaryId, await linksOf(primaryId));
|
|
19575
|
-
const rows = await prisma.project.findMany({ where: { id: { in: views.map((v) => v.other.id) } } });
|
|
19576
|
-
const byId2 = new Map(rows.map((p) => [p.id, p]));
|
|
19577
|
-
const neighbors = [];
|
|
19578
|
-
for (const v of views) {
|
|
19579
|
-
const p = byId2.get(v.other.id);
|
|
19580
|
-
if (!p) continue;
|
|
19581
|
-
const relation = v.direction === "keluar" ? `${primary.name} bergantung pada ${p.name} (${v.kind})` : `${p.name} bergantung pada ${primary.name} (${v.kind})`;
|
|
19582
|
-
neighbors.push({
|
|
19583
|
-
id: p.id,
|
|
19584
|
-
name: p.name,
|
|
19585
|
-
stack: p.stack,
|
|
19586
|
-
repoDir: await resolveRepoDir(p.id),
|
|
19587
|
-
// null = belum di-bind di mesin ini; prompt menandainya
|
|
19588
|
-
relation,
|
|
19589
|
-
note: v.note
|
|
19590
|
-
});
|
|
19591
|
-
}
|
|
19592
|
-
return {
|
|
19593
|
-
ctx: {
|
|
19594
|
-
primary: {
|
|
19595
|
-
id: primary.id,
|
|
19596
|
-
name: primary.name,
|
|
19597
|
-
stack: primary.stack,
|
|
19598
|
-
repoDir: await resolveRepoDir(primary.id)
|
|
19599
|
-
},
|
|
19600
|
-
neighbors,
|
|
19601
|
-
apiUrl: auditApiUrl()
|
|
19602
|
-
},
|
|
19603
|
-
scope: [primary.id, ...neighbors.map((n) => n.id)]
|
|
19604
|
-
};
|
|
19605
|
-
}
|
|
19606
|
-
function crossAuditSessionOpts(scope) {
|
|
19607
|
-
const key = newAuditKey();
|
|
19608
|
-
return {
|
|
19609
|
-
audit: { key, projects: scope },
|
|
19610
|
-
env: { HANOMAN_AUDIT_KEY: key, HANOMAN_AUDIT_URL: auditApiUrl() }
|
|
19611
|
-
};
|
|
19612
|
-
}
|
|
19613
|
-
|
|
19614
|
-
// src/routes/terminal.ts
|
|
19615
19424
|
init_session_phases();
|
|
19425
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
19616
19426
|
|
|
19617
19427
|
// src/services/session-launch.ts
|
|
19618
19428
|
init_db();
|
|
@@ -19621,9 +19431,11 @@ init_pty();
|
|
|
19621
19431
|
init_session_phases();
|
|
19622
19432
|
init_pty();
|
|
19623
19433
|
var LaunchError = class extends Error {
|
|
19624
|
-
|
|
19434
|
+
// SPEC-447 · `blockers` hanya terisi untuk kind "blocked"; route memetakannya ke body 409.
|
|
19435
|
+
constructor(message, kind, blockers = []) {
|
|
19625
19436
|
super(message);
|
|
19626
19437
|
this.kind = kind;
|
|
19438
|
+
this.blockers = blockers;
|
|
19627
19439
|
}
|
|
19628
19440
|
};
|
|
19629
19441
|
function resumeState(repoDir, worktree, branchTo, headSha) {
|
|
@@ -19645,6 +19457,11 @@ async function startSpecSession(spec, opts) {
|
|
|
19645
19457
|
const id = sessionIdForSpec(spec.id);
|
|
19646
19458
|
const pane = getSession(id);
|
|
19647
19459
|
if (pane && !pane.exited) return { id: pane.id, reused: true };
|
|
19460
|
+
if (!opts.force) {
|
|
19461
|
+
const blockers = await blockersForSpec(spec, repoDir);
|
|
19462
|
+
if (blockers.length)
|
|
19463
|
+
throw new LaunchError(`${spec.id} ${blockedNote(blockers)}`, "blocked", blockers);
|
|
19464
|
+
}
|
|
19648
19465
|
if (pane) killSession(id);
|
|
19649
19466
|
const setting = await getSetting();
|
|
19650
19467
|
const agent = opts.agent ?? setting.agent;
|
|
@@ -19704,17 +19521,6 @@ async function startSpecSession(spec, opts) {
|
|
|
19704
19521
|
prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope);
|
|
19705
19522
|
}
|
|
19706
19523
|
const scopeEnv = { HANOMAN_BASE_SHA: baseSha, HANOMAN_VERIFY_SCOPE: verifyScope };
|
|
19707
|
-
let extra = {};
|
|
19708
|
-
if (opts.flow === "cross-audit") {
|
|
19709
|
-
const built = await buildCrossAuditCtx(spec.projectId);
|
|
19710
|
-
if (built) {
|
|
19711
|
-
prompt = startCrossAuditPrompt(
|
|
19712
|
-
{ ...built.ctx, worktree, spec: brief, branchTo },
|
|
19713
|
-
"backlog"
|
|
19714
|
-
);
|
|
19715
|
-
extra = crossAuditSessionOpts(built.scope);
|
|
19716
|
-
}
|
|
19717
|
-
}
|
|
19718
19524
|
const s = createSession(spec.projectId, worktree, {
|
|
19719
19525
|
specId: spec.id,
|
|
19720
19526
|
flow: opts.flow,
|
|
@@ -19725,9 +19531,7 @@ async function startSpecSession(spec, opts) {
|
|
|
19725
19531
|
phaseFile: phaseFilePath(repoDir, id),
|
|
19726
19532
|
decisionFile: decisionFilePath(repoDir, id),
|
|
19727
19533
|
prompt,
|
|
19728
|
-
|
|
19729
|
-
// Digabung SESUDAH `extra` supaya env audit lintas (SPEC-337) tak terhapus dan sebaliknya.
|
|
19730
|
-
env: { ...scopeEnv, ...extra.env ?? {} }
|
|
19534
|
+
env: scopeEnv
|
|
19731
19535
|
});
|
|
19732
19536
|
return resume ? { id: s.id, resumed: true } : { id: s.id };
|
|
19733
19537
|
}
|
|
@@ -19820,12 +19624,16 @@ async function terminal_default(app2) {
|
|
|
19820
19624
|
// SPEC-332 · ADR-0073
|
|
19821
19625
|
agent: parsed.data.agent,
|
|
19822
19626
|
// SPEC-338 · ADR-0074
|
|
19823
|
-
verifyScope: parsed.data.verifyScope
|
|
19627
|
+
verifyScope: parsed.data.verifyScope,
|
|
19824
19628
|
// SPEC-376 · ADR-0080
|
|
19629
|
+
force: parsed.data.force
|
|
19630
|
+
// SPEC-447 · ADR-0093
|
|
19825
19631
|
});
|
|
19826
19632
|
return reply.code(201).send(r.resumed ? { id: r.id, resumed: true } : { id: r.id });
|
|
19827
19633
|
} catch (e) {
|
|
19828
19634
|
if (e instanceof LaunchError) {
|
|
19635
|
+
if (e.kind === "blocked")
|
|
19636
|
+
return reply.code(409).send({ error: e.message, blocked: true, blockers: e.blockers });
|
|
19829
19637
|
return e.kind === "needs-bind" ? reply.code(400).send({ error: e.message, needsBind: true }) : reply.code(422).send({ error: e.message });
|
|
19830
19638
|
}
|
|
19831
19639
|
throw e;
|
|
@@ -19975,32 +19783,6 @@ async function terminal_default(app2) {
|
|
|
19975
19783
|
});
|
|
19976
19784
|
return reply.code(201).send({ id: s2.id });
|
|
19977
19785
|
}
|
|
19978
|
-
if (parsed.data.flow === "cross-audit") {
|
|
19979
|
-
const id = `xaudit-${project.id.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}`;
|
|
19980
|
-
const live = getSession(id);
|
|
19981
|
-
if (live && !live.exited) return reply.code(201).send({ id: live.id });
|
|
19982
|
-
const built = await buildCrossAuditCtx(project.id);
|
|
19983
|
-
if (!built) return reply.code(404).send({ error: "project not found" });
|
|
19984
|
-
const { agent: agent2, model: model2, effort: effort2 } = await sessionAgentDefaults();
|
|
19985
|
-
if (agent2 === "codex") ensureCodexTrust(repoDir);
|
|
19986
|
-
const wt = `${repoDir}/.worktrees/${id}`;
|
|
19987
|
-
let reused;
|
|
19988
|
-
try {
|
|
19989
|
-
reused = ensureWorktree(repoDir, wt, "HEAD");
|
|
19990
|
-
} catch (e) {
|
|
19991
|
-
return reply.code(422).send({ error: `gagal membuat worktree: ${e.message}` });
|
|
19992
|
-
}
|
|
19993
|
-
const s2 = createSession(project.id, wt, {
|
|
19994
|
-
id,
|
|
19995
|
-
model: model2,
|
|
19996
|
-
effort: effort2,
|
|
19997
|
-
agent: agent2,
|
|
19998
|
-
decisionFile: decisionFilePath(repoDir, id),
|
|
19999
|
-
prompt: startCrossAuditPrompt({ ...built.ctx, worktree: wt }, "live") + resumeNote(reused),
|
|
20000
|
-
...crossAuditSessionOpts(built.scope)
|
|
20001
|
-
});
|
|
20002
|
-
return reply.code(201).send({ id: s2.id });
|
|
20003
|
-
}
|
|
20004
19786
|
const { agent, model, effort } = await sessionAgentDefaults();
|
|
20005
19787
|
if (agent === "codex") ensureCodexTrust(repoDir);
|
|
20006
19788
|
const s = createSession(project.id, repoDir, { agent, model, effort });
|
|
@@ -20252,14 +20034,14 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
20252
20034
|
SSH_ASKPASS_REQUIRE: "force",
|
|
20253
20035
|
HANOMAN_SSH_PASSWORD: opts.password
|
|
20254
20036
|
} : process.env;
|
|
20255
|
-
return new Promise((
|
|
20037
|
+
return new Promise((resolve14) => {
|
|
20256
20038
|
const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
20257
20039
|
let out3 = "";
|
|
20258
20040
|
const timer5 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
20259
20041
|
const done = (r) => {
|
|
20260
20042
|
clearTimeout(timer5);
|
|
20261
20043
|
if (askpass) rmSync4(dirname9(askpass), { recursive: true, force: true });
|
|
20262
|
-
|
|
20044
|
+
resolve14(r);
|
|
20263
20045
|
};
|
|
20264
20046
|
p.stdout.on("data", (d) => {
|
|
20265
20047
|
out3 += d;
|
|
@@ -20787,7 +20569,7 @@ import { readFileSync as readFileSync9 } from "node:fs";
|
|
|
20787
20569
|
import { homedir as homedir6 } from "node:os";
|
|
20788
20570
|
import { join as join9 } from "node:path";
|
|
20789
20571
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
20790
|
-
var
|
|
20572
|
+
var TTL_MS2 = 3e4;
|
|
20791
20573
|
var lastOk = null;
|
|
20792
20574
|
var freshUntil = 0;
|
|
20793
20575
|
var LABELS = {
|
|
@@ -20865,7 +20647,7 @@ async function fetchUsage() {
|
|
|
20865
20647
|
async function getLimits() {
|
|
20866
20648
|
if (lastOk && Date.now() < freshUntil) return lastOk;
|
|
20867
20649
|
const dto = await fetchUsage();
|
|
20868
|
-
if (dto.status === "ok") freshUntil = Date.now() +
|
|
20650
|
+
if (dto.status === "ok") freshUntil = Date.now() + TTL_MS2;
|
|
20869
20651
|
return dto;
|
|
20870
20652
|
}
|
|
20871
20653
|
|
|
@@ -20873,7 +20655,7 @@ async function getLimits() {
|
|
|
20873
20655
|
import { open as open2, readdir as readdir2, stat } from "node:fs/promises";
|
|
20874
20656
|
import { homedir as homedir7 } from "node:os";
|
|
20875
20657
|
import { join as join10 } from "node:path";
|
|
20876
|
-
var
|
|
20658
|
+
var TTL_MS3 = 3e4;
|
|
20877
20659
|
var STALE_AFTER_MS = 12 * 36e5;
|
|
20878
20660
|
var TAIL_BYTES = 512 * 1024;
|
|
20879
20661
|
var MAX_FILES = 8;
|
|
@@ -20978,7 +20760,7 @@ async function getCodexLimits(dir2) {
|
|
|
20978
20760
|
const dto = await readLimits(codexSessionsDir());
|
|
20979
20761
|
if (dto.status !== "unavailable") {
|
|
20980
20762
|
cache2 = dto;
|
|
20981
|
-
freshUntil2 = Date.now() +
|
|
20763
|
+
freshUntil2 = Date.now() + TTL_MS3;
|
|
20982
20764
|
}
|
|
20983
20765
|
return dto;
|
|
20984
20766
|
}
|
|
@@ -20996,7 +20778,7 @@ import { execFile as execFile8 } from "node:child_process";
|
|
|
20996
20778
|
import { promisify as promisify8 } from "node:util";
|
|
20997
20779
|
var run = promisify8(execFile8);
|
|
20998
20780
|
var CODEX_MIN_CLIENT = "0.144.0";
|
|
20999
|
-
var
|
|
20781
|
+
var TTL_MS4 = 5 * 6e4;
|
|
21000
20782
|
var cache3 = null;
|
|
21001
20783
|
var codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
|
|
21002
20784
|
function parseCodexVersion(out3) {
|
|
@@ -21004,7 +20786,7 @@ function parseCodexVersion(out3) {
|
|
|
21004
20786
|
return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
|
|
21005
20787
|
}
|
|
21006
20788
|
async function getCodexVersion(now = Date.now()) {
|
|
21007
|
-
if (cache3 && now - cache3.at <
|
|
20789
|
+
if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
|
|
21008
20790
|
let version = null;
|
|
21009
20791
|
try {
|
|
21010
20792
|
const { stdout } = await run(codexBin2(), ["--version"], { timeout: 5e3 });
|
|
@@ -21269,9 +21051,9 @@ init_db();
|
|
|
21269
21051
|
|
|
21270
21052
|
// src/services/device-token.ts
|
|
21271
21053
|
init_db();
|
|
21272
|
-
import { randomBytes
|
|
21273
|
-
var newDeviceToken = () =>
|
|
21274
|
-
var tokenHash = (token) =>
|
|
21054
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
21055
|
+
var newDeviceToken = () => randomBytes(32).toString("base64url");
|
|
21056
|
+
var tokenHash = (token) => createHash("sha256").update(token).digest("hex");
|
|
21275
21057
|
async function issueDeviceToken(userId, name2) {
|
|
21276
21058
|
const token = newDeviceToken();
|
|
21277
21059
|
const row = await prisma.deviceToken.create({ data: { userId, name: name2, tokenHash: tokenHash(token) } });
|
|
@@ -21828,931 +21610,6 @@ async function config_default(app2) {
|
|
|
21828
21610
|
});
|
|
21829
21611
|
}
|
|
21830
21612
|
|
|
21831
|
-
// src/routes/ingest.ts
|
|
21832
|
-
init_src();
|
|
21833
|
-
init_db();
|
|
21834
|
-
|
|
21835
|
-
// src/services/error-ingest.ts
|
|
21836
|
-
init_db();
|
|
21837
|
-
|
|
21838
|
-
// src/services/error-fingerprint.ts
|
|
21839
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
21840
|
-
function normalizeMessage(msg) {
|
|
21841
|
-
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);
|
|
21842
|
-
}
|
|
21843
|
-
function normalizeBundleName(name2) {
|
|
21844
|
-
return name2.replace(/([-.])(?=[a-z0-9_]*\d)[a-z0-9_]{6,}(\.[a-z0-9]+)$/i, "$2");
|
|
21845
|
-
}
|
|
21846
|
-
function baseOf(s) {
|
|
21847
|
-
const clean = s.split(/[?#]/)[0] ?? "";
|
|
21848
|
-
const parts = clean.split(/[/\\]/);
|
|
21849
|
-
return parts[parts.length - 1] || clean;
|
|
21850
|
-
}
|
|
21851
|
-
function topFrame(stack) {
|
|
21852
|
-
if (!stack) return "";
|
|
21853
|
-
for (const raw of stack.split("\n")) {
|
|
21854
|
-
const line = raw.trim();
|
|
21855
|
-
if (!line.startsWith("at ")) continue;
|
|
21856
|
-
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();
|
|
21857
|
-
}
|
|
21858
|
-
return "";
|
|
21859
|
-
}
|
|
21860
|
-
function topFrameFromFrames(frames) {
|
|
21861
|
-
const f = frames.find((x) => x.in_app) ?? frames[0];
|
|
21862
|
-
if (!f) return "";
|
|
21863
|
-
return `at ${f.function ?? "?"} (${normalizeBundleName(baseOf(f.filename ?? ""))})`;
|
|
21864
|
-
}
|
|
21865
|
-
function fingerprint(type, message, stack, frames) {
|
|
21866
|
-
const top = frames && frames.length ? topFrameFromFrames(frames) : topFrame(stack);
|
|
21867
|
-
const basis = `${type}
|
|
21868
|
-
${normalizeMessage(message)}
|
|
21869
|
-
${top}`;
|
|
21870
|
-
return createHash3("sha256").update(basis).digest("hex").slice(0, 32);
|
|
21871
|
-
}
|
|
21872
|
-
|
|
21873
|
-
// src/services/error-ingest.ts
|
|
21874
|
-
init_config2();
|
|
21875
|
-
var MSG_CAP = 2e3;
|
|
21876
|
-
var STACK_CAP = 16e3;
|
|
21877
|
-
var EVENTS_PER_GROUP = () => effectiveInt("HANOMAN_ERROR_EVENTS_PER_GROUP") ?? 50;
|
|
21878
|
-
var RETENTION_DAYS = () => effectiveInt("HANOMAN_ERROR_RETENTION_DAYS") ?? 30;
|
|
21879
|
-
var RATE_PER_MIN = () => effectiveInt("HANOMAN_INGEST_RATE_PER_MIN") ?? 120;
|
|
21880
|
-
var buckets = /* @__PURE__ */ new Map();
|
|
21881
|
-
function rateLimitOk(projectId, now = Date.now()) {
|
|
21882
|
-
const cap = RATE_PER_MIN();
|
|
21883
|
-
const refillPerMs = cap / 6e4;
|
|
21884
|
-
const b = buckets.get(projectId) ?? { tokens: cap, ts: now };
|
|
21885
|
-
b.tokens = Math.min(cap, b.tokens + (now - b.ts) * refillPerMs);
|
|
21886
|
-
b.ts = now;
|
|
21887
|
-
buckets.set(projectId, b);
|
|
21888
|
-
if (b.tokens < 1) return false;
|
|
21889
|
-
b.tokens -= 1;
|
|
21890
|
-
return true;
|
|
21891
|
-
}
|
|
21892
|
-
async function ingestError(projectId, projectName, payload) {
|
|
21893
|
-
const type = payload.type.slice(0, 500);
|
|
21894
|
-
const message = payload.message.slice(0, MSG_CAP);
|
|
21895
|
-
const stack = payload.stack ? payload.stack.slice(0, STACK_CAP) : null;
|
|
21896
|
-
const environment = (payload.environment || "unknown").slice(0, 120);
|
|
21897
|
-
const frames = payload.frames;
|
|
21898
|
-
const fp = fingerprint(type, message, stack ?? void 0, frames);
|
|
21899
|
-
const key = { projectId_fingerprint: { projectId, fingerprint: fp } };
|
|
21900
|
-
let groupId;
|
|
21901
|
-
let isNew = false;
|
|
21902
|
-
const existing = await prisma.errorGroup.findUnique({ where: key });
|
|
21903
|
-
if (existing) {
|
|
21904
|
-
await prisma.errorGroup.update({
|
|
21905
|
-
where: { id: existing.id },
|
|
21906
|
-
data: { count: { increment: 1 }, lastSeenAt: /* @__PURE__ */ new Date(), environment, updatedAt: /* @__PURE__ */ new Date(), release: payload.release ?? void 0 }
|
|
21907
|
-
});
|
|
21908
|
-
groupId = existing.id;
|
|
21909
|
-
} else {
|
|
21910
|
-
try {
|
|
21911
|
-
const g = await prisma.errorGroup.create({
|
|
21912
|
-
data: {
|
|
21913
|
-
projectId,
|
|
21914
|
-
fingerprint: fp,
|
|
21915
|
-
type,
|
|
21916
|
-
message,
|
|
21917
|
-
sampleStack: stack,
|
|
21918
|
-
environment,
|
|
21919
|
-
count: 1,
|
|
21920
|
-
sampleFrames: frames ?? void 0,
|
|
21921
|
-
// SPEC-276 · frame sample (disymbolikasi saat display)
|
|
21922
|
-
release: payload.release ?? null
|
|
21923
|
-
// SPEC-276 · korelasi build
|
|
21924
|
-
}
|
|
21925
|
-
});
|
|
21926
|
-
groupId = g.id;
|
|
21927
|
-
isNew = true;
|
|
21928
|
-
} catch (e) {
|
|
21929
|
-
if (e.code !== "P2002") throw e;
|
|
21930
|
-
const g = await prisma.errorGroup.update({
|
|
21931
|
-
where: key,
|
|
21932
|
-
data: { count: { increment: 1 }, lastSeenAt: /* @__PURE__ */ new Date(), environment, updatedAt: /* @__PURE__ */ new Date() }
|
|
21933
|
-
});
|
|
21934
|
-
groupId = g.id;
|
|
21935
|
-
}
|
|
21936
|
-
}
|
|
21937
|
-
if (isNew && environment === "production")
|
|
21938
|
-
await recordNewErrorGroup(groupId, projectId, projectName, type, message);
|
|
21939
|
-
await prisma.errorEvent.create({
|
|
21940
|
-
data: {
|
|
21941
|
-
groupId,
|
|
21942
|
-
projectId,
|
|
21943
|
-
type,
|
|
21944
|
-
message,
|
|
21945
|
-
stack,
|
|
21946
|
-
environment,
|
|
21947
|
-
frames: frames ?? void 0,
|
|
21948
|
-
// SPEC-276 · frame terstruktur mentah
|
|
21949
|
-
release: payload.release ?? null,
|
|
21950
|
-
context: payload.context ?? void 0
|
|
21951
|
-
}
|
|
21952
|
-
});
|
|
21953
|
-
await pruneGroup(groupId);
|
|
21954
|
-
if (isNew) await notifySynced("errorGroup", groupId);
|
|
21955
|
-
return { groupId, new: isNew };
|
|
21956
|
-
}
|
|
21957
|
-
async function pruneGroup(groupId) {
|
|
21958
|
-
const cutoff = new Date(Date.now() - RETENTION_DAYS() * 864e5);
|
|
21959
|
-
await prisma.errorEvent.deleteMany({ where: { groupId, receivedAt: { lt: cutoff } } });
|
|
21960
|
-
const cap = EVENTS_PER_GROUP();
|
|
21961
|
-
const keep = await prisma.errorEvent.findMany({
|
|
21962
|
-
where: { groupId },
|
|
21963
|
-
orderBy: { receivedAt: "desc" },
|
|
21964
|
-
take: cap,
|
|
21965
|
-
select: { id: true }
|
|
21966
|
-
});
|
|
21967
|
-
if (keep.length >= cap)
|
|
21968
|
-
await prisma.errorEvent.deleteMany({ where: { groupId, id: { notIn: keep.map((k) => k.id) } } });
|
|
21969
|
-
}
|
|
21970
|
-
|
|
21971
|
-
// src/services/sourcemap-store.ts
|
|
21972
|
-
init_db();
|
|
21973
|
-
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
21974
|
-
import { mkdir as mkdir4, writeFile as writeFile4, readFile as readFile5, unlink as unlink3 } from "node:fs/promises";
|
|
21975
|
-
import { join as join13 } from "node:path";
|
|
21976
|
-
function basenameOf(ref) {
|
|
21977
|
-
const clean = ref.split(/[?#]/)[0] ?? "";
|
|
21978
|
-
const parts = clean.split(/[/\\]/);
|
|
21979
|
-
return parts[parts.length - 1] || clean;
|
|
21980
|
-
}
|
|
21981
|
-
async function saveSourceMap(projectId, release, filename, mapText, debugId) {
|
|
21982
|
-
const dir2 = uploadDir();
|
|
21983
|
-
await mkdir4(dir2, { recursive: true });
|
|
21984
|
-
const base = basenameOf(filename);
|
|
21985
|
-
const storageKey = `${randomUUID6()}.map`;
|
|
21986
|
-
const buf = Buffer.from(mapText, "utf8");
|
|
21987
|
-
await writeFile4(join13(dir2, storageKey), buf);
|
|
21988
|
-
const prev = await prisma.sourceMapArtifact.findUnique({
|
|
21989
|
-
where: { projectId_release_filename: { projectId, release, filename: base } }
|
|
21990
|
-
});
|
|
21991
|
-
const row = await prisma.sourceMapArtifact.upsert({
|
|
21992
|
-
where: { projectId_release_filename: { projectId, release, filename: base } },
|
|
21993
|
-
update: { storageKey, size: buf.length, debugId: debugId ?? null },
|
|
21994
|
-
create: { projectId, release, filename: base, storageKey, size: buf.length, debugId: debugId ?? null }
|
|
21995
|
-
});
|
|
21996
|
-
if (prev && prev.storageKey !== storageKey)
|
|
21997
|
-
await unlink3(join13(dir2, prev.storageKey.replace(/[/\\]/g, ""))).catch(() => {
|
|
21998
|
-
});
|
|
21999
|
-
return { id: row.id, storageKey, size: buf.length };
|
|
22000
|
-
}
|
|
22001
|
-
async function findSourceMap(projectId, release, frameFilename) {
|
|
22002
|
-
const base = basenameOf(frameFilename);
|
|
22003
|
-
const row = await prisma.sourceMapArtifact.findUnique({
|
|
22004
|
-
where: { projectId_release_filename: { projectId, release, filename: base } }
|
|
22005
|
-
});
|
|
22006
|
-
if (!row) return null;
|
|
22007
|
-
try {
|
|
22008
|
-
const buf = await readFile5(join13(uploadDir(), row.storageKey.replace(/[/\\]/g, "")));
|
|
22009
|
-
return buf.toString("utf8");
|
|
22010
|
-
} catch {
|
|
22011
|
-
return null;
|
|
22012
|
-
}
|
|
22013
|
-
}
|
|
22014
|
-
async function pruneReleases(projectId, keep = 10) {
|
|
22015
|
-
const rows = await prisma.sourceMapArtifact.findMany({
|
|
22016
|
-
where: { projectId },
|
|
22017
|
-
orderBy: [{ createdAt: "desc" }, { id: "desc" }]
|
|
22018
|
-
});
|
|
22019
|
-
const order = [];
|
|
22020
|
-
for (const r of rows) if (!order.includes(r.release)) order.push(r.release);
|
|
22021
|
-
const keepSet = new Set(order.slice(0, keep));
|
|
22022
|
-
const dir2 = uploadDir();
|
|
22023
|
-
for (const r of rows) if (!keepSet.has(r.release)) {
|
|
22024
|
-
await unlink3(join13(dir2, r.storageKey.replace(/[/\\]/g, ""))).catch(() => {
|
|
22025
|
-
});
|
|
22026
|
-
await prisma.sourceMapArtifact.delete({ where: { id: r.id } }).catch(() => {
|
|
22027
|
-
});
|
|
22028
|
-
}
|
|
22029
|
-
}
|
|
22030
|
-
|
|
22031
|
-
// src/routes/ingest.ts
|
|
22032
|
-
var BODY_CAP = 64e3;
|
|
22033
|
-
var SOURCEMAP_CAP = 30 * 1024 * 1024;
|
|
22034
|
-
function cors(reply) {
|
|
22035
|
-
reply.header("access-control-allow-origin", "*");
|
|
22036
|
-
reply.header("access-control-allow-methods", "POST,OPTIONS");
|
|
22037
|
-
reply.header("access-control-allow-headers", "content-type,x-hanoman-dsn");
|
|
22038
|
-
}
|
|
22039
|
-
async function ingest_default(app2) {
|
|
22040
|
-
app2.options("/ingest/:slug", async (_req, reply) => {
|
|
22041
|
-
cors(reply);
|
|
22042
|
-
return reply.code(204).send();
|
|
22043
|
-
});
|
|
22044
|
-
app2.post("/ingest/:slug", async (req, reply) => {
|
|
22045
|
-
cors(reply);
|
|
22046
|
-
const { slug } = req.params;
|
|
22047
|
-
const key = req.query.key ?? req.headers["x-hanoman-dsn"] ?? "";
|
|
22048
|
-
const raw = JSON.stringify(req.body ?? {});
|
|
22049
|
-
if (raw.length > BODY_CAP) return reply.code(413).send({ error: "payload too large" });
|
|
22050
|
-
const project = await prisma.project.findUnique({ where: { id: slug } });
|
|
22051
|
-
if (!project || !verifyKey(key, project.ingestKeyHash)) return reply.code(401).send({ error: "unauthorized" });
|
|
22052
|
-
if (!rateLimitOk(project.id)) return reply.code(429).send({ error: "rate limited" });
|
|
22053
|
-
const parsed = zIngestPayload.safeParse(req.body);
|
|
22054
|
-
if (!parsed.success) return reply.code(400).send({ error: "invalid payload" });
|
|
22055
|
-
const r = await ingestError(project.id, project.name, parsed.data);
|
|
22056
|
-
return reply.code(202).send({ ok: true, groupId: r.groupId, new: r.new });
|
|
22057
|
-
});
|
|
22058
|
-
app2.options("/ingest/:slug/sourcemaps", async (_req, reply) => {
|
|
22059
|
-
cors(reply);
|
|
22060
|
-
return reply.code(204).send();
|
|
22061
|
-
});
|
|
22062
|
-
app2.post("/ingest/:slug/sourcemaps", { bodyLimit: SOURCEMAP_CAP }, async (req, reply) => {
|
|
22063
|
-
cors(reply);
|
|
22064
|
-
const { slug } = req.params;
|
|
22065
|
-
const key = req.query.key ?? req.headers["x-hanoman-dsn"] ?? "";
|
|
22066
|
-
const project = await prisma.project.findUnique({ where: { id: slug } });
|
|
22067
|
-
if (!project || !verifyKey(key, project.ingestKeyHash)) return reply.code(401).send({ error: "unauthorized" });
|
|
22068
|
-
const parsed = zSourceMapUpload.safeParse(req.body);
|
|
22069
|
-
if (!parsed.success) return reply.code(400).send({ error: "invalid payload" });
|
|
22070
|
-
const total = parsed.data.artifacts.reduce((n, a) => n + a.map.length, 0);
|
|
22071
|
-
if (total > SOURCEMAP_CAP) return reply.code(413).send({ error: "payload too large" });
|
|
22072
|
-
for (const a of parsed.data.artifacts)
|
|
22073
|
-
await saveSourceMap(project.id, parsed.data.release, a.filename, a.map, a.debugId);
|
|
22074
|
-
await pruneReleases(project.id);
|
|
22075
|
-
return reply.code(202).send({ ok: true, stored: parsed.data.artifacts.length });
|
|
22076
|
-
});
|
|
22077
|
-
}
|
|
22078
|
-
|
|
22079
|
-
// src/routes/errors.ts
|
|
22080
|
-
init_src();
|
|
22081
|
-
init_db();
|
|
22082
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
22083
|
-
import { join as join14 } from "node:path";
|
|
22084
|
-
|
|
22085
|
-
// src/services/error-escalate.ts
|
|
22086
|
-
init_db();
|
|
22087
|
-
async function escalateErrorGroup(group2, opts) {
|
|
22088
|
-
if (group2.specId) {
|
|
22089
|
-
const spec2 = await prisma.spec.findUnique({ where: { id: group2.specId } });
|
|
22090
|
-
return { spec: spec2, created: false };
|
|
22091
|
-
}
|
|
22092
|
-
const short = group2.message.length > 80 ? group2.message.slice(0, 77) + "\u2026" : group2.message;
|
|
22093
|
-
const topStack = (group2.sampleStack ?? "").split("\n").slice(0, 12).join("\n");
|
|
22094
|
-
const backlink = `Dari Error monitoring: grup ${group2.id} (${group2.count}\xD7, ${group2.environment}).`;
|
|
22095
|
-
const payload = {
|
|
22096
|
-
severity: "major",
|
|
22097
|
-
steps: "Otomatis dari Error monitoring \u2014 reproduksi dari stack sampel.",
|
|
22098
|
-
expected: "Tidak ada error.",
|
|
22099
|
-
actual: `${group2.type}: ${group2.message}
|
|
22100
|
-
|
|
22101
|
-
${topStack}
|
|
22102
|
-
|
|
22103
|
-
${backlink}`,
|
|
22104
|
-
env: group2.environment,
|
|
22105
|
-
fromErrorGroup: group2.id
|
|
22106
|
-
};
|
|
22107
|
-
const repoDir = await resolveRepoDir(group2.projectId);
|
|
22108
|
-
let spec = null;
|
|
22109
|
-
for (let attempt = 0; attempt < 3 && !spec; attempt++) {
|
|
22110
|
-
const sid = await nextSpecId(repoDir);
|
|
22111
|
-
try {
|
|
22112
|
-
spec = await prisma.spec.create({
|
|
22113
|
-
data: {
|
|
22114
|
-
id: sid,
|
|
22115
|
-
projectId: group2.projectId,
|
|
22116
|
-
title: `${group2.type}: ${short}`,
|
|
22117
|
-
source: "qa",
|
|
22118
|
-
stage: "brainstorming",
|
|
22119
|
-
priority: "tinggi",
|
|
22120
|
-
author: `QA \xB7 ${opts.author}`,
|
|
22121
|
-
objective: `${group2.type}: ${group2.message}. ${backlink}`,
|
|
22122
|
-
payload
|
|
22123
|
-
}
|
|
22124
|
-
});
|
|
22125
|
-
} catch (e) {
|
|
22126
|
-
if (e.code === "P2002" && attempt < 2) continue;
|
|
22127
|
-
throw e;
|
|
22128
|
-
}
|
|
22129
|
-
}
|
|
22130
|
-
await prisma.errorGroup.update({ where: { id: group2.id }, data: { status: "escalated", specId: spec.id } });
|
|
22131
|
-
await notifySynced("spec", spec.id);
|
|
22132
|
-
await notifySynced("errorGroup", group2.id);
|
|
22133
|
-
return { spec, created: true };
|
|
22134
|
-
}
|
|
22135
|
-
|
|
22136
|
-
// ../node_modules/.pnpm/@jridgewell+sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs
|
|
22137
|
-
var comma = ",".charCodeAt(0);
|
|
22138
|
-
var semicolon = ";".charCodeAt(0);
|
|
22139
|
-
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
22140
|
-
var intToChar = new Uint8Array(64);
|
|
22141
|
-
var charToInt = new Uint8Array(128);
|
|
22142
|
-
for (let i = 0; i < chars.length; i++) {
|
|
22143
|
-
const c = chars.charCodeAt(i);
|
|
22144
|
-
intToChar[i] = c;
|
|
22145
|
-
charToInt[c] = i;
|
|
22146
|
-
}
|
|
22147
|
-
function decodeInteger(reader, relative2) {
|
|
22148
|
-
let value = 0;
|
|
22149
|
-
let shift = 0;
|
|
22150
|
-
let integer = 0;
|
|
22151
|
-
do {
|
|
22152
|
-
const c = reader.next();
|
|
22153
|
-
integer = charToInt[c];
|
|
22154
|
-
value |= (integer & 31) << shift;
|
|
22155
|
-
shift += 5;
|
|
22156
|
-
} while (integer & 32);
|
|
22157
|
-
const shouldNegate = value & 1;
|
|
22158
|
-
value >>>= 1;
|
|
22159
|
-
if (shouldNegate) {
|
|
22160
|
-
value = -2147483648 | -value;
|
|
22161
|
-
}
|
|
22162
|
-
return relative2 + value;
|
|
22163
|
-
}
|
|
22164
|
-
function hasMoreVlq(reader, max) {
|
|
22165
|
-
if (reader.pos >= max) return false;
|
|
22166
|
-
return reader.peek() !== comma;
|
|
22167
|
-
}
|
|
22168
|
-
var bufLength = 1024 * 16;
|
|
22169
|
-
var StringReader = class {
|
|
22170
|
-
constructor(buffer) {
|
|
22171
|
-
this.pos = 0;
|
|
22172
|
-
this.buffer = buffer;
|
|
22173
|
-
}
|
|
22174
|
-
next() {
|
|
22175
|
-
return this.buffer.charCodeAt(this.pos++);
|
|
22176
|
-
}
|
|
22177
|
-
peek() {
|
|
22178
|
-
return this.buffer.charCodeAt(this.pos);
|
|
22179
|
-
}
|
|
22180
|
-
indexOf(char) {
|
|
22181
|
-
const { buffer, pos } = this;
|
|
22182
|
-
const idx = buffer.indexOf(char, pos);
|
|
22183
|
-
return idx === -1 ? buffer.length : idx;
|
|
22184
|
-
}
|
|
22185
|
-
};
|
|
22186
|
-
function decode(mappings) {
|
|
22187
|
-
const { length } = mappings;
|
|
22188
|
-
const reader = new StringReader(mappings);
|
|
22189
|
-
const decoded = [];
|
|
22190
|
-
let genColumn = 0;
|
|
22191
|
-
let sourcesIndex = 0;
|
|
22192
|
-
let sourceLine = 0;
|
|
22193
|
-
let sourceColumn = 0;
|
|
22194
|
-
let namesIndex = 0;
|
|
22195
|
-
do {
|
|
22196
|
-
const semi = reader.indexOf(";");
|
|
22197
|
-
const line = [];
|
|
22198
|
-
let sorted = true;
|
|
22199
|
-
let lastCol = 0;
|
|
22200
|
-
genColumn = 0;
|
|
22201
|
-
while (reader.pos < semi) {
|
|
22202
|
-
let seg;
|
|
22203
|
-
genColumn = decodeInteger(reader, genColumn);
|
|
22204
|
-
if (genColumn < lastCol) sorted = false;
|
|
22205
|
-
lastCol = genColumn;
|
|
22206
|
-
if (hasMoreVlq(reader, semi)) {
|
|
22207
|
-
sourcesIndex = decodeInteger(reader, sourcesIndex);
|
|
22208
|
-
sourceLine = decodeInteger(reader, sourceLine);
|
|
22209
|
-
sourceColumn = decodeInteger(reader, sourceColumn);
|
|
22210
|
-
if (hasMoreVlq(reader, semi)) {
|
|
22211
|
-
namesIndex = decodeInteger(reader, namesIndex);
|
|
22212
|
-
seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex];
|
|
22213
|
-
} else {
|
|
22214
|
-
seg = [genColumn, sourcesIndex, sourceLine, sourceColumn];
|
|
22215
|
-
}
|
|
22216
|
-
} else {
|
|
22217
|
-
seg = [genColumn];
|
|
22218
|
-
}
|
|
22219
|
-
line.push(seg);
|
|
22220
|
-
reader.pos++;
|
|
22221
|
-
}
|
|
22222
|
-
if (!sorted) sort(line);
|
|
22223
|
-
decoded.push(line);
|
|
22224
|
-
reader.pos = semi + 1;
|
|
22225
|
-
} while (reader.pos <= length);
|
|
22226
|
-
return decoded;
|
|
22227
|
-
}
|
|
22228
|
-
function sort(line) {
|
|
22229
|
-
line.sort(sortComparator);
|
|
22230
|
-
}
|
|
22231
|
-
function sortComparator(a, b) {
|
|
22232
|
-
return a[0] - b[0];
|
|
22233
|
-
}
|
|
22234
|
-
|
|
22235
|
-
// ../node_modules/.pnpm/@jridgewell+resolve-uri@3.1.2/node_modules/@jridgewell/resolve-uri/dist/resolve-uri.mjs
|
|
22236
|
-
var schemeRegex = /^[\w+.-]+:\/\//;
|
|
22237
|
-
var urlRegex = /^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/;
|
|
22238
|
-
var fileRegex = /^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i;
|
|
22239
|
-
function isAbsoluteUrl(input) {
|
|
22240
|
-
return schemeRegex.test(input);
|
|
22241
|
-
}
|
|
22242
|
-
function isSchemeRelativeUrl(input) {
|
|
22243
|
-
return input.startsWith("//");
|
|
22244
|
-
}
|
|
22245
|
-
function isAbsolutePath(input) {
|
|
22246
|
-
return input.startsWith("/");
|
|
22247
|
-
}
|
|
22248
|
-
function isFileUrl(input) {
|
|
22249
|
-
return input.startsWith("file:");
|
|
22250
|
-
}
|
|
22251
|
-
function isRelative(input) {
|
|
22252
|
-
return /^[.?#]/.test(input);
|
|
22253
|
-
}
|
|
22254
|
-
function parseAbsoluteUrl(input) {
|
|
22255
|
-
const match = urlRegex.exec(input);
|
|
22256
|
-
return makeUrl(match[1], match[2] || "", match[3], match[4] || "", match[5] || "/", match[6] || "", match[7] || "");
|
|
22257
|
-
}
|
|
22258
|
-
function parseFileUrl(input) {
|
|
22259
|
-
const match = fileRegex.exec(input);
|
|
22260
|
-
const path = match[2];
|
|
22261
|
-
return makeUrl("file:", "", match[1] || "", "", isAbsolutePath(path) ? path : "/" + path, match[3] || "", match[4] || "");
|
|
22262
|
-
}
|
|
22263
|
-
function makeUrl(scheme, user, host2, port2, path, query, hash2) {
|
|
22264
|
-
return {
|
|
22265
|
-
scheme,
|
|
22266
|
-
user,
|
|
22267
|
-
host: host2,
|
|
22268
|
-
port: port2,
|
|
22269
|
-
path,
|
|
22270
|
-
query,
|
|
22271
|
-
hash: hash2,
|
|
22272
|
-
type: 7
|
|
22273
|
-
};
|
|
22274
|
-
}
|
|
22275
|
-
function parseUrl(input) {
|
|
22276
|
-
if (isSchemeRelativeUrl(input)) {
|
|
22277
|
-
const url3 = parseAbsoluteUrl("http:" + input);
|
|
22278
|
-
url3.scheme = "";
|
|
22279
|
-
url3.type = 6;
|
|
22280
|
-
return url3;
|
|
22281
|
-
}
|
|
22282
|
-
if (isAbsolutePath(input)) {
|
|
22283
|
-
const url3 = parseAbsoluteUrl("http://foo.com" + input);
|
|
22284
|
-
url3.scheme = "";
|
|
22285
|
-
url3.host = "";
|
|
22286
|
-
url3.type = 5;
|
|
22287
|
-
return url3;
|
|
22288
|
-
}
|
|
22289
|
-
if (isFileUrl(input))
|
|
22290
|
-
return parseFileUrl(input);
|
|
22291
|
-
if (isAbsoluteUrl(input))
|
|
22292
|
-
return parseAbsoluteUrl(input);
|
|
22293
|
-
const url2 = parseAbsoluteUrl("http://foo.com/" + input);
|
|
22294
|
-
url2.scheme = "";
|
|
22295
|
-
url2.host = "";
|
|
22296
|
-
url2.type = input ? input.startsWith("?") ? 3 : input.startsWith("#") ? 2 : 4 : 1;
|
|
22297
|
-
return url2;
|
|
22298
|
-
}
|
|
22299
|
-
function stripPathFilename(path) {
|
|
22300
|
-
if (path.endsWith("/.."))
|
|
22301
|
-
return path;
|
|
22302
|
-
const index = path.lastIndexOf("/");
|
|
22303
|
-
return path.slice(0, index + 1);
|
|
22304
|
-
}
|
|
22305
|
-
function mergePaths(url2, base) {
|
|
22306
|
-
normalizePath(base, base.type);
|
|
22307
|
-
if (url2.path === "/") {
|
|
22308
|
-
url2.path = base.path;
|
|
22309
|
-
} else {
|
|
22310
|
-
url2.path = stripPathFilename(base.path) + url2.path;
|
|
22311
|
-
}
|
|
22312
|
-
}
|
|
22313
|
-
function normalizePath(url2, type) {
|
|
22314
|
-
const rel = type <= 4;
|
|
22315
|
-
const pieces = url2.path.split("/");
|
|
22316
|
-
let pointer = 1;
|
|
22317
|
-
let positive = 0;
|
|
22318
|
-
let addTrailingSlash = false;
|
|
22319
|
-
for (let i = 1; i < pieces.length; i++) {
|
|
22320
|
-
const piece = pieces[i];
|
|
22321
|
-
if (!piece) {
|
|
22322
|
-
addTrailingSlash = true;
|
|
22323
|
-
continue;
|
|
22324
|
-
}
|
|
22325
|
-
addTrailingSlash = false;
|
|
22326
|
-
if (piece === ".")
|
|
22327
|
-
continue;
|
|
22328
|
-
if (piece === "..") {
|
|
22329
|
-
if (positive) {
|
|
22330
|
-
addTrailingSlash = true;
|
|
22331
|
-
positive--;
|
|
22332
|
-
pointer--;
|
|
22333
|
-
} else if (rel) {
|
|
22334
|
-
pieces[pointer++] = piece;
|
|
22335
|
-
}
|
|
22336
|
-
continue;
|
|
22337
|
-
}
|
|
22338
|
-
pieces[pointer++] = piece;
|
|
22339
|
-
positive++;
|
|
22340
|
-
}
|
|
22341
|
-
let path = "";
|
|
22342
|
-
for (let i = 1; i < pointer; i++) {
|
|
22343
|
-
path += "/" + pieces[i];
|
|
22344
|
-
}
|
|
22345
|
-
if (!path || addTrailingSlash && !path.endsWith("/..")) {
|
|
22346
|
-
path += "/";
|
|
22347
|
-
}
|
|
22348
|
-
url2.path = path;
|
|
22349
|
-
}
|
|
22350
|
-
function resolve14(input, base) {
|
|
22351
|
-
if (!input && !base)
|
|
22352
|
-
return "";
|
|
22353
|
-
const url2 = parseUrl(input);
|
|
22354
|
-
let inputType = url2.type;
|
|
22355
|
-
if (base && inputType !== 7) {
|
|
22356
|
-
const baseUrl = parseUrl(base);
|
|
22357
|
-
const baseType = baseUrl.type;
|
|
22358
|
-
switch (inputType) {
|
|
22359
|
-
case 1:
|
|
22360
|
-
url2.hash = baseUrl.hash;
|
|
22361
|
-
// fall through
|
|
22362
|
-
case 2:
|
|
22363
|
-
url2.query = baseUrl.query;
|
|
22364
|
-
// fall through
|
|
22365
|
-
case 3:
|
|
22366
|
-
case 4:
|
|
22367
|
-
mergePaths(url2, baseUrl);
|
|
22368
|
-
// fall through
|
|
22369
|
-
case 5:
|
|
22370
|
-
url2.user = baseUrl.user;
|
|
22371
|
-
url2.host = baseUrl.host;
|
|
22372
|
-
url2.port = baseUrl.port;
|
|
22373
|
-
// fall through
|
|
22374
|
-
case 6:
|
|
22375
|
-
url2.scheme = baseUrl.scheme;
|
|
22376
|
-
}
|
|
22377
|
-
if (baseType > inputType)
|
|
22378
|
-
inputType = baseType;
|
|
22379
|
-
}
|
|
22380
|
-
normalizePath(url2, inputType);
|
|
22381
|
-
const queryHash = url2.query + url2.hash;
|
|
22382
|
-
switch (inputType) {
|
|
22383
|
-
// This is impossible, because of the empty checks at the start of the function.
|
|
22384
|
-
// case UrlType.Empty:
|
|
22385
|
-
case 2:
|
|
22386
|
-
case 3:
|
|
22387
|
-
return queryHash;
|
|
22388
|
-
case 4: {
|
|
22389
|
-
const path = url2.path.slice(1);
|
|
22390
|
-
if (!path)
|
|
22391
|
-
return queryHash || ".";
|
|
22392
|
-
if (isRelative(base || input) && !isRelative(path)) {
|
|
22393
|
-
return "./" + path + queryHash;
|
|
22394
|
-
}
|
|
22395
|
-
return path + queryHash;
|
|
22396
|
-
}
|
|
22397
|
-
case 5:
|
|
22398
|
-
return url2.path + queryHash;
|
|
22399
|
-
default:
|
|
22400
|
-
return url2.scheme + "//" + url2.user + url2.host + url2.port + url2.path + queryHash;
|
|
22401
|
-
}
|
|
22402
|
-
}
|
|
22403
|
-
|
|
22404
|
-
// ../node_modules/.pnpm/@jridgewell+trace-mapping@0.3.31/node_modules/@jridgewell/trace-mapping/dist/trace-mapping.mjs
|
|
22405
|
-
function stripFilename(path) {
|
|
22406
|
-
if (!path) return "";
|
|
22407
|
-
const index = path.lastIndexOf("/");
|
|
22408
|
-
return path.slice(0, index + 1);
|
|
22409
|
-
}
|
|
22410
|
-
function resolver(mapUrl, sourceRoot) {
|
|
22411
|
-
const from = stripFilename(mapUrl);
|
|
22412
|
-
const prefix = sourceRoot ? sourceRoot + "/" : "";
|
|
22413
|
-
return (source) => resolve14(prefix + (source || ""), from);
|
|
22414
|
-
}
|
|
22415
|
-
var COLUMN = 0;
|
|
22416
|
-
var SOURCES_INDEX = 1;
|
|
22417
|
-
var SOURCE_LINE = 2;
|
|
22418
|
-
var SOURCE_COLUMN = 3;
|
|
22419
|
-
var NAMES_INDEX = 4;
|
|
22420
|
-
function maybeSort(mappings, owned) {
|
|
22421
|
-
const unsortedIndex = nextUnsortedSegmentLine(mappings, 0);
|
|
22422
|
-
if (unsortedIndex === mappings.length) return mappings;
|
|
22423
|
-
if (!owned) mappings = mappings.slice();
|
|
22424
|
-
for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) {
|
|
22425
|
-
mappings[i] = sortSegments(mappings[i], owned);
|
|
22426
|
-
}
|
|
22427
|
-
return mappings;
|
|
22428
|
-
}
|
|
22429
|
-
function nextUnsortedSegmentLine(mappings, start) {
|
|
22430
|
-
for (let i = start; i < mappings.length; i++) {
|
|
22431
|
-
if (!isSorted(mappings[i])) return i;
|
|
22432
|
-
}
|
|
22433
|
-
return mappings.length;
|
|
22434
|
-
}
|
|
22435
|
-
function isSorted(line) {
|
|
22436
|
-
for (let j = 1; j < line.length; j++) {
|
|
22437
|
-
if (line[j][COLUMN] < line[j - 1][COLUMN]) {
|
|
22438
|
-
return false;
|
|
22439
|
-
}
|
|
22440
|
-
}
|
|
22441
|
-
return true;
|
|
22442
|
-
}
|
|
22443
|
-
function sortSegments(line, owned) {
|
|
22444
|
-
if (!owned) line = line.slice();
|
|
22445
|
-
return line.sort(sortComparator2);
|
|
22446
|
-
}
|
|
22447
|
-
function sortComparator2(a, b) {
|
|
22448
|
-
return a[COLUMN] - b[COLUMN];
|
|
22449
|
-
}
|
|
22450
|
-
var found = false;
|
|
22451
|
-
function binarySearch(haystack, needle, low, high) {
|
|
22452
|
-
while (low <= high) {
|
|
22453
|
-
const mid = low + (high - low >> 1);
|
|
22454
|
-
const cmp = haystack[mid][COLUMN] - needle;
|
|
22455
|
-
if (cmp === 0) {
|
|
22456
|
-
found = true;
|
|
22457
|
-
return mid;
|
|
22458
|
-
}
|
|
22459
|
-
if (cmp < 0) {
|
|
22460
|
-
low = mid + 1;
|
|
22461
|
-
} else {
|
|
22462
|
-
high = mid - 1;
|
|
22463
|
-
}
|
|
22464
|
-
}
|
|
22465
|
-
found = false;
|
|
22466
|
-
return low - 1;
|
|
22467
|
-
}
|
|
22468
|
-
function upperBound(haystack, needle, index) {
|
|
22469
|
-
for (let i = index + 1; i < haystack.length; index = i++) {
|
|
22470
|
-
if (haystack[i][COLUMN] !== needle) break;
|
|
22471
|
-
}
|
|
22472
|
-
return index;
|
|
22473
|
-
}
|
|
22474
|
-
function lowerBound(haystack, needle, index) {
|
|
22475
|
-
for (let i = index - 1; i >= 0; index = i--) {
|
|
22476
|
-
if (haystack[i][COLUMN] !== needle) break;
|
|
22477
|
-
}
|
|
22478
|
-
return index;
|
|
22479
|
-
}
|
|
22480
|
-
function memoizedState() {
|
|
22481
|
-
return {
|
|
22482
|
-
lastKey: -1,
|
|
22483
|
-
lastNeedle: -1,
|
|
22484
|
-
lastIndex: -1
|
|
22485
|
-
};
|
|
22486
|
-
}
|
|
22487
|
-
function memoizedBinarySearch(haystack, needle, state, key) {
|
|
22488
|
-
const { lastKey, lastNeedle, lastIndex } = state;
|
|
22489
|
-
let low = 0;
|
|
22490
|
-
let high = haystack.length - 1;
|
|
22491
|
-
if (key === lastKey) {
|
|
22492
|
-
if (needle === lastNeedle) {
|
|
22493
|
-
found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle;
|
|
22494
|
-
return lastIndex;
|
|
22495
|
-
}
|
|
22496
|
-
if (needle >= lastNeedle) {
|
|
22497
|
-
low = lastIndex === -1 ? 0 : lastIndex;
|
|
22498
|
-
} else {
|
|
22499
|
-
high = lastIndex;
|
|
22500
|
-
}
|
|
22501
|
-
}
|
|
22502
|
-
state.lastKey = key;
|
|
22503
|
-
state.lastNeedle = needle;
|
|
22504
|
-
return state.lastIndex = binarySearch(haystack, needle, low, high);
|
|
22505
|
-
}
|
|
22506
|
-
function parse2(map) {
|
|
22507
|
-
return typeof map === "string" ? JSON.parse(map) : map;
|
|
22508
|
-
}
|
|
22509
|
-
var LINE_GTR_ZERO = "`line` must be greater than 0 (lines start at line 1)";
|
|
22510
|
-
var COL_GTR_EQ_ZERO = "`column` must be greater than or equal to 0 (columns start at column 0)";
|
|
22511
|
-
var LEAST_UPPER_BOUND = -1;
|
|
22512
|
-
var GREATEST_LOWER_BOUND = 1;
|
|
22513
|
-
var TraceMap = class {
|
|
22514
|
-
constructor(map, mapUrl) {
|
|
22515
|
-
const isString = typeof map === "string";
|
|
22516
|
-
if (!isString && map._decodedMemo) return map;
|
|
22517
|
-
const parsed = parse2(map);
|
|
22518
|
-
const { version, file, names, sourceRoot, sources: sources2, sourcesContent } = parsed;
|
|
22519
|
-
this.version = version;
|
|
22520
|
-
this.file = file;
|
|
22521
|
-
this.names = names || [];
|
|
22522
|
-
this.sourceRoot = sourceRoot;
|
|
22523
|
-
this.sources = sources2;
|
|
22524
|
-
this.sourcesContent = sourcesContent;
|
|
22525
|
-
this.ignoreList = parsed.ignoreList || parsed.x_google_ignoreList || void 0;
|
|
22526
|
-
const resolve15 = resolver(mapUrl, sourceRoot);
|
|
22527
|
-
this.resolvedSources = sources2.map(resolve15);
|
|
22528
|
-
const { mappings } = parsed;
|
|
22529
|
-
if (typeof mappings === "string") {
|
|
22530
|
-
this._encoded = mappings;
|
|
22531
|
-
this._decoded = void 0;
|
|
22532
|
-
} else if (Array.isArray(mappings)) {
|
|
22533
|
-
this._encoded = void 0;
|
|
22534
|
-
this._decoded = maybeSort(mappings, isString);
|
|
22535
|
-
} else if (parsed.sections) {
|
|
22536
|
-
throw new Error(`TraceMap passed sectioned source map, please use FlattenMap export instead`);
|
|
22537
|
-
} else {
|
|
22538
|
-
throw new Error(`invalid source map: ${JSON.stringify(parsed)}`);
|
|
22539
|
-
}
|
|
22540
|
-
this._decodedMemo = memoizedState();
|
|
22541
|
-
this._bySources = void 0;
|
|
22542
|
-
this._bySourceMemos = void 0;
|
|
22543
|
-
}
|
|
22544
|
-
};
|
|
22545
|
-
function cast(map) {
|
|
22546
|
-
return map;
|
|
22547
|
-
}
|
|
22548
|
-
function decodedMappings(map) {
|
|
22549
|
-
var _a;
|
|
22550
|
-
return (_a = cast(map))._decoded || (_a._decoded = decode(cast(map)._encoded));
|
|
22551
|
-
}
|
|
22552
|
-
function originalPositionFor(map, needle) {
|
|
22553
|
-
let { line, column, bias } = needle;
|
|
22554
|
-
line--;
|
|
22555
|
-
if (line < 0) throw new Error(LINE_GTR_ZERO);
|
|
22556
|
-
if (column < 0) throw new Error(COL_GTR_EQ_ZERO);
|
|
22557
|
-
const decoded = decodedMappings(map);
|
|
22558
|
-
if (line >= decoded.length) return OMapping(null, null, null, null);
|
|
22559
|
-
const segments = decoded[line];
|
|
22560
|
-
const index = traceSegmentInternal(
|
|
22561
|
-
segments,
|
|
22562
|
-
cast(map)._decodedMemo,
|
|
22563
|
-
line,
|
|
22564
|
-
column,
|
|
22565
|
-
bias || GREATEST_LOWER_BOUND
|
|
22566
|
-
);
|
|
22567
|
-
if (index === -1) return OMapping(null, null, null, null);
|
|
22568
|
-
const segment = segments[index];
|
|
22569
|
-
if (segment.length === 1) return OMapping(null, null, null, null);
|
|
22570
|
-
const { names, resolvedSources } = map;
|
|
22571
|
-
return OMapping(
|
|
22572
|
-
resolvedSources[segment[SOURCES_INDEX]],
|
|
22573
|
-
segment[SOURCE_LINE] + 1,
|
|
22574
|
-
segment[SOURCE_COLUMN],
|
|
22575
|
-
segment.length === 5 ? names[segment[NAMES_INDEX]] : null
|
|
22576
|
-
);
|
|
22577
|
-
}
|
|
22578
|
-
function sourceIndex(map, source) {
|
|
22579
|
-
const { sources: sources2, resolvedSources } = map;
|
|
22580
|
-
let index = sources2.indexOf(source);
|
|
22581
|
-
if (index === -1) index = resolvedSources.indexOf(source);
|
|
22582
|
-
return index;
|
|
22583
|
-
}
|
|
22584
|
-
function sourceContentFor(map, source) {
|
|
22585
|
-
const { sourcesContent } = map;
|
|
22586
|
-
if (sourcesContent == null) return null;
|
|
22587
|
-
const index = sourceIndex(map, source);
|
|
22588
|
-
return index === -1 ? null : sourcesContent[index];
|
|
22589
|
-
}
|
|
22590
|
-
function OMapping(source, line, column, name2) {
|
|
22591
|
-
return { source, line, column, name: name2 };
|
|
22592
|
-
}
|
|
22593
|
-
function traceSegmentInternal(segments, memo, line, column, bias) {
|
|
22594
|
-
let index = memoizedBinarySearch(segments, column, memo, line);
|
|
22595
|
-
if (found) {
|
|
22596
|
-
index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index);
|
|
22597
|
-
} else if (bias === LEAST_UPPER_BOUND) index++;
|
|
22598
|
-
if (index === -1 || index === segments.length) return -1;
|
|
22599
|
-
return index;
|
|
22600
|
-
}
|
|
22601
|
-
|
|
22602
|
-
// src/services/symbolicate.ts
|
|
22603
|
-
var CONTEXT = 3;
|
|
22604
|
-
async function symbolicateFrames(frames, lookup) {
|
|
22605
|
-
const cache4 = /* @__PURE__ */ new Map();
|
|
22606
|
-
const out3 = [];
|
|
22607
|
-
for (const f of frames) {
|
|
22608
|
-
const base = { ...f, symbolicated: false };
|
|
22609
|
-
if (!f.filename || !f.lineno) {
|
|
22610
|
-
out3.push(base);
|
|
22611
|
-
continue;
|
|
22612
|
-
}
|
|
22613
|
-
let tracer = cache4.get(f.filename);
|
|
22614
|
-
if (tracer === void 0) {
|
|
22615
|
-
const text = await lookup(f.filename);
|
|
22616
|
-
try {
|
|
22617
|
-
tracer = text ? new TraceMap(text) : null;
|
|
22618
|
-
} catch {
|
|
22619
|
-
tracer = null;
|
|
22620
|
-
}
|
|
22621
|
-
cache4.set(f.filename, tracer);
|
|
22622
|
-
}
|
|
22623
|
-
if (!tracer) {
|
|
22624
|
-
out3.push(base);
|
|
22625
|
-
continue;
|
|
22626
|
-
}
|
|
22627
|
-
try {
|
|
22628
|
-
const pos = originalPositionFor(tracer, { line: f.lineno, column: (f.colno ?? 1) - 1 });
|
|
22629
|
-
if (pos.source == null || pos.line == null) {
|
|
22630
|
-
out3.push(base);
|
|
22631
|
-
continue;
|
|
22632
|
-
}
|
|
22633
|
-
const content = sourceContentFor(tracer, pos.source);
|
|
22634
|
-
let contextLine, preContext, postContext;
|
|
22635
|
-
if (content != null) {
|
|
22636
|
-
const lines2 = content.split("\n");
|
|
22637
|
-
const idx = pos.line - 1;
|
|
22638
|
-
contextLine = lines2[idx];
|
|
22639
|
-
preContext = lines2.slice(Math.max(0, idx - CONTEXT), idx);
|
|
22640
|
-
postContext = lines2.slice(idx + 1, idx + 1 + CONTEXT);
|
|
22641
|
-
}
|
|
22642
|
-
out3.push({
|
|
22643
|
-
...f,
|
|
22644
|
-
function: pos.name ?? f.function,
|
|
22645
|
-
source: pos.source,
|
|
22646
|
-
sourceLine: pos.line,
|
|
22647
|
-
sourceColumn: pos.column ?? void 0,
|
|
22648
|
-
in_app: !/node_modules/.test(pos.source),
|
|
22649
|
-
contextLine,
|
|
22650
|
-
preContext,
|
|
22651
|
-
postContext,
|
|
22652
|
-
symbolicated: true
|
|
22653
|
-
});
|
|
22654
|
-
} catch {
|
|
22655
|
-
out3.push(base);
|
|
22656
|
-
}
|
|
22657
|
-
}
|
|
22658
|
-
return out3;
|
|
22659
|
-
}
|
|
22660
|
-
|
|
22661
|
-
// src/routes/errors.ts
|
|
22662
|
-
var groupView = (g) => ({
|
|
22663
|
-
id: g.id,
|
|
22664
|
-
projectId: g.projectId,
|
|
22665
|
-
type: g.type,
|
|
22666
|
-
message: g.message,
|
|
22667
|
-
environment: g.environment,
|
|
22668
|
-
status: g.status,
|
|
22669
|
-
count: g.count,
|
|
22670
|
-
firstSeenAt: g.firstSeenAt,
|
|
22671
|
-
lastSeenAt: g.lastSeenAt,
|
|
22672
|
-
specId: g.specId,
|
|
22673
|
-
release: g.release
|
|
22674
|
-
// SPEC-276 · korelasi build
|
|
22675
|
-
});
|
|
22676
|
-
var eventView = (e) => ({
|
|
22677
|
-
id: e.id,
|
|
22678
|
-
type: e.type,
|
|
22679
|
-
message: e.message,
|
|
22680
|
-
stack: e.stack,
|
|
22681
|
-
environment: e.environment,
|
|
22682
|
-
release: e.release,
|
|
22683
|
-
receivedAt: e.receivedAt
|
|
22684
|
-
});
|
|
22685
|
-
async function errors_default(app2) {
|
|
22686
|
-
app2.get("/errors/integration-guide", async (_req, reply) => {
|
|
22687
|
-
try {
|
|
22688
|
-
const text = await readFile6(join14(repoRoot(), "sdk", "README.md"), "utf8");
|
|
22689
|
-
return { text };
|
|
22690
|
-
} catch {
|
|
22691
|
-
return reply.code(404).send({ error: "panduan integrasi tidak ditemukan" });
|
|
22692
|
-
}
|
|
22693
|
-
});
|
|
22694
|
-
app2.get("/errors", async (req) => {
|
|
22695
|
-
const { project, environment, status, q, page, limit } = req.query;
|
|
22696
|
-
const where = {};
|
|
22697
|
-
if (project) where.projectId = project;
|
|
22698
|
-
if (environment) where.environment = environment;
|
|
22699
|
-
if (status) where.status = status;
|
|
22700
|
-
let groups = await prisma.errorGroup.findMany({ where, orderBy: { lastSeenAt: "desc" } });
|
|
22701
|
-
if (q) {
|
|
22702
|
-
const n = q.toLowerCase();
|
|
22703
|
-
groups = groups.filter((g) => `${g.type} ${g.message}`.toLowerCase().includes(n));
|
|
22704
|
-
}
|
|
22705
|
-
return paginate(groups.map(groupView), page, limit);
|
|
22706
|
-
});
|
|
22707
|
-
app2.get("/errors/:id", async (req, reply) => {
|
|
22708
|
-
const { id } = req.params;
|
|
22709
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22710
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22711
|
-
const events = await prisma.errorEvent.findMany({
|
|
22712
|
-
where: { groupId: id },
|
|
22713
|
-
orderBy: { receivedAt: "desc" },
|
|
22714
|
-
take: 50
|
|
22715
|
-
});
|
|
22716
|
-
const sampleFrames = Array.isArray(g.sampleFrames) ? await symbolicateFrames(
|
|
22717
|
-
g.sampleFrames,
|
|
22718
|
-
(fn) => findSourceMap(g.projectId, g.release ?? "", fn)
|
|
22719
|
-
) : null;
|
|
22720
|
-
return { ...groupView(g), sampleStack: g.sampleStack, sampleFrames, events: events.map(eventView) };
|
|
22721
|
-
});
|
|
22722
|
-
app2.post("/errors/:id/escalate", async (req, reply) => {
|
|
22723
|
-
const { id } = req.params;
|
|
22724
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22725
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22726
|
-
const { spec, created } = await escalateErrorGroup(g, { author: req.user?.email ?? "system" });
|
|
22727
|
-
return created ? reply.code(201).send({ spec }) : reply.code(200).send({ alreadyEscalated: true, spec });
|
|
22728
|
-
});
|
|
22729
|
-
app2.post("/errors/:id/unlink", async (req, reply) => {
|
|
22730
|
-
const { id } = req.params;
|
|
22731
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22732
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22733
|
-
const updated = await prisma.errorGroup.update({ where: { id }, data: { status: "new", specId: null } });
|
|
22734
|
-
await notifySynced("errorGroup", id);
|
|
22735
|
-
return { id: updated.id, status: updated.status, specId: updated.specId };
|
|
22736
|
-
});
|
|
22737
|
-
app2.patch("/errors/:id", async (req, reply) => {
|
|
22738
|
-
const { id } = req.params;
|
|
22739
|
-
const parsed = zErrorStatus.safeParse(req.body?.status);
|
|
22740
|
-
if (!parsed.success) return reply.code(400).send({ error: "status invalid" });
|
|
22741
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22742
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22743
|
-
const updated = await prisma.errorGroup.update({ where: { id }, data: { status: parsed.data } });
|
|
22744
|
-
await notifySynced("errorGroup", id);
|
|
22745
|
-
return { id: updated.id, status: updated.status };
|
|
22746
|
-
});
|
|
22747
|
-
app2.delete("/errors/:id", async (req, reply) => {
|
|
22748
|
-
const { id } = req.params;
|
|
22749
|
-
const g = await prisma.errorGroup.findUnique({ where: { id } });
|
|
22750
|
-
if (!g) return reply.code(404).send({ error: "not found" });
|
|
22751
|
-
await prisma.errorGroup.delete({ where: { id } });
|
|
22752
|
-
return { ok: true };
|
|
22753
|
-
});
|
|
22754
|
-
}
|
|
22755
|
-
|
|
22756
21613
|
// src/routes/help.ts
|
|
22757
21614
|
init_zod();
|
|
22758
21615
|
init_src();
|
|
@@ -22760,17 +21617,17 @@ init_db();
|
|
|
22760
21617
|
|
|
22761
21618
|
// src/services/ticket.ts
|
|
22762
21619
|
init_db();
|
|
22763
|
-
import { createHash as
|
|
21620
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
|
|
22764
21621
|
init_src();
|
|
22765
21622
|
function hashAccessKey(key) {
|
|
22766
|
-
return
|
|
21623
|
+
return createHash2("sha256").update(key).digest("hex");
|
|
22767
21624
|
}
|
|
22768
21625
|
function generateAccessKey() {
|
|
22769
|
-
const key = "hnm_tkt_" +
|
|
21626
|
+
const key = "hnm_tkt_" + randomBytes2(24).toString("hex");
|
|
22770
21627
|
return { key, hash: hashAccessKey(key) };
|
|
22771
21628
|
}
|
|
22772
21629
|
function generateShareToken() {
|
|
22773
|
-
return "hnm_shr_" +
|
|
21630
|
+
return "hnm_shr_" + randomBytes2(24).toString("hex");
|
|
22774
21631
|
}
|
|
22775
21632
|
async function createTicket(input) {
|
|
22776
21633
|
const { key, hash: hash2 } = generateAccessKey();
|
|
@@ -22919,7 +21776,7 @@ init_db();
|
|
|
22919
21776
|
|
|
22920
21777
|
// src/services/ticket-accept.ts
|
|
22921
21778
|
init_db();
|
|
22922
|
-
import { join as
|
|
21779
|
+
import { join as join13 } from "node:path";
|
|
22923
21780
|
async function materializeAttachments(atts) {
|
|
22924
21781
|
const ready = /* @__PURE__ */ new Set();
|
|
22925
21782
|
for (const a of atts) {
|
|
@@ -22934,7 +21791,7 @@ async function materializeAttachments(atts) {
|
|
|
22934
21791
|
var attachmentInstruction = (t, atts, ready) => {
|
|
22935
21792
|
if (atts.length === 0) return "Tanpa lampiran.";
|
|
22936
21793
|
const list2 = atts.map(
|
|
22937
|
-
(a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${
|
|
21794
|
+
(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}`
|
|
22938
21795
|
).join("\n");
|
|
22939
21796
|
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):
|
|
22940
21797
|
${list2}
|
|
@@ -23132,6 +21989,7 @@ async function setScheduler(next) {
|
|
|
23132
21989
|
// src/services/scheduler/queue.ts
|
|
23133
21990
|
init_db();
|
|
23134
21991
|
var RANK = { tinggi: 0, sedang: 1, rendah: 2 };
|
|
21992
|
+
var UNSTARTED_SPEC_WHERE = { baseSha: null, stage: { not: "done" } };
|
|
23135
21993
|
async function enqueue(i) {
|
|
23136
21994
|
await prisma.schedulerQueueItem.upsert({
|
|
23137
21995
|
where: { specId: i.specId },
|
|
@@ -23152,8 +22010,16 @@ async function markLaunched(id, sessionId2) {
|
|
|
23152
22010
|
async function markFailed(id, note) {
|
|
23153
22011
|
await prisma.schedulerQueueItem.update({ where: { id }, data: { status: "failed", note: note ?? null } });
|
|
23154
22012
|
}
|
|
23155
|
-
async function markDone(id) {
|
|
23156
|
-
await prisma.schedulerQueueItem.update({
|
|
22013
|
+
async function markDone(id, note) {
|
|
22014
|
+
await prisma.schedulerQueueItem.update({
|
|
22015
|
+
where: { id },
|
|
22016
|
+
data: { status: "done", ...note ? { note } : {} }
|
|
22017
|
+
});
|
|
22018
|
+
}
|
|
22019
|
+
async function noteQueued(id, note) {
|
|
22020
|
+
const row = await prisma.schedulerQueueItem.findUnique({ where: { id }, select: { note: true } });
|
|
22021
|
+
if (row?.note === note) return;
|
|
22022
|
+
await prisma.schedulerQueueItem.update({ where: { id }, data: { note } });
|
|
23157
22023
|
}
|
|
23158
22024
|
|
|
23159
22025
|
// src/services/scheduler/registry.ts
|
|
@@ -23202,7 +22068,6 @@ async function scheduler_default(app2) {
|
|
|
23202
22068
|
};
|
|
23203
22069
|
const sources2 = [
|
|
23204
22070
|
srcView("backlog", cfg.sources.backlog),
|
|
23205
|
-
srcView("errors", cfg.sources.errors, cfg.sources.errors.minCount),
|
|
23206
22071
|
srcView("triase", cfg.sources.triase)
|
|
23207
22072
|
];
|
|
23208
22073
|
const launchedSpecs = new Set(queue.filter((q) => q.status === "launched").map((q) => q.specId));
|
|
@@ -23211,114 +22076,6 @@ async function scheduler_default(app2) {
|
|
|
23211
22076
|
});
|
|
23212
22077
|
}
|
|
23213
22078
|
|
|
23214
|
-
// src/routes/audit.ts
|
|
23215
|
-
init_db();
|
|
23216
|
-
var REL = /^(\d+)([mhd])$/;
|
|
23217
|
-
var MS = { m: 6e4, h: 36e5, d: 864e5 };
|
|
23218
|
-
function parseWhen(v, fallback2, now) {
|
|
23219
|
-
if (!v) return fallback2;
|
|
23220
|
-
const m = REL.exec(v.trim());
|
|
23221
|
-
if (m) return new Date(now.getTime() - Number(m[1]) * MS[m[2]]);
|
|
23222
|
-
const d = new Date(v);
|
|
23223
|
-
return isNaN(d.getTime()) ? null : d;
|
|
23224
|
-
}
|
|
23225
|
-
var groupView2 = (g) => ({
|
|
23226
|
-
id: g.id,
|
|
23227
|
-
projectId: g.projectId,
|
|
23228
|
-
type: g.type,
|
|
23229
|
-
message: g.message,
|
|
23230
|
-
environment: g.environment,
|
|
23231
|
-
release: g.release,
|
|
23232
|
-
status: g.status,
|
|
23233
|
-
count: g.count,
|
|
23234
|
-
firstSeenAt: g.firstSeenAt,
|
|
23235
|
-
lastSeenAt: g.lastSeenAt,
|
|
23236
|
-
specId: g.specId
|
|
23237
|
-
});
|
|
23238
|
-
var timelineView = (e) => ({
|
|
23239
|
-
at: e.receivedAt,
|
|
23240
|
-
projectId: e.projectId,
|
|
23241
|
-
groupId: e.groupId,
|
|
23242
|
-
type: e.type,
|
|
23243
|
-
message: e.message,
|
|
23244
|
-
environment: e.environment,
|
|
23245
|
-
release: e.release
|
|
23246
|
-
});
|
|
23247
|
-
async function scopeFor(req) {
|
|
23248
|
-
const s = auditScopeFromReq(req);
|
|
23249
|
-
if (s) return s;
|
|
23250
|
-
return (await prisma.project.findMany({ select: { id: true } })).map((p) => p.id);
|
|
23251
|
-
}
|
|
23252
|
-
async function audit_default(app2) {
|
|
23253
|
-
app2.get("/audit/logs", async (req, reply) => {
|
|
23254
|
-
const q = req.query;
|
|
23255
|
-
const now = /* @__PURE__ */ new Date();
|
|
23256
|
-
const since = parseWhen(q.since, new Date(now.getTime() - MS.d), now);
|
|
23257
|
-
const until = parseWhen(q.until, now, now);
|
|
23258
|
-
if (!since || !until) return reply.code(400).send({ error: "since/until tak terparse \u2014 pakai 24h, 7d, atau ISO-8601" });
|
|
23259
|
-
const scope = await scopeFor(req);
|
|
23260
|
-
let projects = scope;
|
|
23261
|
-
if (q.projects) {
|
|
23262
|
-
const want = q.projects.split(",").map((s) => s.trim()).filter(Boolean);
|
|
23263
|
-
const outside = want.filter((p) => !scope.includes(p));
|
|
23264
|
-
if (outside.length) return reply.code(403).send({ error: `di luar scope sesi: ${outside.join(", ")}` });
|
|
23265
|
-
if (want.length) projects = want;
|
|
23266
|
-
}
|
|
23267
|
-
const limit = Math.min(Number(q.limit) || 200, 1e3);
|
|
23268
|
-
const needle = (q.q ?? "").trim().toLowerCase();
|
|
23269
|
-
const envWhere = q.environment ? { environment: q.environment } : {};
|
|
23270
|
-
const raw = await prisma.errorEvent.findMany({
|
|
23271
|
-
where: { projectId: { in: projects }, receivedAt: { gte: since, lte: until }, ...envWhere },
|
|
23272
|
-
orderBy: { receivedAt: "desc" },
|
|
23273
|
-
take: needle ? 2e3 : limit
|
|
23274
|
-
});
|
|
23275
|
-
const events = (needle ? raw.filter((e) => `${e.type} ${e.message}`.toLowerCase().includes(needle)) : raw).slice(0, limit);
|
|
23276
|
-
const rawGroups = await prisma.errorGroup.findMany({
|
|
23277
|
-
where: { projectId: { in: projects }, lastSeenAt: { gte: since }, ...envWhere },
|
|
23278
|
-
orderBy: { lastSeenAt: "desc" },
|
|
23279
|
-
take: 200
|
|
23280
|
-
});
|
|
23281
|
-
const groups = needle ? rawGroups.filter((g) => `${g.type} ${g.message}`.toLowerCase().includes(needle)) : rawGroups;
|
|
23282
|
-
const names = await prisma.project.findMany({ where: { id: { in: projects } }, select: { id: true, name: true } });
|
|
23283
|
-
return {
|
|
23284
|
-
window: { since, until },
|
|
23285
|
-
scope: names,
|
|
23286
|
-
groups: groups.map(groupView2),
|
|
23287
|
-
timeline: events.map(timelineView)
|
|
23288
|
-
};
|
|
23289
|
-
});
|
|
23290
|
-
app2.get("/audit/logs/:groupId", async (req, reply) => {
|
|
23291
|
-
const { groupId } = req.params;
|
|
23292
|
-
const scope = await scopeFor(req);
|
|
23293
|
-
const g = await prisma.errorGroup.findUnique({ where: { id: groupId } });
|
|
23294
|
-
if (!g || !scope.includes(g.projectId)) return reply.code(404).send({ error: "not found" });
|
|
23295
|
-
const events = await prisma.errorEvent.findMany({
|
|
23296
|
-
where: { groupId },
|
|
23297
|
-
orderBy: { receivedAt: "desc" },
|
|
23298
|
-
take: 50
|
|
23299
|
-
});
|
|
23300
|
-
const sampleFrames = Array.isArray(g.sampleFrames) ? await symbolicateFrames(
|
|
23301
|
-
g.sampleFrames,
|
|
23302
|
-
(fn) => findSourceMap(g.projectId, g.release ?? "", fn)
|
|
23303
|
-
) : null;
|
|
23304
|
-
return {
|
|
23305
|
-
...groupView2(g),
|
|
23306
|
-
sampleStack: g.sampleStack,
|
|
23307
|
-
sampleFrames,
|
|
23308
|
-
events: events.map((e) => ({
|
|
23309
|
-
id: e.id,
|
|
23310
|
-
at: e.receivedAt,
|
|
23311
|
-
type: e.type,
|
|
23312
|
-
message: e.message,
|
|
23313
|
-
stack: e.stack,
|
|
23314
|
-
environment: e.environment,
|
|
23315
|
-
release: e.release,
|
|
23316
|
-
context: e.context
|
|
23317
|
-
}))
|
|
23318
|
-
};
|
|
23319
|
-
});
|
|
23320
|
-
}
|
|
23321
|
-
|
|
23322
22079
|
// src/routes/lead.ts
|
|
23323
22080
|
init_src();
|
|
23324
22081
|
init_db();
|
|
@@ -23392,8 +22149,12 @@ function leadPrompt(q, c) {
|
|
|
23392
22149
|
for (const [i, o] of q.options.entries()) lines2.push(`${i + 1}. ${o}`);
|
|
23393
22150
|
}
|
|
23394
22151
|
lines2.push("");
|
|
22152
|
+
lines2.push("## Batas waktu (BACA INI DULU)");
|
|
22153
|
+
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.`);
|
|
22154
|
+
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.`);
|
|
22155
|
+
lines2.push("");
|
|
23395
22156
|
lines2.push("## Cara kerja");
|
|
23396
|
-
lines2.push("1. KUMPULKAN BUKTI DULU sebelum memutuskan: `internal/docs/**` (Source of Truth) dan index-nya, ADR yang relevan, plan `docs/superpowers/plans/**`, kode yang bersangkutan, dan riwayat git. Baca, jangan mengingat.");
|
|
22157
|
+
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.");
|
|
23397
22158
|
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.');
|
|
23398
22159
|
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.");
|
|
23399
22160
|
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.");
|
|
@@ -23548,6 +22309,7 @@ function toDecisionView(r) {
|
|
|
23548
22309
|
|
|
23549
22310
|
// src/services/lead/brain.ts
|
|
23550
22311
|
init_config2();
|
|
22312
|
+
init_pty();
|
|
23551
22313
|
import { execFile as execFile9 } from "node:child_process";
|
|
23552
22314
|
var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
23553
22315
|
function leadArgv(o) {
|
|
@@ -23568,14 +22330,16 @@ function leadArgv(o) {
|
|
|
23568
22330
|
o.prompt
|
|
23569
22331
|
];
|
|
23570
22332
|
}
|
|
22333
|
+
var leadEnv = (agent, base = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base } : { ...base };
|
|
23571
22334
|
function think(prompt, o) {
|
|
23572
22335
|
const bin = binFor(o.agent);
|
|
23573
22336
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
23574
|
-
return new Promise((
|
|
23575
|
-
execFile9(bin, args, {
|
|
22337
|
+
return new Promise((resolve14, reject) => {
|
|
22338
|
+
const child = execFile9(bin, args, {
|
|
23576
22339
|
cwd: o.cwd,
|
|
23577
22340
|
timeout: o.timeoutMs,
|
|
23578
22341
|
maxBuffer: 16 * 1024 * 1024,
|
|
22342
|
+
env: leadEnv(o.agent),
|
|
23579
22343
|
encoding: "utf8",
|
|
23580
22344
|
killSignal: "SIGTERM"
|
|
23581
22345
|
}, (err, stdout, stderr) => {
|
|
@@ -23584,8 +22348,9 @@ function think(prompt, o) {
|
|
|
23584
22348
|
reject(new Error(killed ? `lead ${o.agent} kehabisan waktu ${o.timeoutMs} ms` : `lead ${o.agent} gagal: ${(stderr || err.message).trim().slice(0, 500)}`));
|
|
23585
22349
|
return;
|
|
23586
22350
|
}
|
|
23587
|
-
|
|
22351
|
+
resolve14(stdout);
|
|
23588
22352
|
});
|
|
22353
|
+
child.stdin?.end();
|
|
23589
22354
|
});
|
|
23590
22355
|
}
|
|
23591
22356
|
|
|
@@ -23627,6 +22392,10 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
23627
22392
|
projectId: req.projectId,
|
|
23628
22393
|
projectName: project?.name ?? req.projectId,
|
|
23629
22394
|
repoDir,
|
|
22395
|
+
// SPEC-432 · anggaran yang disebut prompt WAJIB berasal dari cfg yang sama yang dipakai
|
|
22396
|
+
// `think()` di bawah. Dua sumber angka akan berselisih diam-diam begitu operator menggeser
|
|
22397
|
+
// knob-nya, dan agen yang dianggarkan salah gagal persis seperti agen yang tak dianggarkan.
|
|
22398
|
+
timeoutSec: cfg.timeoutSec,
|
|
23630
22399
|
spec,
|
|
23631
22400
|
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 })),
|
|
23632
22401
|
priorDecisions: prior.map((d) => ({
|
|
@@ -23691,6 +22460,11 @@ function takeReply(decisionId) {
|
|
|
23691
22460
|
return v;
|
|
23692
22461
|
}
|
|
23693
22462
|
async function fail(req, deps, reason) {
|
|
22463
|
+
const prev = await prisma.leadDecision.findFirst({
|
|
22464
|
+
where: { projectId: req.projectId, gate: req.gate, kind: req.kind },
|
|
22465
|
+
orderBy: { createdAt: "desc" },
|
|
22466
|
+
select: { status: true }
|
|
22467
|
+
});
|
|
23694
22468
|
const row = await recordDecision({
|
|
23695
22469
|
projectId: req.projectId,
|
|
23696
22470
|
specId: req.specId,
|
|
@@ -23706,13 +22480,15 @@ async function fail(req, deps, reason) {
|
|
|
23706
22480
|
status: "gagal",
|
|
23707
22481
|
weighty: true
|
|
23708
22482
|
});
|
|
23709
|
-
|
|
23710
|
-
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23715
|
-
|
|
22483
|
+
if (prev?.status !== "gagal") {
|
|
22484
|
+
await deps.notify(
|
|
22485
|
+
row.id,
|
|
22486
|
+
`Lead gagal memutuskan: ${req.question.replace(/\s+/g, " ").slice(0, 90)}`,
|
|
22487
|
+
req.projectId,
|
|
22488
|
+
req.specId ?? null,
|
|
22489
|
+
req.sessionId ?? null
|
|
22490
|
+
);
|
|
22491
|
+
}
|
|
23716
22492
|
return row;
|
|
23717
22493
|
}
|
|
23718
22494
|
|
|
@@ -24048,7 +22824,8 @@ var prodPulseDeps = {
|
|
|
24048
22824
|
enqueue,
|
|
24049
22825
|
notify: recordLeadDecision,
|
|
24050
22826
|
optIn: leadProjects,
|
|
24051
|
-
cfg: getLead
|
|
22827
|
+
cfg: getLead,
|
|
22828
|
+
scheduler: getScheduler
|
|
24052
22829
|
};
|
|
24053
22830
|
var lastReadySig = /* @__PURE__ */ new Map();
|
|
24054
22831
|
async function pulse(deps = prodPulseDeps) {
|
|
@@ -24079,7 +22856,10 @@ async function followUpFinished(cfg, optIn, deps) {
|
|
|
24079
22856
|
const bad = (s.exitCode ?? 0) !== 0;
|
|
24080
22857
|
const unfinished = !deps.planDone(s.cwd, s.specId);
|
|
24081
22858
|
if (!bad && !unfinished) continue;
|
|
24082
|
-
const
|
|
22859
|
+
const mark = `Sesi ${s.id} untuk backlog ${s.specId}`;
|
|
22860
|
+
const seen = await prisma.leadDecision.findFirst({
|
|
22861
|
+
where: { sessionId: s.id, gate: "pulse", question: { startsWith: mark } }
|
|
22862
|
+
});
|
|
24083
22863
|
if (seen) continue;
|
|
24084
22864
|
const why = [
|
|
24085
22865
|
bad ? `berakhir dengan kode keluar ${s.exitCode}` : null,
|
|
@@ -24091,7 +22871,7 @@ async function followUpFinished(cfg, optIn, deps) {
|
|
|
24091
22871
|
sessionId: s.id,
|
|
24092
22872
|
gate: "pulse",
|
|
24093
22873
|
kind: "quality",
|
|
24094
|
-
question:
|
|
22874
|
+
question: `${mark} ${why}. Tindak lanjutnya apa: lanjutkan pekerjaan yang terputus, ulangi dari awal, atau hentikan?`,
|
|
24095
22875
|
options: [
|
|
24096
22876
|
"resume-session \u2014 lanjutkan dari keadaan worktree sekarang (ADR-0084)",
|
|
24097
22877
|
"restart-session \u2014 ulangi dari awal",
|
|
@@ -24123,7 +22903,7 @@ async function detectCollisions(optIn, deps) {
|
|
|
24123
22903
|
for (const c of findCollisions(areas)) {
|
|
24124
22904
|
const key = [c.a.sessionId, c.b.sessionId].sort().join("|");
|
|
24125
22905
|
const seen = await prisma.leadDecision.findFirst({
|
|
24126
|
-
where: {
|
|
22906
|
+
where: { gate: "pulse", question: { contains: key } }
|
|
24127
22907
|
});
|
|
24128
22908
|
if (seen) continue;
|
|
24129
22909
|
const row = await deps.decide({
|
|
@@ -24147,36 +22927,57 @@ async function detectCollisions(optIn, deps) {
|
|
|
24147
22927
|
return n;
|
|
24148
22928
|
}
|
|
24149
22929
|
async function orderReadyWork(optIn, deps) {
|
|
22930
|
+
const sched = await deps.scheduler();
|
|
22931
|
+
if (!sched.enabled || sched.paused) return 0;
|
|
24150
22932
|
let total = 0;
|
|
24151
22933
|
for (const projectId of optIn) total += await orderProject(projectId, deps);
|
|
24152
22934
|
return total;
|
|
24153
22935
|
}
|
|
24154
22936
|
async function orderProject(projectId, deps) {
|
|
24155
|
-
const
|
|
24156
|
-
|
|
24157
|
-
|
|
22937
|
+
const project = await prisma.project.findUnique({ where: { id: projectId }, select: { schedulerOptIn: true } });
|
|
22938
|
+
if (!project?.schedulerOptIn) return 0;
|
|
22939
|
+
const readyRaw = await prisma.spec.findMany({
|
|
22940
|
+
where: { ...UNSTARTED_SPEC_WHERE, projectId },
|
|
22941
|
+
select: {
|
|
22942
|
+
id: true,
|
|
22943
|
+
projectId: true,
|
|
22944
|
+
title: true,
|
|
22945
|
+
priority: true,
|
|
22946
|
+
objective: true,
|
|
22947
|
+
branchFrom: true,
|
|
22948
|
+
dependsOn: true
|
|
22949
|
+
},
|
|
24158
22950
|
orderBy: { id: "asc" }
|
|
24159
22951
|
});
|
|
22952
|
+
const repoDir = await resolveRepoDir(projectId);
|
|
22953
|
+
const ready = [];
|
|
22954
|
+
for (const r of readyRaw) if ((await blockersForSpec(r, repoDir)).length === 0) ready.push(r);
|
|
24160
22955
|
if (ready.length < 2) return 0;
|
|
24161
|
-
const
|
|
22956
|
+
const already = new Set((await prisma.schedulerQueueItem.findMany({
|
|
22957
|
+
where: { specId: { in: ready.map((r) => r.id) } },
|
|
22958
|
+
select: { specId: true }
|
|
22959
|
+
})).map((q) => q.specId));
|
|
22960
|
+
const pending = ready.filter((r) => !already.has(r.id));
|
|
22961
|
+
if (pending.length < 2) return 0;
|
|
22962
|
+
const sig = pending.map((r) => r.id).join(",");
|
|
24162
22963
|
if (sig === lastReadySig.get(projectId)) return 0;
|
|
24163
22964
|
lastReadySig.set(projectId, sig);
|
|
24164
22965
|
const row = await deps.decide({
|
|
24165
22966
|
projectId,
|
|
24166
22967
|
gate: "pulse",
|
|
24167
22968
|
kind: "order",
|
|
24168
|
-
question: `Ada ${
|
|
24169
|
-
options:
|
|
24170
|
-
notes:
|
|
22969
|
+
question: `Ada ${pending.length} backlog siap dikerjakan. Urutkan mana yang lebih dulu berdasarkan isi pekerjaannya, lalu tuliskan urutan id-nya (dipisah koma) di \`decision\`.`,
|
|
22970
|
+
options: pending.map((r) => `${r.id} \xB7 [${r.priority}] ${r.title}`),
|
|
22971
|
+
notes: pending.map((r) => `${r.id}: ${r.objective.slice(0, 200)}`)
|
|
24171
22972
|
}, deps.decideDeps);
|
|
24172
22973
|
if (!row || row.status !== "berlaku") return 0;
|
|
24173
|
-
const byId2 = new Map(
|
|
22974
|
+
const byId2 = new Map(pending.map((r) => [r.id.toLowerCase(), r]));
|
|
24174
22975
|
const named = [];
|
|
24175
22976
|
for (const tok of row.answer.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean)) {
|
|
24176
22977
|
const hit = byId2.get(tok);
|
|
24177
22978
|
if (hit && !named.includes(hit)) named.push(hit);
|
|
24178
22979
|
}
|
|
24179
|
-
const ordered = [...named, ...
|
|
22980
|
+
const ordered = [...named, ...pending.filter((r) => !named.includes(r))];
|
|
24180
22981
|
for (const r of ordered) {
|
|
24181
22982
|
await deps.enqueue({ specId: r.id, projectId: r.projectId, source: "lead", priority: r.priority });
|
|
24182
22983
|
}
|
|
@@ -24186,36 +22987,46 @@ async function orderProject(projectId, deps) {
|
|
|
24186
22987
|
|
|
24187
22988
|
// src/services/lead/engine.ts
|
|
24188
22989
|
var TICK_MS = 5e3;
|
|
22990
|
+
var busyDetect = false;
|
|
22991
|
+
var busyPulse = false;
|
|
24189
22992
|
var lastPulseAt = 0;
|
|
24190
|
-
var
|
|
22993
|
+
var pulseEndedAt = 0;
|
|
24191
22994
|
var timer3;
|
|
24192
22995
|
function lastPulse() {
|
|
24193
22996
|
return lastPulseAt;
|
|
24194
22997
|
}
|
|
24195
22998
|
async function tick2(now, deps = {}) {
|
|
24196
|
-
|
|
24197
|
-
busy2 = true;
|
|
22999
|
+
let cfg;
|
|
24198
23000
|
try {
|
|
24199
|
-
|
|
24200
|
-
|
|
24201
|
-
|
|
24202
|
-
|
|
24203
|
-
|
|
23001
|
+
cfg = await getLead();
|
|
23002
|
+
} catch (e) {
|
|
23003
|
+
console.error("lead tick:", e);
|
|
23004
|
+
return;
|
|
23005
|
+
}
|
|
23006
|
+
if (!cfg.enabled) return;
|
|
23007
|
+
const jobs = [];
|
|
23008
|
+
if (!busyDetect) {
|
|
23009
|
+
busyDetect = true;
|
|
23010
|
+
jobs.push(scanAndAnswer(deps.detect ?? prodDetectDeps).then(() => {
|
|
23011
|
+
}).catch((e) => {
|
|
24204
23012
|
console.error("lead detect:", e);
|
|
24205
|
-
}
|
|
24206
|
-
|
|
24207
|
-
|
|
23013
|
+
}).finally(() => {
|
|
23014
|
+
busyDetect = false;
|
|
23015
|
+
}));
|
|
23016
|
+
}
|
|
23017
|
+
if (!cfg.paused && !busyPulse && now - Math.max(lastPulseAt, pulseEndedAt) >= cfg.everyMin * 6e4) {
|
|
23018
|
+
busyPulse = true;
|
|
24208
23019
|
lastPulseAt = now;
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
}
|
|
23020
|
+
const clock = deps.now ?? Date.now;
|
|
23021
|
+
jobs.push(pulse(deps.pulse ?? prodPulseDeps).then(() => {
|
|
23022
|
+
}).catch((e) => {
|
|
24212
23023
|
console.error("lead pulse:", e);
|
|
24213
|
-
}
|
|
24214
|
-
|
|
24215
|
-
|
|
24216
|
-
|
|
24217
|
-
busy2 = false;
|
|
23024
|
+
}).finally(() => {
|
|
23025
|
+
pulseEndedAt = clock();
|
|
23026
|
+
busyPulse = false;
|
|
23027
|
+
}));
|
|
24218
23028
|
}
|
|
23029
|
+
await Promise.all(jobs);
|
|
24219
23030
|
}
|
|
24220
23031
|
function startLead(deps = {}) {
|
|
24221
23032
|
if (timer3) return;
|
|
@@ -24360,13 +23171,13 @@ init_db();
|
|
|
24360
23171
|
|
|
24361
23172
|
// src/services/auth.ts
|
|
24362
23173
|
init_db();
|
|
24363
|
-
import { randomBytes as
|
|
23174
|
+
import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual, createHash as createHash3 } from "node:crypto";
|
|
24364
23175
|
import { promisify as promisify9 } from "node:util";
|
|
24365
23176
|
var scrypt = promisify9(scryptCb);
|
|
24366
23177
|
var COOKIE_NAME = "hn_session";
|
|
24367
23178
|
var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
24368
23179
|
async function hashPassword(pw) {
|
|
24369
|
-
const salt =
|
|
23180
|
+
const salt = randomBytes3(16);
|
|
24370
23181
|
const key = await scrypt(pw, salt, 64);
|
|
24371
23182
|
return `${salt.toString("hex")}:${key.toString("hex")}`;
|
|
24372
23183
|
}
|
|
@@ -24375,10 +23186,10 @@ async function verifyPassword(pw, stored) {
|
|
|
24375
23186
|
if (!saltHex || !hashHex) return false;
|
|
24376
23187
|
const key = await scrypt(pw, Buffer.from(saltHex, "hex"), 64);
|
|
24377
23188
|
const want = Buffer.from(hashHex, "hex");
|
|
24378
|
-
return key.length === want.length &&
|
|
23189
|
+
return key.length === want.length && timingSafeEqual(key, want);
|
|
24379
23190
|
}
|
|
24380
|
-
var newSessionToken = () =>
|
|
24381
|
-
var sessionId = (token) =>
|
|
23191
|
+
var newSessionToken = () => randomBytes3(32).toString("base64url");
|
|
23192
|
+
var sessionId = (token) => createHash3("sha256").update(token).digest("hex");
|
|
24382
23193
|
async function createSession2(userId) {
|
|
24383
23194
|
const token = newSessionToken();
|
|
24384
23195
|
await prisma.session.create({
|
|
@@ -24509,8 +23320,8 @@ init_src();
|
|
|
24509
23320
|
|
|
24510
23321
|
// src/services/agent-token.ts
|
|
24511
23322
|
init_db();
|
|
24512
|
-
import { randomBytes as
|
|
24513
|
-
var hash = (token) =>
|
|
23323
|
+
import { randomBytes as randomBytes4, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
23324
|
+
var hash = (token) => createHash4("sha256").update(token).digest("hex");
|
|
24514
23325
|
function toAgentTokenView(t) {
|
|
24515
23326
|
return {
|
|
24516
23327
|
id: t.id,
|
|
@@ -24525,7 +23336,7 @@ function toAgentTokenView(t) {
|
|
|
24525
23336
|
};
|
|
24526
23337
|
}
|
|
24527
23338
|
async function issueAgentToken(input) {
|
|
24528
|
-
const token = "hnm_agt_" +
|
|
23339
|
+
const token = "hnm_agt_" + randomBytes4(24).toString("hex");
|
|
24529
23340
|
const row = await prisma.agentToken.create({
|
|
24530
23341
|
data: {
|
|
24531
23342
|
name: input.name,
|
|
@@ -24543,7 +23354,7 @@ async function verifyAgentToken(token) {
|
|
|
24543
23354
|
if (!row || !row.enabled || row.revokedAt) return null;
|
|
24544
23355
|
const a = Buffer.from(hash(token), "hex");
|
|
24545
23356
|
const b = Buffer.from(row.tokenHash, "hex");
|
|
24546
|
-
if (a.length !== b.length || !
|
|
23357
|
+
if (a.length !== b.length || !timingSafeEqual2(a, b)) return null;
|
|
24547
23358
|
await prisma.agentToken.update({ where: { id: row.id }, data: { lastUsedAt: /* @__PURE__ */ new Date() } }).catch(() => {
|
|
24548
23359
|
});
|
|
24549
23360
|
return { id: row.id, capabilities: Array.isArray(row.capabilities) ? row.capabilities : [] };
|
|
@@ -24638,7 +23449,7 @@ function capabilityForRoute(method, path) {
|
|
|
24638
23449
|
if (top === "settings" || top === "config") return rw("settings");
|
|
24639
23450
|
if (top === "specs") return rw("backlog");
|
|
24640
23451
|
if (top === "notifications") return rw("notifications");
|
|
24641
|
-
if (top === "
|
|
23452
|
+
if (top === "tickets") return rw("support");
|
|
24642
23453
|
if (top === "vps") return rw("vps");
|
|
24643
23454
|
if (top === "prds") return rw("docs");
|
|
24644
23455
|
if (top === "terminal") {
|
|
@@ -24698,9 +23509,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24698
23509
|
const path = req.url.split("?")[0] ?? req.url;
|
|
24699
23510
|
if (PUBLIC.has(`${req.method} ${path}`)) return;
|
|
24700
23511
|
if (path.startsWith("/api/sync") && path !== "/api/sync/now" && !path.startsWith("/api/sync/conflicts")) return;
|
|
24701
|
-
if (path.startsWith("/api/ingest")) return;
|
|
24702
23512
|
if (path.startsWith("/api/help")) return;
|
|
24703
|
-
if (path.startsWith("/api/audit/") && auditScopeFromReq(req)) return;
|
|
24704
23513
|
if (user) return;
|
|
24705
23514
|
const agentTok = agentTokenFromReq(req);
|
|
24706
23515
|
if (agentTok) {
|
|
@@ -24738,12 +23547,9 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24738
23547
|
await api.register(session_results_default);
|
|
24739
23548
|
await api.register(session_history_default);
|
|
24740
23549
|
await api.register(config_default);
|
|
24741
|
-
await api.register(ingest_default);
|
|
24742
|
-
await api.register(errors_default);
|
|
24743
23550
|
await api.register(help_default);
|
|
24744
23551
|
await api.register(tickets_default);
|
|
24745
23552
|
await api.register(scheduler_default);
|
|
24746
|
-
await api.register(audit_default);
|
|
24747
23553
|
await api.register(codex);
|
|
24748
23554
|
await api.register(lead_default);
|
|
24749
23555
|
}, { prefix: "/api" });
|
|
@@ -24798,6 +23604,7 @@ init_db();
|
|
|
24798
23604
|
init_src();
|
|
24799
23605
|
|
|
24800
23606
|
// src/services/scheduler/governor.ts
|
|
23607
|
+
var ALREADY_DONE_NOTE = "spec sudah selesai \u2014 tak diluncurkan";
|
|
24801
23608
|
var draining = false;
|
|
24802
23609
|
async function drain(cfg, deps) {
|
|
24803
23610
|
if (draining) return;
|
|
@@ -24807,6 +23614,15 @@ async function drain(cfg, deps) {
|
|
|
24807
23614
|
if (slots <= 0) return;
|
|
24808
23615
|
for (const item of await queued()) {
|
|
24809
23616
|
if (slots <= 0) break;
|
|
23617
|
+
if (await deps.isDone(item.specId)) {
|
|
23618
|
+
await markDone(item.id, ALREADY_DONE_NOTE);
|
|
23619
|
+
continue;
|
|
23620
|
+
}
|
|
23621
|
+
const blocked = await deps.blockers(item.specId);
|
|
23622
|
+
if (blocked.length) {
|
|
23623
|
+
await noteQueued(item.id, blockedNote(blocked));
|
|
23624
|
+
continue;
|
|
23625
|
+
}
|
|
24810
23626
|
const liveId = deps.isLive(item.specId);
|
|
24811
23627
|
if (liveId) {
|
|
24812
23628
|
await markLaunched(item.id, liveId);
|
|
@@ -24929,6 +23745,20 @@ var prodDeps = {
|
|
|
24929
23745
|
const s = getSession(sessionIdForSpec(specId));
|
|
24930
23746
|
return s && !s.exited ? s.id : null;
|
|
24931
23747
|
},
|
|
23748
|
+
// SPEC-431 · dibaca ULANG dari DB tepat sebelum launch, bukan dari baris antrean: antrean tak
|
|
23749
|
+
// menyimpan stage, dan item bisa selesai selagi mengantre. Spec yang hilang bukan urusan gerbang
|
|
23750
|
+
// ini — `launch` di bawah yang melempar "spec tak ada" → item ditandai failed dengan alasannya.
|
|
23751
|
+
isDone: async (specId) => {
|
|
23752
|
+
const s = await prisma.spec.findUnique({ where: { id: specId }, select: { stage: true } });
|
|
23753
|
+
return s?.stage === "done";
|
|
23754
|
+
},
|
|
23755
|
+
// SPEC-447 · ADR-0093 · dibaca ULANG dari DB tepat sebelum launch, seperti `isDone`: `dependsOn`
|
|
23756
|
+
// bisa ditulis operator selagi item mengantre, dan merged-ness bergerak sendiri saat ada integrate.
|
|
23757
|
+
blockers: async (specId) => {
|
|
23758
|
+
const spec = await prisma.spec.findUnique({ where: { id: specId } });
|
|
23759
|
+
if (!spec) return [];
|
|
23760
|
+
return blockersForSpec(spec, await resolveRepoDir(spec.projectId));
|
|
23761
|
+
},
|
|
24932
23762
|
launch: async (item, autonomy) => {
|
|
24933
23763
|
const spec = await prisma.spec.findUnique({ where: { id: item.specId } });
|
|
24934
23764
|
if (!spec) throw new Error(`spec ${item.specId} tak ada`);
|
|
@@ -24950,7 +23780,7 @@ init_db();
|
|
|
24950
23780
|
var RANK2 = { tinggi: 0, sedang: 1, rendah: 2 };
|
|
24951
23781
|
async function checkBacklog() {
|
|
24952
23782
|
const specs = await prisma.spec.findMany({
|
|
24953
|
-
where: {
|
|
23783
|
+
where: { ...UNSTARTED_SPEC_WHERE, project: { schedulerOptIn: true } }
|
|
24954
23784
|
});
|
|
24955
23785
|
specs.sort((a, b) => (RANK2[a.priority] ?? 1) - (RANK2[b.priority] ?? 1) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
24956
23786
|
for (const s of specs) {
|
|
@@ -24961,36 +23791,6 @@ function registerBacklogSource() {
|
|
|
24961
23791
|
registerSchedulerSource({ id: "backlog", check: checkBacklog });
|
|
24962
23792
|
}
|
|
24963
23793
|
|
|
24964
|
-
// src/services/scheduler/sources/errors.ts
|
|
24965
|
-
init_db();
|
|
24966
|
-
async function checkErrors() {
|
|
24967
|
-
const minCount = (await getScheduler()).sources.errors.minCount;
|
|
24968
|
-
const groups = await prisma.errorGroup.findMany({
|
|
24969
|
-
where: {
|
|
24970
|
-
status: "new",
|
|
24971
|
-
// escalated/resolved tersaring di query
|
|
24972
|
-
environment: "production",
|
|
24973
|
-
// literal, cermin services/error-ingest.ts
|
|
24974
|
-
specId: null,
|
|
24975
|
-
// grup ber-specId tersaring di query
|
|
24976
|
-
count: { gte: minCount },
|
|
24977
|
-
// ambang dari setelan
|
|
24978
|
-
project: { schedulerOptIn: true }
|
|
24979
|
-
// non-opt-in tak pernah ter-query
|
|
24980
|
-
}
|
|
24981
|
-
});
|
|
24982
|
-
for (const g of groups) {
|
|
24983
|
-
try {
|
|
24984
|
-
const { spec } = await escalateErrorGroup(g, { author: "scheduler" });
|
|
24985
|
-
await enqueue({ specId: spec.id, projectId: spec.projectId, source: "errors", priority: spec.priority });
|
|
24986
|
-
} catch {
|
|
24987
|
-
}
|
|
24988
|
-
}
|
|
24989
|
-
}
|
|
24990
|
-
function registerErrorsSource() {
|
|
24991
|
-
registerSchedulerSource({ id: "errors", check: checkErrors });
|
|
24992
|
-
}
|
|
24993
|
-
|
|
24994
23794
|
// src/services/scheduler/sources/triase.ts
|
|
24995
23795
|
init_db();
|
|
24996
23796
|
async function checkTriase() {
|
|
@@ -25050,7 +23850,6 @@ app.listen({ port, host }).then(() => {
|
|
|
25050
23850
|
}
|
|
25051
23851
|
startVpsMonitor();
|
|
25052
23852
|
registerBacklogSource();
|
|
25053
|
-
registerErrorsSource();
|
|
25054
23853
|
registerTriaseSource();
|
|
25055
23854
|
startScheduler();
|
|
25056
23855
|
startLead();
|