hanoman 0.1.4 → 0.1.6

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
@@ -4151,7 +4151,7 @@ var init_enums = __esm({
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"]);
4154
+ zSpecSource = external_exports.enum(["brief", "qa", "audit", "cross-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"]);
@@ -4184,7 +4184,7 @@ function cmpVersion(a, b) {
4184
4184
  }
4185
4185
  return 0;
4186
4186
  }
4187
- var zProject, zBriefPayload, zQaPayload, 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;
4187
+ var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpec, NOTIFY_SOUNDS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
4188
4188
  var init_entities = __esm({
4189
4189
  "../shared/src/entities.ts"() {
4190
4190
  "use strict";
@@ -4223,6 +4223,12 @@ var init_entities = __esm({
4223
4223
  // SPEC-244 · qa dinaikkan dari audit → sinyal skip fase Audit (ADR-0059)
4224
4224
  fromErrorGroup: external_exports.string().optional()
4225
4225
  });
4226
+ zGoalPayload = external_exports.object({
4227
+ goal: external_exports.string(),
4228
+ done: external_exports.string(),
4229
+ constraints: external_exports.string(),
4230
+ priority: zPriority
4231
+ });
4226
4232
  zSpec = external_exports.object({
4227
4233
  id: external_exports.string(),
4228
4234
  projectId: external_exports.string(),
@@ -4232,11 +4238,16 @@ var init_entities = __esm({
4232
4238
  priority: zPriority,
4233
4239
  author: external_exports.string(),
4234
4240
  objective: external_exports.string(),
4235
- payload: external_exports.union([zBriefPayload, zQaPayload]).nullable(),
4241
+ payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).nullable(),
4242
+ // SPEC-407 · +goal
4236
4243
  branchFrom: external_exports.string().nullable(),
4237
4244
  // SPEC-143 · null = default project (main)
4238
- baseSha: external_exports.string().nullable()
4245
+ baseSha: external_exports.string().nullable(),
4239
4246
  // SPEC-186 · null = belum pernah ada sesi (belum dimulai)
4247
+ // SPEC-408 · ADR-0090 · stempel waktu backlog (ISO string di wire — kolom DateTime di DB).
4248
+ // `startedAt` null = belum pernah dikerjakan; ia tak pernah ditulis ulang saat sesi dilanjutkan.
4249
+ createdAt: external_exports.string(),
4250
+ startedAt: external_exports.string().nullable()
4240
4251
  });
4241
4252
  NOTIFY_SOUNDS = [
4242
4253
  "off",
@@ -4303,6 +4314,31 @@ var init_entities = __esm({
4303
4314
  effort: external_exports.string().default("xhigh")
4304
4315
  });
4305
4316
  CONFLICT_DEFAULTS = zConflict.parse({});
4317
+ zLeadEngine = external_exports.object({
4318
+ enabled: external_exports.boolean().default(false),
4319
+ agent: zAgent.default("claude"),
4320
+ model: external_exports.string().default("claude-opus-5"),
4321
+ effort: external_exports.string().default("xhigh")
4322
+ });
4323
+ zLead = external_exports.object({
4324
+ enabled: external_exports.boolean().default(false),
4325
+ // master switch (AC-30)
4326
+ paused: external_exports.boolean().default(false),
4327
+ // rem darurat global (AC-27)
4328
+ pausedProjects: external_exports.array(external_exports.string()).default([]),
4329
+ // rem per project (AC-15/US-4)
4330
+ everyMin: external_exports.number().int().min(1).max(1440).default(5),
4331
+ // denyut proaktif (OQ-2)
4332
+ timeoutSec: external_exports.number().int().min(10).max(900).default(120),
4333
+ // batas waktu satu putusan (AC-4/35)
4334
+ // AC-11 / OQ-10 · berapa jawaban otomatis berturut-turut untuk SATU sesi sebelum lead berhenti.
4335
+ maxAutoAnswers: external_exports.number().int().min(1).max(20).default(3),
4336
+ // OQ-3 · syarat objektif sebelum lead boleh mengintegrasikan ke branch utama. Default MENYALA:
4337
+ // risiko "kode masuk main tanpa mata manusia" diterima sadar, tapi syaratnya tetap terukur.
4338
+ requireGreenBeforeIntegrate: external_exports.boolean().default(true),
4339
+ engine: zLeadEngine.default({})
4340
+ });
4341
+ LEAD_DEFAULTS = zLead.parse({});
4306
4342
  zSetting = external_exports.object({
4307
4343
  model: external_exports.string().default("claude-opus-5"),
4308
4344
  effort: external_exports.string().default("xhigh"),
@@ -4329,13 +4365,17 @@ var init_entities = __esm({
4329
4365
  // SPEC-338 · ADR-0074 · model/effort codex
4330
4366
  verifyScope: zVerifyScope.default("changed"),
4331
4367
  // SPEC-376 · ADR-0080 · scope verifikasi sesi
4332
- conflict: zConflict.default(CONFLICT_DEFAULTS)
4368
+ conflict: zConflict.default(CONFLICT_DEFAULTS),
4333
4369
  // SPEC-383 · ADR-0081 · default sesi konflik rebase/merge
4370
+ lead: zLead.default(LEAD_DEFAULTS)
4371
+ // SPEC-409 · ADR-0091 · hanoman-lead (default mati)
4334
4372
  });
4335
4373
  zNotification = external_exports.object({
4336
4374
  id: external_exports.string(),
4337
- type: external_exports.enum(["done", "decision", "error", "ticket", "fail"]).default("done"),
4338
4375
  // SPEC-249 · +error; SPEC-253 · +ticket; SPEC-298 · +fail (sesi scheduler gagal/limit)
4376
+ // SPEC-409 · +lead (ADR-0091): keputusan berbobot / ragu / tindakan terkunci ditolak. MEMBERI
4377
+ // TAHU, bukan meminta izin — tak ada pekerjaan yang menunggu notifikasi ini dibaca (AC-25).
4378
+ type: external_exports.enum(["done", "decision", "error", "ticket", "fail", "lead"]).default("done"),
4339
4379
  specId: external_exports.string().nullable(),
4340
4380
  sessionId: external_exports.string().nullable(),
4341
4381
  title: external_exports.string(),
@@ -4390,7 +4430,13 @@ var init_agent = __esm({
4390
4430
  "support:read",
4391
4431
  "support:write",
4392
4432
  "notifications:read",
4393
- "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"
4394
4440
  ];
4395
4441
  zCapability = external_exports.enum(CAPABILITY_IDS);
4396
4442
  zCapabilityInfo = external_exports.object({
@@ -4419,7 +4465,9 @@ var init_agent = __esm({
4419
4465
  { id: "support:read", domain: "support", access: "read", label: "Errors & Tiket \u2014 baca", desc: "Lihat error monitoring & tiket Help Center." },
4420
4466
  { id: "support:write", domain: "support", access: "write", label: "Errors & Tiket \u2014 tulis", desc: "Eskalasi error, ubah status, terima/tolak tiket." },
4421
4467
  { id: "notifications:read", domain: "notifications", access: "read", label: "Notifikasi \u2014 baca", desc: "Lihat notifikasi." },
4422
- { 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" }
4423
4471
  ];
4424
4472
  zAgentTokenView = external_exports.object({
4425
4473
  id: external_exports.string(),
@@ -4444,16 +4492,103 @@ var init_agent = __esm({
4444
4492
  }
4445
4493
  });
4446
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
+
4447
4581
  // ../shared/src/dto.ts
4448
4582
  function flowForSource(source) {
4449
- return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "cross-audit" ? "cross-audit" : "feature";
4583
+ return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "cross-audit" ? "cross-audit" : source === "goal" ? "goal" : "feature";
4450
4584
  }
4451
- 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, zStackFrame, zSymbolicatedFrame, zSourceMapUpload, zIngestPayload, zErrorGroupView, zErrorEventView, zErrorGroupDetail, zIngestKeyView, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
4585
+ var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zCreateLink, zUpdateProject, zCreateSpec, zPatchSpec, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerState, zLeadDecisionView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zStackFrame, zSymbolicatedFrame, zSourceMapUpload, zIngestPayload, zErrorGroupView, zErrorEventView, zErrorGroupDetail, zIngestKeyView, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
4452
4586
  var init_dto = __esm({
4453
4587
  "../shared/src/dto.ts"() {
4454
4588
  "use strict";
4455
4589
  init_zod();
4456
4590
  init_entities();
4591
+ init_lead();
4457
4592
  init_enums();
4458
4593
  zIssueDeviceToken = external_exports.object({ name: external_exports.string().min(1) });
4459
4594
  zSessionResult = external_exports.object({
@@ -4509,19 +4644,22 @@ var init_dto = __esm({
4509
4644
  // SPEC-213 · set git remote resmi project
4510
4645
  repoDir: external_exports.string().nullable().optional(),
4511
4646
  // SPEC-217 · path default/server editable (null = kosongkan)
4512
- schedulerOptIn: external_exports.boolean().optional()
4647
+ schedulerOptIn: external_exports.boolean().optional(),
4513
4648
  // SPEC-294 · opt-in scheduler otonom (lokal, tak disync)
4649
+ leadOptIn: external_exports.boolean().optional()
4650
+ // SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal, tak disync)
4514
4651
  });
4515
4652
  zCreateSpec = external_exports.object({
4516
4653
  project: external_exports.string(),
4517
4654
  source: zSpecSource,
4518
4655
  title: external_exports.string().min(1),
4519
4656
  priority: zPriority,
4520
- payload: external_exports.union([zBriefPayload, zQaPayload]),
4657
+ payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]),
4521
4658
  branchFrom: external_exports.string().min(1).optional()
4522
4659
  }).superRefine((o, ctx) => {
4523
- const isQa = "severity" in o.payload;
4524
- if (o.source === "qa" !== isQa)
4660
+ const shape = "severity" in o.payload ? "qa" : "goal" in o.payload ? "goal" : "brief";
4661
+ const want = o.source === "qa" ? "qa" : o.source === "goal" ? "goal" : "brief";
4662
+ if (shape !== want)
4525
4663
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
4526
4664
  });
4527
4665
  zPatchSpec = external_exports.object({
@@ -4531,7 +4669,8 @@ var init_dto = __esm({
4531
4669
  // SPEC-186 · edit konten selagi item belum dimulai. Ditolak server bila sudah mulai.
4532
4670
  title: external_exports.string().min(1).optional(),
4533
4671
  priority: zPriority.optional(),
4534
- payload: external_exports.union([zBriefPayload, zQaPayload]).optional()
4672
+ payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional()
4673
+ // SPEC-407 · +goal
4535
4674
  });
4536
4675
  zIntegrate = external_exports.object({
4537
4676
  op: external_exports.enum(["merge", "rebase"]),
@@ -4556,7 +4695,9 @@ var init_dto = __esm({
4556
4695
  // SPEC-249 · hint prefix DSN (bukan hash/rahasia)
4557
4696
  helpEnabled: external_exports.boolean().default(false),
4558
4697
  // SPEC-253 · Help Center publik aktif
4559
- schedulerOptIn: external_exports.boolean().default(false)
4698
+ schedulerOptIn: external_exports.boolean().default(false),
4699
+ // SPEC-294 · opt-in scheduler otonom
4700
+ leadOptIn: external_exports.boolean().default(false)
4560
4701
  });
4561
4702
  zSchedulerQueueItem = external_exports.object({
4562
4703
  id: external_exports.string(),
@@ -4596,7 +4737,51 @@ var init_dto = __esm({
4596
4737
  queue: external_exports.array(zSchedulerQueueItem),
4597
4738
  sessions: external_exports.array(zSchedulerSessionView)
4598
4739
  });
4599
- zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "cross-audit"]);
4740
+ zLeadDecisionView = external_exports.object({
4741
+ id: external_exports.string(),
4742
+ projectId: external_exports.string(),
4743
+ specId: external_exports.string().nullable(),
4744
+ sessionId: external_exports.string().nullable(),
4745
+ gate: zLeadGate,
4746
+ kind: zLeadKind,
4747
+ question: external_exports.string(),
4748
+ answer: external_exports.string(),
4749
+ reason: external_exports.string(),
4750
+ refs: external_exports.array(external_exports.string()),
4751
+ confidence: zLeadConfidence,
4752
+ action: zLeadAction,
4753
+ status: zLeadStatus,
4754
+ weighty: external_exports.boolean(),
4755
+ supersededById: external_exports.string().nullable(),
4756
+ createdAt: external_exports.string()
4757
+ });
4758
+ zLeadAnswer = external_exports.object({
4759
+ id: external_exports.string(),
4760
+ decision: external_exports.string(),
4761
+ reason: external_exports.string(),
4762
+ refs: external_exports.array(external_exports.string()),
4763
+ confidence: zLeadConfidence,
4764
+ action: zLeadAction
4765
+ });
4766
+ zLeadProjectStatus = external_exports.object({
4767
+ projectId: external_exports.string(),
4768
+ name: external_exports.string(),
4769
+ optIn: external_exports.boolean(),
4770
+ paused: external_exports.boolean(),
4771
+ decisions24h: external_exports.number(),
4772
+ openSessions: external_exports.number()
4773
+ });
4774
+ zLeadStatusView = external_exports.object({
4775
+ config: zLead,
4776
+ projects: external_exports.array(zLeadProjectStatus),
4777
+ queue: external_exports.array(zSchedulerQueueItem),
4778
+ deciding: external_exports.array(external_exports.string()),
4779
+ // id sesi yang sedang disusun keputusannya (AC-3)
4780
+ waiting: external_exports.array(external_exports.string()),
4781
+ // id sesi ber-marker keputusan terisi
4782
+ lastPulseAt: external_exports.string().nullable()
4783
+ });
4784
+ zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "cross-audit", "goal"]);
4600
4785
  zPrdBrief = external_exports.object({
4601
4786
  title: external_exports.string().min(1),
4602
4787
  context: external_exports.string(),
@@ -4742,6 +4927,8 @@ var init_dto = __esm({
4742
4927
  });
4743
4928
  zAttest = external_exports.object({ note: external_exports.string().max(500).optional() });
4744
4929
  zRemediate = external_exports.object({ items: external_exports.array(external_exports.string()).min(1).max(64) });
4930
+ UPDATE_RESTART_EXIT = 75;
4931
+ zUpdateApplyBody = external_exports.object({ confirm: external_exports.boolean().optional() });
4745
4932
  zStackFrame = external_exports.object({
4746
4933
  function: external_exports.string().max(500).optional(),
4747
4934
  filename: external_exports.string().max(2e3).optional(),
@@ -4984,6 +5171,12 @@ var init_api = __esm({
4984
5171
  // SPEC-299 · panel scheduler (daun #6) — konsumen read-only fondasi SPEC-294/ADR-0072.
4985
5172
  schedulerConfig: `${API}/scheduler/config`,
4986
5173
  schedulerState: `${API}/scheduler/state`,
5174
+ // SPEC-409 · ADR-0091 · hanoman-lead. Semua HTTP (polling) — tak ada kanal WS baru (AC-26).
5175
+ leadConfig: `${API}/lead/config`,
5176
+ leadStatus: `${API}/lead/status`,
5177
+ leadDecisions: `${API}/lead/decisions`,
5178
+ leadDecisionOverride: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/override`,
5179
+ leadDecisionCancel: (id) => `${API}/lead/decisions/${encodeURIComponent(id)}/cancel`,
4987
5180
  // SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
4988
5181
  syncNow: `${API}/sync/now`,
4989
5182
  // SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
@@ -5317,6 +5510,7 @@ var init_src = __esm({
5317
5510
  init_enums();
5318
5511
  init_entities();
5319
5512
  init_agent();
5513
+ init_lead();
5320
5514
  init_dto();
5321
5515
  init_api();
5322
5516
  init_coverage();
@@ -5520,6 +5714,22 @@ var init_verify_scope = __esm({
5520
5714
  }
5521
5715
  });
5522
5716
 
5717
+ // ../runner/src/goal-spec.ts
5718
+ function readGoalPayload(payload) {
5719
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
5720
+ const p = payload;
5721
+ const goal = str(p.goal);
5722
+ if (!goal) return null;
5723
+ return { goal, done: str(p.done), constraints: str(p.constraints) };
5724
+ }
5725
+ var str;
5726
+ var init_goal_spec = __esm({
5727
+ "../runner/src/goal-spec.ts"() {
5728
+ "use strict";
5729
+ str = (v) => typeof v === "string" ? v.trim() : "";
5730
+ }
5731
+ });
5732
+
5523
5733
  // ../runner/src/prompt.ts
5524
5734
  function startPrompt(flow, spec, branchTo, autonomy, verifyScope) {
5525
5735
  const detail = spec.payload ? `
@@ -5560,7 +5770,7 @@ Detail: ${JSON.stringify(spec.payload)}` : "";
5560
5770
  const auditDecided = resume.recorded.some((line) => line.startsWith("Audit "));
5561
5771
  return [
5562
5772
  `hanoman ${flow} \u2014 MELANJUTKAN sesi backlog yang sudah berjalan. Ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.`,
5563
- resumeClause(resume, branchTo),
5773
+ resumeClause(resume, branchTo, PIPELINES[flow].includes("Plan")),
5564
5774
  phaseInstruction(PIPELINES[flow]),
5565
5775
  auditDecided ? "" : auditDecisionInstruction(flow),
5566
5776
  autonomyClause(autonomy),
@@ -5572,6 +5782,27 @@ Judul: ${spec.title}
5572
5782
  Objective: ${spec.objective}${detail}`
5573
5783
  ].filter(Boolean).join("\n\n");
5574
5784
  }
5785
+ function startGoalPrompt(spec, branchTo, opts = {}) {
5786
+ const g = readGoalPayload(spec.payload);
5787
+ const detail = [
5788
+ `Goal: ${g?.goal ?? spec.objective}`,
5789
+ g?.done ? `Selesai bila: ${g.done}` : "",
5790
+ g?.constraints ? `Batasan: ${g.constraints}` : ""
5791
+ ].filter(Boolean).join("\n");
5792
+ return [
5793
+ "hanoman goal \u2014 sesi ini mengejar SATU goal sampai tercapai. TIDAK ada fase Brainstorm, Objective, Spec, maupun Plan: jangan menulis design doc, jangan menulis plan berkotak, jangan memecah pekerjaan ini jadi backlog baru. Langsung kerjakan goal-nya. Tetap ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.",
5794
+ opts.resume ? resumeClause(opts.resume, branchTo, false) : "",
5795
+ detail,
5796
+ phaseInstruction(PIPELINES.goal),
5797
+ "Fase Verifikasi bukan formalitas: jalankan perintah yang membuktikan goal-nya tercapai (test/typecheck/benchmark/perintah yang relevan) dan baca outputnya. Klaim tanpa output bukan bukti.",
5798
+ autonomyClause(opts.autonomy),
5799
+ scopeClause("goal", opts.verifyScope),
5800
+ skillInstruction(PIPELINES.goal),
5801
+ `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
5802
+ `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
5803
+ Judul: ${spec.title}`
5804
+ ].filter(Boolean).join("\n\n");
5805
+ }
5575
5806
  function startProjectPrompt(flow, project, branchTo) {
5576
5807
  return [
5577
5808
  `hanoman ${flow}. Susun Source of Truth repo ini dari kodenya di internal/docs/**, mengikuti STANDAR DOCS di bagian bawah prompt ini.`,
@@ -5676,12 +5907,13 @@ Judul: ${spec.title}
5676
5907
  Objective: ${spec.objective}${detail}`
5677
5908
  ].filter(Boolean).join("\n\n");
5678
5909
  }
5679
- var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, PHASE_SKILLS, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, scopeClause, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE, projectLine, crossAuditLogGuide, CROSS_AUDIT_FOCUS;
5910
+ var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, PHASE_SKILLS, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE, projectLine, crossAuditLogGuide, CROSS_AUDIT_FOCUS;
5680
5911
  var init_prompt = __esm({
5681
5912
  "../runner/src/prompt.ts"() {
5682
5913
  "use strict";
5683
5914
  init_reverse_standard();
5684
5915
  init_verify_scope();
5916
+ init_goal_spec();
5685
5917
  PIPELINES = {
5686
5918
  feature: ["Brainstorm", "Objective", "Spec", "Plan", "Execute"],
5687
5919
  qa: ["Audit", "Spec", "Plan", "Execute"],
@@ -5692,7 +5924,11 @@ var init_prompt = __esm({
5692
5924
  breakdown: ["Analisis", "Breakdown"],
5693
5925
  // SPEC-337 · ADR-0075 · audit lintas project: fase & stage-map identik audit-only, scope-nya
5694
5926
  // yang berbeda (project utama + tetangga ProjectLink).
5695
- "cross-audit": ["Audit", "Laporan"]
5927
+ "cross-audit": ["Audit", "Laporan"],
5928
+ // SPEC-407 · ADR-0089 · backlog goal: tak ada fase perencanaan sama sekali. `Goal` = kerjakan,
5929
+ // `Verifikasi` = buktikan. Kedua nama unik lintas PIPELINES — syarat peta REACHED di server,
5930
+ // yang berkunci nama fase saja.
5931
+ goal: ["Goal", "Verifikasi"]
5696
5932
  };
5697
5933
  AUTONOMY_CLAUSE = 'Jalankan seluruh pipeline sampai tuntas tanpa berhenti di batas antar-fase. Checkpoint "review"/"approval"/"need review" milik skill superpowers BUKAN titik berhenti di sini \u2014 lanjut saja ke fase berikutnya. Berhenti HANYA saat butuh keputusan manusia sejati (percabangan yang mengubah bentuk kerja: data model, kontrak API, scope); saat itu tanyakan di terminal ini dan tunggu jawabannya. Selain itu, terus lanjut.';
5698
5934
  AUTONOMY_CLAUSE_FULL = "Kamu berjalan TANPA pengawas \u2014 tak ada manusia yang menonton terminal ini untuk menjawab. Putuskan sendiri di SETIAP percabangan (termasuk yang mengubah bentuk kerja: data model, kontrak API, scope) berdasarkan Source of Truth dan penilaian terbaikmu; JANGAN berhenti bertanya. Tembus seluruh pipeline sampai stage `done`, lalu commit & push. Jangan menunggu review/persetujuan siapa pun \u2014 catat asumsi & keputusan penting di pesan commit agar bisa di-review pasca-fakta. Merge ke branch utama tetap dilakukan manusia, bukan kamu.";
@@ -5712,7 +5948,10 @@ Execute BELUM selesai selama plan (\`docs/superpowers/plans/**\`) masih punya ta
5712
5948
  "superpowers:executing-plans",
5713
5949
  "superpowers:test-driven-development",
5714
5950
  "superpowers:verification-before-completion"
5715
- ]
5951
+ ],
5952
+ // SPEC-407 · fase `Goal` sengaja TANPA skill: seluruh inti flow ini adalah membebaskan sesi
5953
+ // dari proses kaku. Yang tetap dijaga cuma pintu keluarnya.
5954
+ Verifikasi: ["superpowers:verification-before-completion"]
5716
5955
  };
5717
5956
  skillInstruction = (phases) => {
5718
5957
  const lines2 = phases.filter((p) => PHASE_SKILLS[p]).map((p) => `- ${p}: ${PHASE_SKILLS[p].join(", ")}`);
@@ -5752,17 +5991,19 @@ ${lines2.join("\n")}` : "";
5752
5991
  return `Backlog brief ini LANJUTAN dari audit ${fromAudit}. Worktree ini lahir dari branch audit itu, jadi dokumen audit sudah ada di ${doc}. BACA dokumen itu lebih dulu dan pakai sebagai bahan fase Brainstorm & Objective \u2014 temuannya sudah terbukti, jangan menginvestigasi ulang dari nol. Semua fase tetap dijalankan: dokumen audit memuat TEMUAN, bukan bentuk solusi, jadi perancangan fitur tetap pekerjaanmu.`;
5753
5992
  return `Backlog qa ini LANJUTAN dari audit ${fromAudit}. Worktree ini lahir dari branch audit itu, jadi dokumen audit sudah ada di ${doc}. JANGAN mengulang investigasi fase Audit dari nol \u2014 baca dokumen audit itu sebagai temuan, tandai fase Audit dilewati (\`echo "Audit skipped" >> "$HANOMAN_PHASE_FILE"\`), lalu ambil keputusan pasca-Audit: perbaikan jelas & kecil \u2192 langsung Execute (tandai \`Spec skipped\` dan \`Plan skipped\` bila sesuai); selain itu Spec \u2192 Plan \u2192 Execute penuh.`;
5754
5993
  };
5755
- scopeClause = (flow, scope) => scope && PIPELINES[flow].includes("Execute") ? verifyScopeClause(scope) : "";
5756
- resumeClause = (r, branchTo) => {
5994
+ writesCode = (flow) => PIPELINES[flow].includes("Execute") || PIPELINES[flow].includes("Goal");
5995
+ scopeClause = (flow, scope) => scope && writesCode(flow) ? verifyScopeClause(scope) : "";
5996
+ resumeClause = (r, branchTo, hasPlan = true) => {
5757
5997
  const fase = r.recorded.length ? `Fase yang SUDAH tercatat di $HANOMAN_PHASE_FILE: ${r.recorded.join(" \xB7 ")}. JANGAN mengulang fase itu dan JANGAN menulis ulang barisnya.` : "Belum ada fase yang tercatat di $HANOMAN_PHASE_FILE \u2014 worktree ini sendiri yang jadi alasan melanjutkan.";
5758
- const lanjut = r.next ? `Lanjutkan dari fase: ${r.next}.` : "Semua fase sudah tercatat. Periksa apakah plan di `docs/superpowers/plans/**` masih menyisakan task `- [ ]` dan selesaikan sisanya; bila sudah bersih, tinggal commit & push.";
5998
+ const lanjut = r.next ? `Lanjutkan dari fase: ${r.next}.` : hasPlan ? "Semua fase sudah tercatat. Periksa apakah plan di `docs/superpowers/plans/**` masih menyisakan task `- [ ]` dan selesaikan sisanya; bila sudah bersih, tinggal commit & push." : "Semua fase sudah tercatat. Buktikan sekali lagi goal-nya benar-benar tercapai, lalu commit & push.";
5759
5999
  const worktree = r.worktreeKept ? "Worktree ini adalah worktree sesi sebelumnya apa adanya \u2014 termasuk perubahan yang belum di-commit." : `Worktree ini DIBANGUN ULANG dari tip branch sesi \`${branchTo}\`: commit sesi sebelumnya ada, tetapi perubahan yang belum sempat di-commit TIDAK ada.`;
6000
+ const baca = hasPlan ? "Sebelum menulis apa pun: baca `git log --oneline` dan `git status`, lalu plan di `docs/superpowers/plans/**` untuk backlog item ini (`- [x]` sudah selesai, `- [ ]` belum). Jangan menulis ulang yang sudah ada." : "Sebelum menulis apa pun: baca `git log --oneline` dan `git status` untuk melihat apa yang sudah dikerjakan. Jangan menulis ulang yang sudah ada.";
5760
6001
  return [
5761
6002
  "Sesi ini MELANJUTKAN pekerjaan sesi sebelumnya untuk backlog item yang sama \u2014 bukan memulai dari nol.",
5762
6003
  fase,
5763
6004
  lanjut,
5764
6005
  worktree,
5765
- "Sebelum menulis apa pun: baca `git log --oneline` dan `git status`, lalu plan di `docs/superpowers/plans/**` untuk backlog item ini (`- [x]` sudah selesai, `- [ ]` belum). Jangan menulis ulang yang sudah ada."
6006
+ baca
5766
6007
  ].join(" ");
5767
6008
  };
5768
6009
  RESUMED_WORKTREE_NOTE = "Catatan: worktree ini BUKAN kosong \u2014 ia berisi pekerjaan sesi sebelumnya untuk tugas yang sama (sesi itu berhenti sebelum selesai). Baca `git log --oneline` dan `git status` lebih dulu, lalu lanjutkan dari sana alih-alih menulis ulang semuanya dari awal.";
@@ -5930,7 +6171,21 @@ var init_settings = __esm({
5930
6171
  });
5931
6172
 
5932
6173
  // ../runner/src/goal.ts
5933
- function defaultGoalCondition({ flow, specId, branchTo }) {
6174
+ function goalFlowCondition(specId, branchTo, spec) {
6175
+ const g = readGoalPayload(spec?.payload);
6176
+ const goal = g?.goal || (spec?.objective ?? "").trim() || "(goal tak tercatat di backlog item)";
6177
+ const bukti = g?.done || goal;
6178
+ return [
6179
+ `Sesi goal hanoman ${specId}. GOAL: ${goal}`,
6180
+ "Sesi ini hanya boleh berhenti bila transkrip TERBARU memuat bukti langsung semua hal berikut:",
6181
+ `1. goal tercapai \u2014 ${bukti};`,
6182
+ `2. output \`cat "$HANOMAN_PHASE_FILE"\` yang memuat satu baris untuk SETIAP fase ${PIPELINES.goal.join(" \u2192 ")}, masing-masing berakhiran \`done\` atau \`skipped\`;`,
6183
+ `3. output \`git push origin HEAD:refs/heads/${branchTo}\` yang SUKSES sesudah commit terakhir.`,
6184
+ "Bila salah satu bukti tak ada di transkrip terbaru, kondisi BELUM terpenuhi: jalankan perintah verifikasinya, tuntaskan yang masih kurang, lalu lanjutkan \u2014 jangan berhenti."
6185
+ ].join("\n");
6186
+ }
6187
+ function defaultGoalCondition({ flow, specId, branchTo, spec }) {
6188
+ if (flow === "goal") return goalFlowCondition(specId, branchTo, spec);
5934
6189
  const phases = PIPELINES[flow];
5935
6190
  const planGate = phases.includes("Plan") && phases.includes("Execute");
5936
6191
  const clauses = [
@@ -5964,6 +6219,7 @@ var init_goal = __esm({
5964
6219
  "../runner/src/goal.ts"() {
5965
6220
  "use strict";
5966
6221
  init_prompt();
6222
+ init_goal_spec();
5967
6223
  GOAL_MAX = 4e3;
5968
6224
  goalOneLine = (cond) => cond.replace(/\s+/g, " ").trim();
5969
6225
  GOAL_CHUNK = 500;
@@ -6128,6 +6384,7 @@ var init_src2 = __esm({
6128
6384
  init_git();
6129
6385
  init_settings();
6130
6386
  init_goal();
6387
+ init_goal_spec();
6131
6388
  init_codex_settings();
6132
6389
  init_agent_cli();
6133
6390
  init_verify_scope();
@@ -6199,7 +6456,8 @@ function readPhases(file, flow) {
6199
6456
  function stageFor(phases) {
6200
6457
  let best = -1;
6201
6458
  for (const p of phases) {
6202
- if (p.name === "Execute" && p.state === "active") best = Math.max(best, STAGES.indexOf("executing"));
6459
+ if ((p.name === "Execute" || p.name === "Goal") && p.state === "active")
6460
+ best = Math.max(best, STAGES.indexOf("executing"));
6203
6461
  if (p.state !== "done" && p.state !== "skipped") continue;
6204
6462
  const s = REACHED[p.name];
6205
6463
  if (s) best = Math.max(best, STAGES.indexOf(s));
@@ -6244,7 +6502,11 @@ var init_session_phases = __esm({
6244
6502
  Spec: "spec-ready",
6245
6503
  Plan: "planned",
6246
6504
  Laporan: "done",
6247
- Execute: "done"
6505
+ Execute: "done",
6506
+ // SPEC-407 · ADR-0089 · flow goal (Goal → Verifikasi): fase kerja mencapai `executing`, fase
6507
+ // verifikasi yang mencapai `done`. Kedua nama unik lintas PIPELINES — peta ini berkunci nama.
6508
+ Goal: "executing",
6509
+ Verifikasi: "done"
6248
6510
  };
6249
6511
  }
6250
6512
  });
@@ -6441,6 +6703,9 @@ function createSession(projectId, cwd, opts = {}) {
6441
6703
  argv = [sq(agentBin(agent)), promptArg, flags].filter(Boolean).join(" ");
6442
6704
  }
6443
6705
  const envPairs = [];
6706
+ if (!opts.command && agent === "claude") {
6707
+ for (const [k, v] of Object.entries(rootBypassEnv())) envPairs.push(`${k}=${sq(v)}`);
6708
+ }
6444
6709
  if (opts.phaseFile) {
6445
6710
  mkdirSync3(dirname4(opts.phaseFile), { recursive: true });
6446
6711
  envPairs.push(`HANOMAN_PHASE_FILE=${sq(opts.phaseFile)}`);
@@ -6531,6 +6796,29 @@ function createSession(projectId, cwd, opts = {}) {
6531
6796
  });
6532
6797
  return { id, projectId, specId: opts.specId, flow: opts.flow, cwd, branch: opts.branch, exited: false, decision: false, agent };
6533
6798
  }
6799
+ function capturePane(id, lines2 = 200) {
6800
+ try {
6801
+ return tmux("capture-pane", "-p", "-J", "-S", `-${lines2}`, "-t", name(id));
6802
+ } catch {
6803
+ return "";
6804
+ }
6805
+ }
6806
+ async function sendToPane(id, text, chunkMs = 50) {
6807
+ const p = getSession(id);
6808
+ if (!p || p.exited) return false;
6809
+ const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
6810
+ if (!line) return false;
6811
+ try {
6812
+ for (const chunk of goalChunks(line)) {
6813
+ tmux("send-keys", "-t", name(id), "-l", chunk);
6814
+ await sleep(chunkMs);
6815
+ }
6816
+ tmux("send-keys", "-t", name(id), "Enter");
6817
+ return true;
6818
+ } catch {
6819
+ return false;
6820
+ }
6821
+ }
6534
6822
  async function armGoalInTui(id, condition, o = {}) {
6535
6823
  const pollMs = o.pollMs ?? 500, readyTries = o.readyTries ?? 20;
6536
6824
  const settleMs = o.settleMs ?? 1200, verifyTries = o.verifyTries ?? 12;
@@ -6703,7 +6991,7 @@ function spawnPty(...args) {
6703
6991
  );
6704
6992
  }
6705
6993
  }
6706
- 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;
6994
+ var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, sleep, paneText, GOAL_ARMED_MARKERS, goalArmed, poll, detach;
6707
6995
  var init_pty = __esm({
6708
6996
  "src/services/pty.ts"() {
6709
6997
  "use strict";
@@ -6727,6 +7015,7 @@ var init_pty = __esm({
6727
7015
  shellBin = () => effectiveStr("HANOMAN_SHELL") ?? process.env.SHELL ?? "/bin/bash";
6728
7016
  codexBin = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
6729
7017
  agentBin = (agent) => agent === "codex" ? codexBin() : claudeBin();
7018
+ rootBypassEnv = (uid = process.getuid?.()) => uid === 0 ? { IS_SANDBOX: "1" } : {};
6730
7019
  frame = (f) => JSON.stringify(f);
6731
7020
  name = (id) => PREFIX + id;
6732
7021
  promptFilePath = (id) => `${tmpdir()}/hanoman-prompts/${id}`;
@@ -7050,7 +7339,9 @@ var init_sync = __esm({
7050
7339
  };
7051
7340
  FIELDS = {
7052
7341
  project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
7053
- spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "updatedAt"],
7342
+ // SPEC-408 · ADR-0090 · createdAt/startedAt ikut menyeberang sejajar baseSha/headSha. Tanpa
7343
+ // ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
7344
+ spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "createdAt", "startedAt", "updatedAt"],
7054
7345
  vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
7055
7346
  sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
7056
7347
  // SPEC-268 · ADR-0066 · agregat grup error (ErrorEvent mentah tak disync).
@@ -7063,7 +7354,7 @@ var init_sync = __esm({
7063
7354
  };
7064
7355
  DATE_FIELDS = {
7065
7356
  project: ["updatedAt"],
7066
- spec: ["updatedAt"],
7357
+ spec: ["createdAt", "startedAt", "updatedAt"],
7067
7358
  vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
7068
7359
  sessionResult: ["createdAt", "updatedAt"],
7069
7360
  errorGroup: ["firstSeenAt", "lastSeenAt", "updatedAt"],
@@ -11032,7 +11323,7 @@ function syncStatus() {
11032
11323
  async function startSyncClient(base, token, tickMs2) {
11033
11324
  started = true;
11034
11325
  const transport = fetchTransport(base, token);
11035
- const tick3 = async () => {
11326
+ const tick4 = async () => {
11036
11327
  try {
11037
11328
  await syncOnce(transport);
11038
11329
  } catch {
@@ -11043,13 +11334,13 @@ async function startSyncClient(base, token, tickMs2) {
11043
11334
  const wsUrl = base.replace(/^http/, "ws").replace(/\/$/, "") + `/api/sync/ws?token=${encodeURIComponent(token)}`;
11044
11335
  ws = new WebSocket2(wsUrl);
11045
11336
  ws.on("open", () => {
11046
- void tick3();
11337
+ void tick4();
11047
11338
  });
11048
11339
  ws.on("message", async (raw) => {
11049
11340
  try {
11050
11341
  const msg = JSON.parse(raw.toString());
11051
11342
  if (msg.t !== "sync") return;
11052
- if (!await applyFeedFrame(msg)) void tick3();
11343
+ if (!await applyFeedFrame(msg)) void tick4();
11053
11344
  } catch {
11054
11345
  }
11055
11346
  });
@@ -11066,11 +11357,11 @@ async function startSyncClient(base, token, tickMs2) {
11066
11357
  }
11067
11358
  });
11068
11359
  };
11069
- await tick3();
11360
+ await tick4();
11070
11361
  void connectWs();
11071
11362
  const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
11072
11363
  timer2 = setInterval(() => {
11073
- void tick3();
11364
+ void tick4();
11074
11365
  }, ms);
11075
11366
  timer2.unref?.();
11076
11367
  }
@@ -12269,7 +12560,7 @@ var require_parseParams = __commonJS({
12269
12560
  var STATE_VALUE = 1;
12270
12561
  var STATE_CHARSET = 2;
12271
12562
  var STATE_LANG = 3;
12272
- function parseParams(str) {
12563
+ function parseParams(str2) {
12273
12564
  const res = [];
12274
12565
  let state = STATE_KEY;
12275
12566
  let charset = "";
@@ -12277,9 +12568,9 @@ var require_parseParams = __commonJS({
12277
12568
  let escaping = false;
12278
12569
  let p = 0;
12279
12570
  let tmp = "";
12280
- const len = str.length;
12571
+ const len = str2.length;
12281
12572
  for (var i = 0; i < len; ++i) {
12282
- const char = str[i];
12573
+ const char = str2[i];
12283
12574
  if (char === "\\" && inquote) {
12284
12575
  if (escaping) {
12285
12576
  escaping = false;
@@ -12292,7 +12583,7 @@ var require_parseParams = __commonJS({
12292
12583
  if (inquote) {
12293
12584
  inquote = false;
12294
12585
  state = STATE_KEY;
12295
- while (i + 1 < len && str[i + 1] !== ";") {
12586
+ while (i + 1 < len && str2[i + 1] !== ";") {
12296
12587
  ++i;
12297
12588
  }
12298
12589
  } else {
@@ -12811,29 +13102,29 @@ var require_Decoder = __commonJS({
12811
13102
  function Decoder() {
12812
13103
  this.buffer = void 0;
12813
13104
  }
12814
- Decoder.prototype.write = function(str) {
12815
- str = str.replace(RE_PLUS, " ");
13105
+ Decoder.prototype.write = function(str2) {
13106
+ str2 = str2.replace(RE_PLUS, " ");
12816
13107
  let res = "";
12817
13108
  let i = 0;
12818
13109
  let p = 0;
12819
- const len = str.length;
13110
+ const len = str2.length;
12820
13111
  for (; i < len; ++i) {
12821
13112
  if (this.buffer !== void 0) {
12822
- if (!HEX[str.charCodeAt(i)]) {
13113
+ if (!HEX[str2.charCodeAt(i)]) {
12823
13114
  res += "%" + this.buffer;
12824
13115
  this.buffer = void 0;
12825
13116
  --i;
12826
13117
  } else {
12827
- this.buffer += str[i];
13118
+ this.buffer += str2[i];
12828
13119
  ++p;
12829
13120
  if (this.buffer.length === 2) {
12830
13121
  res += String.fromCharCode(parseInt(this.buffer, 16));
12831
13122
  this.buffer = void 0;
12832
13123
  }
12833
13124
  }
12834
- } else if (str[i] === "%") {
13125
+ } else if (str2[i] === "%") {
12835
13126
  if (i > p) {
12836
- res += str.substring(p, i);
13127
+ res += str2.substring(p, i);
12837
13128
  p = i;
12838
13129
  }
12839
13130
  this.buffer = "";
@@ -12841,7 +13132,7 @@ var require_Decoder = __commonJS({
12841
13132
  }
12842
13133
  }
12843
13134
  if (p < len && this.buffer === void 0) {
12844
- res += str.substring(p);
13135
+ res += str2.substring(p);
12845
13136
  }
12846
13137
  return res;
12847
13138
  };
@@ -13505,9 +13796,9 @@ var require_generateId = __commonJS({
13505
13796
  }
13506
13797
  module.exports.generateId = function generateIdFn() {
13507
13798
  let num = 0;
13508
- let str = seed();
13799
+ let str2 = seed();
13509
13800
  return function generateId() {
13510
- return num === 255 ? (str = seed()) + HEX[num = 0] : str + HEX[++num];
13801
+ return num === 255 ? (str2 = seed()) + HEX[num = 0] : str2 + HEX[++num];
13511
13802
  };
13512
13803
  }();
13513
13804
  }
@@ -14340,7 +14631,7 @@ import websocket from "@fastify/websocket";
14340
14631
  import cookie from "@fastify/cookie";
14341
14632
  import { fileURLToPath as fileURLToPath4 } from "node:url";
14342
14633
  import { dirname as dirname12 } from "node:path";
14343
- import { existsSync as existsSync10 } from "node:fs";
14634
+ import { existsSync as existsSync11 } from "node:fs";
14344
14635
 
14345
14636
  // src/web-dir.ts
14346
14637
  import { resolve } from "node:path";
@@ -14559,7 +14850,9 @@ async function toProjectView(p, sessions) {
14559
14850
  // SPEC-253 · Help Center publik aktif.
14560
14851
  helpEnabled: p.helpEnabled,
14561
14852
  // SPEC-294 · opt-in scheduler otonom (lokal per-instance).
14562
- schedulerOptIn: p.schedulerOptIn
14853
+ schedulerOptIn: p.schedulerOptIn,
14854
+ // SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal per-instance, cermin schedulerOptIn).
14855
+ leadOptIn: p.leadOptIn
14563
14856
  };
14564
14857
  }
14565
14858
 
@@ -15012,8 +15305,10 @@ var DEFAULT_SETTING = {
15012
15305
  // SPEC-338 · ADR-0074 · model/effort codex
15013
15306
  verifyScope: "changed",
15014
15307
  // SPEC-376 · ADR-0080 · uji hanya yang berubah
15015
- conflict: CONFLICT_DEFAULTS
15308
+ conflict: CONFLICT_DEFAULTS,
15016
15309
  // SPEC-383 · ADR-0081 · default sesi konflik (opt-in, mati)
15310
+ lead: LEAD_DEFAULTS
15311
+ // SPEC-409 · ADR-0091 · hanoman-lead (master switch mati)
15017
15312
  };
15018
15313
  async function getSetting() {
15019
15314
  const raw = (await prisma.setting.findUnique({ where: { id: 1 } }))?.data;
@@ -15450,10 +15745,10 @@ function cleanUrl(href) {
15450
15745
  }
15451
15746
  var noopTest = { exec: () => null };
15452
15747
  function splitCells(tableRow, count) {
15453
- const row = tableRow.replace(/\|/g, (match, offset, str) => {
15748
+ const row = tableRow.replace(/\|/g, (match, offset, str2) => {
15454
15749
  let escaped = false;
15455
15750
  let curr = offset;
15456
- while (--curr >= 0 && str[curr] === "\\")
15751
+ while (--curr >= 0 && str2[curr] === "\\")
15457
15752
  escaped = !escaped;
15458
15753
  if (escaped) {
15459
15754
  return "|";
@@ -15481,14 +15776,14 @@ function splitCells(tableRow, count) {
15481
15776
  }
15482
15777
  return cells;
15483
15778
  }
15484
- function rtrim(str, c, invert) {
15485
- const l = str.length;
15779
+ function rtrim(str2, c, invert) {
15780
+ const l = str2.length;
15486
15781
  if (l === 0) {
15487
15782
  return "";
15488
15783
  }
15489
15784
  let suffLen = 0;
15490
15785
  while (suffLen < l) {
15491
- const currChar = str.charAt(l - suffLen - 1);
15786
+ const currChar = str2.charAt(l - suffLen - 1);
15492
15787
  if (currChar === c && !invert) {
15493
15788
  suffLen++;
15494
15789
  } else if (currChar !== c && invert) {
@@ -15497,19 +15792,19 @@ function rtrim(str, c, invert) {
15497
15792
  break;
15498
15793
  }
15499
15794
  }
15500
- return str.slice(0, l - suffLen);
15795
+ return str2.slice(0, l - suffLen);
15501
15796
  }
15502
- function findClosingBracket(str, b) {
15503
- if (str.indexOf(b[1]) === -1) {
15797
+ function findClosingBracket(str2, b) {
15798
+ if (str2.indexOf(b[1]) === -1) {
15504
15799
  return -1;
15505
15800
  }
15506
15801
  let level = 0;
15507
- for (let i = 0; i < str.length; i++) {
15508
- if (str[i] === "\\") {
15802
+ for (let i = 0; i < str2.length; i++) {
15803
+ if (str2[i] === "\\") {
15509
15804
  i++;
15510
- } else if (str[i] === b[0]) {
15805
+ } else if (str2[i] === b[0]) {
15511
15806
  level++;
15512
- } else if (str[i] === b[1]) {
15807
+ } else if (str2[i] === b[1]) {
15513
15808
  level--;
15514
15809
  if (level < 0) {
15515
15810
  return i;
@@ -17735,6 +18030,27 @@ async function sendReviewDownload(reply, fmt, rf, a) {
17735
18030
  });
17736
18031
  }
17737
18032
 
18033
+ // src/services/date-range.ts
18034
+ var DAY = /^\d{4}-\d{2}-\d{2}$/;
18035
+ function dayStart(s) {
18036
+ if (!s || !DAY.test(s)) return null;
18037
+ const [y, m, d] = s.split("-").map(Number);
18038
+ const dt = new Date(y, m - 1, d, 0, 0, 0, 0);
18039
+ if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;
18040
+ return dt;
18041
+ }
18042
+ function dayEnd(s) {
18043
+ const start = dayStart(s);
18044
+ if (!start) return null;
18045
+ return new Date(start.getFullYear(), start.getMonth(), start.getDate(), 23, 59, 59, 999);
18046
+ }
18047
+ function inDayRange(at, from, to) {
18048
+ if (!from && !to) return true;
18049
+ if (!at) return false;
18050
+ const t = at.getTime();
18051
+ return (!from || t >= from.getTime()) && (!to || t <= to.getTime());
18052
+ }
18053
+
17738
18054
  // src/services/live-specs.ts
17739
18055
  init_db();
17740
18056
  init_pty();
@@ -17784,6 +18100,12 @@ async function recordNewTicket(ticketId, projectId, projectName, category, title
17784
18100
  }).catch(() => {
17785
18101
  });
17786
18102
  }
18103
+ async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
18104
+ await prisma.notification.create({
18105
+ data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
18106
+ }).catch(() => {
18107
+ });
18108
+ }
17787
18109
  var awaiting = /* @__PURE__ */ new Set();
17788
18110
  async function scanDecisions(read = liveDecisions) {
17789
18111
  const next = /* @__PURE__ */ new Set();
@@ -17839,6 +18161,13 @@ async function liveSpecs(filter = {}) {
17839
18161
  // src/routes/specs.ts
17840
18162
  var branchUnknown = async (repoDir, branch) => !(await branchFromCandidates(repoDir)).includes(branch);
17841
18163
  function deriveSpecFields(source, payload, manualPriority) {
18164
+ if (source === "goal") {
18165
+ const pick = (v) => typeof v === "string" ? v.trim() : "";
18166
+ return {
18167
+ priority: manualPriority,
18168
+ objective: pick(payload?.goal) || pick(payload?.done) || "\u2014 goal belum diisi."
18169
+ };
18170
+ }
17842
18171
  const isQa = source === "qa";
17843
18172
  const priority = isQa && payload && "severity" in payload ? payload.severity === "minor" ? "sedang" : "tinggi" : manualPriority;
17844
18173
  const objective = isQa && payload && "actual" in payload ? payload.actual || payload.steps || "\u2014 audit untuk menelusuri akar masalah." : payload && "outcome" in payload ? payload.outcome || payload.context || "\u2014 brainstorm untuk memperjelas objective." : "";
@@ -17846,13 +18175,16 @@ function deriveSpecFields(source, payload, manualPriority) {
17846
18175
  }
17847
18176
  function filterSpecs(specs, f) {
17848
18177
  const needle = (f.q ?? "").trim().toLowerCase();
17849
- 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)));
18178
+ const from = dayStart(f.from);
18179
+ const to = dayEnd(f.to);
18180
+ const byStarted = f.dateField === "started";
18181
+ 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)));
17850
18182
  }
17851
18183
  async function specs_default(app2) {
17852
18184
  app2.get("/specs", async (req) => {
17853
- const { project, source, q, stage, priority, startable, page, limit } = req.query;
18185
+ const { project, source, q, stage, priority, startable, dateField, from, to, page, limit } = req.query;
17854
18186
  const overlaid = await liveSpecs({ project, source });
17855
- return paginate(filterSpecs(overlaid, { q, stage, priority, startable }), page, limit);
18187
+ return paginate(filterSpecs(overlaid, { q, stage, priority, startable, dateField, from, to }), page, limit);
17856
18188
  });
17857
18189
  app2.post("/specs", async (req, reply) => {
17858
18190
  const parsed = zCreateSpec.safeParse(req.body);
@@ -17878,7 +18210,7 @@ async function specs_default(app2) {
17878
18210
  source: b.source,
17879
18211
  stage: "brainstorming",
17880
18212
  priority,
17881
- author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "cross-audit" ? `Audit lintas \xB7 ${author}` : author,
18213
+ author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "cross-audit" ? `Audit lintas \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
17882
18214
  objective,
17883
18215
  payload: b.payload,
17884
18216
  branchFrom: b.branchFrom ?? null
@@ -19299,6 +19631,14 @@ function resumeState(repoDir, worktree, branchTo, headSha) {
19299
19631
  const base = realGit.revParse(repoDir, `origin/${branchTo}`) ?? realGit.revParse(repoDir, branchTo) ?? (headSha ? realGit.revParse(repoDir, headSha) : null);
19300
19632
  return base ? { worktreeKept: false, base } : null;
19301
19633
  }
19634
+ function buildResumeCtx(repoDir, id, flow, worktreeKept) {
19635
+ const phases = readPhases(phaseFilePath(repoDir, id), flow);
19636
+ return {
19637
+ recorded: phases.filter((p) => p.state === "done" || p.state === "skipped").map((p) => `${p.name} ${p.state}`),
19638
+ next: phases.find((p) => p.state === "active")?.name,
19639
+ worktreeKept
19640
+ };
19641
+ }
19302
19642
  async function startSpecSession(spec, opts) {
19303
19643
  const repoDir = await resolveRepoDir(spec.projectId);
19304
19644
  if (!repoDir) throw new LaunchError(`project "${spec.projectId}" belum di-bind ke checkout lokal`, "needs-bind");
@@ -19315,11 +19655,14 @@ async function startSpecSession(spec, opts) {
19315
19655
  const worktree = `${repoDir}/.worktrees/${id}`;
19316
19656
  const branchTo = `hanoman/${id}`;
19317
19657
  const resume = !isContinue && spec.baseSha ? resumeState(repoDir, worktree, branchTo, spec.headSha) : null;
19318
- const goal = opts.goal ?? setting.goal.enabled ? resolveGoalCondition(
19319
- { flow: opts.flow, specId: spec.id, branchTo },
19320
- opts.goalCondition,
19321
- setting.goal.condition
19322
- ) : void 0;
19658
+ const isGoalFlow = opts.flow === "goal";
19659
+ const goalArgs = {
19660
+ flow: opts.flow,
19661
+ specId: spec.id,
19662
+ branchTo,
19663
+ spec: { payload: spec.payload ?? void 0, objective: spec.objective }
19664
+ };
19665
+ const goal = isGoalFlow || (opts.goal ?? setting.goal.enabled) ? resolveGoalCondition(goalArgs, opts.goalCondition, isGoalFlow ? null : setting.goal.condition) : void 0;
19323
19666
  const verifyScope = opts.verifyScope ?? setting.verifyScope;
19324
19667
  if (agent === "codex") ensureCodexTrust(repoDir);
19325
19668
  let baseSha;
@@ -19332,7 +19675,10 @@ async function startSpecSession(spec, opts) {
19332
19675
  } catch (e) {
19333
19676
  throw new LaunchError(`gagal membuat worktree: ${e.message}`, "worktree");
19334
19677
  }
19335
- if (!resume) await prisma.spec.update({ where: { id: spec.id }, data: { baseSha, headSha: null } });
19678
+ if (!resume) await prisma.spec.update({
19679
+ where: { id: spec.id },
19680
+ data: { baseSha, headSha: null, startedAt: /* @__PURE__ */ new Date() }
19681
+ });
19336
19682
  }
19337
19683
  const brief = {
19338
19684
  id: spec.id,
@@ -19342,17 +19688,18 @@ async function startSpecSession(spec, opts) {
19342
19688
  objective: spec.objective,
19343
19689
  payload: spec.payload ?? void 0
19344
19690
  };
19691
+ const resumeCtx = resume ? buildResumeCtx(repoDir, id, opts.flow, resume.worktreeKept) : void 0;
19345
19692
  let prompt;
19346
- if (isContinue) {
19693
+ if (isGoalFlow) {
19694
+ prompt = startGoalPrompt(brief, branchTo, {
19695
+ autonomy: opts.autonomy,
19696
+ verifyScope,
19697
+ resume: resumeCtx
19698
+ });
19699
+ } else if (isContinue) {
19347
19700
  prompt = continuePrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope);
19348
- } else if (resume) {
19349
- const phases = readPhases(phaseFilePath(repoDir, id), opts.flow);
19350
- const ctx = {
19351
- recorded: phases.filter((p) => p.state === "done" || p.state === "skipped").map((p) => `${p.name} ${p.state}`),
19352
- next: phases.find((p) => p.state === "active")?.name,
19353
- worktreeKept: resume.worktreeKept
19354
- };
19355
- prompt = resumePrompt(opts.flow, brief, branchTo, ctx, opts.autonomy, verifyScope);
19701
+ } else if (resumeCtx) {
19702
+ prompt = resumePrompt(opts.flow, brief, branchTo, resumeCtx, opts.autonomy, verifyScope);
19356
19703
  } else {
19357
19704
  prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope);
19358
19705
  }
@@ -19908,9 +20255,9 @@ function sshExec(t, remoteCmd, opts = {}) {
19908
20255
  return new Promise((resolve15) => {
19909
20256
  const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
19910
20257
  let out3 = "";
19911
- const timer4 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
20258
+ const timer5 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
19912
20259
  const done = (r) => {
19913
- clearTimeout(timer4);
20260
+ clearTimeout(timer5);
19914
20261
  if (askpass) rmSync4(dirname9(askpass), { recursive: true, force: true });
19915
20262
  resolve15(r);
19916
20263
  };
@@ -20681,6 +21028,9 @@ async function codex(app2) {
20681
21028
  app2.get("/codex/version", async () => codexVersionInfo());
20682
21029
  }
20683
21030
 
21031
+ // src/routes/update.ts
21032
+ init_src();
21033
+
20684
21034
  // src/services/update.ts
20685
21035
  init_src();
20686
21036
  init_config2();
@@ -20695,7 +21045,11 @@ function composeUpdate(x) {
20695
21045
  latestVersion: x.latestVersion,
20696
21046
  registry: { status: x.registryStatus, checkedAt: x.checkedAt },
20697
21047
  updateAvailable: available,
20698
- command: available ? UPDATE_COMMAND : ""
21048
+ command: available ? UPDATE_COMMAND : "",
21049
+ // Diwariskan apa adanya. Ia fakta tentang cara proses ini dilahirkan, bukan kesimpulan
21050
+ // tentang ada-tidaknya update — menurunkannya dari `available` akan menyembunyikan
21051
+ // instalasi tak-tersupervisi tepat saat tombolnya paling ingin ditekan.
21052
+ canApply: x.canApply
20699
21053
  };
20700
21054
  }
20701
21055
  var RESULT_TTL_MS = 15e3;
@@ -20716,6 +21070,15 @@ function runningVersion() {
20716
21070
  }
20717
21071
  return "0.0.0";
20718
21072
  }
21073
+ function supervised() {
21074
+ const v = process.env.HANOMAN_SUPERVISOR;
21075
+ return v === "1" || v === "true";
21076
+ }
21077
+ var exiter = null;
21078
+ function requestRestartForUpdate() {
21079
+ const delay = effectiveInt("HANOMAN_UPDATE_RESTART_DELAY_MS") ?? 250;
21080
+ setTimeout(() => (exiter ?? ((c) => process.exit(c)))(UPDATE_RESTART_EXIT), delay);
21081
+ }
20719
21082
  async function maybeFetch() {
20720
21083
  if (!effectiveBool("HANOMAN_UPDATE_FETCH")) return;
20721
21084
  if (lastFetchAt && Date.now() - lastFetchAt < FETCH_TTL_MS) return;
@@ -20748,25 +21111,64 @@ async function getUpdateStatus() {
20748
21111
  currentVersion: runningVersion(),
20749
21112
  latestVersion: lastLatest,
20750
21113
  registryStatus: lastStatus,
20751
- checkedAt: lastFetchAt ? new Date(lastFetchAt).toISOString() : null
21114
+ checkedAt: lastFetchAt ? new Date(lastFetchAt).toISOString() : null,
21115
+ canApply: supervised()
20752
21116
  });
20753
21117
  cached = { at: Date.now(), value };
20754
21118
  return value;
20755
21119
  }
20756
21120
 
20757
21121
  // src/routes/update.ts
21122
+ init_pty();
20758
21123
  async function update(app2) {
20759
21124
  app2.get("/update", async () => getUpdateStatus());
21125
+ app2.post("/update/apply", async (req, reply) => {
21126
+ const parsed = zUpdateApplyBody.safeParse(req.body ?? {});
21127
+ if (!parsed.success) return reply.code(400).send({ error: "bad-body" });
21128
+ const u = await getUpdateStatus();
21129
+ if (!u.canApply) return reply.code(409).send({ error: "unsupervised" });
21130
+ if (!u.updateAvailable) return reply.code(409).send({ error: "up-to-date", current: u.currentVersion });
21131
+ const liveSessions = listSessions().filter((s) => !s.exited).length;
21132
+ const from = u.currentVersion;
21133
+ const to = u.latestVersion;
21134
+ if (!parsed.data.confirm) return reply.code(409).send({ error: "confirm-required", liveSessions, from, to });
21135
+ requestRestartForUpdate();
21136
+ return reply.code(202).send({ accepted: true, from, to, liveSessions });
21137
+ });
20760
21138
  }
20761
21139
 
20762
21140
  // src/services/events.ts
20763
21141
  init_pty();
21142
+
21143
+ // src/services/lead/deciding.ts
21144
+ var deciding = /* @__PURE__ */ new Set();
21145
+ function markDeciding(sessionId2) {
21146
+ deciding.add(sessionId2);
21147
+ }
21148
+ function clearDeciding(sessionId2) {
21149
+ deciding.delete(sessionId2);
21150
+ }
21151
+ function isDeciding(sessionId2) {
21152
+ return deciding.has(sessionId2);
21153
+ }
21154
+ function decidingIds() {
21155
+ return [...deciding];
21156
+ }
21157
+
21158
+ // src/services/events.ts
20764
21159
  init_db();
20765
21160
  init_config2();
20766
21161
  var clients = /* @__PURE__ */ new Set();
20767
21162
  var tickMs = () => effectiveInt("HANOMAN_EVENTS_TICK_MS") ?? 1e3;
20768
21163
  var GROUPS = [
20769
- { everyTicks: 1, last: "", build: async () => ({ t: "sessions", sessions: listSessions() }) },
21164
+ // SPEC-409 · ADR-0091 · AC-3 · `deciding` menandai sesi yang sedang DISUSUN keputusannya oleh
21165
+ // hanoman-lead. Tanpa penanda ini sesi yang justru sedang dilayani terbaca persis seperti sesi
21166
+ // yang mandek — bentuknya sama: diam, marker keputusan terisi. Dihias DI SINI, bukan di pty.ts:
21167
+ // service itu sengaja tak tahu apa-apa soal lead maupun DB.
21168
+ { everyTicks: 1, last: "", build: async () => ({
21169
+ t: "sessions",
21170
+ sessions: listSessions().map((s) => isDeciding(s.id) ? { ...s, deciding: true } : s)
21171
+ }) },
20770
21172
  { everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
20771
21173
  { everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
20772
21174
  // ponytail: cermin GET /vps (orderBy createdAt asc). Query sepele — tak diekstrak.
@@ -21183,11 +21585,11 @@ function clamp(text) {
21183
21585
  const buf = Buffer.from(text, "utf8");
21184
21586
  if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
21185
21587
  const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
21186
- let tail = buf.subarray(cut).toString("utf8");
21187
- const nl = tail.indexOf("\n");
21188
- if (nl >= 0) tail = tail.slice(nl + 1);
21588
+ let tail2 = buf.subarray(cut).toString("utf8");
21589
+ const nl = tail2.indexOf("\n");
21590
+ if (nl >= 0) tail2 = tail2.slice(nl + 1);
21189
21591
  return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
21190
- ${tail}`, truncated: true };
21592
+ ${tail2}`, truncated: true };
21191
21593
  }
21192
21594
  async function saveTranscript(text) {
21193
21595
  if (!text.trim()) return { key: "", bytes: 0, truncated: false };
@@ -21742,7 +22144,7 @@ for (let i = 0; i < chars.length; i++) {
21742
22144
  intToChar[i] = c;
21743
22145
  charToInt[c] = i;
21744
22146
  }
21745
- function decodeInteger(reader, relative) {
22147
+ function decodeInteger(reader, relative2) {
21746
22148
  let value = 0;
21747
22149
  let shift = 0;
21748
22150
  let integer = 0;
@@ -21757,7 +22159,7 @@ function decodeInteger(reader, relative) {
21757
22159
  if (shouldNegate) {
21758
22160
  value = -2147483648 | -value;
21759
22161
  }
21760
- return relative + value;
22162
+ return relative2 + value;
21761
22163
  }
21762
22164
  function hasMoreVlq(reader, max) {
21763
22165
  if (reader.pos >= max) return false;
@@ -22917,6 +23319,1038 @@ async function audit_default(app2) {
22917
23319
  });
22918
23320
  }
22919
23321
 
23322
+ // src/routes/lead.ts
23323
+ init_src();
23324
+ init_db();
23325
+ init_pty();
23326
+
23327
+ // src/services/lead/config.ts
23328
+ init_db();
23329
+ init_src();
23330
+ async function getLead() {
23331
+ return (await getSetting()).lead;
23332
+ }
23333
+ async function setLead(next) {
23334
+ const cur = await getSetting();
23335
+ const data = { ...cur, lead: next };
23336
+ await prisma.setting.upsert({ where: { id: 1 }, update: { data }, create: { id: 1, data } });
23337
+ return next;
23338
+ }
23339
+ async function leadAgentDefaults() {
23340
+ const s = await getSetting();
23341
+ const e = s.lead.engine;
23342
+ if (!e.enabled) return sessionAgentDefaults();
23343
+ return e.agent === "codex" ? { agent: "codex", model: e.model, effort: coerceCodexEffort(e.model, e.effort) } : { agent: "claude", model: e.model, effort: e.effort };
23344
+ }
23345
+ function leadActive(cfg, projectId) {
23346
+ if (!cfg.enabled || cfg.paused) return false;
23347
+ if (projectId && cfg.pausedProjects.includes(projectId)) return false;
23348
+ return true;
23349
+ }
23350
+ async function leadProjects() {
23351
+ const rows = await prisma.project.findMany({ where: { leadOptIn: true }, select: { id: true } });
23352
+ return rows.map((r) => r.id);
23353
+ }
23354
+
23355
+ // src/services/lead/decide.ts
23356
+ init_src();
23357
+ init_db();
23358
+ init_pty();
23359
+
23360
+ // src/services/lead/prompt.ts
23361
+ init_src();
23362
+ var bullet2 = (s) => `- ${s}`;
23363
+ function leadPrompt(q, c) {
23364
+ const lines2 = [];
23365
+ lines2.push("Kamu adalah **hanoman-lead**: tech lead mesin di atas semua agen yang bekerja di workspace ini.");
23366
+ lines2.push("Kamu MEMUTUSKAN, lalu melapor. Tidak ada manusia yang menunggu untuk menyetujui jawabanmu.");
23367
+ lines2.push("");
23368
+ lines2.push("## Konteks");
23369
+ lines2.push(bullet2(`Project: ${c.projectName} (${c.projectId})`));
23370
+ if (c.repoDir) lines2.push(bullet2(`Checkout: ${c.repoDir}`));
23371
+ if (c.spec) {
23372
+ lines2.push(bullet2(`Backlog item: ${c.spec.id} \u2014 ${c.spec.title} \xB7 stage ${c.spec.stage} \xB7 prioritas ${c.spec.priority}`));
23373
+ if (c.spec.objective) lines2.push(bullet2(`Objective: ${c.spec.objective}`));
23374
+ }
23375
+ for (const s of c.liveSessions ?? []) {
23376
+ lines2.push(bullet2(`Sesi berjalan: ${s.id}${s.specId ? ` (${s.specId})` : ""}${s.flow ? ` \xB7 flow ${s.flow}` : ""}${s.branch ? ` \xB7 branch ${s.branch}` : ""}`));
23377
+ }
23378
+ for (const n of c.notes ?? []) lines2.push(bullet2(n));
23379
+ lines2.push("");
23380
+ if (c.priorDecisions?.length) {
23381
+ lines2.push("## Keputusan yang sudah kamu ambil sebelumnya (jangan bertentangan tanpa alasan)");
23382
+ for (const d of c.priorDecisions.slice(0, 10)) {
23383
+ lines2.push(`- [${d.createdAt}] "${d.question.slice(0, 200)}" \u2192 ${d.answer.slice(0, 300)} (${d.reason.slice(0, 200)})`);
23384
+ }
23385
+ lines2.push("");
23386
+ }
23387
+ lines2.push("## Yang harus kamu putuskan");
23388
+ lines2.push(q.question.trim());
23389
+ if (q.options?.length) {
23390
+ lines2.push("");
23391
+ lines2.push("Opsi yang dilihat peminta:");
23392
+ for (const [i, o] of q.options.entries()) lines2.push(`${i + 1}. ${o}`);
23393
+ }
23394
+ lines2.push("");
23395
+ lines2.push("## Cara kerja");
23396
+ lines2.push("1. KUMPULKAN BUKTI DULU sebelum memutuskan: `internal/docs/**` (Source of Truth) dan index-nya, ADR yang relevan, plan `docs/superpowers/plans/**`, kode yang bersangkutan, dan riwayat git. Baca, jangan mengingat.");
23397
+ lines2.push('2. Putuskan. Kalau setelah membaca kamu masih ragu, TETAP putuskan: pilih opsi yang PALING MUDAH DIBATALKAN, lalu tandai `confidence: "ragu"`. Jangan pernah menjawab "tidak tahu" atau meminta manusia memutuskan \u2014 itu persis keadaan yang kamu ada untuk menghapusnya.');
23398
+ lines2.push("3. Rujuk bukti yang BENAR-BENAR kamu baca. Rujukan berupa path berkas relatif terhadap checkout, nomor ADR (`ADR-0091`), atau sha commit. Rujukan yang tak ada di repo akan dibuang server dan membuat jawabanmu tampak tanpa dasar.");
23399
+ 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.");
23400
+ 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:");
23401
+ lines2.push(` ${LEAD_ACTIONS.join(" \xB7 ")}`);
23402
+ lines2.push(" Deploy, perintah/konsol VPS, data produksi, dan penghapusan apa pun (project, backlog, branch, worktree, notifikasi, jejak) TERKUNCI dan tidak akan pernah dijalankan.");
23403
+ lines2.push("");
23404
+ lines2.push("## Bentuk jawaban (WAJIB)");
23405
+ lines2.push("Akhiri jawabanmu dengan TEPAT SATU blok berikut, tanpa teks sesudahnya:");
23406
+ lines2.push("");
23407
+ lines2.push("```json");
23408
+ lines2.push(JSON.stringify({
23409
+ decision: "keputusan yang dipilih, satu kalimat",
23410
+ reason: "alasannya, menyebut bukti",
23411
+ refs: ["internal/docs/\u2026", "ADR-00xx"],
23412
+ confidence: "tinggi | sedang | ragu",
23413
+ action: "none",
23414
+ reply: "teks yang akan diketikkan ke terminal agen peminta (kosongkan bila tak relevan)"
23415
+ }, null, 2));
23416
+ lines2.push("```");
23417
+ return lines2.join("\n");
23418
+ }
23419
+
23420
+ // src/services/lead/verdict.ts
23421
+ init_src();
23422
+ import { existsSync as existsSync10 } from "node:fs";
23423
+ import { isAbsolute as isAbsolute3, resolve as resolvePath, relative } from "node:path";
23424
+ function extractJsonBlock(raw) {
23425
+ const re = /```json\s*\n([\s\S]*?)```/g;
23426
+ let last = null;
23427
+ for (const m of raw.matchAll(re)) if (m[1]) last = m[1];
23428
+ if (last) return last;
23429
+ const t = raw.trim();
23430
+ return t.startsWith("{") && t.endsWith("}") ? t : null;
23431
+ }
23432
+ function parseLeadVerdict(raw) {
23433
+ const block2 = extractJsonBlock(raw);
23434
+ if (!block2) return null;
23435
+ let data;
23436
+ try {
23437
+ data = JSON.parse(block2);
23438
+ } catch {
23439
+ return null;
23440
+ }
23441
+ const parsed = zLeadVerdict.safeParse(data);
23442
+ return parsed.success ? parsed.data : null;
23443
+ }
23444
+ var SHA = /^[0-9a-f]{7,40}$/i;
23445
+ var isNonFileRef = (ref) => SHA.test(ref) || /^ADR-\d{4}$/i.test(ref);
23446
+ function keepExistingRefs(refs2, repoDir) {
23447
+ const out3 = [];
23448
+ for (const raw of refs2) {
23449
+ const ref = raw.trim();
23450
+ if (!ref) continue;
23451
+ if (isNonFileRef(ref)) {
23452
+ out3.push(ref);
23453
+ continue;
23454
+ }
23455
+ if (!repoDir) continue;
23456
+ if (isAbsolute3(ref)) continue;
23457
+ const full = resolvePath(repoDir, ref);
23458
+ const rel = relative(repoDir, full);
23459
+ if (rel.startsWith("..")) continue;
23460
+ if (existsSync10(full)) out3.push(ref);
23461
+ }
23462
+ return [...new Set(out3)];
23463
+ }
23464
+
23465
+ // src/services/lead/trail.ts
23466
+ init_db();
23467
+ async function recordDecision(i) {
23468
+ return prisma.leadDecision.create({
23469
+ data: {
23470
+ projectId: i.projectId,
23471
+ specId: i.specId ?? null,
23472
+ sessionId: i.sessionId ?? null,
23473
+ gate: i.gate,
23474
+ kind: i.kind,
23475
+ question: i.question,
23476
+ answer: i.answer,
23477
+ reason: i.reason,
23478
+ refs: i.refs,
23479
+ confidence: i.confidence,
23480
+ action: i.action,
23481
+ status: i.status ?? "berlaku",
23482
+ weighty: i.weighty ?? false,
23483
+ actor: i.actor ?? "lead"
23484
+ }
23485
+ });
23486
+ }
23487
+ async function listDecisions(f = {}) {
23488
+ return prisma.leadDecision.findMany({
23489
+ where: {
23490
+ ...f.projectId ? { projectId: f.projectId } : {},
23491
+ ...f.specId ? { specId: f.specId } : {},
23492
+ ...f.sessionId ? { sessionId: f.sessionId } : {},
23493
+ ...f.status ? { status: f.status } : {}
23494
+ },
23495
+ orderBy: { createdAt: "desc" },
23496
+ take: Math.min(f.take ?? 50, 200),
23497
+ skip: f.skip ?? 0
23498
+ });
23499
+ }
23500
+ async function overrideDecision(id, answer, reason) {
23501
+ const old = await prisma.leadDecision.findUnique({ where: { id } });
23502
+ if (!old || old.status !== "berlaku") return null;
23503
+ const next = await recordDecision({
23504
+ projectId: old.projectId,
23505
+ specId: old.specId,
23506
+ sessionId: old.sessionId,
23507
+ gate: old.gate,
23508
+ kind: old.kind,
23509
+ question: old.question,
23510
+ answer,
23511
+ reason: reason || "ditimpa operator",
23512
+ refs: [],
23513
+ confidence: "tinggi",
23514
+ action: "none",
23515
+ actor: "operator"
23516
+ });
23517
+ const updated = await prisma.leadDecision.update({
23518
+ where: { id },
23519
+ data: { status: "ditimpa", supersededById: next.id }
23520
+ });
23521
+ return { old: updated, next };
23522
+ }
23523
+ async function cancelDecision(id) {
23524
+ const row = await prisma.leadDecision.findUnique({ where: { id } });
23525
+ if (!row || row.status !== "berlaku") return null;
23526
+ return prisma.leadDecision.update({ where: { id }, data: { status: "dibatalkan" } });
23527
+ }
23528
+ function toDecisionView(r) {
23529
+ return {
23530
+ id: r.id,
23531
+ projectId: r.projectId,
23532
+ specId: r.specId,
23533
+ sessionId: r.sessionId,
23534
+ gate: r.gate,
23535
+ kind: r.kind,
23536
+ question: r.question,
23537
+ answer: r.answer,
23538
+ reason: r.reason,
23539
+ refs: Array.isArray(r.refs) ? r.refs.map(String) : [],
23540
+ confidence: r.confidence,
23541
+ action: r.action,
23542
+ status: r.status,
23543
+ weighty: r.weighty,
23544
+ supersededById: r.supersededById,
23545
+ createdAt: r.createdAt.toISOString()
23546
+ };
23547
+ }
23548
+
23549
+ // src/services/lead/brain.ts
23550
+ init_config2();
23551
+ import { execFile as execFile9 } from "node:child_process";
23552
+ var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
23553
+ function leadArgv(o) {
23554
+ if (o.agent === "codex") {
23555
+ return [
23556
+ "exec",
23557
+ ...o.model ? ["-m", o.model] : [],
23558
+ ...o.effort ? ["-c", `model_reasoning_effort="${o.effort}"`] : [],
23559
+ "--dangerously-bypass-approvals-and-sandbox",
23560
+ o.prompt
23561
+ ];
23562
+ }
23563
+ return [
23564
+ "-p",
23565
+ ...o.model ? ["--model", o.model] : [],
23566
+ ...o.effort ? ["--effort", o.effort] : [],
23567
+ "--dangerously-skip-permissions",
23568
+ o.prompt
23569
+ ];
23570
+ }
23571
+ function think(prompt, o) {
23572
+ const bin = binFor(o.agent);
23573
+ const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
23574
+ return new Promise((resolve15, reject) => {
23575
+ execFile9(bin, args, {
23576
+ cwd: o.cwd,
23577
+ timeout: o.timeoutMs,
23578
+ maxBuffer: 16 * 1024 * 1024,
23579
+ encoding: "utf8",
23580
+ killSignal: "SIGTERM"
23581
+ }, (err, stdout, stderr) => {
23582
+ if (err) {
23583
+ const killed = err.killed;
23584
+ reject(new Error(killed ? `lead ${o.agent} kehabisan waktu ${o.timeoutMs} ms` : `lead ${o.agent} gagal: ${(stderr || err.message).trim().slice(0, 500)}`));
23585
+ return;
23586
+ }
23587
+ resolve15(stdout);
23588
+ });
23589
+ });
23590
+ }
23591
+
23592
+ // src/services/lead/decide.ts
23593
+ var prodDecideDeps = {
23594
+ think,
23595
+ defaults: leadAgentDefaults,
23596
+ repoDir: resolveRepoDir,
23597
+ liveSessions: () => {
23598
+ try {
23599
+ return listSessions();
23600
+ } catch {
23601
+ return [];
23602
+ }
23603
+ },
23604
+ notify: recordLeadDecision
23605
+ };
23606
+ var notifTitle = (kind, question, answer, confidence) => {
23607
+ const q = question.replace(/\s+/g, " ").trim().slice(0, 70);
23608
+ const a = answer.replace(/\s+/g, " ").trim().slice(0, 90);
23609
+ const tag2 = confidence === "ragu" ? "Lead (ragu)" : "Lead";
23610
+ return `${tag2} \xB7 ${kind}: ${q ? `"${q}" \u2192 ` : ""}${a}`;
23611
+ };
23612
+ async function decide(req, deps = prodDecideDeps) {
23613
+ const cfg = await getLead();
23614
+ if (!leadActive(cfg, req.projectId)) return null;
23615
+ const project = await prisma.project.findUnique({ where: { id: req.projectId }, select: { name: true } });
23616
+ const spec = req.specId ? await prisma.spec.findUnique({
23617
+ where: { id: req.specId },
23618
+ select: { id: true, title: true, objective: true, stage: true, priority: true }
23619
+ }) : null;
23620
+ const repoDir = await deps.repoDir(req.projectId).catch(() => null);
23621
+ const prior = await prisma.leadDecision.findMany({
23622
+ where: { projectId: req.projectId, status: "berlaku" },
23623
+ orderBy: { createdAt: "desc" },
23624
+ take: 10
23625
+ });
23626
+ const ctx = {
23627
+ projectId: req.projectId,
23628
+ projectName: project?.name ?? req.projectId,
23629
+ repoDir,
23630
+ spec,
23631
+ liveSessions: deps.liveSessions().filter((s) => !s.exited && s.projectId === req.projectId && s.id !== req.sessionId).map((s) => ({ id: s.id, specId: s.specId, flow: s.flow, branch: s.branch })),
23632
+ priorDecisions: prior.map((d) => ({
23633
+ question: d.question,
23634
+ answer: d.answer,
23635
+ reason: d.reason,
23636
+ createdAt: d.createdAt.toISOString()
23637
+ })),
23638
+ notes: req.notes
23639
+ };
23640
+ const { agent, model, effort } = await deps.defaults();
23641
+ const prompt = leadPrompt({ kind: req.kind, question: req.question, options: req.options }, ctx);
23642
+ if (req.sessionId) markDeciding(req.sessionId);
23643
+ let raw;
23644
+ try {
23645
+ raw = await deps.think(prompt, { agent, model, effort, cwd: repoDir ?? void 0, timeoutMs: cfg.timeoutSec * 1e3 });
23646
+ } catch (e) {
23647
+ return fail(req, deps, `lead tak menghasilkan keputusan: ${e.message}`);
23648
+ } finally {
23649
+ if (req.sessionId) clearDeciding(req.sessionId);
23650
+ }
23651
+ const verdict = parseLeadVerdict(raw);
23652
+ if (!verdict) return fail(req, deps, "keluaran lead tak memuat blok json keputusan yang sah");
23653
+ const refs2 = keepExistingRefs(verdict.refs, repoDir);
23654
+ const allowed = leadActionAllowed(verdict.action);
23655
+ const action = allowed ? verdict.action : "none";
23656
+ const kind = allowed ? req.kind : "refusal";
23657
+ const reason = allowed ? verdict.reason : `${verdict.reason}
23658
+
23659
+ DITOLAK: ${leadRefusalReason(verdict.action)} berada di luar permukaan tindakan lead (ADR-0091 \xB7 AC-31/32).`;
23660
+ const weighty = isWeightyDecision({ kind, action, confidence: verdict.confidence });
23661
+ const row = await recordDecision({
23662
+ projectId: req.projectId,
23663
+ specId: req.specId,
23664
+ sessionId: req.sessionId,
23665
+ gate: req.gate,
23666
+ kind,
23667
+ question: req.question,
23668
+ answer: verdict.decision,
23669
+ reason,
23670
+ refs: refs2,
23671
+ confidence: verdict.confidence,
23672
+ action,
23673
+ weighty
23674
+ });
23675
+ if (weighty) {
23676
+ await deps.notify(
23677
+ row.id,
23678
+ notifTitle(kind, req.question, verdict.decision, verdict.confidence),
23679
+ req.projectId,
23680
+ req.specId ?? null,
23681
+ req.sessionId ?? null
23682
+ );
23683
+ }
23684
+ lastReply.set(row.id, verdict.reply || verdict.decision);
23685
+ return row;
23686
+ }
23687
+ var lastReply = /* @__PURE__ */ new Map();
23688
+ function takeReply(decisionId) {
23689
+ const v = lastReply.get(decisionId) ?? "";
23690
+ lastReply.delete(decisionId);
23691
+ return v;
23692
+ }
23693
+ async function fail(req, deps, reason) {
23694
+ const row = await recordDecision({
23695
+ projectId: req.projectId,
23696
+ specId: req.specId,
23697
+ sessionId: req.sessionId,
23698
+ gate: req.gate,
23699
+ kind: req.kind,
23700
+ question: req.question,
23701
+ answer: "",
23702
+ reason,
23703
+ refs: [],
23704
+ confidence: "ragu",
23705
+ action: "none",
23706
+ status: "gagal",
23707
+ weighty: true
23708
+ });
23709
+ await deps.notify(
23710
+ row.id,
23711
+ `Lead gagal memutuskan: ${req.question.replace(/\s+/g, " ").slice(0, 90)}`,
23712
+ req.projectId,
23713
+ req.specId ?? null,
23714
+ req.sessionId ?? null
23715
+ );
23716
+ return row;
23717
+ }
23718
+
23719
+ // src/services/lead/apply.ts
23720
+ init_src();
23721
+ init_db();
23722
+ init_pty();
23723
+ init_session_phases();
23724
+ var prodApplyDeps = {
23725
+ killSession: (id) => {
23726
+ try {
23727
+ return killSession(id);
23728
+ } catch {
23729
+ return false;
23730
+ }
23731
+ },
23732
+ sessionExists: (id) => {
23733
+ try {
23734
+ return !!getSession(id);
23735
+ } catch {
23736
+ return false;
23737
+ }
23738
+ },
23739
+ startSpec: startSpecSession,
23740
+ repoDir: resolveRepoDir,
23741
+ planDone: planComplete,
23742
+ integrate,
23743
+ notify: recordLeadDecision
23744
+ };
23745
+ async function applyAction(row, deps = prodApplyDeps) {
23746
+ const action = row.action;
23747
+ if (!leadActionAllowed(action)) {
23748
+ await deps.notify(
23749
+ row.id,
23750
+ `Lead menolak tindakan terkunci: ${leadRefusalReason(action)}`,
23751
+ row.projectId,
23752
+ row.specId,
23753
+ row.sessionId
23754
+ );
23755
+ return { ok: false, detail: `tindakan terkunci: ${leadRefusalReason(action)}` };
23756
+ }
23757
+ switch (action) {
23758
+ // Keputusan yang wujudnya memang hanya baris jejak.
23759
+ case "none":
23760
+ case "answer-session":
23761
+ // sudah diketikkan ke pane oleh pintu deteksi
23762
+ case "order-queue":
23763
+ return { ok: true, detail: "tak ada tindakan tersisa" };
23764
+ // Ditata sebagai keputusan, dieksekusi operator di versi ini (lihat ADR-0091 §Konsekuensi).
23765
+ case "hold-work":
23766
+ case "push-branch":
23767
+ case "run-migration":
23768
+ return { ok: true, detail: `dicatat sebagai keputusan; "${action}" dieksekusi operator di versi ini` };
23769
+ case "stop-session":
23770
+ return stopSession(row, deps);
23771
+ case "start-session":
23772
+ case "resume-session":
23773
+ case "restart-session":
23774
+ return relaunch(row, deps);
23775
+ case "integrate-main":
23776
+ return integrateMain(row, deps);
23777
+ }
23778
+ }
23779
+ function stopSession(row, deps) {
23780
+ if (!row.sessionId) return { ok: false, detail: "keputusan ini tak menunjuk sesi" };
23781
+ const killed = deps.killSession(row.sessionId);
23782
+ return killed ? { ok: true, detail: `sesi ${row.sessionId} dihentikan; worktree dibiarkan utuh (AC-32a)` } : { ok: false, detail: `sesi ${row.sessionId} tak ada` };
23783
+ }
23784
+ async function relaunch(row, deps) {
23785
+ if (!row.specId) return { ok: false, detail: "keputusan ini tak menunjuk backlog" };
23786
+ const spec = await prisma.spec.findUnique({ where: { id: row.specId } });
23787
+ if (!spec) return { ok: false, detail: `backlog ${row.specId} tak ada` };
23788
+ const sid = sessionIdForSpec(spec.id);
23789
+ if (deps.sessionExists(sid)) deps.killSession(sid);
23790
+ const r = await deps.startSpec(spec, { flow: flowForSource(spec.source) });
23791
+ return { ok: true, detail: `sesi ${r.id}${r.resumed ? " dilanjutkan" : " dimulai"}` };
23792
+ }
23793
+ async function integrateMain(row, deps) {
23794
+ if (!row.specId) return { ok: false, detail: "keputusan ini tak menunjuk backlog" };
23795
+ const cfg = await getLead();
23796
+ const repoDir = await deps.repoDir(row.projectId);
23797
+ if (!repoDir) return { ok: false, detail: `project ${row.projectId} belum di-bind ke checkout lokal` };
23798
+ const spec = await prisma.spec.findUnique({ where: { id: row.specId } });
23799
+ if (!spec) return { ok: false, detail: `backlog ${row.specId} tak ada` };
23800
+ const evidence = [];
23801
+ if (cfg.requireGreenBeforeIntegrate) {
23802
+ const wt = worktreeDir(repoDir, spec.id);
23803
+ const done = deps.planDone(wt, spec.id);
23804
+ evidence.push(done ? "plan tak menyisakan `- [ ]`" : "plan MASIH menyisakan `- [ ]`");
23805
+ const sid = sessionIdForSpec(spec.id);
23806
+ const pane = deps.sessionExists(sid);
23807
+ evidence.push(pane ? `pane ${sid} masih ada` : `pane ${sid} sudah tak ada`);
23808
+ if (!done) {
23809
+ await recordEvidence(row, evidence, "integrasi DIBATALKAN \u2014 syarat objektif tak terpenuhi");
23810
+ await deps.notify(
23811
+ row.id,
23812
+ `Lead membatalkan integrasi ${spec.id}: plan belum tuntas`,
23813
+ row.projectId,
23814
+ row.specId,
23815
+ row.sessionId
23816
+ );
23817
+ return { ok: false, detail: "syarat integrasi tak terpenuhi: plan masih menyisakan `- [ ]`" };
23818
+ }
23819
+ } else {
23820
+ evidence.push("syarat objektif dimatikan operator (lead.requireGreenBeforeIntegrate = false)");
23821
+ }
23822
+ const res = await deps.integrate(repoDir, spec.id, "merge", "local:main");
23823
+ evidence.push(`integrate \u2192 ${res.status}`);
23824
+ await recordEvidence(row, evidence, res.status === "clean" ? "integrasi bersih" : `integrasi tak bersih: ${res.status}`);
23825
+ if (res.status !== "clean") {
23826
+ await deps.notify(
23827
+ row.id,
23828
+ `Integrasi ${spec.id} oleh lead tak bersih (${res.status}) \u2014 butuh operator`,
23829
+ row.projectId,
23830
+ row.specId,
23831
+ row.sessionId
23832
+ );
23833
+ }
23834
+ return { ok: res.status === "clean", detail: evidence.join("; ") };
23835
+ }
23836
+ async function recordEvidence(row, evidence, verdict) {
23837
+ await prisma.leadDecision.update({
23838
+ where: { id: row.id },
23839
+ data: { reason: `${row.reason}
23840
+
23841
+ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
23842
+ }).catch(() => {
23843
+ });
23844
+ }
23845
+
23846
+ // src/services/lead/detect.ts
23847
+ init_pty();
23848
+
23849
+ // src/services/lead/pane.ts
23850
+ var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
23851
+ var tail = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
23852
+ var CODEX_FINISHED = [
23853
+ /Goal achieved/i,
23854
+ /Goal unmet/i,
23855
+ /\btokens used\b/i,
23856
+ /\bTo continue this session\b/i
23857
+ ];
23858
+ var ASK_SIGNALS = [
23859
+ /\?\s*$/m,
23860
+ /^\s*\[?\d+[).\]]\s+\S/m,
23861
+ /\b(pilih|apakah|haruskah|mana yang|opsi|which|should i|do you want|proceed\?)\b/i
23862
+ ];
23863
+ function readPaneQuestion(text, agent) {
23864
+ const lines2 = tail(text, 40);
23865
+ const body = lines2.join("\n").trim();
23866
+ if (!body) return { asking: false, question: "", reason: "layar kosong" };
23867
+ const question = tail(text, 25).join("\n").trim();
23868
+ if (agent === "codex") {
23869
+ const finished = CODEX_FINISHED.find((re) => re.test(body));
23870
+ if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)" };
23871
+ if (!ASK_SIGNALS.some((re) => re.test(body)))
23872
+ return { asking: false, question, reason: "tak ada sinyal pertanyaan di layar codex" };
23873
+ }
23874
+ return { asking: true, question, reason: "" };
23875
+ }
23876
+
23877
+ // src/services/lead/detect.ts
23878
+ var answers = /* @__PURE__ */ new Map();
23879
+ var capped = /* @__PURE__ */ new Set();
23880
+ function resetSession(sessionId2) {
23881
+ answers.delete(sessionId2);
23882
+ capped.delete(sessionId2);
23883
+ }
23884
+ var prodDetectDeps = {
23885
+ live: () => {
23886
+ try {
23887
+ return liveDecisions();
23888
+ } catch {
23889
+ return [];
23890
+ }
23891
+ },
23892
+ filled: markerFilled,
23893
+ pane: (id) => capturePane(id),
23894
+ agentOf: (id) => {
23895
+ try {
23896
+ return getSession(id)?.agent ?? null;
23897
+ } catch {
23898
+ return null;
23899
+ }
23900
+ },
23901
+ // SPEC-402 · tmux tak terbaca ≠ pane mati. Ragu → perlakukan sebagai mati: yang hilang cuma satu
23902
+ // jawaban otomatis (sesi jatuh ke perilaku hari ini), sementara salah arah membuat lead mengetik
23903
+ // ke pane yang sudah tak ada.
23904
+ exited: (id) => {
23905
+ try {
23906
+ return getSession(id)?.exited ?? true;
23907
+ } catch {
23908
+ return true;
23909
+ }
23910
+ },
23911
+ send: (id, text) => sendToPane(id, text),
23912
+ decide,
23913
+ decideDeps: prodDecideDeps,
23914
+ optIn: leadProjects,
23915
+ notify: recordLeadDecision,
23916
+ cfg: getLead
23917
+ };
23918
+ async function scanAndAnswer(deps = prodDetectDeps) {
23919
+ const out3 = { answered: [], skipped: [] };
23920
+ const cfg = await deps.cfg();
23921
+ if (!cfg.enabled || cfg.paused) return out3;
23922
+ const optIn = new Set(await deps.optIn());
23923
+ const sessions = deps.live();
23924
+ sweep(sessions.map((s) => s.id));
23925
+ for (const s of sessions) {
23926
+ const skip = (reason) => out3.skipped.push({ id: s.id, reason });
23927
+ if (!optIn.has(s.projectId)) {
23928
+ skip("project tak opt-in lead");
23929
+ continue;
23930
+ }
23931
+ if (!leadActive(cfg, s.projectId)) {
23932
+ skip("lead dijeda untuk project ini");
23933
+ continue;
23934
+ }
23935
+ if (!deps.filled(s.decisionFile)) continue;
23936
+ if (deps.exited(s.id)) {
23937
+ skip("pane mati");
23938
+ continue;
23939
+ }
23940
+ if ((answers.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
23941
+ if (!capped.has(s.id)) {
23942
+ capped.add(s.id);
23943
+ const row2 = await recordDecision({
23944
+ projectId: s.projectId,
23945
+ specId: s.specId,
23946
+ sessionId: s.id,
23947
+ gate: "detected",
23948
+ kind: "quality",
23949
+ question: `Sesi ${s.id} sudah dijawab otomatis ${cfg.maxAutoAnswers}\xD7 berturut-turut.`,
23950
+ answer: "Berhenti menjawab sesi ini; serahkan ke operator.",
23951
+ reason: "Batas jawaban otomatis per sesi tercapai \u2014 pengulangan menandakan lead tak benar-benar membuka jalan buntunya (AC-11).",
23952
+ refs: [],
23953
+ confidence: "tinggi",
23954
+ action: "none",
23955
+ weighty: true
23956
+ });
23957
+ await deps.notify(
23958
+ row2.id,
23959
+ `Lead berhenti menjawab sesi ${s.id} (batas ${cfg.maxAutoAnswers}\xD7 tercapai)`,
23960
+ s.projectId,
23961
+ s.specId ?? null,
23962
+ s.id
23963
+ );
23964
+ }
23965
+ skip("batas jawaban otomatis tercapai");
23966
+ continue;
23967
+ }
23968
+ const agent = deps.agentOf(s.id) ?? "claude";
23969
+ const read = readPaneQuestion(deps.pane(s.id), agent);
23970
+ if (!read.asking) {
23971
+ skip(read.reason);
23972
+ continue;
23973
+ }
23974
+ const row = await deps.decide({
23975
+ projectId: s.projectId,
23976
+ specId: s.specId,
23977
+ sessionId: s.id,
23978
+ gate: "detected",
23979
+ kind: "answer",
23980
+ question: read.question,
23981
+ notes: [`Sesi ini menunggu di terminal; teks di bawah adalah layar terakhirnya. Jawablah sebagai masukan yang bisa langsung diketik ke terminal itu (isi \`reply\`).`]
23982
+ }, deps.decideDeps);
23983
+ if (!row || row.status !== "berlaku") {
23984
+ skip("lead tak menghasilkan keputusan yang berlaku");
23985
+ continue;
23986
+ }
23987
+ const reply = takeReply(row.id) || row.answer;
23988
+ const sent = await deps.send(s.id, reply);
23989
+ if (!sent) {
23990
+ skip("gagal mengetik ke pane");
23991
+ continue;
23992
+ }
23993
+ answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
23994
+ out3.answered.push(s.id);
23995
+ }
23996
+ return out3;
23997
+ }
23998
+ function sweep(liveIds) {
23999
+ const live = new Set(liveIds);
24000
+ for (const id of [...answers.keys()]) if (!live.has(id)) answers.delete(id);
24001
+ for (const id of [...capped]) if (!live.has(id)) capped.delete(id);
24002
+ }
24003
+
24004
+ // src/services/lead/pulse.ts
24005
+ init_db();
24006
+ init_pty();
24007
+ init_session_phases();
24008
+ var moduleOf = (p) => p.split("/").slice(0, 2).join("/");
24009
+ function findCollisions(areas) {
24010
+ const out3 = [];
24011
+ for (let i = 0; i < areas.length; i++) {
24012
+ for (let j = i + 1; j < areas.length; j++) {
24013
+ const a = areas[i], b = areas[j];
24014
+ if (a.projectId !== b.projectId) continue;
24015
+ const bp = new Set(b.paths);
24016
+ const shared = a.paths.filter((p) => bp.has(p));
24017
+ const bm = new Set(b.paths.map(moduleOf));
24018
+ const nearby = [...new Set(a.paths.map(moduleOf))].filter((m) => bm.has(m) && !shared.some((s) => moduleOf(s) === m));
24019
+ if (shared.length || nearby.length) out3.push({ a, b, shared, nearby });
24020
+ }
24021
+ }
24022
+ return out3;
24023
+ }
24024
+ var prodPulseDeps = {
24025
+ sessions: () => {
24026
+ try {
24027
+ return listSessions();
24028
+ } catch {
24029
+ return [];
24030
+ }
24031
+ },
24032
+ areas: async (s) => {
24033
+ const repoDir = await resolveRepoDir(s.projectId);
24034
+ if (!repoDir) return [];
24035
+ const spec = await prisma.spec.findUnique({ where: { id: s.specId }, select: { baseSha: true, branchFrom: true } });
24036
+ if (!spec) return [];
24037
+ try {
24038
+ const r = await specReview(repoDir, s.specId, spec.baseSha, spec.branchFrom);
24039
+ return r.changed.map((c) => c.path);
24040
+ } catch {
24041
+ return [];
24042
+ }
24043
+ },
24044
+ planDone: planComplete,
24045
+ decide,
24046
+ decideDeps: prodDecideDeps,
24047
+ apply: applyAction,
24048
+ enqueue,
24049
+ notify: recordLeadDecision,
24050
+ optIn: leadProjects,
24051
+ cfg: getLead
24052
+ };
24053
+ var lastReadySig = /* @__PURE__ */ new Map();
24054
+ async function pulse(deps = prodPulseDeps) {
24055
+ const res = { ordered: 0, collisions: 0, quality: 0 };
24056
+ const cfg = await deps.cfg();
24057
+ if (!cfg.enabled || cfg.paused) return res;
24058
+ const optIn = (await deps.optIn()).filter((p) => leadActive(cfg, p));
24059
+ if (!optIn.length) return res;
24060
+ try {
24061
+ res.quality = await followUpFinished(cfg, optIn, deps);
24062
+ } catch {
24063
+ }
24064
+ try {
24065
+ res.collisions = await detectCollisions(optIn, deps);
24066
+ } catch {
24067
+ }
24068
+ try {
24069
+ res.ordered = await orderReadyWork(optIn, deps);
24070
+ } catch {
24071
+ }
24072
+ return res;
24073
+ }
24074
+ async function followUpFinished(cfg, optIn, deps) {
24075
+ let n = 0;
24076
+ const opt = new Set(optIn);
24077
+ for (const s of deps.sessions()) {
24078
+ if (!s.exited || !s.specId || !opt.has(s.projectId)) continue;
24079
+ const bad = (s.exitCode ?? 0) !== 0;
24080
+ const unfinished = !deps.planDone(s.cwd, s.specId);
24081
+ if (!bad && !unfinished) continue;
24082
+ const seen = await prisma.leadDecision.findFirst({ where: { sessionId: s.id, kind: "quality", gate: "pulse" } });
24083
+ if (seen) continue;
24084
+ const why = [
24085
+ bad ? `berakhir dengan kode keluar ${s.exitCode}` : null,
24086
+ unfinished ? "plan-nya masih menyisakan kotak `- [ ]`" : null
24087
+ ].filter(Boolean).join(" dan ");
24088
+ const row = await deps.decide({
24089
+ projectId: s.projectId,
24090
+ specId: s.specId,
24091
+ sessionId: s.id,
24092
+ gate: "pulse",
24093
+ kind: "quality",
24094
+ question: `Sesi ${s.id} untuk backlog ${s.specId} ${why}. Tindak lanjutnya apa: lanjutkan pekerjaan yang terputus, ulangi dari awal, atau hentikan?`,
24095
+ options: [
24096
+ "resume-session \u2014 lanjutkan dari keadaan worktree sekarang (ADR-0084)",
24097
+ "restart-session \u2014 ulangi dari awal",
24098
+ "none \u2014 terima apa adanya, sertakan alasannya"
24099
+ ],
24100
+ notes: [`Worktree sesi: ${s.cwd}`]
24101
+ }, deps.decideDeps);
24102
+ if (!row) continue;
24103
+ n++;
24104
+ if (row.status === "berlaku" && row.action !== "none") {
24105
+ try {
24106
+ await deps.apply(row);
24107
+ } catch {
24108
+ }
24109
+ }
24110
+ }
24111
+ return n;
24112
+ }
24113
+ async function detectCollisions(optIn, deps) {
24114
+ const opt = new Set(optIn);
24115
+ const live = deps.sessions().filter((s) => !s.exited && s.specId && opt.has(s.projectId));
24116
+ if (live.length < 2) return 0;
24117
+ const areas = [];
24118
+ for (const s of live) {
24119
+ const paths2 = await deps.areas({ id: s.id, projectId: s.projectId, specId: s.specId });
24120
+ if (paths2.length) areas.push({ specId: s.specId, sessionId: s.id, projectId: s.projectId, paths: paths2 });
24121
+ }
24122
+ let n = 0;
24123
+ for (const c of findCollisions(areas)) {
24124
+ const key = [c.a.sessionId, c.b.sessionId].sort().join("|");
24125
+ const seen = await prisma.leadDecision.findFirst({
24126
+ where: { kind: "collision", gate: "pulse", question: { contains: key } }
24127
+ });
24128
+ if (seen) continue;
24129
+ const row = await deps.decide({
24130
+ projectId: c.a.projectId,
24131
+ specId: c.a.specId,
24132
+ sessionId: c.a.sessionId,
24133
+ gate: "pulse",
24134
+ kind: "collision",
24135
+ question: `Dua pekerjaan menyentuh area yang sama [${key}]: ${c.a.specId} dan ${c.b.specId}. Tunda salah satu, gabungkan, atau biarkan?`,
24136
+ options: [
24137
+ `hold-work \u2014 tunda salah satu sampai yang lain terintegrasi`,
24138
+ `none \u2014 biarkan berjalan, sertakan alasan kenapa tabrakan ini aman`
24139
+ ],
24140
+ notes: [
24141
+ c.shared.length ? `Berkas yang sama: ${c.shared.slice(0, 20).join(", ")}` : "",
24142
+ c.nearby.length ? `Modul yang sama: ${c.nearby.slice(0, 20).join(", ")}` : ""
24143
+ ].filter(Boolean)
24144
+ }, deps.decideDeps);
24145
+ if (row) n++;
24146
+ }
24147
+ return n;
24148
+ }
24149
+ async function orderReadyWork(optIn, deps) {
24150
+ let total = 0;
24151
+ for (const projectId of optIn) total += await orderProject(projectId, deps);
24152
+ return total;
24153
+ }
24154
+ async function orderProject(projectId, deps) {
24155
+ const ready = await prisma.spec.findMany({
24156
+ where: { baseSha: null, projectId },
24157
+ select: { id: true, projectId: true, title: true, priority: true, objective: true },
24158
+ orderBy: { id: "asc" }
24159
+ });
24160
+ if (ready.length < 2) return 0;
24161
+ const sig = ready.map((r) => r.id).join(",");
24162
+ if (sig === lastReadySig.get(projectId)) return 0;
24163
+ lastReadySig.set(projectId, sig);
24164
+ const row = await deps.decide({
24165
+ projectId,
24166
+ gate: "pulse",
24167
+ kind: "order",
24168
+ question: `Ada ${ready.length} backlog siap dikerjakan. Urutkan mana yang lebih dulu berdasarkan isi pekerjaannya, lalu tuliskan urutan id-nya (dipisah koma) di \`decision\`.`,
24169
+ options: ready.map((r) => `${r.id} \xB7 [${r.priority}] ${r.title}`),
24170
+ notes: ready.map((r) => `${r.id}: ${r.objective.slice(0, 200)}`)
24171
+ }, deps.decideDeps);
24172
+ if (!row || row.status !== "berlaku") return 0;
24173
+ const byId2 = new Map(ready.map((r) => [r.id.toLowerCase(), r]));
24174
+ const named = [];
24175
+ for (const tok of row.answer.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean)) {
24176
+ const hit = byId2.get(tok);
24177
+ if (hit && !named.includes(hit)) named.push(hit);
24178
+ }
24179
+ const ordered = [...named, ...ready.filter((r) => !named.includes(r))];
24180
+ for (const r of ordered) {
24181
+ await deps.enqueue({ specId: r.id, projectId: r.projectId, source: "lead", priority: r.priority });
24182
+ }
24183
+ await deps.notify(row.id, `Lead menata ${ordered.length} backlog siap kerja`, projectId, null, null);
24184
+ return ordered.length;
24185
+ }
24186
+
24187
+ // src/services/lead/engine.ts
24188
+ var TICK_MS = 5e3;
24189
+ var lastPulseAt = 0;
24190
+ var busy2 = false;
24191
+ var timer3;
24192
+ function lastPulse() {
24193
+ return lastPulseAt;
24194
+ }
24195
+ async function tick2(now, deps = {}) {
24196
+ if (busy2) return;
24197
+ busy2 = true;
24198
+ try {
24199
+ const cfg = await getLead();
24200
+ if (!cfg.enabled) return;
24201
+ try {
24202
+ await scanAndAnswer(deps.detect ?? prodDetectDeps);
24203
+ } catch (e) {
24204
+ console.error("lead detect:", e);
24205
+ }
24206
+ if (cfg.paused) return;
24207
+ if (now - lastPulseAt < cfg.everyMin * 6e4) return;
24208
+ lastPulseAt = now;
24209
+ try {
24210
+ await pulse(deps.pulse ?? prodPulseDeps);
24211
+ } catch (e) {
24212
+ console.error("lead pulse:", e);
24213
+ }
24214
+ } catch (e) {
24215
+ console.error("lead tick:", e);
24216
+ } finally {
24217
+ busy2 = false;
24218
+ }
24219
+ }
24220
+ function startLead(deps = {}) {
24221
+ if (timer3) return;
24222
+ timer3 = setInterval(() => void tick2(Date.now(), deps), TICK_MS);
24223
+ timer3.unref();
24224
+ void tick2(Date.now(), deps);
24225
+ }
24226
+
24227
+ // src/routes/lead.ts
24228
+ async function lead_default(app2) {
24229
+ app2.get("/lead/config", async () => getLead());
24230
+ app2.put("/lead/config", async (req, reply) => {
24231
+ const parsed = zLead.safeParse(req.body);
24232
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24233
+ return setLead(parsed.data);
24234
+ });
24235
+ app2.get("/lead/status", async () => {
24236
+ const cfg = await getLead();
24237
+ const projects = await prisma.project.findMany({
24238
+ where: { leadOptIn: true },
24239
+ select: { id: true, name: true },
24240
+ orderBy: { id: "asc" }
24241
+ });
24242
+ const since = new Date(Date.now() - 24 * 60 * 60 * 1e3);
24243
+ let live = [];
24244
+ try {
24245
+ live = listSessions().filter((s) => !s.exited);
24246
+ } catch {
24247
+ }
24248
+ let waiting = [];
24249
+ try {
24250
+ waiting = liveDecisions().filter((d) => markerFilled(d.decisionFile)).map((d) => d.id);
24251
+ } catch {
24252
+ }
24253
+ const rows = await Promise.all(projects.map(async (p) => ({
24254
+ projectId: p.id,
24255
+ name: p.name,
24256
+ optIn: true,
24257
+ paused: !leadActive(cfg, p.id),
24258
+ decisions24h: await prisma.leadDecision.count({ where: { projectId: p.id, createdAt: { gte: since } } }),
24259
+ openSessions: live.filter((s) => s.projectId === p.id).length
24260
+ })));
24261
+ const last = lastPulse();
24262
+ return {
24263
+ config: cfg,
24264
+ projects: rows,
24265
+ queue: (await listQueue()).map((q) => ({
24266
+ id: q.id,
24267
+ specId: q.specId,
24268
+ projectId: q.projectId,
24269
+ source: q.source,
24270
+ priority: q.priority,
24271
+ status: q.status,
24272
+ sessionId: q.sessionId,
24273
+ note: q.note,
24274
+ enqueuedAt: q.enqueuedAt.toISOString(),
24275
+ launchedAt: q.launchedAt ? q.launchedAt.toISOString() : null
24276
+ })),
24277
+ deciding: decidingIds(),
24278
+ waiting,
24279
+ lastPulseAt: last ? new Date(last).toISOString() : null
24280
+ };
24281
+ });
24282
+ app2.get("/lead/decisions", async (req) => {
24283
+ const q = req.query;
24284
+ const rows = await listDecisions({
24285
+ projectId: q.projectId,
24286
+ specId: q.specId,
24287
+ sessionId: q.sessionId,
24288
+ status: q.status,
24289
+ take: q.take ? Number(q.take) : void 0,
24290
+ skip: q.skip ? Number(q.skip) : void 0
24291
+ });
24292
+ return { items: rows.map(toDecisionView) };
24293
+ });
24294
+ app2.post("/lead/decisions", async (req, reply) => {
24295
+ const parsed = zLeadAsk.safeParse(req.body);
24296
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24297
+ const ask = parsed.data;
24298
+ const project = await prisma.project.findUnique({ where: { id: ask.projectId }, select: { leadOptIn: true } });
24299
+ if (!project) return reply.code(404).send({ error: "project not found" });
24300
+ const cfg = await getLead();
24301
+ if (!project.leadOptIn || !leadActive(cfg, ask.projectId))
24302
+ return reply.code(409).send({ error: "lead tidak aktif untuk project ini" });
24303
+ const row = await decide({
24304
+ projectId: ask.projectId,
24305
+ specId: ask.specId ?? null,
24306
+ sessionId: ask.sessionId ?? null,
24307
+ gate: "contract",
24308
+ kind: "answer",
24309
+ question: ask.question,
24310
+ options: ask.options,
24311
+ notes: ask.context ? [ask.context] : void 0
24312
+ });
24313
+ if (!row) return reply.code(409).send({ error: "lead tidak aktif untuk project ini" });
24314
+ takeReply(row.id);
24315
+ if (row.status === "gagal") return reply.code(504).send({ error: row.reason, id: row.id });
24316
+ if (row.action !== "none") {
24317
+ try {
24318
+ await applyAction(row);
24319
+ } catch {
24320
+ }
24321
+ }
24322
+ const answer = {
24323
+ id: row.id,
24324
+ decision: row.answer,
24325
+ reason: row.reason,
24326
+ refs: Array.isArray(row.refs) ? row.refs.map(String) : [],
24327
+ confidence: row.confidence,
24328
+ action: row.action
24329
+ };
24330
+ return reply.code(201).send(answer);
24331
+ });
24332
+ app2.post("/lead/decisions/:id/override", async (req, reply) => {
24333
+ const { id } = req.params;
24334
+ const parsed = zLeadOverride.safeParse(req.body);
24335
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
24336
+ const r = await overrideDecision(id, parsed.data.answer, parsed.data.reason);
24337
+ if (!r) return reply.code(409).send({ error: "keputusan tak ada atau sudah tak berlaku" });
24338
+ let delivered = false;
24339
+ if (r.next.sessionId) {
24340
+ resetSession(r.next.sessionId);
24341
+ delivered = await sendToPane(r.next.sessionId, parsed.data.answer).catch(() => false);
24342
+ }
24343
+ return { old: toDecisionView(r.old), next: toDecisionView(r.next), delivered };
24344
+ });
24345
+ app2.post("/lead/decisions/:id/cancel", async (req, reply) => {
24346
+ const { id } = req.params;
24347
+ const row = await cancelDecision(id);
24348
+ if (!row) return reply.code(409).send({ error: "keputusan tak ada atau sudah tak berlaku" });
24349
+ if (row.sessionId) resetSession(row.sessionId);
24350
+ return toDecisionView(row);
24351
+ });
24352
+ }
24353
+
22920
24354
  // src/app.ts
22921
24355
  var import_multipart = __toESM(require_multipart2(), 1);
22922
24356
 
@@ -23197,8 +24631,10 @@ function capabilityForRoute(method, path) {
23197
24631
  const seg = path.replace(/^\/api\/?/, "").replace(/\/+$/, "").split("/").filter(Boolean);
23198
24632
  const top = seg[0] ?? "";
23199
24633
  if (top === "auth" || top === "agent-tokens" || top === "device-tokens" || top === "sync") return "COOKIE_ONLY";
23200
- if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health") return "GLOBAL_READ";
24634
+ if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health")
24635
+ return read ? "GLOBAL_READ" : "COOKIE_ONLY";
23201
24636
  if (top === "scheduler") return rw("settings");
24637
+ if (top === "lead") return rw("lead");
23202
24638
  if (top === "settings" || top === "config") return rw("settings");
23203
24639
  if (top === "specs") return rw("backlog");
23204
24640
  if (top === "notifications") return rw("notifications");
@@ -23309,9 +24745,10 @@ function buildApp({ requireAuth = true } = {}) {
23309
24745
  await api.register(scheduler_default);
23310
24746
  await api.register(audit_default);
23311
24747
  await api.register(codex);
24748
+ await api.register(lead_default);
23312
24749
  }, { prefix: "/api" });
23313
24750
  if (process.env.NODE_ENV === "production") {
23314
- const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync10);
24751
+ const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
23315
24752
  if (dist) {
23316
24753
  app2.register(fastifyStatic, { root: dist });
23317
24754
  app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));
@@ -23458,7 +24895,7 @@ var prodEnd = {
23458
24895
  reconcile: () => reconcile(reconcileProdDeps),
23459
24896
  scanDecisions: () => scanDecisions()
23460
24897
  };
23461
- async function tick2(now, deps, end2 = prodEnd) {
24898
+ async function tick3(now, deps, end2 = prodEnd) {
23462
24899
  const cfg = await getScheduler();
23463
24900
  if (!cfg.enabled) return;
23464
24901
  for (const src of listSources()) {
@@ -23499,13 +24936,13 @@ var prodDeps = {
23499
24936
  return r.id;
23500
24937
  }
23501
24938
  };
23502
- var TICK_MS = 1e4;
23503
- var timer3;
24939
+ var TICK_MS2 = 1e4;
24940
+ var timer4;
23504
24941
  function startScheduler(deps = prodDeps) {
23505
- if (timer3) return;
23506
- timer3 = setInterval(() => void tick2(Date.now(), deps), TICK_MS);
23507
- timer3.unref();
23508
- void tick2(Date.now(), deps);
24942
+ if (timer4) return;
24943
+ timer4 = setInterval(() => void tick3(Date.now(), deps), TICK_MS2);
24944
+ timer4.unref();
24945
+ void tick3(Date.now(), deps);
23509
24946
  }
23510
24947
 
23511
24948
  // src/services/scheduler/sources/backlog.ts
@@ -23616,6 +25053,7 @@ app.listen({ port, host }).then(() => {
23616
25053
  registerErrorsSource();
23617
25054
  registerTriaseSource();
23618
25055
  startScheduler();
25056
+ startLead();
23619
25057
  void (async () => {
23620
25058
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
23621
25059
  const { applyConfigOnBoot: applyConfigOnBoot2 } = await Promise.resolve().then(() => (init_config_apply(), config_apply_exports));