hanoman 0.1.5 → 0.1.7

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