hanoman 0.1.5 → 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/build-info.json +3 -3
- package/dist/cli.js +167 -8
- package/dist/server.js +1349 -41
- package/package.json +1 -1
- package/prisma/migrations/20260731000000_spec_created_started_at/migration.sql +43 -0
- package/prisma/migrations/20260731120000_lead_decision/migration.sql +33 -0
- package/prisma/schema.prisma +41 -0
- package/web/assets/{index-CeJYUp36.js → index-DB8a8_aD.js} +1533 -1533
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -4184,7 +4184,7 @@ function cmpVersion(a, b) {
|
|
|
4184
4184
|
}
|
|
4185
4185
|
return 0;
|
|
4186
4186
|
}
|
|
4187
|
-
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpec, NOTIFY_SOUNDS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
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";
|
|
@@ -4242,8 +4242,12 @@ var init_entities = __esm({
|
|
|
4242
4242
|
// SPEC-407 · +goal
|
|
4243
4243
|
branchFrom: external_exports.string().nullable(),
|
|
4244
4244
|
// SPEC-143 · null = default project (main)
|
|
4245
|
-
baseSha: external_exports.string().nullable()
|
|
4245
|
+
baseSha: external_exports.string().nullable(),
|
|
4246
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()
|
|
4247
4251
|
});
|
|
4248
4252
|
NOTIFY_SOUNDS = [
|
|
4249
4253
|
"off",
|
|
@@ -4310,6 +4314,31 @@ var init_entities = __esm({
|
|
|
4310
4314
|
effort: external_exports.string().default("xhigh")
|
|
4311
4315
|
});
|
|
4312
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({});
|
|
4313
4342
|
zSetting = external_exports.object({
|
|
4314
4343
|
model: external_exports.string().default("claude-opus-5"),
|
|
4315
4344
|
effort: external_exports.string().default("xhigh"),
|
|
@@ -4336,13 +4365,17 @@ var init_entities = __esm({
|
|
|
4336
4365
|
// SPEC-338 · ADR-0074 · model/effort codex
|
|
4337
4366
|
verifyScope: zVerifyScope.default("changed"),
|
|
4338
4367
|
// SPEC-376 · ADR-0080 · scope verifikasi sesi
|
|
4339
|
-
conflict: zConflict.default(CONFLICT_DEFAULTS)
|
|
4368
|
+
conflict: zConflict.default(CONFLICT_DEFAULTS),
|
|
4340
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)
|
|
4341
4372
|
});
|
|
4342
4373
|
zNotification = external_exports.object({
|
|
4343
4374
|
id: external_exports.string(),
|
|
4344
|
-
type: external_exports.enum(["done", "decision", "error", "ticket", "fail"]).default("done"),
|
|
4345
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"),
|
|
4346
4379
|
specId: external_exports.string().nullable(),
|
|
4347
4380
|
sessionId: external_exports.string().nullable(),
|
|
4348
4381
|
title: external_exports.string(),
|
|
@@ -4397,7 +4430,13 @@ var init_agent = __esm({
|
|
|
4397
4430
|
"support:read",
|
|
4398
4431
|
"support:write",
|
|
4399
4432
|
"notifications:read",
|
|
4400
|
-
"notifications:write"
|
|
4433
|
+
"notifications:write",
|
|
4434
|
+
// SPEC-409 · ADR-0091 · minta putusan ke hanoman-lead + baca jejak keputusan. Domain TERSENDIRI,
|
|
4435
|
+
// bukan menumpang prefix baca yang sudah ada: meminta putusan adalah operasi TULIS (ia melahirkan
|
|
4436
|
+
// baris jejak & bisa menggerakkan sesi), dan SPEC-405 sudah membuktikan apa yang terjadi saat
|
|
4437
|
+
// endpoint tulis menumpang prefix status yang dipetakan tanpa melihat method (AC-5).
|
|
4438
|
+
"lead:read",
|
|
4439
|
+
"lead:write"
|
|
4401
4440
|
];
|
|
4402
4441
|
zCapability = external_exports.enum(CAPABILITY_IDS);
|
|
4403
4442
|
zCapabilityInfo = external_exports.object({
|
|
@@ -4426,7 +4465,9 @@ var init_agent = __esm({
|
|
|
4426
4465
|
{ id: "support:read", domain: "support", access: "read", label: "Errors & Tiket \u2014 baca", desc: "Lihat error monitoring & tiket Help Center." },
|
|
4427
4466
|
{ id: "support:write", domain: "support", access: "write", label: "Errors & Tiket \u2014 tulis", desc: "Eskalasi error, ubah status, terima/tolak tiket." },
|
|
4428
4467
|
{ id: "notifications:read", domain: "notifications", access: "read", label: "Notifikasi \u2014 baca", desc: "Lihat notifikasi." },
|
|
4429
|
-
{ id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." }
|
|
4468
|
+
{ id: "notifications:write", domain: "notifications", access: "write", label: "Notifikasi \u2014 tulis", desc: "Tandai terbaca / bersihkan notifikasi." },
|
|
4469
|
+
{ id: "lead:read", domain: "lead", access: "read", label: "Lead \u2014 baca", desc: "Baca jejak keputusan hanoman-lead & statusnya." },
|
|
4470
|
+
{ id: "lead:write", domain: "lead", access: "write", label: "Lead \u2014 tulis", desc: "Minta putusan ke hanoman-lead (keputusan bisa menggerakkan sesi).", risk: "exec" }
|
|
4430
4471
|
];
|
|
4431
4472
|
zAgentTokenView = external_exports.object({
|
|
4432
4473
|
id: external_exports.string(),
|
|
@@ -4451,16 +4492,103 @@ var init_agent = __esm({
|
|
|
4451
4492
|
}
|
|
4452
4493
|
});
|
|
4453
4494
|
|
|
4495
|
+
// ../shared/src/lead.ts
|
|
4496
|
+
function leadActionAllowed(action) {
|
|
4497
|
+
return LEAD_ACTIONS.includes(action);
|
|
4498
|
+
}
|
|
4499
|
+
function leadRefusalReason(action) {
|
|
4500
|
+
return LEAD_FORBIDDEN[action] ?? `tindakan "${action}" di luar permukaan tindakan lead`;
|
|
4501
|
+
}
|
|
4502
|
+
function isWeightyDecision(o) {
|
|
4503
|
+
if (o.confidence === "ragu") return true;
|
|
4504
|
+
if (o.kind === "collision" || o.kind === "refusal") return true;
|
|
4505
|
+
return o.action === "integrate-main" || o.action === "run-migration" || o.action === "stop-session" || o.action === "restart-session";
|
|
4506
|
+
}
|
|
4507
|
+
var zLeadGate, zLeadKind, zLeadConfidence, zLeadStatus, LEAD_ACTIONS, zLeadAction, LEAD_FORBIDDEN, zLeadVerdict, zLeadAsk, zLeadOverride;
|
|
4508
|
+
var init_lead = __esm({
|
|
4509
|
+
"../shared/src/lead.ts"() {
|
|
4510
|
+
"use strict";
|
|
4511
|
+
init_zod();
|
|
4512
|
+
zLeadGate = external_exports.enum(["contract", "detected", "pulse"]);
|
|
4513
|
+
zLeadKind = external_exports.enum(["answer", "order", "collision", "quality", "refusal"]);
|
|
4514
|
+
zLeadConfidence = external_exports.enum(["tinggi", "sedang", "ragu"]);
|
|
4515
|
+
zLeadStatus = external_exports.enum(["berlaku", "ditimpa", "dibatalkan", "gagal"]);
|
|
4516
|
+
LEAD_ACTIONS = [
|
|
4517
|
+
"none",
|
|
4518
|
+
// keputusan murni jawaban; tak ada tindakan menyusul
|
|
4519
|
+
"answer-session",
|
|
4520
|
+
// ketik jawaban ke pane sesi (pintu deteksi otomatis)
|
|
4521
|
+
"start-session",
|
|
4522
|
+
// mulai sesi untuk sebuah backlog (lewat antrean scheduler)
|
|
4523
|
+
"stop-session",
|
|
4524
|
+
// hentikan sesi — worktree DIBIARKAN utuh (AC-32a)
|
|
4525
|
+
"resume-session",
|
|
4526
|
+
// lanjutkan sesi terputus (jalur ADR-0084)
|
|
4527
|
+
"restart-session",
|
|
4528
|
+
// ulangi pekerjaan dari awal
|
|
4529
|
+
"order-queue",
|
|
4530
|
+
// tata urutan antrean yang sudah ada (ADR-0072) — bukan antrean kedua
|
|
4531
|
+
"hold-work",
|
|
4532
|
+
// tunda satu pekerjaan karena tabrakan area kerja
|
|
4533
|
+
"push-branch",
|
|
4534
|
+
// dorong perubahan ke branch
|
|
4535
|
+
"integrate-main",
|
|
4536
|
+
// integrasikan ke branch utama (risiko yang diterima sadar, PRD §Risiko)
|
|
4537
|
+
"run-migration"
|
|
4538
|
+
// jalankan migration pada basis data LOKAL operator
|
|
4539
|
+
];
|
|
4540
|
+
zLeadAction = external_exports.enum(LEAD_ACTIONS);
|
|
4541
|
+
LEAD_FORBIDDEN = {
|
|
4542
|
+
deploy: "deploy ke produksi",
|
|
4543
|
+
"vps-exec": "menjalankan perintah di VPS",
|
|
4544
|
+
"vps-console": "membuka konsol VPS",
|
|
4545
|
+
"prod-data": "menyentuh data produksi",
|
|
4546
|
+
"delete-project": "menghapus project",
|
|
4547
|
+
"delete-spec": "menghapus backlog",
|
|
4548
|
+
"delete-branch": "menghapus branch",
|
|
4549
|
+
"delete-worktree": "menghapus worktree",
|
|
4550
|
+
"delete-notification": "menghapus notifikasi",
|
|
4551
|
+
"delete-decision": "menghapus jejak keputusan"
|
|
4552
|
+
};
|
|
4553
|
+
zLeadVerdict = external_exports.object({
|
|
4554
|
+
decision: external_exports.string().min(1),
|
|
4555
|
+
reason: external_exports.string().min(1),
|
|
4556
|
+
refs: external_exports.array(external_exports.string()).default([]),
|
|
4557
|
+
confidence: zLeadConfidence.default("sedang"),
|
|
4558
|
+
// Sengaja `string`, BUKAN `zLeadAction`: tindakan di luar allowlist harus bisa MASUK supaya
|
|
4559
|
+
// server menolaknya secara sadar — mencatat penolakan & menotifikasi operator (AC-33). Kalau
|
|
4560
|
+
// enum yang menyaring di sini, permintaan "deploy ke produksi" hanya akan tampak sebagai
|
|
4561
|
+
// keluaran rusak, dan justru peristiwa paling layak dilaporkan itulah yang hilang dari jejak.
|
|
4562
|
+
action: external_exports.string().default("none"),
|
|
4563
|
+
/** Teks yang benar-benar diketik ke pane sesi (pintu deteksi otomatis). */
|
|
4564
|
+
reply: external_exports.string().default("")
|
|
4565
|
+
});
|
|
4566
|
+
zLeadAsk = external_exports.object({
|
|
4567
|
+
projectId: external_exports.string().min(1),
|
|
4568
|
+
specId: external_exports.string().nullish(),
|
|
4569
|
+
sessionId: external_exports.string().nullish(),
|
|
4570
|
+
question: external_exports.string().min(1).max(8e3),
|
|
4571
|
+
options: external_exports.array(external_exports.string().max(2e3)).max(20).default([]),
|
|
4572
|
+
context: external_exports.string().max(2e4).default("")
|
|
4573
|
+
});
|
|
4574
|
+
zLeadOverride = external_exports.object({
|
|
4575
|
+
answer: external_exports.string().min(1).max(8e3),
|
|
4576
|
+
reason: external_exports.string().max(8e3).default("")
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
});
|
|
4580
|
+
|
|
4454
4581
|
// ../shared/src/dto.ts
|
|
4455
4582
|
function flowForSource(source) {
|
|
4456
4583
|
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "cross-audit" ? "cross-audit" : source === "goal" ? "goal" : "feature";
|
|
4457
4584
|
}
|
|
4458
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zCreateLink, zUpdateProject, zCreateSpec, zPatchSpec, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerState, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zStackFrame, zSymbolicatedFrame, zSourceMapUpload, zIngestPayload, zErrorGroupView, zErrorEventView, zErrorGroupDetail, zIngestKeyView, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
4585
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, 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;
|
|
4459
4586
|
var init_dto = __esm({
|
|
4460
4587
|
"../shared/src/dto.ts"() {
|
|
4461
4588
|
"use strict";
|
|
4462
4589
|
init_zod();
|
|
4463
4590
|
init_entities();
|
|
4591
|
+
init_lead();
|
|
4464
4592
|
init_enums();
|
|
4465
4593
|
zIssueDeviceToken = external_exports.object({ name: external_exports.string().min(1) });
|
|
4466
4594
|
zSessionResult = external_exports.object({
|
|
@@ -4516,8 +4644,10 @@ var init_dto = __esm({
|
|
|
4516
4644
|
// SPEC-213 · set git remote resmi project
|
|
4517
4645
|
repoDir: external_exports.string().nullable().optional(),
|
|
4518
4646
|
// SPEC-217 · path default/server editable (null = kosongkan)
|
|
4519
|
-
schedulerOptIn: external_exports.boolean().optional()
|
|
4647
|
+
schedulerOptIn: external_exports.boolean().optional(),
|
|
4520
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)
|
|
4521
4651
|
});
|
|
4522
4652
|
zCreateSpec = external_exports.object({
|
|
4523
4653
|
project: external_exports.string(),
|
|
@@ -4565,7 +4695,9 @@ var init_dto = __esm({
|
|
|
4565
4695
|
// SPEC-249 · hint prefix DSN (bukan hash/rahasia)
|
|
4566
4696
|
helpEnabled: external_exports.boolean().default(false),
|
|
4567
4697
|
// SPEC-253 · Help Center publik aktif
|
|
4568
|
-
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)
|
|
4569
4701
|
});
|
|
4570
4702
|
zSchedulerQueueItem = external_exports.object({
|
|
4571
4703
|
id: external_exports.string(),
|
|
@@ -4605,6 +4737,50 @@ var init_dto = __esm({
|
|
|
4605
4737
|
queue: external_exports.array(zSchedulerQueueItem),
|
|
4606
4738
|
sessions: external_exports.array(zSchedulerSessionView)
|
|
4607
4739
|
});
|
|
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
|
+
});
|
|
4608
4784
|
zFlow = external_exports.enum(["feature", "qa", "scaffold", "reverse", "prd", "audit", "breakdown", "cross-audit", "goal"]);
|
|
4609
4785
|
zPrdBrief = external_exports.object({
|
|
4610
4786
|
title: external_exports.string().min(1),
|
|
@@ -4995,6 +5171,12 @@ var init_api = __esm({
|
|
|
4995
5171
|
// SPEC-299 · panel scheduler (daun #6) — konsumen read-only fondasi SPEC-294/ADR-0072.
|
|
4996
5172
|
schedulerConfig: `${API}/scheduler/config`,
|
|
4997
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`,
|
|
4998
5180
|
// SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
|
|
4999
5181
|
syncNow: `${API}/sync/now`,
|
|
5000
5182
|
// SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
|
|
@@ -5328,6 +5510,7 @@ var init_src = __esm({
|
|
|
5328
5510
|
init_enums();
|
|
5329
5511
|
init_entities();
|
|
5330
5512
|
init_agent();
|
|
5513
|
+
init_lead();
|
|
5331
5514
|
init_dto();
|
|
5332
5515
|
init_api();
|
|
5333
5516
|
init_coverage();
|
|
@@ -6520,6 +6703,9 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6520
6703
|
argv = [sq(agentBin(agent)), promptArg, flags].filter(Boolean).join(" ");
|
|
6521
6704
|
}
|
|
6522
6705
|
const envPairs = [];
|
|
6706
|
+
if (!opts.command && agent === "claude") {
|
|
6707
|
+
for (const [k, v] of Object.entries(rootBypassEnv())) envPairs.push(`${k}=${sq(v)}`);
|
|
6708
|
+
}
|
|
6523
6709
|
if (opts.phaseFile) {
|
|
6524
6710
|
mkdirSync3(dirname4(opts.phaseFile), { recursive: true });
|
|
6525
6711
|
envPairs.push(`HANOMAN_PHASE_FILE=${sq(opts.phaseFile)}`);
|
|
@@ -6610,6 +6796,29 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
6610
6796
|
});
|
|
6611
6797
|
return { id, projectId, specId: opts.specId, flow: opts.flow, cwd, branch: opts.branch, exited: false, decision: false, agent };
|
|
6612
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
|
+
}
|
|
6613
6822
|
async function armGoalInTui(id, condition, o = {}) {
|
|
6614
6823
|
const pollMs = o.pollMs ?? 500, readyTries = o.readyTries ?? 20;
|
|
6615
6824
|
const settleMs = o.settleMs ?? 1200, verifyTries = o.verifyTries ?? 12;
|
|
@@ -6782,7 +6991,7 @@ function spawnPty(...args) {
|
|
|
6782
6991
|
);
|
|
6783
6992
|
}
|
|
6784
6993
|
}
|
|
6785
|
-
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, frame, name, promptFilePath, goalGatePath, goalStatePath, NO_SERVER, TmuxError, sq, sessionIdForSpec, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, sleep, paneText, GOAL_ARMED_MARKERS, goalArmed, poll, detach;
|
|
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;
|
|
6786
6995
|
var init_pty = __esm({
|
|
6787
6996
|
"src/services/pty.ts"() {
|
|
6788
6997
|
"use strict";
|
|
@@ -6806,6 +7015,7 @@ var init_pty = __esm({
|
|
|
6806
7015
|
shellBin = () => effectiveStr("HANOMAN_SHELL") ?? process.env.SHELL ?? "/bin/bash";
|
|
6807
7016
|
codexBin = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
|
|
6808
7017
|
agentBin = (agent) => agent === "codex" ? codexBin() : claudeBin();
|
|
7018
|
+
rootBypassEnv = (uid = process.getuid?.()) => uid === 0 ? { IS_SANDBOX: "1" } : {};
|
|
6809
7019
|
frame = (f) => JSON.stringify(f);
|
|
6810
7020
|
name = (id) => PREFIX + id;
|
|
6811
7021
|
promptFilePath = (id) => `${tmpdir()}/hanoman-prompts/${id}`;
|
|
@@ -7129,7 +7339,9 @@ var init_sync = __esm({
|
|
|
7129
7339
|
};
|
|
7130
7340
|
FIELDS = {
|
|
7131
7341
|
project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
|
|
7132
|
-
|
|
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"],
|
|
7133
7345
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7134
7346
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
7135
7347
|
// SPEC-268 · ADR-0066 · agregat grup error (ErrorEvent mentah tak disync).
|
|
@@ -7142,7 +7354,7 @@ var init_sync = __esm({
|
|
|
7142
7354
|
};
|
|
7143
7355
|
DATE_FIELDS = {
|
|
7144
7356
|
project: ["updatedAt"],
|
|
7145
|
-
spec: ["updatedAt"],
|
|
7357
|
+
spec: ["createdAt", "startedAt", "updatedAt"],
|
|
7146
7358
|
vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7147
7359
|
sessionResult: ["createdAt", "updatedAt"],
|
|
7148
7360
|
errorGroup: ["firstSeenAt", "lastSeenAt", "updatedAt"],
|
|
@@ -11111,7 +11323,7 @@ function syncStatus() {
|
|
|
11111
11323
|
async function startSyncClient(base, token, tickMs2) {
|
|
11112
11324
|
started = true;
|
|
11113
11325
|
const transport = fetchTransport(base, token);
|
|
11114
|
-
const
|
|
11326
|
+
const tick4 = async () => {
|
|
11115
11327
|
try {
|
|
11116
11328
|
await syncOnce(transport);
|
|
11117
11329
|
} catch {
|
|
@@ -11122,13 +11334,13 @@ async function startSyncClient(base, token, tickMs2) {
|
|
|
11122
11334
|
const wsUrl = base.replace(/^http/, "ws").replace(/\/$/, "") + `/api/sync/ws?token=${encodeURIComponent(token)}`;
|
|
11123
11335
|
ws = new WebSocket2(wsUrl);
|
|
11124
11336
|
ws.on("open", () => {
|
|
11125
|
-
void
|
|
11337
|
+
void tick4();
|
|
11126
11338
|
});
|
|
11127
11339
|
ws.on("message", async (raw) => {
|
|
11128
11340
|
try {
|
|
11129
11341
|
const msg = JSON.parse(raw.toString());
|
|
11130
11342
|
if (msg.t !== "sync") return;
|
|
11131
|
-
if (!await applyFeedFrame(msg)) void
|
|
11343
|
+
if (!await applyFeedFrame(msg)) void tick4();
|
|
11132
11344
|
} catch {
|
|
11133
11345
|
}
|
|
11134
11346
|
});
|
|
@@ -11145,11 +11357,11 @@ async function startSyncClient(base, token, tickMs2) {
|
|
|
11145
11357
|
}
|
|
11146
11358
|
});
|
|
11147
11359
|
};
|
|
11148
|
-
await
|
|
11360
|
+
await tick4();
|
|
11149
11361
|
void connectWs();
|
|
11150
11362
|
const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
|
|
11151
11363
|
timer2 = setInterval(() => {
|
|
11152
|
-
void
|
|
11364
|
+
void tick4();
|
|
11153
11365
|
}, ms);
|
|
11154
11366
|
timer2.unref?.();
|
|
11155
11367
|
}
|
|
@@ -14419,7 +14631,7 @@ import websocket from "@fastify/websocket";
|
|
|
14419
14631
|
import cookie from "@fastify/cookie";
|
|
14420
14632
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
14421
14633
|
import { dirname as dirname12 } from "node:path";
|
|
14422
|
-
import { existsSync as
|
|
14634
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
14423
14635
|
|
|
14424
14636
|
// src/web-dir.ts
|
|
14425
14637
|
import { resolve } from "node:path";
|
|
@@ -14638,7 +14850,9 @@ async function toProjectView(p, sessions) {
|
|
|
14638
14850
|
// SPEC-253 · Help Center publik aktif.
|
|
14639
14851
|
helpEnabled: p.helpEnabled,
|
|
14640
14852
|
// SPEC-294 · opt-in scheduler otonom (lokal per-instance).
|
|
14641
|
-
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
|
|
14642
14856
|
};
|
|
14643
14857
|
}
|
|
14644
14858
|
|
|
@@ -15091,8 +15305,10 @@ var DEFAULT_SETTING = {
|
|
|
15091
15305
|
// SPEC-338 · ADR-0074 · model/effort codex
|
|
15092
15306
|
verifyScope: "changed",
|
|
15093
15307
|
// SPEC-376 · ADR-0080 · uji hanya yang berubah
|
|
15094
|
-
conflict: CONFLICT_DEFAULTS
|
|
15308
|
+
conflict: CONFLICT_DEFAULTS,
|
|
15095
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)
|
|
15096
15312
|
};
|
|
15097
15313
|
async function getSetting() {
|
|
15098
15314
|
const raw = (await prisma.setting.findUnique({ where: { id: 1 } }))?.data;
|
|
@@ -17814,6 +18030,27 @@ async function sendReviewDownload(reply, fmt, rf, a) {
|
|
|
17814
18030
|
});
|
|
17815
18031
|
}
|
|
17816
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
|
+
|
|
17817
18054
|
// src/services/live-specs.ts
|
|
17818
18055
|
init_db();
|
|
17819
18056
|
init_pty();
|
|
@@ -17863,6 +18100,12 @@ async function recordNewTicket(ticketId, projectId, projectName, category, title
|
|
|
17863
18100
|
}).catch(() => {
|
|
17864
18101
|
});
|
|
17865
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
|
+
}
|
|
17866
18109
|
var awaiting = /* @__PURE__ */ new Set();
|
|
17867
18110
|
async function scanDecisions(read = liveDecisions) {
|
|
17868
18111
|
const next = /* @__PURE__ */ new Set();
|
|
@@ -17932,13 +18175,16 @@ function deriveSpecFields(source, payload, manualPriority) {
|
|
|
17932
18175
|
}
|
|
17933
18176
|
function filterSpecs(specs, f) {
|
|
17934
18177
|
const needle = (f.q ?? "").trim().toLowerCase();
|
|
17935
|
-
|
|
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)));
|
|
17936
18182
|
}
|
|
17937
18183
|
async function specs_default(app2) {
|
|
17938
18184
|
app2.get("/specs", async (req) => {
|
|
17939
|
-
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;
|
|
17940
18186
|
const overlaid = await liveSpecs({ project, source });
|
|
17941
|
-
return paginate(filterSpecs(overlaid, { q, stage, priority, startable }), page, limit);
|
|
18187
|
+
return paginate(filterSpecs(overlaid, { q, stage, priority, startable, dateField, from, to }), page, limit);
|
|
17942
18188
|
});
|
|
17943
18189
|
app2.post("/specs", async (req, reply) => {
|
|
17944
18190
|
const parsed = zCreateSpec.safeParse(req.body);
|
|
@@ -19429,7 +19675,10 @@ async function startSpecSession(spec, opts) {
|
|
|
19429
19675
|
} catch (e) {
|
|
19430
19676
|
throw new LaunchError(`gagal membuat worktree: ${e.message}`, "worktree");
|
|
19431
19677
|
}
|
|
19432
|
-
if (!resume) await prisma.spec.update({
|
|
19678
|
+
if (!resume) await prisma.spec.update({
|
|
19679
|
+
where: { id: spec.id },
|
|
19680
|
+
data: { baseSha, headSha: null, startedAt: /* @__PURE__ */ new Date() }
|
|
19681
|
+
});
|
|
19433
19682
|
}
|
|
19434
19683
|
const brief = {
|
|
19435
19684
|
id: spec.id,
|
|
@@ -20006,9 +20255,9 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
20006
20255
|
return new Promise((resolve15) => {
|
|
20007
20256
|
const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
20008
20257
|
let out3 = "";
|
|
20009
|
-
const
|
|
20258
|
+
const timer5 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
20010
20259
|
const done = (r) => {
|
|
20011
|
-
clearTimeout(
|
|
20260
|
+
clearTimeout(timer5);
|
|
20012
20261
|
if (askpass) rmSync4(dirname9(askpass), { recursive: true, force: true });
|
|
20013
20262
|
resolve15(r);
|
|
20014
20263
|
};
|
|
@@ -20890,12 +21139,36 @@ async function update(app2) {
|
|
|
20890
21139
|
|
|
20891
21140
|
// src/services/events.ts
|
|
20892
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
|
|
20893
21159
|
init_db();
|
|
20894
21160
|
init_config2();
|
|
20895
21161
|
var clients = /* @__PURE__ */ new Set();
|
|
20896
21162
|
var tickMs = () => effectiveInt("HANOMAN_EVENTS_TICK_MS") ?? 1e3;
|
|
20897
21163
|
var GROUPS = [
|
|
20898
|
-
|
|
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
|
+
}) },
|
|
20899
21172
|
{ everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
|
|
20900
21173
|
{ everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
|
|
20901
21174
|
// ponytail: cermin GET /vps (orderBy createdAt asc). Query sepele — tak diekstrak.
|
|
@@ -21312,11 +21585,11 @@ function clamp(text) {
|
|
|
21312
21585
|
const buf = Buffer.from(text, "utf8");
|
|
21313
21586
|
if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
|
|
21314
21587
|
const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
|
|
21315
|
-
let
|
|
21316
|
-
const nl =
|
|
21317
|
-
if (nl >= 0)
|
|
21588
|
+
let tail2 = buf.subarray(cut).toString("utf8");
|
|
21589
|
+
const nl = tail2.indexOf("\n");
|
|
21590
|
+
if (nl >= 0) tail2 = tail2.slice(nl + 1);
|
|
21318
21591
|
return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
|
|
21319
|
-
${
|
|
21592
|
+
${tail2}`, truncated: true };
|
|
21320
21593
|
}
|
|
21321
21594
|
async function saveTranscript(text) {
|
|
21322
21595
|
if (!text.trim()) return { key: "", bytes: 0, truncated: false };
|
|
@@ -21871,7 +22144,7 @@ for (let i = 0; i < chars.length; i++) {
|
|
|
21871
22144
|
intToChar[i] = c;
|
|
21872
22145
|
charToInt[c] = i;
|
|
21873
22146
|
}
|
|
21874
|
-
function decodeInteger(reader,
|
|
22147
|
+
function decodeInteger(reader, relative2) {
|
|
21875
22148
|
let value = 0;
|
|
21876
22149
|
let shift = 0;
|
|
21877
22150
|
let integer = 0;
|
|
@@ -21886,7 +22159,7 @@ function decodeInteger(reader, relative) {
|
|
|
21886
22159
|
if (shouldNegate) {
|
|
21887
22160
|
value = -2147483648 | -value;
|
|
21888
22161
|
}
|
|
21889
|
-
return
|
|
22162
|
+
return relative2 + value;
|
|
21890
22163
|
}
|
|
21891
22164
|
function hasMoreVlq(reader, max) {
|
|
21892
22165
|
if (reader.pos >= max) return false;
|
|
@@ -23046,6 +23319,1038 @@ async function audit_default(app2) {
|
|
|
23046
23319
|
});
|
|
23047
23320
|
}
|
|
23048
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
|
+
|
|
23049
24354
|
// src/app.ts
|
|
23050
24355
|
var import_multipart = __toESM(require_multipart2(), 1);
|
|
23051
24356
|
|
|
@@ -23329,6 +24634,7 @@ function capabilityForRoute(method, path) {
|
|
|
23329
24634
|
if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health")
|
|
23330
24635
|
return read ? "GLOBAL_READ" : "COOKIE_ONLY";
|
|
23331
24636
|
if (top === "scheduler") return rw("settings");
|
|
24637
|
+
if (top === "lead") return rw("lead");
|
|
23332
24638
|
if (top === "settings" || top === "config") return rw("settings");
|
|
23333
24639
|
if (top === "specs") return rw("backlog");
|
|
23334
24640
|
if (top === "notifications") return rw("notifications");
|
|
@@ -23439,9 +24745,10 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
23439
24745
|
await api.register(scheduler_default);
|
|
23440
24746
|
await api.register(audit_default);
|
|
23441
24747
|
await api.register(codex);
|
|
24748
|
+
await api.register(lead_default);
|
|
23442
24749
|
}, { prefix: "/api" });
|
|
23443
24750
|
if (process.env.NODE_ENV === "production") {
|
|
23444
|
-
const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env,
|
|
24751
|
+
const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
|
|
23445
24752
|
if (dist) {
|
|
23446
24753
|
app2.register(fastifyStatic, { root: dist });
|
|
23447
24754
|
app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));
|
|
@@ -23588,7 +24895,7 @@ var prodEnd = {
|
|
|
23588
24895
|
reconcile: () => reconcile(reconcileProdDeps),
|
|
23589
24896
|
scanDecisions: () => scanDecisions()
|
|
23590
24897
|
};
|
|
23591
|
-
async function
|
|
24898
|
+
async function tick3(now, deps, end2 = prodEnd) {
|
|
23592
24899
|
const cfg = await getScheduler();
|
|
23593
24900
|
if (!cfg.enabled) return;
|
|
23594
24901
|
for (const src of listSources()) {
|
|
@@ -23629,13 +24936,13 @@ var prodDeps = {
|
|
|
23629
24936
|
return r.id;
|
|
23630
24937
|
}
|
|
23631
24938
|
};
|
|
23632
|
-
var
|
|
23633
|
-
var
|
|
24939
|
+
var TICK_MS2 = 1e4;
|
|
24940
|
+
var timer4;
|
|
23634
24941
|
function startScheduler(deps = prodDeps) {
|
|
23635
|
-
if (
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
-
void
|
|
24942
|
+
if (timer4) return;
|
|
24943
|
+
timer4 = setInterval(() => void tick3(Date.now(), deps), TICK_MS2);
|
|
24944
|
+
timer4.unref();
|
|
24945
|
+
void tick3(Date.now(), deps);
|
|
23639
24946
|
}
|
|
23640
24947
|
|
|
23641
24948
|
// src/services/scheduler/sources/backlog.ts
|
|
@@ -23746,6 +25053,7 @@ app.listen({ port, host }).then(() => {
|
|
|
23746
25053
|
registerErrorsSource();
|
|
23747
25054
|
registerTriaseSource();
|
|
23748
25055
|
startScheduler();
|
|
25056
|
+
startLead();
|
|
23749
25057
|
void (async () => {
|
|
23750
25058
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
23751
25059
|
const { applyConfigOnBoot: applyConfigOnBoot2 } = await Promise.resolve().then(() => (init_config_apply(), config_apply_exports));
|