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/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, zErrorStatus, zTicketCategory, zTicketStatus, zLinkKind, VERIFY_SCOPES, zVerifyScope;
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", "cross-audit", "help", "goal"]);
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
- timeoutSec: external_exports.number().int().min(10).max(900).default(120),
4333
- // batas waktu satu putusan (AC-4/35)
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-249 · +error; SPEC-253 · +ticket; SPEC-298 · +fail (sesi scheduler gagal/limit)
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
- type: external_exports.enum(["done", "decision", "error", "ticket", "fail", "lead"]).default("done"),
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, DSN, Help Center." },
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: "Errors & Tiket \u2014 baca", desc: "Lihat error monitoring & tiket Help Center." },
4466
- { id: "support:write", domain: "support", access: "write", label: "Errors & Tiket \u2014 tulis", desc: "Eskalasi error, ubah status, terima/tolak tiket." },
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 === "cross-audit" ? "cross-audit" : source === "goal" ? "goal" : "feature";
4583
+ return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
4584
4584
  }
4585
- var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zCreateLink, 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, zStackFrame, zSymbolicatedFrame, zSourceMapUpload, zIngestPayload, zErrorGroupView, zErrorEventView, zErrorGroupDetail, zIngestKeyView, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
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", "cross-audit", "goal"]);
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
- function startCrossAuditPrompt(ctx, mode) {
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 json = JSON.stringify(phases);
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
- const phases = sessionPhases(id);
6955
- if (phases) {
6956
- a.lastPhases = JSON.stringify(phases);
6957
- c.send(frame({ t: "phase", phases }));
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, errorGroup, ticket, notification, sessionResult, errorEvent, ticketAttachment, localBinding };
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", "errorGroup", "ticket", "ticketAttachment"];
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 parse3(header) {
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: parse3 };
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: randomBytes7, createHash: createHash7 } = __require("crypto");
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: parse3 } = require_extension();
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 = randomBytes7(16).toString("base64");
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 = createHash7("sha1").update(key + GUID).digest("base64");
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 = parse3(secWebSocketExtensions);
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 parse3(header) {
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: parse3 };
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: createHash7 } = __require("crypto");
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 = createHash7("sha1").update(key + GUID).digest("base64");
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((resolve15, reject) => {
13709
+ return new Promise((resolve14, reject) => {
13898
13710
  if (typeof stream.resume !== "function") {
13899
- return resolve15();
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 resolve15();
13716
+ return resolve14();
13905
13717
  }
13906
13718
  if (!stream.readable || stream.destroyed) {
13907
- return resolve15();
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
- resolve15();
13728
+ resolve14();
13917
13729
  }
13918
13730
  function onError(err) {
13919
13731
  cleanup();
13920
13732
  if (throwError) {
13921
13733
  reject(err);
13922
13734
  } else {
13923
- resolve15();
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 parse3(text, reviver, options2) {
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 = parse3;
14160
- module.exports.default = parse3;
14161
- module.exports.parse = parse3;
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: unlink4 } = __require("node:fs/promises");
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((resolve15, reject) => {
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
- resolve15(null);
14174
+ resolve14(null);
14363
14175
  } else {
14364
14176
  reject(val);
14365
14177
  }
14366
14178
  } else {
14367
- resolve15(val);
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 unlink4(filepath);
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 found2 = await findAuditDoc(specId, sessions);
15645
- if (!found2) return null;
15646
- const content = readDocFile(found2.dir, found2.path);
15647
- return content === null ? null : { path: found2.path, content };
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 found2 = await findAuditDoc(specId, sessions);
15651
- if (!found2) {
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(found2.dir, found2.path);
15355
+ const content = readDocFile(found.dir, found.path);
15656
15356
  return {
15657
15357
  escalation: content === null ? null : parseEscalation(content),
15658
- docPath: found2.path,
15659
- live: found2.live
15358
+ docPath: found.path,
15359
+ live: found.live
15660
15360
  };
15661
15361
  }
15662
15362
 
@@ -15694,8 +15394,8 @@ var escapeReplacements = {
15694
15394
  "'": "&#39;"
15695
15395
  };
15696
15396
  var getEscapeReplacement = (ch) => escapeReplacements[ch];
15697
- function escape$1(html2, encode2) {
15698
- if (encode2) {
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((resolve15) => doc.on("end", () => resolve15(Buffer.concat(chunks))));
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 === "cross-audit" ? `Audit lintas \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
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
- ...extra,
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((resolve15) => {
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
- resolve15(r);
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 as randomBytes3, createHash as createHash2 } from "node:crypto";
21273
- var newDeviceToken = () => randomBytes3(32).toString("base64url");
21274
- var tokenHash = (token) => createHash2("sha256").update(token).digest("hex");
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 createHash4, randomBytes as randomBytes4 } from "node:crypto";
21429
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
22764
21430
  init_src();
22765
21431
  function hashAccessKey(key) {
22766
- return createHash4("sha256").update(key).digest("hex");
21432
+ return createHash2("sha256").update(key).digest("hex");
22767
21433
  }
22768
21434
  function generateAccessKey() {
22769
- const key = "hnm_tkt_" + randomBytes4(24).toString("hex");
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_" + randomBytes4(24).toString("hex");
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 join15 } from "node:path";
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 ${join15(uploadDir(), a.storageKey)}` : `- ${a.filename} (${a.mimeType}) \u2192 BELUM TERUNDUH, ambil via GET /api/tickets/${t.id}/attachments/${a.id}`
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({ where: { id }, data: { status: "done" } });
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((resolve15, reject) => {
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
- resolve15(stdout);
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
- await deps.notify(
23710
- row.id,
23711
- `Lead gagal memutuskan: ${req.question.replace(/\s+/g, " ").slice(0, 90)}`,
23712
- req.projectId,
23713
- req.specId ?? null,
23714
- req.sessionId ?? null
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 seen = await prisma.leadDecision.findFirst({ where: { sessionId: s.id, kind: "quality", gate: "pulse" } });
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: `Sesi ${s.id} untuk backlog ${s.specId} ${why}. Tindak lanjutnya apa: lanjutkan pekerjaan yang terputus, ulangi dari awal, atau hentikan?`,
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: { kind: "collision", gate: "pulse", question: { contains: key } }
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: { baseSha: null, projectId },
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 sig = ready.map((r) => r.id).join(",");
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 ${ready.length} backlog siap dikerjakan. Urutkan mana yang lebih dulu berdasarkan isi pekerjaannya, lalu tuliskan urutan id-nya (dipisah koma) di \`decision\`.`,
24169
- options: ready.map((r) => `${r.id} \xB7 [${r.priority}] ${r.title}`),
24170
- notes: ready.map((r) => `${r.id}: ${r.objective.slice(0, 200)}`)
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(ready.map((r) => [r.id.toLowerCase(), r]));
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, ...ready.filter((r) => !named.includes(r))];
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 busy2 = false;
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
- if (busy2) return;
24197
- busy2 = true;
22788
+ let cfg;
24198
22789
  try {
24199
- const cfg = await getLead();
24200
- if (!cfg.enabled) return;
24201
- try {
24202
- await scanAndAnswer(deps.detect ?? prodDetectDeps);
24203
- } catch (e) {
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
- if (cfg.paused) return;
24207
- if (now - lastPulseAt < cfg.everyMin * 6e4) return;
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
- try {
24210
- await pulse(deps.pulse ?? prodPulseDeps);
24211
- } catch (e) {
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
- } catch (e) {
24215
- console.error("lead tick:", e);
24216
- } finally {
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 randomBytes5, scrypt as scryptCb, timingSafeEqual as timingSafeEqual2, createHash as createHash5 } from "node:crypto";
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 = randomBytes5(16);
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 && timingSafeEqual2(key, want);
22978
+ return key.length === want.length && timingSafeEqual(key, want);
24379
22979
  }
24380
- var newSessionToken = () => randomBytes5(32).toString("base64url");
24381
- var sessionId = (token) => createHash5("sha256").update(token).digest("hex");
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 randomBytes6, createHash as createHash6, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
24513
- var hash = (token) => createHash6("sha256").update(token).digest("hex");
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_" + randomBytes6(24).toString("hex");
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 || !timingSafeEqual3(a, b)) return null;
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 === "errors" || top === "tickets") return rw("support");
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: { baseSha: null, project: { schedulerOptIn: true } }
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();