hanoman 0.1.6 → 0.1.8

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