hanoman 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +14 -113
- package/dist/server.js +199 -1623
- package/package.json +1 -1
- package/prisma/migrations/20260731180000_drop_errors_sdk_crossaudit/migration.sql +58 -0
- package/prisma/schema.prisma +4 -88
- package/web/assets/{index-DB8a8_aD.js → index-BWyQwoAk.js} +1536 -1537
- 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
|
}
|
|
@@ -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(),
|
|
@@ -4296,7 +4292,6 @@ var init_entities = __esm({
|
|
|
4296
4292
|
// dikonsumsi daun #5
|
|
4297
4293
|
sources: external_exports.object({
|
|
4298
4294
|
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
4295
|
triase: external_exports.object({ ...zSourceCommon, everyMin: external_exports.number().int().min(1).default(30) }).default({})
|
|
4301
4296
|
}).default({})
|
|
4302
4297
|
});
|
|
@@ -4329,8 +4324,12 @@ var init_entities = __esm({
|
|
|
4329
4324
|
// rem per project (AC-15/US-4)
|
|
4330
4325
|
everyMin: external_exports.number().int().min(1).max(1440).default(5),
|
|
4331
4326
|
// denyut proaktif (OQ-2)
|
|
4332
|
-
|
|
4333
|
-
//
|
|
4327
|
+
// SPEC-432 · batas waktu satu putusan (AC-4/35). Default 120 dtk MELAWAN prompt lead sendiri
|
|
4328
|
+
// ("kumpulkan bukti dulu: SoT, ADR, plan, kode, riwayat git") dan kalah: satu keputusan `order`
|
|
4329
|
+
// nyata terukur 306 dtk pada claude-opus-5 · xhigh, jadi 7/7 baris jejak operator berstatus
|
|
4330
|
+
// `gagal`. 600 dtk memberi kelonggaran di atas ongkos terukur itu; yang memangkas ongkosnya
|
|
4331
|
+
// sendiri adalah paragraf anggaran waktu di `leadPrompt` (306 dtk → 101 dtk).
|
|
4332
|
+
timeoutSec: external_exports.number().int().min(10).max(900).default(600),
|
|
4334
4333
|
// AC-11 / OQ-10 · berapa jawaban otomatis berturut-turut untuk SATU sesi sebelum lead berhenti.
|
|
4335
4334
|
maxAutoAnswers: external_exports.number().int().min(1).max(20).default(3),
|
|
4336
4335
|
// OQ-3 · syarat objektif sebelum lead boleh mengintegrasikan ke branch utama. Default MENYALA:
|
|
@@ -4372,10 +4371,11 @@ var init_entities = __esm({
|
|
|
4372
4371
|
});
|
|
4373
4372
|
zNotification = external_exports.object({
|
|
4374
4373
|
id: external_exports.string(),
|
|
4375
|
-
// SPEC-
|
|
4374
|
+
// SPEC-253 · +ticket; SPEC-298 · +fail (sesi scheduler gagal/limit)
|
|
4376
4375
|
// SPEC-409 · +lead (ADR-0091): keputusan berbobot / ragu / tindakan terkunci ditolak. MEMBERI
|
|
4377
4376
|
// TAHU, bukan meminta izin — tak ada pekerjaan yang menunggu notifikasi ini dibaca (AC-25).
|
|
4378
|
-
|
|
4377
|
+
// SPEC-384 · −error (ADR-0092) · dicabut bersama error monitoring.
|
|
4378
|
+
type: external_exports.enum(["done", "decision", "ticket", "fail", "lead"]).default("done"),
|
|
4379
4379
|
specId: external_exports.string().nullable(),
|
|
4380
4380
|
sessionId: external_exports.string().nullable(),
|
|
4381
4381
|
title: external_exports.string(),
|
|
@@ -4449,7 +4449,7 @@ var init_agent = __esm({
|
|
|
4449
4449
|
});
|
|
4450
4450
|
CAPABILITIES = [
|
|
4451
4451
|
{ 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,
|
|
4452
|
+
{ id: "projects:write", domain: "projects", access: "write", label: "Projects \u2014 tulis", desc: "Buat/ubah/hapus project, rename, clone, Help Center." },
|
|
4453
4453
|
{ id: "backlog:read", domain: "backlog", access: "read", label: "Backlog \u2014 baca", desc: "Lihat spec/backlog, dokumen, review diff." },
|
|
4454
4454
|
{ id: "backlog:write", domain: "backlog", access: "write", label: "Backlog \u2014 tulis", desc: "Buat/ubah/hapus spec, integrate branch." },
|
|
4455
4455
|
{ id: "sessions:read", domain: "sessions", access: "read", label: "Sesi \u2014 baca", desc: "Lihat sesi terminal, fase, review." },
|
|
@@ -4462,8 +4462,8 @@ var init_agent = __esm({
|
|
|
4462
4462
|
{ id: "vps:write", domain: "vps", access: "write", label: "VPS \u2014 tulis", desc: "Kelola VPS, audit, harden, remediasi, konsol (remote exec).", risk: "exec" },
|
|
4463
4463
|
{ id: "settings:read", domain: "settings", access: "read", label: "Settings \u2014 baca", desc: "Baca setelan & config runtime." },
|
|
4464
4464
|
{ 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: "
|
|
4465
|
+
{ id: "support:read", domain: "support", access: "read", label: "Tiket \u2014 baca", desc: "Lihat tiket Help Center." },
|
|
4466
|
+
{ id: "support:write", domain: "support", access: "write", label: "Tiket \u2014 tulis", desc: "Terima/tolak tiket, ubah status, promosikan ke backlog." },
|
|
4467
4467
|
{ id: "notifications:read", domain: "notifications", access: "read", label: "Notifikasi \u2014 baca", desc: "Lihat notifikasi." },
|
|
4468
4468
|
{ id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." },
|
|
4469
4469
|
{ id: "lead:read", domain: "lead", access: "read", label: "Lead \u2014 baca", desc: "Baca jejak keputusan hanoman-lead & statusnya." },
|
|
@@ -4580,9 +4580,9 @@ var init_lead = __esm({
|
|
|
4580
4580
|
|
|
4581
4581
|
// ../shared/src/dto.ts
|
|
4582
4582
|
function flowForSource(source) {
|
|
4583
|
-
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "
|
|
4583
|
+
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
4584
4584
|
}
|
|
4585
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject,
|
|
4585
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerState, zLeadDecisionView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
4586
4586
|
var init_dto = __esm({
|
|
4587
4587
|
"../shared/src/dto.ts"() {
|
|
4588
4588
|
"use strict";
|
|
@@ -4632,11 +4632,6 @@ var init_dto = __esm({
|
|
|
4632
4632
|
});
|
|
4633
4633
|
zProjectId = external_exports.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "slug tak sah");
|
|
4634
4634
|
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
4635
|
zUpdateProject = external_exports.object({
|
|
4641
4636
|
name: external_exports.string().min(1).optional(),
|
|
4642
4637
|
desc: external_exports.string().optional(),
|
|
@@ -4689,10 +4684,6 @@ var init_dto = __esm({
|
|
|
4689
4684
|
session: zSessionSummary,
|
|
4690
4685
|
activity: external_exports.string(),
|
|
4691
4686
|
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
4687
|
helpEnabled: external_exports.boolean().default(false),
|
|
4697
4688
|
// SPEC-253 · Help Center publik aktif
|
|
4698
4689
|
schedulerOptIn: external_exports.boolean().default(false),
|
|
@@ -4715,8 +4706,6 @@ var init_dto = __esm({
|
|
|
4715
4706
|
id: external_exports.string(),
|
|
4716
4707
|
enabled: external_exports.boolean(),
|
|
4717
4708
|
everyMin: external_exports.number(),
|
|
4718
|
-
minCount: external_exports.number().optional(),
|
|
4719
|
-
// hanya errors
|
|
4720
4709
|
lastRunAt: external_exports.string().nullable(),
|
|
4721
4710
|
nextRunAt: external_exports.string().nullable()
|
|
4722
4711
|
});
|
|
@@ -4781,7 +4770,7 @@ var init_dto = __esm({
|
|
|
4781
4770
|
// id sesi ber-marker keputusan terisi
|
|
4782
4771
|
lastPulseAt: external_exports.string().nullable()
|
|
4783
4772
|
});
|
|
4784
|
-
zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "
|
|
4773
|
+
zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "goal"]);
|
|
4785
4774
|
zPrdBrief = external_exports.object({
|
|
4786
4775
|
title: external_exports.string().min(1),
|
|
4787
4776
|
context: external_exports.string(),
|
|
@@ -4856,8 +4845,6 @@ var init_dto = __esm({
|
|
|
4856
4845
|
}),
|
|
4857
4846
|
// SPEC-273 · sesi breakdown project-level: pecah SATU PRD (prdPath) → manifest N backlog.
|
|
4858
4847
|
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
4848
|
// SPEC-222 · scaffold: sesi project-level from-scratch, menyusun SoT dari ide. Tanpa brief
|
|
4862
4849
|
// (diseed dari Project.desc), tanpa Spec — cermin reverse.
|
|
4863
4850
|
external_exports.object({ project: external_exports.string(), flow: external_exports.literal("scaffold") }),
|
|
@@ -4929,75 +4916,6 @@ var init_dto = __esm({
|
|
|
4929
4916
|
zRemediate = external_exports.object({ items: external_exports.array(external_exports.string()).min(1).max(64) });
|
|
4930
4917
|
UPDATE_RESTART_EXIT = 75;
|
|
4931
4918
|
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
4919
|
zTicketView = external_exports.object({
|
|
5002
4920
|
id: external_exports.string(),
|
|
5003
4921
|
projectId: external_exports.string(),
|
|
@@ -5186,15 +5104,6 @@ var init_api = __esm({
|
|
|
5186
5104
|
agentTokens: `${API}/agent-tokens`,
|
|
5187
5105
|
agentToken: (id) => `${API}/agent-tokens/${id}`,
|
|
5188
5106
|
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
5107
|
// SPEC-253 · Help Center publik (bypass gate cookie; otorisasi helpEnabled + kunci opaque tiket).
|
|
5199
5108
|
help: (slug) => `${API}/help/${encodeURIComponent(slug)}`,
|
|
5200
5109
|
helpTickets: (slug) => `${API}/help/${encodeURIComponent(slug)}/tickets`,
|
|
@@ -5208,9 +5117,6 @@ var init_api = __esm({
|
|
|
5208
5117
|
ticketUnlink: (id) => `${API}/tickets/${id}/unlink`,
|
|
5209
5118
|
// SPEC-271 · lepas tautan backlog
|
|
5210
5119
|
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
5120
|
// SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
|
|
5215
5121
|
// (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
|
|
5216
5122
|
download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
|
|
@@ -5300,7 +5206,6 @@ var init_session_kind = __esm({
|
|
|
5300
5206
|
"prd",
|
|
5301
5207
|
"scaffold",
|
|
5302
5208
|
"breakdown",
|
|
5303
|
-
"cross-audit",
|
|
5304
5209
|
"vps",
|
|
5305
5210
|
"shell",
|
|
5306
5211
|
"worktree",
|
|
@@ -5869,45 +5774,7 @@ Stack: ${project.stack || "\u2014"}`,
|
|
|
5869
5774
|
${REVERSE_STANDARD}`
|
|
5870
5775
|
].filter(Boolean).join("\n\n");
|
|
5871
5776
|
}
|
|
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;
|
|
5777
|
+
var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, PHASE_SKILLS, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
|
|
5911
5778
|
var init_prompt = __esm({
|
|
5912
5779
|
"../runner/src/prompt.ts"() {
|
|
5913
5780
|
"use strict";
|
|
@@ -5924,7 +5791,6 @@ var init_prompt = __esm({
|
|
|
5924
5791
|
breakdown: ["Analisis", "Breakdown"],
|
|
5925
5792
|
// SPEC-337 · ADR-0075 · audit lintas project: fase & stage-map identik audit-only, scope-nya
|
|
5926
5793
|
// yang berbeda (project utama + tetangga ProjectLink).
|
|
5927
|
-
"cross-audit": ["Audit", "Laporan"],
|
|
5928
5794
|
// SPEC-407 · ADR-0089 · backlog goal: tak ada fase perencanaan sama sekali. `Goal` = kerjakan,
|
|
5929
5795
|
// `Verifikasi` = buktikan. Kedua nama unik lintas PIPELINES — syarat peta REACHED di server,
|
|
5930
5796
|
// yang berkunci nama fase saja.
|
|
@@ -6019,27 +5885,6 @@ ${lines2.join("\n")}` : "";
|
|
|
6019
5885
|
"- Objective: kunci SATU MVP objective yang terukur dari hasil brainstorm, tulis ringkas di docs.",
|
|
6020
5886
|
"- Doc index: tulis SELURUH internal/docs/** dari ide+objective+jawaban, mengikuti STANDAR DOCS di bawah \u2014 entrypoints, product, business, requirements (+EARS dari perilaku yang diinginkan), research, architecture (stack/data-model/api-contract/nfr), adr awal (Status accepted), design-system/frontend bila ada UI, operations, security, plus README index bernomor + CLAUDE.md + AGENTS.md + Stop hook. Lengkap dan spesifik terhadap ide ini, BUKAN kerangka."
|
|
6021
5887
|
].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
5888
|
}
|
|
6044
5889
|
});
|
|
6045
5890
|
|
|
@@ -6483,12 +6328,16 @@ function planComplete(worktree, specId) {
|
|
|
6483
6328
|
}
|
|
6484
6329
|
return true;
|
|
6485
6330
|
}
|
|
6331
|
+
function sessionComplete(phases, worktree, specId) {
|
|
6332
|
+
if (!phasesComplete(phases)) return false;
|
|
6333
|
+
return specId ? planComplete(worktree, specId) : true;
|
|
6334
|
+
}
|
|
6486
6335
|
function stageForRun(phases, worktree, specId) {
|
|
6487
6336
|
const s = stageFor(phases);
|
|
6488
6337
|
if (s === "done" && !planComplete(worktree, specId)) return "executing";
|
|
6489
6338
|
return s;
|
|
6490
6339
|
}
|
|
6491
|
-
var phaseFilePath, decisionFilePath, REACHED;
|
|
6340
|
+
var phaseFilePath, decisionFilePath, REACHED, phasesComplete;
|
|
6492
6341
|
var init_session_phases = __esm({
|
|
6493
6342
|
"src/services/session-phases.ts"() {
|
|
6494
6343
|
"use strict";
|
|
@@ -6508,6 +6357,7 @@ var init_session_phases = __esm({
|
|
|
6508
6357
|
Goal: "executing",
|
|
6509
6358
|
Verifikasi: "done"
|
|
6510
6359
|
};
|
|
6360
|
+
phasesComplete = (phases) => phases.length > 0 && phases.every((p) => p.state === "done" || p.state === "skipped");
|
|
6511
6361
|
}
|
|
6512
6362
|
});
|
|
6513
6363
|
|
|
@@ -6595,21 +6445,7 @@ function listPanes() {
|
|
|
6595
6445
|
throw e;
|
|
6596
6446
|
}
|
|
6597
6447
|
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(" ");
|
|
6448
|
+
const [n, projectId, specId, flow, phaseFile, cwd, dead, code, decisionFile, branch, agent] = line.split(" ");
|
|
6613
6449
|
if (!n?.startsWith(PREFIX)) return [];
|
|
6614
6450
|
const exited = dead === "1";
|
|
6615
6451
|
return [{
|
|
@@ -6622,9 +6458,6 @@ function listPanes() {
|
|
|
6622
6458
|
exited,
|
|
6623
6459
|
code: Number(code) || 0,
|
|
6624
6460
|
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
6461
|
// SPEC-230 · branch integrasi sesi project-level (PRD: prd/<slug>). Kosong = tak ada.
|
|
6629
6462
|
branch: branch || void 0,
|
|
6630
6463
|
// SPEC-196 · sesi hidup dengan marker keputusan terisi = menunggu manusia.
|
|
@@ -6640,7 +6473,6 @@ function registerSessionHooks(h) {
|
|
|
6640
6473
|
function sessionKind(o, projectId, cwd) {
|
|
6641
6474
|
if (o.specId) return "spec";
|
|
6642
6475
|
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
6476
|
if (projectId.startsWith("vps")) return "vps";
|
|
6645
6477
|
if (o.command) return "shell";
|
|
6646
6478
|
if (cwd.includes("/.worktrees/")) return "worktree";
|
|
@@ -6654,13 +6486,6 @@ function captureTranscript(id) {
|
|
|
6654
6486
|
return null;
|
|
6655
6487
|
}
|
|
6656
6488
|
}
|
|
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
6489
|
function createSession(projectId, cwd, opts = {}) {
|
|
6665
6490
|
const id = opts.id ?? idFor(opts.specId);
|
|
6666
6491
|
const existing = getSession(id);
|
|
@@ -6776,10 +6601,6 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6776
6601
|
tmux("set-option", "-t", name(id), "@hanoman_agent", agent);
|
|
6777
6602
|
if (opts.phaseFile) tmux("set-option", "-t", name(id), "@hanoman_phase_file", opts.phaseFile);
|
|
6778
6603
|
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
6604
|
if (opts.goal && !opts.command) void armGoalInTui(id, opts.goal, { agent }).catch(() => {
|
|
6784
6605
|
});
|
|
6785
6606
|
emitBirth({
|
|
@@ -6907,10 +6728,11 @@ function end(id, code) {
|
|
|
6907
6728
|
function pollPhases(p, a) {
|
|
6908
6729
|
if (!p.flow || !p.phaseFile) return;
|
|
6909
6730
|
const phases = readPhases(p.phaseFile, p.flow);
|
|
6910
|
-
const
|
|
6731
|
+
const complete = sessionComplete(phases, p.cwd, p.specId);
|
|
6732
|
+
const json = phaseKey(phases, complete);
|
|
6911
6733
|
if (json === a.lastPhases) return;
|
|
6912
6734
|
a.lastPhases = json;
|
|
6913
|
-
broadcast(a, { t: "phase", phases });
|
|
6735
|
+
broadcast(a, { t: "phase", phases, complete });
|
|
6914
6736
|
}
|
|
6915
6737
|
function startPoll() {
|
|
6916
6738
|
if (poll) return;
|
|
@@ -6951,10 +6773,11 @@ function attach(id, c) {
|
|
|
6951
6773
|
const a = attached.get(id) ?? open(id);
|
|
6952
6774
|
a.clients.add(c);
|
|
6953
6775
|
if (a.scrollback) c.send(frame({ t: "data", d: a.scrollback }));
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6776
|
+
if (p.flow && p.phaseFile) {
|
|
6777
|
+
const phases = readPhases(p.phaseFile, p.flow);
|
|
6778
|
+
const complete = sessionComplete(phases, p.cwd, p.specId);
|
|
6779
|
+
a.lastPhases = phaseKey(phases, complete);
|
|
6780
|
+
c.send(frame({ t: "phase", phases, complete }));
|
|
6958
6781
|
}
|
|
6959
6782
|
}
|
|
6960
6783
|
function writeTo(id, d) {
|
|
@@ -6991,7 +6814,7 @@ function spawnPty(...args) {
|
|
|
6991
6814
|
);
|
|
6992
6815
|
}
|
|
6993
6816
|
}
|
|
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;
|
|
6817
|
+
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, sleep, paneText, GOAL_ARMED_MARKERS, goalArmed, phaseKey, poll, detach;
|
|
6995
6818
|
var init_pty = __esm({
|
|
6996
6819
|
"src/services/pty.ts"() {
|
|
6997
6820
|
"use strict";
|
|
@@ -7042,11 +6865,7 @@ var init_pty = __esm({
|
|
|
7042
6865
|
"#{pane_dead_status}",
|
|
7043
6866
|
"#{@hanoman_decision_file}",
|
|
7044
6867
|
"#{@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}"
|
|
6868
|
+
"#{@hanoman_agent}"
|
|
7050
6869
|
].join(" ");
|
|
7051
6870
|
listSessions = () => listPanes().map(({ id, projectId, specId, flow, cwd, exited, code, branch, decision, agent }) => ({
|
|
7052
6871
|
id,
|
|
@@ -7093,6 +6912,7 @@ var init_pty = __esm({
|
|
|
7093
6912
|
const text = paneText(id);
|
|
7094
6913
|
return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
|
|
7095
6914
|
};
|
|
6915
|
+
phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
|
|
7096
6916
|
detach = (id, c) => {
|
|
7097
6917
|
attached.get(id)?.clients.delete(c);
|
|
7098
6918
|
};
|
|
@@ -7127,11 +6947,9 @@ var init_outbox = __esm({
|
|
|
7127
6947
|
async function renameProjectCore(tx, oldId, newId) {
|
|
7128
6948
|
await tx.project.update({ where: { id: oldId }, data: { id: newId, updatedAt: /* @__PURE__ */ new Date() } });
|
|
7129
6949
|
const spec = await tx.spec.count({ where: { projectId: newId } });
|
|
7130
|
-
const errorGroup = await tx.errorGroup.count({ where: { projectId: newId } });
|
|
7131
6950
|
const ticket = await tx.ticket.count({ where: { projectId: newId } });
|
|
7132
6951
|
const notification = (await tx.notification.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
7133
6952
|
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
6953
|
const ticketAttachment = (await tx.ticketAttachment.updateMany({ where: { projectId: oldId }, data: { projectId: newId } })).count;
|
|
7136
6954
|
const binding = await tx.localBinding.findUnique({ where: { projectId: oldId } });
|
|
7137
6955
|
let localBinding = 0;
|
|
@@ -7140,7 +6958,7 @@ async function renameProjectCore(tx, oldId, newId) {
|
|
|
7140
6958
|
await tx.localBinding.create({ data: { projectId: newId, repoDir: binding.repoDir, createdAt: binding.createdAt } });
|
|
7141
6959
|
localBinding = 1;
|
|
7142
6960
|
}
|
|
7143
|
-
return { spec,
|
|
6961
|
+
return { spec, ticket, notification, sessionResult, ticketAttachment, localBinding };
|
|
7144
6962
|
}
|
|
7145
6963
|
async function renameProject(oldId, newId) {
|
|
7146
6964
|
if (!zProjectId.safeParse(newId).success) return { ok: false, code: 400, error: "id baru tak sah (slug)" };
|
|
@@ -7169,11 +6987,9 @@ var init_rename_project = __esm({
|
|
|
7169
6987
|
RENAME_SEP = " ";
|
|
7170
6988
|
ZERO_AFFECTED = {
|
|
7171
6989
|
spec: 0,
|
|
7172
|
-
errorGroup: 0,
|
|
7173
6990
|
ticket: 0,
|
|
7174
6991
|
notification: 0,
|
|
7175
6992
|
sessionResult: 0,
|
|
7176
|
-
errorEvent: 0,
|
|
7177
6993
|
ticketAttachment: 0,
|
|
7178
6994
|
localBinding: 0
|
|
7179
6995
|
};
|
|
@@ -7327,13 +7143,12 @@ var init_sync = __esm({
|
|
|
7327
7143
|
"use strict";
|
|
7328
7144
|
init_db();
|
|
7329
7145
|
init_rename_project();
|
|
7330
|
-
SYNCED = ["project", "spec", "vps", "sessionResult", "
|
|
7146
|
+
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment"];
|
|
7331
7147
|
DELEGATE = {
|
|
7332
7148
|
project: prisma.project,
|
|
7333
7149
|
spec: prisma.spec,
|
|
7334
7150
|
vps: prisma.vps,
|
|
7335
7151
|
sessionResult: prisma.sessionResult,
|
|
7336
|
-
errorGroup: prisma.errorGroup,
|
|
7337
7152
|
ticket: prisma.ticket,
|
|
7338
7153
|
ticketAttachment: prisma.ticketAttachment
|
|
7339
7154
|
};
|
|
@@ -7344,8 +7159,6 @@ var init_sync = __esm({
|
|
|
7344
7159
|
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "createdAt", "startedAt", "updatedAt"],
|
|
7345
7160
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7346
7161
|
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
7162
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
7350
7163
|
// (kolom required @unique tanpa default); kunci plaintext tak pernah menyeberang.
|
|
7351
7164
|
ticket: ["projectId", "number", "category", "title", "detail", "reporterEmail", "status", "accessKeyHash", "specId", "createdAt", "updatedAt"],
|
|
@@ -7357,7 +7170,6 @@ var init_sync = __esm({
|
|
|
7357
7170
|
spec: ["createdAt", "startedAt", "updatedAt"],
|
|
7358
7171
|
vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7359
7172
|
sessionResult: ["createdAt", "updatedAt"],
|
|
7360
|
-
errorGroup: ["firstSeenAt", "lastSeenAt", "updatedAt"],
|
|
7361
7173
|
ticket: ["createdAt", "updatedAt"],
|
|
7362
7174
|
ticketAttachment: ["createdAt", "updatedAt"]
|
|
7363
7175
|
};
|
|
@@ -9526,7 +9338,7 @@ var require_extension = __commonJS({
|
|
|
9526
9338
|
if (dest[name2] === void 0) dest[name2] = [elem];
|
|
9527
9339
|
else dest[name2].push(elem);
|
|
9528
9340
|
}
|
|
9529
|
-
function
|
|
9341
|
+
function parse2(header) {
|
|
9530
9342
|
const offers = /* @__PURE__ */ Object.create(null);
|
|
9531
9343
|
let params = /* @__PURE__ */ Object.create(null);
|
|
9532
9344
|
let mustUnescape = false;
|
|
@@ -9666,7 +9478,7 @@ var require_extension = __commonJS({
|
|
|
9666
9478
|
}).join(", ");
|
|
9667
9479
|
}).join(", ");
|
|
9668
9480
|
}
|
|
9669
|
-
module.exports = { format, parse:
|
|
9481
|
+
module.exports = { format, parse: parse2 };
|
|
9670
9482
|
}
|
|
9671
9483
|
});
|
|
9672
9484
|
|
|
@@ -9679,7 +9491,7 @@ var require_websocket = __commonJS({
|
|
|
9679
9491
|
var http = __require("http");
|
|
9680
9492
|
var net = __require("net");
|
|
9681
9493
|
var tls = __require("tls");
|
|
9682
|
-
var { randomBytes:
|
|
9494
|
+
var { randomBytes: randomBytes5, createHash: createHash5 } = __require("crypto");
|
|
9683
9495
|
var { Duplex, Readable } = __require("stream");
|
|
9684
9496
|
var { URL: URL2 } = __require("url");
|
|
9685
9497
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -9700,7 +9512,7 @@ var require_websocket = __commonJS({
|
|
|
9700
9512
|
var {
|
|
9701
9513
|
EventTarget: { addEventListener, removeEventListener }
|
|
9702
9514
|
} = require_event_target();
|
|
9703
|
-
var { format, parse:
|
|
9515
|
+
var { format, parse: parse2 } = require_extension();
|
|
9704
9516
|
var { toBuffer } = require_buffer_util();
|
|
9705
9517
|
var kAborted = Symbol("kAborted");
|
|
9706
9518
|
var protocolVersions = [8, 13];
|
|
@@ -10217,7 +10029,7 @@ var require_websocket = __commonJS({
|
|
|
10217
10029
|
}
|
|
10218
10030
|
}
|
|
10219
10031
|
const defaultPort = isSecure ? 443 : 80;
|
|
10220
|
-
const key =
|
|
10032
|
+
const key = randomBytes5(16).toString("base64");
|
|
10221
10033
|
const request = isSecure ? https.request : http.request;
|
|
10222
10034
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
10223
10035
|
let perMessageDeflate;
|
|
@@ -10347,7 +10159,7 @@ var require_websocket = __commonJS({
|
|
|
10347
10159
|
abortHandshake(websocket2, socket2, "Invalid Upgrade header");
|
|
10348
10160
|
return;
|
|
10349
10161
|
}
|
|
10350
|
-
const digest =
|
|
10162
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
10351
10163
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
10352
10164
|
abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
|
|
10353
10165
|
return;
|
|
@@ -10377,7 +10189,7 @@ var require_websocket = __commonJS({
|
|
|
10377
10189
|
}
|
|
10378
10190
|
let extensions;
|
|
10379
10191
|
try {
|
|
10380
|
-
extensions =
|
|
10192
|
+
extensions = parse2(secWebSocketExtensions);
|
|
10381
10193
|
} catch (err) {
|
|
10382
10194
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
10383
10195
|
abortHandshake(websocket2, socket2, message);
|
|
@@ -10669,7 +10481,7 @@ var require_subprotocol = __commonJS({
|
|
|
10669
10481
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
|
|
10670
10482
|
"use strict";
|
|
10671
10483
|
var { tokenChars } = require_validation();
|
|
10672
|
-
function
|
|
10484
|
+
function parse2(header) {
|
|
10673
10485
|
const protocols = /* @__PURE__ */ new Set();
|
|
10674
10486
|
let start = -1;
|
|
10675
10487
|
let end2 = -1;
|
|
@@ -10705,7 +10517,7 @@ var require_subprotocol = __commonJS({
|
|
|
10705
10517
|
protocols.add(protocol);
|
|
10706
10518
|
return protocols;
|
|
10707
10519
|
}
|
|
10708
|
-
module.exports = { parse:
|
|
10520
|
+
module.exports = { parse: parse2 };
|
|
10709
10521
|
}
|
|
10710
10522
|
});
|
|
10711
10523
|
|
|
@@ -10716,7 +10528,7 @@ var require_websocket_server = __commonJS({
|
|
|
10716
10528
|
var EventEmitter = __require("events");
|
|
10717
10529
|
var http = __require("http");
|
|
10718
10530
|
var { Duplex } = __require("stream");
|
|
10719
|
-
var { createHash:
|
|
10531
|
+
var { createHash: createHash5 } = __require("crypto");
|
|
10720
10532
|
var extension2 = require_extension();
|
|
10721
10533
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
10722
10534
|
var subprotocol2 = require_subprotocol();
|
|
@@ -11023,7 +10835,7 @@ var require_websocket_server = __commonJS({
|
|
|
11023
10835
|
);
|
|
11024
10836
|
}
|
|
11025
10837
|
if (this._state > RUNNING) return abortHandshake(socket2, 503);
|
|
11026
|
-
const digest =
|
|
10838
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
11027
10839
|
const headers = [
|
|
11028
10840
|
"HTTP/1.1 101 Switching Protocols",
|
|
11029
10841
|
"Upgrade: websocket",
|
|
@@ -13894,17 +13706,17 @@ var require_stream_wormhole = __commonJS({
|
|
|
13894
13706
|
"../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
|
|
13895
13707
|
"use strict";
|
|
13896
13708
|
module.exports = (stream, throwError) => {
|
|
13897
|
-
return new Promise((
|
|
13709
|
+
return new Promise((resolve14, reject) => {
|
|
13898
13710
|
if (typeof stream.resume !== "function") {
|
|
13899
|
-
return
|
|
13711
|
+
return resolve14();
|
|
13900
13712
|
}
|
|
13901
13713
|
stream.unpipe && stream.unpipe();
|
|
13902
13714
|
stream.resume();
|
|
13903
13715
|
if (stream._readableState && stream._readableState.ended) {
|
|
13904
|
-
return
|
|
13716
|
+
return resolve14();
|
|
13905
13717
|
}
|
|
13906
13718
|
if (!stream.readable || stream.destroyed) {
|
|
13907
|
-
return
|
|
13719
|
+
return resolve14();
|
|
13908
13720
|
}
|
|
13909
13721
|
function cleanup() {
|
|
13910
13722
|
stream.removeListener("end", onEnd);
|
|
@@ -13913,14 +13725,14 @@ var require_stream_wormhole = __commonJS({
|
|
|
13913
13725
|
}
|
|
13914
13726
|
function onEnd() {
|
|
13915
13727
|
cleanup();
|
|
13916
|
-
|
|
13728
|
+
resolve14();
|
|
13917
13729
|
}
|
|
13918
13730
|
function onError(err) {
|
|
13919
13731
|
cleanup();
|
|
13920
13732
|
if (throwError) {
|
|
13921
13733
|
reject(err);
|
|
13922
13734
|
} else {
|
|
13923
|
-
|
|
13735
|
+
resolve14();
|
|
13924
13736
|
}
|
|
13925
13737
|
}
|
|
13926
13738
|
stream.on("end", onEnd);
|
|
@@ -14136,7 +13948,7 @@ var require_secure_json_parse = __commonJS({
|
|
|
14136
13948
|
}
|
|
14137
13949
|
return obj;
|
|
14138
13950
|
}
|
|
14139
|
-
function
|
|
13951
|
+
function parse2(text, reviver, options2) {
|
|
14140
13952
|
const stackTraceLimit = Error.stackTraceLimit;
|
|
14141
13953
|
Error.stackTraceLimit = 0;
|
|
14142
13954
|
try {
|
|
@@ -14156,9 +13968,9 @@ var require_secure_json_parse = __commonJS({
|
|
|
14156
13968
|
Error.stackTraceLimit = stackTraceLimit;
|
|
14157
13969
|
}
|
|
14158
13970
|
}
|
|
14159
|
-
module.exports =
|
|
14160
|
-
module.exports.default =
|
|
14161
|
-
module.exports.parse =
|
|
13971
|
+
module.exports = parse2;
|
|
13972
|
+
module.exports.default = parse2;
|
|
13973
|
+
module.exports.parse = parse2;
|
|
14162
13974
|
module.exports.safeParse = safeParse;
|
|
14163
13975
|
module.exports.scan = filter;
|
|
14164
13976
|
}
|
|
@@ -14172,7 +13984,7 @@ var require_multipart2 = __commonJS({
|
|
|
14172
13984
|
var os = __require("node:os");
|
|
14173
13985
|
var fp = require_plugin();
|
|
14174
13986
|
var { createWriteStream } = __require("node:fs");
|
|
14175
|
-
var { unlink:
|
|
13987
|
+
var { unlink: unlink3 } = __require("node:fs/promises");
|
|
14176
13988
|
var path = __require("node:path");
|
|
14177
13989
|
var { generateId } = require_generateId();
|
|
14178
13990
|
var util2 = __require("node:util");
|
|
@@ -14355,16 +14167,16 @@ var require_multipart2 = __commonJS({
|
|
|
14355
14167
|
}
|
|
14356
14168
|
};
|
|
14357
14169
|
const parts = () => {
|
|
14358
|
-
return new Promise((
|
|
14170
|
+
return new Promise((resolve14, reject) => {
|
|
14359
14171
|
handle((val) => {
|
|
14360
14172
|
if (val instanceof Error) {
|
|
14361
14173
|
if (val.message === "Unexpected end of multipart data") {
|
|
14362
|
-
|
|
14174
|
+
resolve14(null);
|
|
14363
14175
|
} else {
|
|
14364
14176
|
reject(val);
|
|
14365
14177
|
}
|
|
14366
14178
|
} else {
|
|
14367
|
-
|
|
14179
|
+
resolve14(val);
|
|
14368
14180
|
}
|
|
14369
14181
|
});
|
|
14370
14182
|
});
|
|
@@ -14567,7 +14379,7 @@ var require_multipart2 = __commonJS({
|
|
|
14567
14379
|
for (let i = 0; i < this.tmpUploads.length; ++i) {
|
|
14568
14380
|
const filepath = this.tmpUploads[i];
|
|
14569
14381
|
try {
|
|
14570
|
-
await
|
|
14382
|
+
await unlink3(filepath);
|
|
14571
14383
|
} catch (error) {
|
|
14572
14384
|
this.log.error(error, "Could not delete file");
|
|
14573
14385
|
}
|
|
@@ -14654,41 +14466,6 @@ async function health_default(app2) {
|
|
|
14654
14466
|
|
|
14655
14467
|
// src/routes/projects.ts
|
|
14656
14468
|
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
14469
|
init_db();
|
|
14693
14470
|
init_rename_project();
|
|
14694
14471
|
|
|
@@ -14844,9 +14621,6 @@ async function toProjectView(p, sessions) {
|
|
|
14844
14621
|
session,
|
|
14845
14622
|
activity: session.status === "running" ? `running \xB7 ${session.flow ?? "sesi"}` : "idle",
|
|
14846
14623
|
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
14624
|
// SPEC-253 · Help Center publik aktif.
|
|
14851
14625
|
helpEnabled: p.helpEnabled,
|
|
14852
14626
|
// SPEC-294 · opt-in scheduler otonom (lokal per-instance).
|
|
@@ -14906,25 +14680,6 @@ function paginate(items, page, limit) {
|
|
|
14906
14680
|
return { items: pageSize ? items.slice(start, start + pageSize) : items, total, page: p, pageSize };
|
|
14907
14681
|
}
|
|
14908
14682
|
|
|
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
14683
|
// src/routes/projects.ts
|
|
14929
14684
|
init_src2();
|
|
14930
14685
|
async function projects_default(app2) {
|
|
@@ -14991,8 +14746,6 @@ async function projects_default(app2) {
|
|
|
14991
14746
|
const base = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
14992
14747
|
return {
|
|
14993
14748
|
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
14749
|
helpUrl: p?.helpEnabled ? `${base}/help/${encodeURIComponent(newId)}` : void 0,
|
|
14997
14750
|
affected: r.affected
|
|
14998
14751
|
};
|
|
@@ -15012,30 +14765,6 @@ async function projects_default(app2) {
|
|
|
15012
14765
|
const repoDir = await resolveRepoDir(id);
|
|
15013
14766
|
return { branches: await listRepoBranches(repoDir), remotes: await listRepoRemoteBranches(repoDir) };
|
|
15014
14767
|
});
|
|
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
14768
|
app2.get("/projects/:id/help-center", async (req, reply) => {
|
|
15040
14769
|
const { id } = req.params;
|
|
15041
14770
|
const p = await prisma.project.findUnique({ where: { id } });
|
|
@@ -15060,35 +14789,6 @@ async function projects_default(app2) {
|
|
|
15060
14789
|
await notifySynced("project", id);
|
|
15061
14790
|
return reply.code(204).send();
|
|
15062
14791
|
});
|
|
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
14792
|
}
|
|
15093
14793
|
|
|
15094
14794
|
// src/routes/specs.ts
|
|
@@ -15641,22 +15341,22 @@ async function findAuditDoc(specId, sessions) {
|
|
|
15641
15341
|
return doc ? { dir: dir2, path: doc.path, live } : null;
|
|
15642
15342
|
}
|
|
15643
15343
|
async function readAuditDoc(specId, sessions = listSessions()) {
|
|
15644
|
-
const
|
|
15645
|
-
if (!
|
|
15646
|
-
const content = readDocFile(
|
|
15647
|
-
return content === null ? null : { path:
|
|
15344
|
+
const found = await findAuditDoc(specId, sessions);
|
|
15345
|
+
if (!found) return null;
|
|
15346
|
+
const content = readDocFile(found.dir, found.path);
|
|
15347
|
+
return content === null ? null : { path: found.path, content };
|
|
15648
15348
|
}
|
|
15649
15349
|
async function readEscalation(specId, sessions = listSessions()) {
|
|
15650
|
-
const
|
|
15651
|
-
if (!
|
|
15350
|
+
const found = await findAuditDoc(specId, sessions);
|
|
15351
|
+
if (!found) {
|
|
15652
15352
|
const live = sessions.some((s) => s.specId === specId && !s.exited && s.cwd);
|
|
15653
15353
|
return { escalation: null, docPath: null, live };
|
|
15654
15354
|
}
|
|
15655
|
-
const content = readDocFile(
|
|
15355
|
+
const content = readDocFile(found.dir, found.path);
|
|
15656
15356
|
return {
|
|
15657
15357
|
escalation: content === null ? null : parseEscalation(content),
|
|
15658
|
-
docPath:
|
|
15659
|
-
live:
|
|
15358
|
+
docPath: found.path,
|
|
15359
|
+
live: found.live
|
|
15660
15360
|
};
|
|
15661
15361
|
}
|
|
15662
15362
|
|
|
@@ -15694,8 +15394,8 @@ var escapeReplacements = {
|
|
|
15694
15394
|
"'": "'"
|
|
15695
15395
|
};
|
|
15696
15396
|
var getEscapeReplacement = (ch) => escapeReplacements[ch];
|
|
15697
|
-
function escape$1(html2,
|
|
15698
|
-
if (
|
|
15397
|
+
function escape$1(html2, encode) {
|
|
15398
|
+
if (encode) {
|
|
15699
15399
|
if (escapeTest.test(html2)) {
|
|
15700
15400
|
return html2.replace(escapeReplace, getEscapeReplacement);
|
|
15701
15401
|
}
|
|
@@ -17982,7 +17682,7 @@ function renderDocPdf(text, name2, meta) {
|
|
|
17982
17682
|
});
|
|
17983
17683
|
const chunks = [];
|
|
17984
17684
|
doc.on("data", (c) => chunks.push(c));
|
|
17985
|
-
const done = new Promise((
|
|
17685
|
+
const done = new Promise((resolve14) => doc.on("end", () => resolve14(Buffer.concat(chunks))));
|
|
17986
17686
|
doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
|
|
17987
17687
|
doc.moveDown(0.25);
|
|
17988
17688
|
doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
|
|
@@ -18084,14 +17784,6 @@ async function recordFailure(specId, title, projectId, reason) {
|
|
|
18084
17784
|
}).catch(() => {
|
|
18085
17785
|
});
|
|
18086
17786
|
}
|
|
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
17787
|
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
18096
17788
|
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
18097
17789
|
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
@@ -18210,7 +17902,7 @@ async function specs_default(app2) {
|
|
|
18210
17902
|
source: b.source,
|
|
18211
17903
|
stage: "brainstorming",
|
|
18212
17904
|
priority,
|
|
18213
|
-
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "
|
|
17905
|
+
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
|
|
18214
17906
|
objective,
|
|
18215
17907
|
payload: b.payload,
|
|
18216
17908
|
branchFrom: b.branchFrom ?? null
|
|
@@ -19549,70 +19241,8 @@ async function fs_default(app2) {
|
|
|
19549
19241
|
init_db();
|
|
19550
19242
|
init_src();
|
|
19551
19243
|
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
19244
|
init_session_phases();
|
|
19245
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
19616
19246
|
|
|
19617
19247
|
// src/services/session-launch.ts
|
|
19618
19248
|
init_db();
|
|
@@ -19704,17 +19334,6 @@ async function startSpecSession(spec, opts) {
|
|
|
19704
19334
|
prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope);
|
|
19705
19335
|
}
|
|
19706
19336
|
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
19337
|
const s = createSession(spec.projectId, worktree, {
|
|
19719
19338
|
specId: spec.id,
|
|
19720
19339
|
flow: opts.flow,
|
|
@@ -19725,9 +19344,7 @@ async function startSpecSession(spec, opts) {
|
|
|
19725
19344
|
phaseFile: phaseFilePath(repoDir, id),
|
|
19726
19345
|
decisionFile: decisionFilePath(repoDir, id),
|
|
19727
19346
|
prompt,
|
|
19728
|
-
|
|
19729
|
-
// Digabung SESUDAH `extra` supaya env audit lintas (SPEC-337) tak terhapus dan sebaliknya.
|
|
19730
|
-
env: { ...scopeEnv, ...extra.env ?? {} }
|
|
19347
|
+
env: scopeEnv
|
|
19731
19348
|
});
|
|
19732
19349
|
return resume ? { id: s.id, resumed: true } : { id: s.id };
|
|
19733
19350
|
}
|
|
@@ -19975,32 +19592,6 @@ async function terminal_default(app2) {
|
|
|
19975
19592
|
});
|
|
19976
19593
|
return reply.code(201).send({ id: s2.id });
|
|
19977
19594
|
}
|
|
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
19595
|
const { agent, model, effort } = await sessionAgentDefaults();
|
|
20005
19596
|
if (agent === "codex") ensureCodexTrust(repoDir);
|
|
20006
19597
|
const s = createSession(project.id, repoDir, { agent, model, effort });
|
|
@@ -20252,14 +19843,14 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
20252
19843
|
SSH_ASKPASS_REQUIRE: "force",
|
|
20253
19844
|
HANOMAN_SSH_PASSWORD: opts.password
|
|
20254
19845
|
} : process.env;
|
|
20255
|
-
return new Promise((
|
|
19846
|
+
return new Promise((resolve14) => {
|
|
20256
19847
|
const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
20257
19848
|
let out3 = "";
|
|
20258
19849
|
const timer5 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
20259
19850
|
const done = (r) => {
|
|
20260
19851
|
clearTimeout(timer5);
|
|
20261
19852
|
if (askpass) rmSync4(dirname9(askpass), { recursive: true, force: true });
|
|
20262
|
-
|
|
19853
|
+
resolve14(r);
|
|
20263
19854
|
};
|
|
20264
19855
|
p.stdout.on("data", (d) => {
|
|
20265
19856
|
out3 += d;
|
|
@@ -21269,9 +20860,9 @@ init_db();
|
|
|
21269
20860
|
|
|
21270
20861
|
// src/services/device-token.ts
|
|
21271
20862
|
init_db();
|
|
21272
|
-
import { randomBytes
|
|
21273
|
-
var newDeviceToken = () =>
|
|
21274
|
-
var tokenHash = (token) =>
|
|
20863
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
20864
|
+
var newDeviceToken = () => randomBytes(32).toString("base64url");
|
|
20865
|
+
var tokenHash = (token) => createHash("sha256").update(token).digest("hex");
|
|
21275
20866
|
async function issueDeviceToken(userId, name2) {
|
|
21276
20867
|
const token = newDeviceToken();
|
|
21277
20868
|
const row = await prisma.deviceToken.create({ data: { userId, name: name2, tokenHash: tokenHash(token) } });
|
|
@@ -21828,931 +21419,6 @@ async function config_default(app2) {
|
|
|
21828
21419
|
});
|
|
21829
21420
|
}
|
|
21830
21421
|
|
|
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
21422
|
// src/routes/help.ts
|
|
22757
21423
|
init_zod();
|
|
22758
21424
|
init_src();
|
|
@@ -22760,17 +21426,17 @@ init_db();
|
|
|
22760
21426
|
|
|
22761
21427
|
// src/services/ticket.ts
|
|
22762
21428
|
init_db();
|
|
22763
|
-
import { createHash as
|
|
21429
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
|
|
22764
21430
|
init_src();
|
|
22765
21431
|
function hashAccessKey(key) {
|
|
22766
|
-
return
|
|
21432
|
+
return createHash2("sha256").update(key).digest("hex");
|
|
22767
21433
|
}
|
|
22768
21434
|
function generateAccessKey() {
|
|
22769
|
-
const key = "hnm_tkt_" +
|
|
21435
|
+
const key = "hnm_tkt_" + randomBytes2(24).toString("hex");
|
|
22770
21436
|
return { key, hash: hashAccessKey(key) };
|
|
22771
21437
|
}
|
|
22772
21438
|
function generateShareToken() {
|
|
22773
|
-
return "hnm_shr_" +
|
|
21439
|
+
return "hnm_shr_" + randomBytes2(24).toString("hex");
|
|
22774
21440
|
}
|
|
22775
21441
|
async function createTicket(input) {
|
|
22776
21442
|
const { key, hash: hash2 } = generateAccessKey();
|
|
@@ -22919,7 +21585,7 @@ init_db();
|
|
|
22919
21585
|
|
|
22920
21586
|
// src/services/ticket-accept.ts
|
|
22921
21587
|
init_db();
|
|
22922
|
-
import { join as
|
|
21588
|
+
import { join as join13 } from "node:path";
|
|
22923
21589
|
async function materializeAttachments(atts) {
|
|
22924
21590
|
const ready = /* @__PURE__ */ new Set();
|
|
22925
21591
|
for (const a of atts) {
|
|
@@ -22934,7 +21600,7 @@ async function materializeAttachments(atts) {
|
|
|
22934
21600
|
var attachmentInstruction = (t, atts, ready) => {
|
|
22935
21601
|
if (atts.length === 0) return "Tanpa lampiran.";
|
|
22936
21602
|
const list2 = atts.map(
|
|
22937
|
-
(a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${
|
|
21603
|
+
(a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${join13(uploadDir(), a.storageKey)}` : `- ${a.filename} (${a.mimeType}) \u2192 BELUM TERUNDUH, ambil via GET /api/tickets/${t.id}/attachments/${a.id}`
|
|
22938
21604
|
).join("\n");
|
|
22939
21605
|
return `LAMPIRAN (${atts.length}) dari pelapor \u2014 biasanya screenshot yang menunjukkan masalah. PERIKSA setiap lampiran untuk memahami konteks keluhan sebelum bekerja; jangan berasumsi dari teks saja. Berkas ada di direktori upload server (baca langsung dengan tool Read):
|
|
22940
21606
|
${list2}
|
|
@@ -23132,6 +21798,7 @@ async function setScheduler(next) {
|
|
|
23132
21798
|
// src/services/scheduler/queue.ts
|
|
23133
21799
|
init_db();
|
|
23134
21800
|
var RANK = { tinggi: 0, sedang: 1, rendah: 2 };
|
|
21801
|
+
var UNSTARTED_SPEC_WHERE = { baseSha: null, stage: { not: "done" } };
|
|
23135
21802
|
async function enqueue(i) {
|
|
23136
21803
|
await prisma.schedulerQueueItem.upsert({
|
|
23137
21804
|
where: { specId: i.specId },
|
|
@@ -23152,8 +21819,11 @@ async function markLaunched(id, sessionId2) {
|
|
|
23152
21819
|
async function markFailed(id, note) {
|
|
23153
21820
|
await prisma.schedulerQueueItem.update({ where: { id }, data: { status: "failed", note: note ?? null } });
|
|
23154
21821
|
}
|
|
23155
|
-
async function markDone(id) {
|
|
23156
|
-
await prisma.schedulerQueueItem.update({
|
|
21822
|
+
async function markDone(id, note) {
|
|
21823
|
+
await prisma.schedulerQueueItem.update({
|
|
21824
|
+
where: { id },
|
|
21825
|
+
data: { status: "done", ...note ? { note } : {} }
|
|
21826
|
+
});
|
|
23157
21827
|
}
|
|
23158
21828
|
|
|
23159
21829
|
// src/services/scheduler/registry.ts
|
|
@@ -23202,7 +21872,6 @@ async function scheduler_default(app2) {
|
|
|
23202
21872
|
};
|
|
23203
21873
|
const sources2 = [
|
|
23204
21874
|
srcView("backlog", cfg.sources.backlog),
|
|
23205
|
-
srcView("errors", cfg.sources.errors, cfg.sources.errors.minCount),
|
|
23206
21875
|
srcView("triase", cfg.sources.triase)
|
|
23207
21876
|
];
|
|
23208
21877
|
const launchedSpecs = new Set(queue.filter((q) => q.status === "launched").map((q) => q.specId));
|
|
@@ -23211,114 +21880,6 @@ async function scheduler_default(app2) {
|
|
|
23211
21880
|
});
|
|
23212
21881
|
}
|
|
23213
21882
|
|
|
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
21883
|
// src/routes/lead.ts
|
|
23323
21884
|
init_src();
|
|
23324
21885
|
init_db();
|
|
@@ -23392,8 +21953,12 @@ function leadPrompt(q, c) {
|
|
|
23392
21953
|
for (const [i, o] of q.options.entries()) lines2.push(`${i + 1}. ${o}`);
|
|
23393
21954
|
}
|
|
23394
21955
|
lines2.push("");
|
|
21956
|
+
lines2.push("## Batas waktu (BACA INI DULU)");
|
|
21957
|
+
lines2.push(`Kamu punya **${c.timeoutSec} detik** sejak sekarang. Lewat dari itu prosesmu dihentikan paksa, keluaranmu DIBUANG, dan permintaan ini dicatat sebagai kegagalan \u2014 peminta kembali mandek menunggu manusia. Jadi keputusan tepat waktu di atas bukti secukupnya jauh lebih berguna daripada pembacaan lengkap yang tak pernah sampai.`);
|
|
21958
|
+
lines2.push(`Anggarkan begini: pakai paling banyak separuh waktu untuk mengumpulkan bukti, lalu **berhenti membaca** dan tulis jawabannya dengan apa yang sudah kamu punya. Kalau buktinya jadi tipis karena itu, turunkan \`confidence\`-nya \u2014 jangan menambah waktu baca.`);
|
|
21959
|
+
lines2.push("");
|
|
23395
21960
|
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.");
|
|
21961
|
+
lines2.push("1. KUMPULKAN BUKTI DULU sebelum memutuskan, DI DALAM anggaran waktu di atas: `internal/docs/**` (Source of Truth) dan index-nya, ADR yang relevan, plan `docs/superpowers/plans/**`, kode yang bersangkutan, dan riwayat git. Baca, jangan mengingat \u2014 tapi baca seperlunya, bukan sehabisnya.");
|
|
23397
21962
|
lines2.push('2. Putuskan. Kalau setelah membaca kamu masih ragu, TETAP putuskan: pilih opsi yang PALING MUDAH DIBATALKAN, lalu tandai `confidence: "ragu"`. Jangan pernah menjawab "tidak tahu" atau meminta manusia memutuskan \u2014 itu persis keadaan yang kamu ada untuk menghapusnya.');
|
|
23398
21963
|
lines2.push("3. Rujuk bukti yang BENAR-BENAR kamu baca. Rujukan berupa path berkas relatif terhadap checkout, nomor ADR (`ADR-0091`), atau sha commit. Rujukan yang tak ada di repo akan dibuang server dan membuat jawabanmu tampak tanpa dasar.");
|
|
23399
21964
|
lines2.push("4. JANGAN membaca atau mengutip kredensial (isi `.env*`, token, kunci privat). Jejak keputusan disimpan di basis data; rahasia tak boleh mendarat di sana.");
|
|
@@ -23571,7 +22136,7 @@ function leadArgv(o) {
|
|
|
23571
22136
|
function think(prompt, o) {
|
|
23572
22137
|
const bin = binFor(o.agent);
|
|
23573
22138
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
23574
|
-
return new Promise((
|
|
22139
|
+
return new Promise((resolve14, reject) => {
|
|
23575
22140
|
execFile9(bin, args, {
|
|
23576
22141
|
cwd: o.cwd,
|
|
23577
22142
|
timeout: o.timeoutMs,
|
|
@@ -23584,7 +22149,7 @@ function think(prompt, o) {
|
|
|
23584
22149
|
reject(new Error(killed ? `lead ${o.agent} kehabisan waktu ${o.timeoutMs} ms` : `lead ${o.agent} gagal: ${(stderr || err.message).trim().slice(0, 500)}`));
|
|
23585
22150
|
return;
|
|
23586
22151
|
}
|
|
23587
|
-
|
|
22152
|
+
resolve14(stdout);
|
|
23588
22153
|
});
|
|
23589
22154
|
});
|
|
23590
22155
|
}
|
|
@@ -23627,6 +22192,10 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
23627
22192
|
projectId: req.projectId,
|
|
23628
22193
|
projectName: project?.name ?? req.projectId,
|
|
23629
22194
|
repoDir,
|
|
22195
|
+
// SPEC-432 · anggaran yang disebut prompt WAJIB berasal dari cfg yang sama yang dipakai
|
|
22196
|
+
// `think()` di bawah. Dua sumber angka akan berselisih diam-diam begitu operator menggeser
|
|
22197
|
+
// knob-nya, dan agen yang dianggarkan salah gagal persis seperti agen yang tak dianggarkan.
|
|
22198
|
+
timeoutSec: cfg.timeoutSec,
|
|
23630
22199
|
spec,
|
|
23631
22200
|
liveSessions: deps.liveSessions().filter((s) => !s.exited && s.projectId === req.projectId && s.id !== req.sessionId).map((s) => ({ id: s.id, specId: s.specId, flow: s.flow, branch: s.branch })),
|
|
23632
22201
|
priorDecisions: prior.map((d) => ({
|
|
@@ -23691,6 +22260,11 @@ function takeReply(decisionId) {
|
|
|
23691
22260
|
return v;
|
|
23692
22261
|
}
|
|
23693
22262
|
async function fail(req, deps, reason) {
|
|
22263
|
+
const prev = await prisma.leadDecision.findFirst({
|
|
22264
|
+
where: { projectId: req.projectId, gate: req.gate, kind: req.kind },
|
|
22265
|
+
orderBy: { createdAt: "desc" },
|
|
22266
|
+
select: { status: true }
|
|
22267
|
+
});
|
|
23694
22268
|
const row = await recordDecision({
|
|
23695
22269
|
projectId: req.projectId,
|
|
23696
22270
|
specId: req.specId,
|
|
@@ -23706,13 +22280,15 @@ async function fail(req, deps, reason) {
|
|
|
23706
22280
|
status: "gagal",
|
|
23707
22281
|
weighty: true
|
|
23708
22282
|
});
|
|
23709
|
-
|
|
23710
|
-
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23715
|
-
|
|
22283
|
+
if (prev?.status !== "gagal") {
|
|
22284
|
+
await deps.notify(
|
|
22285
|
+
row.id,
|
|
22286
|
+
`Lead gagal memutuskan: ${req.question.replace(/\s+/g, " ").slice(0, 90)}`,
|
|
22287
|
+
req.projectId,
|
|
22288
|
+
req.specId ?? null,
|
|
22289
|
+
req.sessionId ?? null
|
|
22290
|
+
);
|
|
22291
|
+
}
|
|
23716
22292
|
return row;
|
|
23717
22293
|
}
|
|
23718
22294
|
|
|
@@ -24048,7 +22624,8 @@ var prodPulseDeps = {
|
|
|
24048
22624
|
enqueue,
|
|
24049
22625
|
notify: recordLeadDecision,
|
|
24050
22626
|
optIn: leadProjects,
|
|
24051
|
-
cfg: getLead
|
|
22627
|
+
cfg: getLead,
|
|
22628
|
+
scheduler: getScheduler
|
|
24052
22629
|
};
|
|
24053
22630
|
var lastReadySig = /* @__PURE__ */ new Map();
|
|
24054
22631
|
async function pulse(deps = prodPulseDeps) {
|
|
@@ -24079,7 +22656,10 @@ async function followUpFinished(cfg, optIn, deps) {
|
|
|
24079
22656
|
const bad = (s.exitCode ?? 0) !== 0;
|
|
24080
22657
|
const unfinished = !deps.planDone(s.cwd, s.specId);
|
|
24081
22658
|
if (!bad && !unfinished) continue;
|
|
24082
|
-
const
|
|
22659
|
+
const mark = `Sesi ${s.id} untuk backlog ${s.specId}`;
|
|
22660
|
+
const seen = await prisma.leadDecision.findFirst({
|
|
22661
|
+
where: { sessionId: s.id, gate: "pulse", question: { startsWith: mark } }
|
|
22662
|
+
});
|
|
24083
22663
|
if (seen) continue;
|
|
24084
22664
|
const why = [
|
|
24085
22665
|
bad ? `berakhir dengan kode keluar ${s.exitCode}` : null,
|
|
@@ -24091,7 +22671,7 @@ async function followUpFinished(cfg, optIn, deps) {
|
|
|
24091
22671
|
sessionId: s.id,
|
|
24092
22672
|
gate: "pulse",
|
|
24093
22673
|
kind: "quality",
|
|
24094
|
-
question:
|
|
22674
|
+
question: `${mark} ${why}. Tindak lanjutnya apa: lanjutkan pekerjaan yang terputus, ulangi dari awal, atau hentikan?`,
|
|
24095
22675
|
options: [
|
|
24096
22676
|
"resume-session \u2014 lanjutkan dari keadaan worktree sekarang (ADR-0084)",
|
|
24097
22677
|
"restart-session \u2014 ulangi dari awal",
|
|
@@ -24123,7 +22703,7 @@ async function detectCollisions(optIn, deps) {
|
|
|
24123
22703
|
for (const c of findCollisions(areas)) {
|
|
24124
22704
|
const key = [c.a.sessionId, c.b.sessionId].sort().join("|");
|
|
24125
22705
|
const seen = await prisma.leadDecision.findFirst({
|
|
24126
|
-
where: {
|
|
22706
|
+
where: { gate: "pulse", question: { contains: key } }
|
|
24127
22707
|
});
|
|
24128
22708
|
if (seen) continue;
|
|
24129
22709
|
const row = await deps.decide({
|
|
@@ -24147,36 +22727,46 @@ async function detectCollisions(optIn, deps) {
|
|
|
24147
22727
|
return n;
|
|
24148
22728
|
}
|
|
24149
22729
|
async function orderReadyWork(optIn, deps) {
|
|
22730
|
+
const sched = await deps.scheduler();
|
|
22731
|
+
if (!sched.enabled || sched.paused) return 0;
|
|
24150
22732
|
let total = 0;
|
|
24151
22733
|
for (const projectId of optIn) total += await orderProject(projectId, deps);
|
|
24152
22734
|
return total;
|
|
24153
22735
|
}
|
|
24154
22736
|
async function orderProject(projectId, deps) {
|
|
22737
|
+
const project = await prisma.project.findUnique({ where: { id: projectId }, select: { schedulerOptIn: true } });
|
|
22738
|
+
if (!project?.schedulerOptIn) return 0;
|
|
24155
22739
|
const ready = await prisma.spec.findMany({
|
|
24156
|
-
where: {
|
|
22740
|
+
where: { ...UNSTARTED_SPEC_WHERE, projectId },
|
|
24157
22741
|
select: { id: true, projectId: true, title: true, priority: true, objective: true },
|
|
24158
22742
|
orderBy: { id: "asc" }
|
|
24159
22743
|
});
|
|
24160
22744
|
if (ready.length < 2) return 0;
|
|
24161
|
-
const
|
|
22745
|
+
const already = new Set((await prisma.schedulerQueueItem.findMany({
|
|
22746
|
+
where: { specId: { in: ready.map((r) => r.id) } },
|
|
22747
|
+
select: { specId: true }
|
|
22748
|
+
})).map((q) => q.specId));
|
|
22749
|
+
const pending = ready.filter((r) => !already.has(r.id));
|
|
22750
|
+
if (pending.length < 2) return 0;
|
|
22751
|
+
const sig = pending.map((r) => r.id).join(",");
|
|
24162
22752
|
if (sig === lastReadySig.get(projectId)) return 0;
|
|
24163
22753
|
lastReadySig.set(projectId, sig);
|
|
24164
22754
|
const row = await deps.decide({
|
|
24165
22755
|
projectId,
|
|
24166
22756
|
gate: "pulse",
|
|
24167
22757
|
kind: "order",
|
|
24168
|
-
question: `Ada ${
|
|
24169
|
-
options:
|
|
24170
|
-
notes:
|
|
22758
|
+
question: `Ada ${pending.length} backlog siap dikerjakan. Urutkan mana yang lebih dulu berdasarkan isi pekerjaannya, lalu tuliskan urutan id-nya (dipisah koma) di \`decision\`.`,
|
|
22759
|
+
options: pending.map((r) => `${r.id} \xB7 [${r.priority}] ${r.title}`),
|
|
22760
|
+
notes: pending.map((r) => `${r.id}: ${r.objective.slice(0, 200)}`)
|
|
24171
22761
|
}, deps.decideDeps);
|
|
24172
22762
|
if (!row || row.status !== "berlaku") return 0;
|
|
24173
|
-
const byId2 = new Map(
|
|
22763
|
+
const byId2 = new Map(pending.map((r) => [r.id.toLowerCase(), r]));
|
|
24174
22764
|
const named = [];
|
|
24175
22765
|
for (const tok of row.answer.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean)) {
|
|
24176
22766
|
const hit = byId2.get(tok);
|
|
24177
22767
|
if (hit && !named.includes(hit)) named.push(hit);
|
|
24178
22768
|
}
|
|
24179
|
-
const ordered = [...named, ...
|
|
22769
|
+
const ordered = [...named, ...pending.filter((r) => !named.includes(r))];
|
|
24180
22770
|
for (const r of ordered) {
|
|
24181
22771
|
await deps.enqueue({ specId: r.id, projectId: r.projectId, source: "lead", priority: r.priority });
|
|
24182
22772
|
}
|
|
@@ -24186,36 +22776,46 @@ async function orderProject(projectId, deps) {
|
|
|
24186
22776
|
|
|
24187
22777
|
// src/services/lead/engine.ts
|
|
24188
22778
|
var TICK_MS = 5e3;
|
|
22779
|
+
var busyDetect = false;
|
|
22780
|
+
var busyPulse = false;
|
|
24189
22781
|
var lastPulseAt = 0;
|
|
24190
|
-
var
|
|
22782
|
+
var pulseEndedAt = 0;
|
|
24191
22783
|
var timer3;
|
|
24192
22784
|
function lastPulse() {
|
|
24193
22785
|
return lastPulseAt;
|
|
24194
22786
|
}
|
|
24195
22787
|
async function tick2(now, deps = {}) {
|
|
24196
|
-
|
|
24197
|
-
busy2 = true;
|
|
22788
|
+
let cfg;
|
|
24198
22789
|
try {
|
|
24199
|
-
|
|
24200
|
-
|
|
24201
|
-
|
|
24202
|
-
|
|
24203
|
-
|
|
22790
|
+
cfg = await getLead();
|
|
22791
|
+
} catch (e) {
|
|
22792
|
+
console.error("lead tick:", e);
|
|
22793
|
+
return;
|
|
22794
|
+
}
|
|
22795
|
+
if (!cfg.enabled) return;
|
|
22796
|
+
const jobs = [];
|
|
22797
|
+
if (!busyDetect) {
|
|
22798
|
+
busyDetect = true;
|
|
22799
|
+
jobs.push(scanAndAnswer(deps.detect ?? prodDetectDeps).then(() => {
|
|
22800
|
+
}).catch((e) => {
|
|
24204
22801
|
console.error("lead detect:", e);
|
|
24205
|
-
}
|
|
24206
|
-
|
|
24207
|
-
|
|
22802
|
+
}).finally(() => {
|
|
22803
|
+
busyDetect = false;
|
|
22804
|
+
}));
|
|
22805
|
+
}
|
|
22806
|
+
if (!cfg.paused && !busyPulse && now - Math.max(lastPulseAt, pulseEndedAt) >= cfg.everyMin * 6e4) {
|
|
22807
|
+
busyPulse = true;
|
|
24208
22808
|
lastPulseAt = now;
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
}
|
|
22809
|
+
const clock = deps.now ?? Date.now;
|
|
22810
|
+
jobs.push(pulse(deps.pulse ?? prodPulseDeps).then(() => {
|
|
22811
|
+
}).catch((e) => {
|
|
24212
22812
|
console.error("lead pulse:", e);
|
|
24213
|
-
}
|
|
24214
|
-
|
|
24215
|
-
|
|
24216
|
-
|
|
24217
|
-
busy2 = false;
|
|
22813
|
+
}).finally(() => {
|
|
22814
|
+
pulseEndedAt = clock();
|
|
22815
|
+
busyPulse = false;
|
|
22816
|
+
}));
|
|
24218
22817
|
}
|
|
22818
|
+
await Promise.all(jobs);
|
|
24219
22819
|
}
|
|
24220
22820
|
function startLead(deps = {}) {
|
|
24221
22821
|
if (timer3) return;
|
|
@@ -24360,13 +22960,13 @@ init_db();
|
|
|
24360
22960
|
|
|
24361
22961
|
// src/services/auth.ts
|
|
24362
22962
|
init_db();
|
|
24363
|
-
import { randomBytes as
|
|
22963
|
+
import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual, createHash as createHash3 } from "node:crypto";
|
|
24364
22964
|
import { promisify as promisify9 } from "node:util";
|
|
24365
22965
|
var scrypt = promisify9(scryptCb);
|
|
24366
22966
|
var COOKIE_NAME = "hn_session";
|
|
24367
22967
|
var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
24368
22968
|
async function hashPassword(pw) {
|
|
24369
|
-
const salt =
|
|
22969
|
+
const salt = randomBytes3(16);
|
|
24370
22970
|
const key = await scrypt(pw, salt, 64);
|
|
24371
22971
|
return `${salt.toString("hex")}:${key.toString("hex")}`;
|
|
24372
22972
|
}
|
|
@@ -24375,10 +22975,10 @@ async function verifyPassword(pw, stored) {
|
|
|
24375
22975
|
if (!saltHex || !hashHex) return false;
|
|
24376
22976
|
const key = await scrypt(pw, Buffer.from(saltHex, "hex"), 64);
|
|
24377
22977
|
const want = Buffer.from(hashHex, "hex");
|
|
24378
|
-
return key.length === want.length &&
|
|
22978
|
+
return key.length === want.length && timingSafeEqual(key, want);
|
|
24379
22979
|
}
|
|
24380
|
-
var newSessionToken = () =>
|
|
24381
|
-
var sessionId = (token) =>
|
|
22980
|
+
var newSessionToken = () => randomBytes3(32).toString("base64url");
|
|
22981
|
+
var sessionId = (token) => createHash3("sha256").update(token).digest("hex");
|
|
24382
22982
|
async function createSession2(userId) {
|
|
24383
22983
|
const token = newSessionToken();
|
|
24384
22984
|
await prisma.session.create({
|
|
@@ -24509,8 +23109,8 @@ init_src();
|
|
|
24509
23109
|
|
|
24510
23110
|
// src/services/agent-token.ts
|
|
24511
23111
|
init_db();
|
|
24512
|
-
import { randomBytes as
|
|
24513
|
-
var hash = (token) =>
|
|
23112
|
+
import { randomBytes as randomBytes4, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
23113
|
+
var hash = (token) => createHash4("sha256").update(token).digest("hex");
|
|
24514
23114
|
function toAgentTokenView(t) {
|
|
24515
23115
|
return {
|
|
24516
23116
|
id: t.id,
|
|
@@ -24525,7 +23125,7 @@ function toAgentTokenView(t) {
|
|
|
24525
23125
|
};
|
|
24526
23126
|
}
|
|
24527
23127
|
async function issueAgentToken(input) {
|
|
24528
|
-
const token = "hnm_agt_" +
|
|
23128
|
+
const token = "hnm_agt_" + randomBytes4(24).toString("hex");
|
|
24529
23129
|
const row = await prisma.agentToken.create({
|
|
24530
23130
|
data: {
|
|
24531
23131
|
name: input.name,
|
|
@@ -24543,7 +23143,7 @@ async function verifyAgentToken(token) {
|
|
|
24543
23143
|
if (!row || !row.enabled || row.revokedAt) return null;
|
|
24544
23144
|
const a = Buffer.from(hash(token), "hex");
|
|
24545
23145
|
const b = Buffer.from(row.tokenHash, "hex");
|
|
24546
|
-
if (a.length !== b.length || !
|
|
23146
|
+
if (a.length !== b.length || !timingSafeEqual2(a, b)) return null;
|
|
24547
23147
|
await prisma.agentToken.update({ where: { id: row.id }, data: { lastUsedAt: /* @__PURE__ */ new Date() } }).catch(() => {
|
|
24548
23148
|
});
|
|
24549
23149
|
return { id: row.id, capabilities: Array.isArray(row.capabilities) ? row.capabilities : [] };
|
|
@@ -24638,7 +23238,7 @@ function capabilityForRoute(method, path) {
|
|
|
24638
23238
|
if (top === "settings" || top === "config") return rw("settings");
|
|
24639
23239
|
if (top === "specs") return rw("backlog");
|
|
24640
23240
|
if (top === "notifications") return rw("notifications");
|
|
24641
|
-
if (top === "
|
|
23241
|
+
if (top === "tickets") return rw("support");
|
|
24642
23242
|
if (top === "vps") return rw("vps");
|
|
24643
23243
|
if (top === "prds") return rw("docs");
|
|
24644
23244
|
if (top === "terminal") {
|
|
@@ -24698,9 +23298,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24698
23298
|
const path = req.url.split("?")[0] ?? req.url;
|
|
24699
23299
|
if (PUBLIC.has(`${req.method} ${path}`)) return;
|
|
24700
23300
|
if (path.startsWith("/api/sync") && path !== "/api/sync/now" && !path.startsWith("/api/sync/conflicts")) return;
|
|
24701
|
-
if (path.startsWith("/api/ingest")) return;
|
|
24702
23301
|
if (path.startsWith("/api/help")) return;
|
|
24703
|
-
if (path.startsWith("/api/audit/") && auditScopeFromReq(req)) return;
|
|
24704
23302
|
if (user) return;
|
|
24705
23303
|
const agentTok = agentTokenFromReq(req);
|
|
24706
23304
|
if (agentTok) {
|
|
@@ -24738,12 +23336,9 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24738
23336
|
await api.register(session_results_default);
|
|
24739
23337
|
await api.register(session_history_default);
|
|
24740
23338
|
await api.register(config_default);
|
|
24741
|
-
await api.register(ingest_default);
|
|
24742
|
-
await api.register(errors_default);
|
|
24743
23339
|
await api.register(help_default);
|
|
24744
23340
|
await api.register(tickets_default);
|
|
24745
23341
|
await api.register(scheduler_default);
|
|
24746
|
-
await api.register(audit_default);
|
|
24747
23342
|
await api.register(codex);
|
|
24748
23343
|
await api.register(lead_default);
|
|
24749
23344
|
}, { prefix: "/api" });
|
|
@@ -24798,6 +23393,7 @@ init_db();
|
|
|
24798
23393
|
init_src();
|
|
24799
23394
|
|
|
24800
23395
|
// src/services/scheduler/governor.ts
|
|
23396
|
+
var ALREADY_DONE_NOTE = "spec sudah selesai \u2014 tak diluncurkan";
|
|
24801
23397
|
var draining = false;
|
|
24802
23398
|
async function drain(cfg, deps) {
|
|
24803
23399
|
if (draining) return;
|
|
@@ -24807,6 +23403,10 @@ async function drain(cfg, deps) {
|
|
|
24807
23403
|
if (slots <= 0) return;
|
|
24808
23404
|
for (const item of await queued()) {
|
|
24809
23405
|
if (slots <= 0) break;
|
|
23406
|
+
if (await deps.isDone(item.specId)) {
|
|
23407
|
+
await markDone(item.id, ALREADY_DONE_NOTE);
|
|
23408
|
+
continue;
|
|
23409
|
+
}
|
|
24810
23410
|
const liveId = deps.isLive(item.specId);
|
|
24811
23411
|
if (liveId) {
|
|
24812
23412
|
await markLaunched(item.id, liveId);
|
|
@@ -24929,6 +23529,13 @@ var prodDeps = {
|
|
|
24929
23529
|
const s = getSession(sessionIdForSpec(specId));
|
|
24930
23530
|
return s && !s.exited ? s.id : null;
|
|
24931
23531
|
},
|
|
23532
|
+
// SPEC-431 · dibaca ULANG dari DB tepat sebelum launch, bukan dari baris antrean: antrean tak
|
|
23533
|
+
// menyimpan stage, dan item bisa selesai selagi mengantre. Spec yang hilang bukan urusan gerbang
|
|
23534
|
+
// ini — `launch` di bawah yang melempar "spec tak ada" → item ditandai failed dengan alasannya.
|
|
23535
|
+
isDone: async (specId) => {
|
|
23536
|
+
const s = await prisma.spec.findUnique({ where: { id: specId }, select: { stage: true } });
|
|
23537
|
+
return s?.stage === "done";
|
|
23538
|
+
},
|
|
24932
23539
|
launch: async (item, autonomy) => {
|
|
24933
23540
|
const spec = await prisma.spec.findUnique({ where: { id: item.specId } });
|
|
24934
23541
|
if (!spec) throw new Error(`spec ${item.specId} tak ada`);
|
|
@@ -24950,7 +23557,7 @@ init_db();
|
|
|
24950
23557
|
var RANK2 = { tinggi: 0, sedang: 1, rendah: 2 };
|
|
24951
23558
|
async function checkBacklog() {
|
|
24952
23559
|
const specs = await prisma.spec.findMany({
|
|
24953
|
-
where: {
|
|
23560
|
+
where: { ...UNSTARTED_SPEC_WHERE, project: { schedulerOptIn: true } }
|
|
24954
23561
|
});
|
|
24955
23562
|
specs.sort((a, b) => (RANK2[a.priority] ?? 1) - (RANK2[b.priority] ?? 1) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
24956
23563
|
for (const s of specs) {
|
|
@@ -24961,36 +23568,6 @@ function registerBacklogSource() {
|
|
|
24961
23568
|
registerSchedulerSource({ id: "backlog", check: checkBacklog });
|
|
24962
23569
|
}
|
|
24963
23570
|
|
|
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
23571
|
// src/services/scheduler/sources/triase.ts
|
|
24995
23572
|
init_db();
|
|
24996
23573
|
async function checkTriase() {
|
|
@@ -25050,7 +23627,6 @@ app.listen({ port, host }).then(() => {
|
|
|
25050
23627
|
}
|
|
25051
23628
|
startVpsMonitor();
|
|
25052
23629
|
registerBacklogSource();
|
|
25053
|
-
registerErrorsSource();
|
|
25054
23630
|
registerTriaseSource();
|
|
25055
23631
|
startScheduler();
|
|
25056
23632
|
startLead();
|