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/README.md +11 -3
- package/dist/build-info.json +3 -3
- package/dist/cli.js +1560 -1280
- package/dist/server.js +1542 -104
- 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-a6SFYnK5.js → index-DB8a8_aD.js} +1549 -1549
- package/web/index.html +1 -1
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
|
|
4524
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5756
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11360
|
+
await tick4();
|
|
11070
11361
|
void connectWs();
|
|
11071
11362
|
const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
|
|
11072
11363
|
timer2 = setInterval(() => {
|
|
11073
|
-
void
|
|
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(
|
|
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 =
|
|
12571
|
+
const len = str2.length;
|
|
12281
12572
|
for (var i = 0; i < len; ++i) {
|
|
12282
|
-
const char =
|
|
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 &&
|
|
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(
|
|
12815
|
-
|
|
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 =
|
|
13110
|
+
const len = str2.length;
|
|
12820
13111
|
for (; i < len; ++i) {
|
|
12821
13112
|
if (this.buffer !== void 0) {
|
|
12822
|
-
if (!HEX[
|
|
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 +=
|
|
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 (
|
|
13125
|
+
} else if (str2[i] === "%") {
|
|
12835
13126
|
if (i > p) {
|
|
12836
|
-
res +=
|
|
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 +=
|
|
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
|
|
13799
|
+
let str2 = seed();
|
|
13509
13800
|
return function generateId() {
|
|
13510
|
-
return num === 255 ? (
|
|
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
|
|
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,
|
|
15748
|
+
const row = tableRow.replace(/\|/g, (match, offset, str2) => {
|
|
15454
15749
|
let escaped = false;
|
|
15455
15750
|
let curr = offset;
|
|
15456
|
-
while (--curr >= 0 &&
|
|
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(
|
|
15485
|
-
const l =
|
|
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 =
|
|
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
|
|
15795
|
+
return str2.slice(0, l - suffLen);
|
|
15501
15796
|
}
|
|
15502
|
-
function findClosingBracket(
|
|
15503
|
-
if (
|
|
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 <
|
|
15508
|
-
if (
|
|
15802
|
+
for (let i = 0; i < str2.length; i++) {
|
|
15803
|
+
if (str2[i] === "\\") {
|
|
15509
15804
|
i++;
|
|
15510
|
-
} else if (
|
|
15805
|
+
} else if (str2[i] === b[0]) {
|
|
15511
15806
|
level++;
|
|
15512
|
-
} else if (
|
|
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
|
-
|
|
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
|
|
19319
|
-
|
|
19320
|
-
opts.
|
|
19321
|
-
|
|
19322
|
-
|
|
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({
|
|
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 (
|
|
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 (
|
|
19349
|
-
|
|
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
|
|
20258
|
+
const timer5 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
19912
20259
|
const done = (r) => {
|
|
19913
|
-
clearTimeout(
|
|
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
|
-
|
|
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
|
|
21187
|
-
const nl =
|
|
21188
|
-
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);
|
|
21189
21591
|
return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
|
|
21190
|
-
${
|
|
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,
|
|
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
|
|
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")
|
|
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,
|
|
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
|
|
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
|
|
23503
|
-
var
|
|
24939
|
+
var TICK_MS2 = 1e4;
|
|
24940
|
+
var timer4;
|
|
23504
24941
|
function startScheduler(deps = prodDeps) {
|
|
23505
|
-
if (
|
|
23506
|
-
|
|
23507
|
-
|
|
23508
|
-
void
|
|
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));
|