hanoman 0.3.3 → 0.4.0

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.
Files changed (25) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +120 -8
  3. package/dist/server.js +1430 -794
  4. package/package.json +1 -1
  5. package/prisma/migrations/20260914120000_agent_invocation_phase/migration.sql +3 -0
  6. package/prisma/schema.prisma +3 -0
  7. package/web/assets/{ChangelogScreen-D_-RmJkz.js → ChangelogScreen-aqsJmmfe.js} +1 -1
  8. package/web/assets/{DalangHanomanScreen-pVfnLx9z.js → DalangHanomanScreen-D3rNURz7.js} +1 -1
  9. package/web/assets/{DocPreviewModal-ZFjISCL0.js → DocPreviewModal-B7r_7p8N.js} +1 -1
  10. package/web/assets/{DocsWorkspace-Vp_vik3v.js → DocsWorkspace-_w6pncu9.js} +1 -1
  11. package/web/assets/{IdeScreen-DQSRs3fr.js → IdeScreen-etZ2QyO-.js} +1 -1
  12. package/web/assets/{LeadScreen-Bv5LiudD.js → LeadScreen-Bo0K2-ML.js} +1 -1
  13. package/web/assets/{ReviewScreen-Cime4Tci.js → ReviewScreen-lRf2FW30.js} +1 -1
  14. package/web/assets/{SchedulerScreen-DKmRNbTs.js → SchedulerScreen-D75twy4J.js} +1 -1
  15. package/web/assets/SettingsScreen-rMIR22vv.js +62 -0
  16. package/web/assets/{TeamScreen-5ugoXsOv.js → TeamScreen-nsttkE2J.js} +1 -1
  17. package/web/assets/TerminalScreen-BG9vRE1-.js +9 -0
  18. package/web/assets/{TriageScreen-BajACF5K.js → TriageScreen-7eM8gjr_.js} +1 -1
  19. package/web/assets/{VpsScreen-C7muNRX5.js → VpsScreen-C29j52Tu.js} +1 -1
  20. package/web/assets/{diff-view-CsZPBznT.js → diff-view-RZNZ9Nz6.js} +1 -1
  21. package/web/assets/{index-DI7G_sit.js → index-BHI_f9DD.js} +192 -192
  22. package/web/assets/{live-DVDstDzi.js → live-CniaVDeP.js} +1 -1
  23. package/web/index.html +1 -1
  24. package/web/assets/SettingsScreen-Bh8XhyVQ.js +0 -62
  25. package/web/assets/TerminalScreen-DsAn5dlH.js +0 -9
package/dist/server.js CHANGED
@@ -4607,6 +4607,11 @@ function replaceModelCatalog(claude, codex2) {
4607
4607
  function claudeEfforts(modelId) {
4608
4608
  return MODELS.find((m) => m.id === modelId)?.efforts ?? EFFORTS;
4609
4609
  }
4610
+ function coerceClaudeEffort(modelId, effort2) {
4611
+ const levels = MODELS.find((m) => m.id === modelId)?.efforts;
4612
+ if (!levels?.length || levels.includes(effort2)) return effort2;
4613
+ return levels.includes("xhigh") ? "xhigh" : levels[0];
4614
+ }
4610
4615
  function codexModel(id2) {
4611
4616
  return CODEX_MODELS.find((m) => m.id === id2);
4612
4617
  }
@@ -4627,7 +4632,7 @@ function cmpVersion(a, b) {
4627
4632
  }
4628
4633
  return 0;
4629
4634
  }
4630
- var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, 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, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zPortalChat, PORTAL_CHAT_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
4635
+ var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, 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, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zPortalChat, PORTAL_CHAT_DEFAULTS, zPhaseCell, zFlowOrchestration, zOrchestration, ORCHESTRATION_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
4631
4636
  var init_entities = __esm({
4632
4637
  "../shared/src/entities.ts"() {
4633
4638
  "use strict";
@@ -4858,6 +4863,27 @@ var init_entities = __esm({
4858
4863
  timeoutSec: external_exports.number().int().min(10).max(900).default(180)
4859
4864
  });
4860
4865
  PORTAL_CHAT_DEFAULTS = zPortalChat.parse({});
4866
+ zPhaseCell = external_exports.object({
4867
+ model: external_exports.string().nullable().default(null),
4868
+ effort: external_exports.string().nullable().default(null)
4869
+ });
4870
+ zFlowOrchestration = external_exports.object({
4871
+ enabled: external_exports.boolean().default(true),
4872
+ claude: external_exports.record(external_exports.string(), zPhaseCell).default({}),
4873
+ codex: external_exports.record(external_exports.string(), zPhaseCell).default({})
4874
+ });
4875
+ zOrchestration = external_exports.object({
4876
+ feature: zFlowOrchestration.default({}),
4877
+ qa: zFlowOrchestration.default({}),
4878
+ scaffold: zFlowOrchestration.default({}),
4879
+ reverse: zFlowOrchestration.default({}),
4880
+ prd: zFlowOrchestration.default({}),
4881
+ audit: zFlowOrchestration.default({}),
4882
+ breakdown: zFlowOrchestration.default({}),
4883
+ goal: zFlowOrchestration.default({}),
4884
+ no_effort: zFlowOrchestration.default({})
4885
+ });
4886
+ ORCHESTRATION_DEFAULTS = zOrchestration.parse({});
4861
4887
  zSetting = external_exports.object({
4862
4888
  model: external_exports.string().default("claude-opus-5"),
4863
4889
  effort: external_exports.string().default("xhigh"),
@@ -4900,6 +4926,8 @@ var init_entities = __esm({
4900
4926
  // SPEC-518 · agen pembuat changelog (opt-in, mati)
4901
4927
  portalChat: zPortalChat.default(PORTAL_CHAT_DEFAULTS),
4902
4928
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
4929
+ orchestration: zOrchestration.default(ORCHESTRATION_DEFAULTS),
4930
+ // ADR-0164 · orkestrasi subagent per fase (default aktif)
4903
4931
  // SPEC-881 · ADR-0136 · sidik jari isi bawaan yang TERAKHIR ditulis seed di mesin ini, per nama
4904
4932
  // agen. Dipakai seed untuk membedakan "belum pernah disunting operator" dari "sudah". WAJIB
4905
4933
  // dideklarasikan di sini: zod membuang kunci tak dikenal dan `PUT /settings` menulis balik hasil
@@ -4944,6 +4972,73 @@ var init_entities = __esm({
4944
4972
  }
4945
4973
  });
4946
4974
 
4975
+ // ../shared/src/orchestration.ts
4976
+ function phaseAgentName(phase) {
4977
+ return PHASE_AGENT_PREFIX + phase.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4978
+ }
4979
+ var FLOW_PHASES, PHASE_AGENT_PREFIX, isPhaseAgentName, PHASE_EVIDENCE_GRACE_MS;
4980
+ var init_orchestration = __esm({
4981
+ "../shared/src/orchestration.ts"() {
4982
+ "use strict";
4983
+ FLOW_PHASES = {
4984
+ feature: ["Brainstorm", "Objective", "Spec", "Plan", "Execute"],
4985
+ qa: ["Audit", "Spec", "Plan", "Execute"],
4986
+ scaffold: ["Brainstorm", "Objective", "Doc index"],
4987
+ reverse: ["Scan", "Docs teknis", "Wawancara", "Konvensi & index", "Serah terima"],
4988
+ prd: ["Brainstorm", "PRD"],
4989
+ audit: ["Audit", "Laporan"],
4990
+ breakdown: ["Analisis", "Breakdown"],
4991
+ // SPEC-337 · ADR-0075 · audit lintas project: fase & stage-map identik audit-only, scope-nya
4992
+ // yang berbeda (project utama + tetangga ProjectLink).
4993
+ // SPEC-407 · ADR-0089 · backlog goal: tak ada fase perencanaan sama sekali. `Goal` = kerjakan,
4994
+ // `Verifikasi` = buktikan. Kedua nama unik lintas daftar ini — syarat peta REACHED di server,
4995
+ // yang berkunci nama fase saja.
4996
+ goal: ["Goal", "Verifikasi"],
4997
+ // SPEC-825 · ADR-0123 · task remeh: SATU fase. Nama `Kerjakan` unik lintas daftar ini — syarat
4998
+ // peta REACHED server, yang berkunci nama fase saja.
4999
+ no_effort: ["Kerjakan"]
5000
+ };
5001
+ PHASE_AGENT_PREFIX = "hanoman-fase-";
5002
+ isPhaseAgentName = (name2) => name2.startsWith(PHASE_AGENT_PREFIX);
5003
+ PHASE_EVIDENCE_GRACE_MS = 6e4;
5004
+ }
5005
+ });
5006
+
5007
+ // ../shared/src/orchestration-plan.ts
5008
+ function codexNativeAgentsSupported(version) {
5009
+ const parsed = version ? /(\d+)\.(\d+)\.(\d+)/.exec(version)?.[0] : null;
5010
+ return parsed ? cmpVersion(parsed, CODEX_NATIVE_AGENTS_MIN_CLIENT) >= 0 : false;
5011
+ }
5012
+ function resolvePhasePlan(input) {
5013
+ const cfg = input.orchestration?.[input.flow];
5014
+ if (!input.nativeAgents || cfg?.enabled === false) return null;
5015
+ const cells = cfg?.[input.runtime] ?? {};
5016
+ const coerce3 = input.runtime === "codex" ? coerceCodexEffort : coerceClaudeEffort;
5017
+ return {
5018
+ flow: input.flow,
5019
+ runtime: input.runtime,
5020
+ phases: FLOW_PHASES[input.flow].map((phase) => {
5021
+ const cell = cells[phase];
5022
+ const model = cell?.model ?? input.orchestrator.model;
5023
+ return {
5024
+ phase,
5025
+ agentName: phaseAgentName(phase),
5026
+ model,
5027
+ effort: coerce3(model, cell?.effort ?? input.orchestrator.effort)
5028
+ };
5029
+ })
5030
+ };
5031
+ }
5032
+ var CODEX_NATIVE_AGENTS_MIN_CLIENT;
5033
+ var init_orchestration_plan = __esm({
5034
+ "../shared/src/orchestration-plan.ts"() {
5035
+ "use strict";
5036
+ init_entities();
5037
+ init_orchestration();
5038
+ CODEX_NATIVE_AGENTS_MIN_CLIENT = "0.151.0";
5039
+ }
5040
+ });
5041
+
4947
5042
  // ../shared/src/agent.ts
4948
5043
  function grantsCapability(granted, need) {
4949
5044
  if (granted.includes(need)) return true;
@@ -5154,6 +5249,7 @@ var init_custom_agent = __esm({
5154
5249
  "../shared/src/custom-agent.ts"() {
5155
5250
  "use strict";
5156
5251
  init_zod();
5252
+ init_orchestration();
5157
5253
  AGENT_RUNTIMES = ["claude", "codex"];
5158
5254
  zAgentRuntime = external_exports.enum(AGENT_RUNTIMES);
5159
5255
  AGENT_RUNTIME_LABELS = {
@@ -5200,7 +5296,9 @@ var init_custom_agent = __esm({
5200
5296
  });
5201
5297
  zCreateCustomAgentFields = external_exports.object({
5202
5298
  projectId: external_exports.string().nullable().optional(),
5203
- name: external_exports.string().regex(AGENT_NAME_RE),
5299
+ // ADR-0164 · awalan agen fase dicadangkan — registry native berkunci nama, jadi custom agent
5300
+ // bernama sama akan menimpa agen fase sesi orchestrator tanpa satu pun galat.
5301
+ name: external_exports.string().regex(AGENT_NAME_RE).refine((n2) => !isPhaseAgentName(n2), { message: `awalan ${PHASE_AGENT_PREFIX} dicadangkan untuk agen fase hanoman` }),
5204
5302
  description: external_exports.string().trim().min(1).max(500),
5205
5303
  instructions: external_exports.string().trim().min(1).max(2e4),
5206
5304
  tools: external_exports.array(external_exports.string()).nullable().optional(),
@@ -12202,6 +12300,8 @@ var init_src = __esm({
12202
12300
  init_method_status();
12203
12301
  init_agent_engine();
12204
12302
  init_entities();
12303
+ init_orchestration();
12304
+ init_orchestration_plan();
12205
12305
  init_agent();
12206
12306
  init_custom_agent();
12207
12307
  init_builtin_agents();
@@ -12467,12 +12567,62 @@ var init_goal_spec = __esm({
12467
12567
  });
12468
12568
 
12469
12569
  // ../runner/src/prompt.ts
12470
- function startPrompt(flow, spec, branchTo, autonomy, verifyScope, method, attachments) {
12570
+ function guideLine(guide, phase) {
12571
+ return guide.split("\n").find((line) => line.startsWith(`- ${phase}:`)) ?? "";
12572
+ }
12573
+ function phaseSkillsFor(flow, phase, method) {
12574
+ const own = method.phaseSkills[phase] ?? [];
12575
+ const phases = PIPELINES[flow];
12576
+ return writesCode(flow) && phase === phases[phases.length - 1] ? [.../* @__PURE__ */ new Set([...own, ...method.exitSkills])] : [...own];
12577
+ }
12578
+ function orchestratorClause(plan, o = {}) {
12579
+ const codex2 = plan.runtime === "codex";
12580
+ const call = codex2 ? "spawn_agent" : "tool Agent";
12581
+ const resume = codex2 ? "send_input ke agent id yang sama" : "SendMessage ke agent ID yang kamu terima";
12582
+ const describeRule = codex2 ? "Deskripsi/label pemanggilan diisi persis `Fase <Nama Fase>` \u2014 tanpa nomor urut, dan BUKAN baris pertama blok serah-terima di atas." : "Deskripsi pemanggilan (parameter `description`) diisi persis `Fase <Nama Fase>` \u2014 tanpa nomor urut, dan BUKAN baris pertama blok serah-terima di atas.";
12583
+ const askEscalation = codex2 ? "tanyakan di terminal ini lalu tunggu jawaban" : "tanyakan lewat AskUserQuestion";
12584
+ const askReport = codex2 ? "tanyakan ke manusia di terminal ini lalu tunggu jawaban" : "tanyakan ke manusia (AskUserQuestion; di fase yang memang bergiliran dengan manusia \u2014 Wawancara, Brainstorm prd/scaffold \u2014 tanyakan di terminal ini)";
12585
+ const list2 = plan.phases.map((p3, i) => `${i + 1}. ${p3.phase} \u2192 \`${p3.agentName}\` \xB7 ${p3.model} \xB7 ${p3.effort}`).join("\n");
12586
+ return [
12587
+ "Sesi ini ORCHESTRATOR. Setiap fase dikerjakan subagent fase miliknya dengan model & effort yang sudah terkunci di definisinya \u2014 kamu TIDAK mengerjakan isi fase sendiri.",
12588
+ `Fase berurutan:
12589
+ ${list2}`,
12590
+ "Untuk SETIAP fase, berurutan:",
12591
+ `1. Panggil subagent fasenya lewat ${call}, tugasnya berupa blok serah-terima berbentuk tetap:
12592
+ Urutan: <n>/<total> \xB7 <Nama Fase>
12593
+ Tujuan: <objective backlog/project>
12594
+ Base SHA: $HANOMAN_BASE_SHA (atau -)
12595
+ Artefak fase sebelumnya: <path yang dilaporkan, atau ->
12596
+ Keputusan manusia sejauh ini: <ringkas, atau ->
12597
+ Lampiran: <path INDEX.md lampiran, atau ->
12598
+ Percobaan: <k>/2
12599
+
12600
+ ${describeRule}`,
12601
+ '2. Baca laporannya. `Status: selesai` DENGAN bukti \u2192 SEBELUM hal lain (memanggil fase berikutnya, commit, atau push): jalankan persis `echo "<Nama Fase> done" >> "$HANOMAN_PHASE_FILE"`, lalu verifikasi dengan `tail -1 "$HANOMAN_PHASE_FILE"` bahwa barisnya benar tertulis. Kamu satu-satunya penulis berkas itu.',
12602
+ `3. \`Status: sebagian\`/\`terhalang\`, galat, atau laporan tanpa bukti \u2192 delegasikan ULANG SEKALI ke subagent fase yang sama dengan laporan gagalnya disertakan (\`Percobaan: 2/2\`). Gagal lagi \u2192 BERHENTI dan ${askEscalation} apa yang harus dilakukan. Aturan ini berlaku walau klausa otonomi di prompt ini menyuruhmu tak bertanya.`,
12603
+ `4. \`Pertanyaan untuk manusia:\` di laporan \u2192 ${askReport}, lalu LANJUTKAN subagent yang SAMA lewat ${resume} dengan jawabannya. Giliran relay ini bukan percobaan ulang. Di sesi tanpa pengawas, putuskan sendiri lalu teruskan keputusanmu dengan cara yang sama.`,
12604
+ o.fastPath ? '5. `Rekomendasi fase: jalur-cepat` sesudah Audit \u2192 SEBELUM lanjut ke Execute: jalankan `echo "Spec skipped" >> "$HANOMAN_PHASE_FILE"` lalu `echo "Plan skipped" >> "$HANOMAN_PHASE_FILE"` (gerbang yang sama seperti langkah 2), lalu lanjut ke Execute. `penuh` \u2192 Spec \u2192 Plan \u2192 Execute.' : "",
12605
+ "DILARANG mengerjakan isi fase sendiri \u2014 termasuk saat subagent gagal. Menulis `skipped` untuk fase yang dilewati bukan mengerjakannya.",
12606
+ 'Pekerjaan ini BELUM selesai sampai SEMUA fase di daftar di atas punya baris `done` atau `skipped` di $HANOMAN_PHASE_FILE \u2014 JANGAN commit/push final atau menyatakan tuntas sebelum itu. Periksa dengan `cat "$HANOMAN_PHASE_FILE"` sebelum commit terakhir.'
12607
+ ].filter(Boolean).join("\n\n");
12608
+ }
12609
+ function startPrompt(flow, spec, branchTo, autonomy, verifyScope, method, attachments, plan) {
12471
12610
  const m = resolveMethod(method);
12472
- const detail = spec.payload ? `
12473
- Detail: ${JSON.stringify(spec.payload)}` : "";
12611
+ const head = `hanoman ${flow}. Ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.`;
12612
+ const push = `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`;
12613
+ if (plan) {
12614
+ return [
12615
+ head,
12616
+ orchestratorClause(plan, { fastPath: flow === "qa" }),
12617
+ auditContinuationForOrchestrator(flow, spec),
12618
+ autonomyClause(autonomy),
12619
+ attachmentClause(attachments),
12620
+ push,
12621
+ specContext(spec)
12622
+ ].filter(Boolean).join("\n\n");
12623
+ }
12474
12624
  return [
12475
- `hanoman ${flow}. Ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.`,
12625
+ head,
12476
12626
  phaseInstruction(PIPELINES[flow], m),
12477
12627
  auditDecisionInstruction(flow),
12478
12628
  auditContinuationInstruction(flow, spec),
@@ -12482,40 +12632,59 @@ Detail: ${JSON.stringify(spec.payload)}` : "";
12482
12632
  codeStyleClause(flow),
12483
12633
  methodClause(m),
12484
12634
  attachmentClause(attachments),
12485
- skillInstruction(PIPELINES[flow], m, writesCode(flow)),
12486
- `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
12487
- `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
12488
- Judul: ${spec.title}
12489
- Objective: ${spec.objective}${detail}`
12635
+ skillInstruction(flow, PIPELINES[flow], m),
12636
+ push,
12637
+ specContext(spec)
12490
12638
  ].filter(Boolean).join("\n\n");
12491
12639
  }
12492
- function continuePrompt(flow, spec, branchTo, autonomy, verifyScope, method, attachments) {
12640
+ function continuePrompt(flow, spec, branchTo, autonomy, verifyScope, method, attachments, plan) {
12493
12641
  const m = resolveMethod(method);
12494
- const detail = spec.payload ? `
12495
- Detail: ${JSON.stringify(spec.payload)}` : "";
12642
+ const head = `hanoman ${flow} \u2014 MELANJUTKAN backlog item yang sebelumnya ditandai selesai padahal pekerjaannya belum tuntas. Ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.`;
12643
+ const push = `Setelah selesai: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`;
12644
+ if (plan) {
12645
+ return [
12646
+ head,
12647
+ `JANGAN mengulang fase awal \u2014 spec & plan sudah ada di ${m.planDir}/**. Lanjutkan HANYA fase Execute lewat subagent fasenya.`,
12648
+ orchestratorClause(plan),
12649
+ autonomyClause(autonomy),
12650
+ attachmentClause(attachments),
12651
+ push,
12652
+ specContext(spec)
12653
+ ].filter(Boolean).join("\n\n");
12654
+ }
12496
12655
  return [
12497
- `hanoman ${flow} \u2014 MELANJUTKAN backlog item yang sebelumnya ditandai selesai padahal pekerjaannya belum tuntas. Ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.`,
12656
+ head,
12498
12657
  `JANGAN mengulang fase awal \u2014 spec & plan sudah ada. Lanjut di fase Execute: baca plan di ${m.planDir}/** untuk backlog item ini, periksa task yang sudah \`[x]\` dan selesaikan yang masih \`[ ]\`. Verifikasi nyata sebelum klaim selesai.`,
12499
12658
  autonomyClause(autonomy),
12500
12659
  scopeClause(flow, verifyScope),
12501
12660
  codeStyleClause(flow),
12502
12661
  methodClause(m),
12503
12662
  attachmentClause(attachments),
12504
- skillInstruction(["Execute"], m, writesCode(flow)),
12505
- `Setelah selesai: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
12506
- `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
12507
- Judul: ${spec.title}
12508
- Objective: ${spec.objective}${detail}`
12663
+ skillInstruction(flow, ["Execute"], m),
12664
+ push,
12665
+ specContext(spec)
12509
12666
  ].filter(Boolean).join("\n\n");
12510
12667
  }
12511
- function resumePrompt(flow, spec, branchTo, resume, autonomy, verifyScope, method, attachments) {
12668
+ function resumePrompt(flow, spec, branchTo, resume, autonomy, verifyScope, method, attachments, plan) {
12512
12669
  const m = resolveMethod(method);
12513
- const detail = spec.payload ? `
12514
- Detail: ${JSON.stringify(spec.payload)}` : "";
12515
12670
  const auditDecided = resume.recorded.some((line) => line.startsWith("Audit "));
12671
+ const head = `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.`;
12672
+ const resumed = resumeClause(resume, branchTo, m.planDir, PIPELINES[flow].includes("Plan"));
12673
+ const push = `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`;
12674
+ if (plan) {
12675
+ return [
12676
+ head,
12677
+ resumed,
12678
+ orchestratorClause(plan, { fastPath: flow === "qa" && !auditDecided }),
12679
+ autonomyClause(autonomy),
12680
+ attachmentClause(attachments),
12681
+ push,
12682
+ specContext(spec)
12683
+ ].filter(Boolean).join("\n\n");
12684
+ }
12516
12685
  return [
12517
- `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.`,
12518
- resumeClause(resume, branchTo, m.planDir, PIPELINES[flow].includes("Plan")),
12686
+ head,
12687
+ resumed,
12519
12688
  phaseInstruction(PIPELINES[flow], m),
12520
12689
  auditDecided ? "" : auditDecisionInstruction(flow),
12521
12690
  autonomyClause(autonomy),
@@ -12523,26 +12692,33 @@ Detail: ${JSON.stringify(spec.payload)}` : "";
12523
12692
  codeStyleClause(flow),
12524
12693
  methodClause(m),
12525
12694
  attachmentClause(attachments),
12526
- skillInstruction(PIPELINES[flow], m, writesCode(flow)),
12527
- `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
12528
- `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
12529
- Judul: ${spec.title}
12530
- Objective: ${spec.objective}${detail}`
12695
+ skillInstruction(flow, PIPELINES[flow], m),
12696
+ push,
12697
+ specContext(spec)
12531
12698
  ].filter(Boolean).join("\n\n");
12532
12699
  }
12533
12700
  function startGoalPrompt(flow, spec, branchTo, opts = {}) {
12534
12701
  const m = resolveMethod(opts.method);
12535
- const g = readGoalPayload(spec.payload);
12536
12702
  const noEffort = flow === "no_effort";
12537
- const detail = [
12538
- `Goal: ${g?.goal ?? spec.objective}`,
12539
- g?.done ? `Selesai bila: ${g.done}` : "",
12540
- g?.constraints ? `Batasan: ${g.constraints}` : ""
12541
- ].filter(Boolean).join("\n");
12703
+ const head = noEffort ? "hanoman no-effort \u2014 sesi ini mengerjakan SATU pekerjaan remeh lalu berhenti. TIDAK ada fase Brainstorm, Objective, Spec, Plan, maupun fase pembuktian terpisah: jangan menulis design doc, jangan menulis plan berkotak, jangan memecah pekerjaan ini jadi backlog baru, dan jangan menambah fase sendiri. Langsung kerjakan, buktikan seperlunya di fase yang sama, lalu berhenti. Tetap ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama." : "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.";
12704
+ const resumed = opts.resume ? resumeClause(opts.resume, branchTo, m.planDir, false) : "";
12705
+ const push = `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`;
12706
+ if (opts.plan) {
12707
+ return [
12708
+ head,
12709
+ resumed,
12710
+ goalDetail(spec),
12711
+ orchestratorClause(opts.plan),
12712
+ autonomyClause(opts.autonomy),
12713
+ attachmentClause(opts.attachments),
12714
+ push,
12715
+ goalBlock(spec)
12716
+ ].filter(Boolean).join("\n\n");
12717
+ }
12542
12718
  return [
12543
- noEffort ? "hanoman no-effort \u2014 sesi ini mengerjakan SATU pekerjaan remeh lalu berhenti. TIDAK ada fase Brainstorm, Objective, Spec, Plan, maupun fase pembuktian terpisah: jangan menulis design doc, jangan menulis plan berkotak, jangan memecah pekerjaan ini jadi backlog baru, dan jangan menambah fase sendiri. Langsung kerjakan, buktikan seperlunya di fase yang sama, lalu berhenti. Tetap ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama." : "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.",
12544
- opts.resume ? resumeClause(opts.resume, branchTo, m.planDir, false) : "",
12545
- detail,
12719
+ head,
12720
+ resumed,
12721
+ goalDetail(spec),
12546
12722
  phaseInstruction(PIPELINES[flow], m),
12547
12723
  noEffort ? "" : "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.",
12548
12724
  autonomyClause(opts.autonomy),
@@ -12550,74 +12726,95 @@ function startGoalPrompt(flow, spec, branchTo, opts = {}) {
12550
12726
  codeStyleClause(flow),
12551
12727
  methodClause(m),
12552
12728
  attachmentClause(opts.attachments),
12553
- skillInstruction(PIPELINES[flow], m, writesCode(flow)),
12554
- `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
12555
- `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
12556
- Judul: ${spec.title}`
12729
+ skillInstruction(flow, PIPELINES[flow], m),
12730
+ push,
12731
+ goalBlock(spec)
12557
12732
  ].filter(Boolean).join("\n\n");
12558
12733
  }
12559
- function startProjectPrompt(flow, project, branchTo) {
12734
+ function startProjectPrompt(flow, project, branchTo, plan) {
12735
+ const push = `Setiap fase selesai: commit hasilnya, lalu \`git push origin HEAD:refs/heads/${branchTo}\` \u2014 push per fase, supaya pekerjaan tak hilang bila worktree lenyap. Bila remote origin tidak ada, lewati push dan catat itu di laporan akhir \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia yang me-review dan merge branch ${branchTo}.`;
12736
+ if (plan) {
12737
+ return [
12738
+ `hanoman ${flow}. Susun Source of Truth repo ini dari kodenya di internal/docs/** lewat subagent fase; STANDAR DOCS ada di definisi agen fase penulis docs.`,
12739
+ orchestratorClause(plan),
12740
+ push,
12741
+ projectContext(project)
12742
+ ].join("\n\n");
12743
+ }
12560
12744
  return [
12561
12745
  `hanoman ${flow}. Susun Source of Truth repo ini dari kodenya di internal/docs/**, mengikuti STANDAR DOCS di bagian bawah prompt ini.`,
12562
12746
  phaseInstruction(PIPELINES[flow], PROJECT_METHOD),
12563
12747
  REVERSE_PHASE_GUIDE,
12564
- `Setiap fase selesai: commit hasilnya, lalu \`git push origin HEAD:refs/heads/${branchTo}\` \u2014 push per fase, supaya pekerjaan tak hilang bila worktree lenyap. Bila remote origin tidak ada, lewati push dan catat itu di laporan akhir \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia yang me-review dan merge branch ${branchTo}.`,
12565
- `Project ${project.id} \xB7 ${project.name}
12566
- Deskripsi: ${project.desc || "\u2014"}
12567
- Stack: ${project.stack || "\u2014"}`,
12748
+ push,
12749
+ projectContext(project),
12568
12750
  `=== STANDAR DOCS ===
12569
12751
  ${REVERSE_STANDARD}`
12570
12752
  ].join("\n\n");
12571
12753
  }
12572
- function startPrdPrompt(project, brief, branchTo, audit) {
12754
+ function startPrdPrompt(project, brief, branchTo, audit, plan) {
12573
12755
  const slug = branchTo.slice(branchTo.lastIndexOf("/") + 1);
12574
- const auditBlock = audit ? `=== DOKUMEN AUDIT ${audit.id} (${audit.path}) ===
12575
- PRD ini adalah TINDAK LANJUT audit di bawah. Pakai temuannya sebagai bahan brainstorm \u2014 jangan menginvestigasi ulang, dan jangan pula menyalinnya mentah-mentah ke PRD.
12576
-
12577
- ${audit.content}` : "";
12756
+ const push = `Setelah PRD ditulis: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Bila remote origin tidak ada, lewati push dan catat itu di terminal \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia yang me-review lalu merge branch ${branchTo}.`;
12757
+ if (plan) {
12758
+ return [
12759
+ "hanoman prd. Kamu memimpin penyusunan SATU dokumen PRD untuk project ini lewat subagent fase. Keluaranmu HANYA dokumen PRD \u2014 JANGAN menulis kode fitur.",
12760
+ orchestratorClause(plan),
12761
+ push,
12762
+ prdBriefBlock(project, brief)
12763
+ ].join("\n\n");
12764
+ }
12765
+ const lines2 = prdPhaseLines(slug);
12578
12766
  return [
12579
12767
  `hanoman prd. Kamu memandu PM/PO menyusun SATU dokumen PRD untuk project ini dari brief + brainstorm. Keluaranmu HANYA dokumen PRD \u2014 JANGAN menulis kode fitur.`,
12580
12768
  phaseInstruction(PIPELINES.prd, PROJECT_METHOD),
12581
- `- Brainstorm: pandu PM secara interaktif. Ajukan SATU pertanyaan per giliran ke manusia di terminal ini, tunggu jawabannya, perdalam brief sampai jelas (masalah, pengguna, scope, metrik sukses). Jangan mengarang; topik yang PM belum jawab tandai sebagai open question.`,
12582
- `- PRD: tulis dokumen ke \`docs/prd/${slug}.md\`. Awali dengan heading \`# <judul PRD>\`, lalu bagian: Ringkasan \xB7 Masalah & konteks \xB7 Persona/pengguna \xB7 Goals & non-goals \xB7 Scope (in/out) \xB7 User stories \xB7 Acceptance criteria (gaya EARS) \xB7 Metrik sukses \xB7 Open questions. Isi lengkap dan spesifik dari hasil brainstorm, bukan kerangka kosong.`,
12583
- skillInstruction(PIPELINES.prd, PROJECT_METHOD, false),
12584
- `Setelah PRD ditulis: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Bila remote origin tidak ada, lewati push dan catat itu di terminal \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia yang me-review lalu merge branch ${branchTo}.`,
12585
- `Project ${project.id} \xB7 ${project.name}
12586
- Brief \u2014 Judul: ${brief.title}
12587
- Konteks: ${brief.context}
12588
- Outcome: ${brief.outcome}${brief.constraints ? `
12589
- Batasan: ${brief.constraints}` : ""}`,
12590
- auditBlock
12769
+ lines2.Brainstorm,
12770
+ lines2.PRD,
12771
+ skillInstruction("prd", PIPELINES.prd, PROJECT_METHOD),
12772
+ push,
12773
+ prdBriefBlock(project, brief),
12774
+ prdAuditBlock(audit)
12591
12775
  ].filter(Boolean).join("\n\n");
12592
12776
  }
12593
- function startBreakdownPrompt(project, prd, branchTo) {
12777
+ function startBreakdownPrompt(project, prd, branchTo, plan) {
12594
12778
  const slug = branchTo.slice(branchTo.lastIndexOf("/") + 1);
12779
+ const push = `Setelah manifest ditulis: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Bila remote origin tidak ada, lewati push dan catat itu di terminal \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia me-review manifest lalu materialize backlog darinya.`;
12780
+ if (plan) {
12781
+ return [
12782
+ "hanoman breakdown. Kamu memimpin pemecahan SATU PRD kompleks menjadi BEBERAPA backlog kecil yang bisa dikerjakan PARALEL lewat subagent fase. Keluaranmu HANYA dokumen manifest \u2014 JANGAN menulis kode fitur.",
12783
+ orchestratorClause(plan),
12784
+ AUTONOMY_CLAUSE,
12785
+ push,
12786
+ `Project ${project.id} \xB7 ${project.name}
12787
+ PRD: ${prd.title} (${prd.path})`
12788
+ ].join("\n\n");
12789
+ }
12790
+ const lines2 = breakdownPhaseLines(slug, prd.title);
12595
12791
  return [
12596
12792
  `hanoman breakdown. Kamu memecah SATU PRD kompleks menjadi BEBERAPA backlog kecil yang bisa dikerjakan PARALEL tanpa saling bergantung. Keluaranmu HANYA dokumen manifest \u2014 JANGAN menulis kode fitur.`,
12597
12793
  phaseInstruction(PIPELINES.breakdown, PROJECT_METHOD),
12598
- `- Analisis: baca PRD (di bawah) sampai paham SELURUH scope in-PRD. Petakan pekerjaan menjadi unit-unit yang: (a) kecil & terukur \u2014 tiap unit tuntas dalam satu sesi; (b) non-overlapping \u2014 cakupan tak tumpang tindih; (c) TANPA cross-dependency \u2014 urutan bebas, bisa jalan bersamaan; (d) gabungannya MENUTUP seluruh scope PRD. Bila dua unit terpaksa berurutan, gabung jadi satu.`,
12599
- `- Breakdown: tulis manifest ke \`docs/prd/${slug}.breakdown.md\`. Awali heading \`# Breakdown: ${prd.title}\`, lalu prosa: ringkasan + untuk TIAP backlog satu paragraf (judul, cakupan, dan SATU kalimat kenapa aman-paralel / tak bergantung yang lain). Di AKHIR dokumen sertakan TEPAT SATU blok kode berpagar json berisi kontrak mesin PERSIS bentuk ini (tanpa komentar, priority \u2208 tinggi|sedang|rendah):
12600
- \`\`\`json
12601
- { "items": [ { "title": "\u2026", "context": "\u2026", "outcome": "\u2026", "priority": "sedang" } ] }
12602
- \`\`\`
12603
- \`context\` = bagian PRD yang dicakup; \`outcome\` = kondisi selesai terukur; \`title\` ringkas. Minimal 2 item bila PRD memang kompleks; bila PRD ternyata sekecil 1 unit, katakan itu di prosa dan tetap tulis 1 item.`,
12794
+ lines2.Analisis,
12795
+ lines2.Breakdown,
12604
12796
  AUTONOMY_CLAUSE,
12605
- `Setelah manifest ditulis: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Bila remote origin tidak ada, lewati push dan catat itu di terminal \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia me-review manifest lalu materialize backlog darinya.`,
12606
- `Project ${project.id} \xB7 ${project.name}
12607
- === PRD: ${prd.title} (${prd.path}) ===
12608
- ${prd.content}`
12797
+ push,
12798
+ breakdownContext(project, prd)
12609
12799
  ].filter(Boolean).join("\n\n");
12610
12800
  }
12611
- function startScaffoldPrompt(project, branchTo) {
12801
+ function startScaffoldPrompt(project, branchTo, plan) {
12802
+ const push = `Setiap fase selesai: commit hasilnya, lalu \`git push origin HEAD:refs/heads/${branchTo}\` \u2014 push per fase, supaya pekerjaan tak hilang bila worktree lenyap. Bila remote origin tidak ada, lewati push dan catat itu di laporan akhir \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia yang me-review dan merge branch ${branchTo}.`;
12803
+ if (plan) {
12804
+ return [
12805
+ "hanoman scaffold. Susun Source of Truth LENGKAP untuk project from-scratch ini di internal/docs/** DARI IDE-nya lewat subagent fase; STANDAR DOCS ada di definisi agen fase Doc index. Belum ada kode \u2014 docs dulu.",
12806
+ orchestratorClause(plan),
12807
+ push,
12808
+ scaffoldContext(project)
12809
+ ].join("\n\n");
12810
+ }
12612
12811
  return [
12613
12812
  `hanoman scaffold. Susun Source of Truth LENGKAP untuk project from-scratch ini di internal/docs/** DARI IDE-nya, mengikuti STANDAR DOCS di bagian bawah prompt ini. Belum ada kode \u2014 docs dulu.`,
12614
12813
  phaseInstruction(PIPELINES.scaffold, PROJECT_METHOD),
12615
12814
  SCAFFOLD_PHASE_GUIDE,
12616
- `Setiap fase selesai: commit hasilnya, lalu \`git push origin HEAD:refs/heads/${branchTo}\` \u2014 push per fase, supaya pekerjaan tak hilang bila worktree lenyap. Bila remote origin tidak ada, lewati push dan catat itu di laporan akhir \u2014 jangan gagal diam-diam. Worktree ini detached HEAD \u2014 memang disengaja. Manusia yang me-review dan merge branch ${branchTo}.`,
12617
- skillInstruction(PIPELINES.scaffold, PROJECT_METHOD, false),
12618
- `Project ${project.id} \xB7 ${project.name}
12619
- Ide awal: ${project.desc || "\u2014"}
12620
- Stack: ${project.stack || "\u2014"}`,
12815
+ push,
12816
+ skillInstruction("scaffold", PIPELINES.scaffold, PROJECT_METHOD),
12817
+ scaffoldContext(project),
12621
12818
  `=== STANDAR DOCS ===
12622
12819
  ${REVERSE_STANDARD}`
12623
12820
  ].filter(Boolean).join("\n\n");
@@ -12636,7 +12833,7 @@ Stack: ${project.stack || "\u2014"}`,
12636
12833
  ${cron.prompt}`
12637
12834
  ].join("\n\n");
12638
12835
  }
12639
- var PIPELINES, WORK_PHASES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, codeStyleClause, methodClause, attachmentClause, PROJECT_METHOD, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
12836
+ var PIPELINES, WORK_PHASES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, auditContinuationForOrchestrator, writesCode, scopeClause, codeStyleClause, methodClause, attachmentClause, PROJECT_METHOD, specContext, goalDetail, goalBlock, goalContext, projectContext, scaffoldContext, prdBriefBlock, prdAuditBlock, prdContext, breakdownContext, prdPhaseLines, breakdownPhaseLines, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
12640
12837
  var init_prompt = __esm({
12641
12838
  "../runner/src/prompt.ts"() {
12642
12839
  "use strict";
@@ -12645,26 +12842,7 @@ var init_prompt = __esm({
12645
12842
  init_verify_scope();
12646
12843
  init_code_style();
12647
12844
  init_goal_spec();
12648
- PIPELINES = {
12649
- feature: ["Brainstorm", "Objective", "Spec", "Plan", "Execute"],
12650
- qa: ["Audit", "Spec", "Plan", "Execute"],
12651
- scaffold: ["Brainstorm", "Objective", "Doc index"],
12652
- reverse: ["Scan", "Docs teknis", "Wawancara", "Konvensi & index", "Serah terima"],
12653
- prd: ["Brainstorm", "PRD"],
12654
- audit: ["Audit", "Laporan"],
12655
- breakdown: ["Analisis", "Breakdown"],
12656
- // SPEC-337 · ADR-0075 · audit lintas project: fase & stage-map identik audit-only, scope-nya
12657
- // yang berbeda (project utama + tetangga ProjectLink).
12658
- // SPEC-407 · ADR-0089 · backlog goal: tak ada fase perencanaan sama sekali. `Goal` = kerjakan,
12659
- // `Verifikasi` = buktikan. Kedua nama unik lintas PIPELINES — syarat peta REACHED di server,
12660
- // yang berkunci nama fase saja.
12661
- goal: ["Goal", "Verifikasi"],
12662
- // SPEC-825 · ADR-0123 · task remeh: SATU fase. Fase `Verifikasi` milik flow goal menghabiskan
12663
- // satu giliran agen untuk membuktikan sesuatu yang diff-nya sendiri sudah membuktikan; untuk
12664
- // ganti copy / bump konstanta / typo docs itu murni biaya. Nama `Kerjakan` unik lintas
12665
- // PIPELINES — syarat peta REACHED server, yang berkunci nama fase saja.
12666
- no_effort: ["Kerjakan"]
12667
- };
12845
+ PIPELINES = FLOW_PHASES;
12668
12846
  WORK_PHASES = ["Execute", "Goal", "Kerjakan"];
12669
12847
  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.';
12670
12848
  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.";
@@ -12676,11 +12854,9 @@ Setiap kali sebuah fase selesai (atau kamu putuskan dilewati), append satu baris
12676
12854
  return base2 + `
12677
12855
  Execute BELUM selesai selama plan (\`${method.planDir}/**\`) masih punya task \`- [ ]\`: kerjakan SEMUA PR/task sampai tiap kotak jadi \`- [x]\` sebelum menulis \`Execute done\`. hanoman menahan backlog di \`executing\`, bukan \`done\`, selama masih ada \`- [ ]\`.`;
12678
12856
  };
12679
- skillInstruction = (phases, method, withExit) => {
12680
- const last = phases[phases.length - 1];
12857
+ skillInstruction = (flow, phases, method) => {
12681
12858
  const lines2 = phases.map((p3) => {
12682
- const own = method.phaseSkills[p3] ?? [];
12683
- const skills = withExit && p3 === last ? [.../* @__PURE__ */ new Set([...own, ...method.exitSkills])] : own;
12859
+ const skills = phaseSkillsFor(flow, p3, method);
12684
12860
  return skills.length ? `- ${p3}: ${skills.join(", ")}` : "";
12685
12861
  }).filter(Boolean);
12686
12862
  return lines2.length ? `Skills ${method.label} WAJIB: sebelum mengerjakan fase di bawah, muat & ikuti skill-nya dengan mekanisme yang tersedia di agenmu \u2014 bila skill relevan tersedia, pakai.
@@ -12719,6 +12895,15 @@ ${lines2.join("\n")}` : "";
12719
12895
  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.`;
12720
12896
  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.`;
12721
12897
  };
12898
+ auditContinuationForOrchestrator = (flow, spec) => {
12899
+ if (flow !== "qa" && flow !== "feature") return "";
12900
+ const fromAudit = spec.payload && typeof spec.payload === "object" ? spec.payload.fromAudit : void 0;
12901
+ if (typeof fromAudit !== "string" || !fromAudit) return "";
12902
+ const doc = `internal/docs/research/audit-${fromAudit.toLowerCase()}-*.md`;
12903
+ if (flow === "feature")
12904
+ return `Backlog brief ini LANJUTAN dari audit ${fromAudit}. Worktree ini lahir dari branch audit itu, jadi dokumen audit sudah ada di ${doc}. Saat memanggil agen fase Brainstorm, cantumkan path itu di baris \`Artefak fase sebelumnya:\` blok serah-terima \u2014 JANGAN membacanya sendiri untuk merancang fitur ini. Saat memanggil agen fase Objective, baris itu biasanya sudah membawa path artefak Brainstorm (dokumen spec yang ditulis fase itu); tambahkan path dokumen audit di samping path itu, bukan menggantikannya. Temuannya sudah terbukti, tapi bentuk solusinya belum; itu tetap dikerjakan agen fasenya masing-masing, bukan olehmu.`;
12905
+ return `Backlog qa ini LANJUTAN dari audit ${fromAudit}. Temuannya sudah terbukti di ${doc} \u2014 JANGAN mendelegasikan ulang fase Audit. Sebelum fase lain: jalankan persis \`echo "Audit skipped" >> "$HANOMAN_PHASE_FILE"\`, verifikasi dengan \`tail -1 "$HANOMAN_PHASE_FILE"\` bahwa barisnya benar tertulis (gerbang yang sama seperti langkah 2), lalu cantumkan path dokumen audit itu di baris \`Artefak fase sebelumnya:\` saat memanggil agen fase Spec. Keputusan routing di sini menggantikan langkah 5 di atas \u2014 JANGAN menunggu frasa \`Rekomendasi fase: jalur-cepat\` dari agen Audit karena Audit tidak dijalankan pada kelanjutan ini. Kamu diizinkan mengambil SATU keputusan ROUTING dari isi dokumen audit itu SAJA, dibaca langsung dari kriterianya: temuan berconfidence tinggi dan perbaikan langsung berdiff kecil \u2192 tandai \`Spec skipped\` lalu \`Plan skipped\` (gerbang yang sama) lalu panggil agen fase Execute; selebihnya panggil Spec \u2192 Plan \u2192 Execute penuh lewat agen fasenya masing-masing. Kamu TIDAK menginvestigasi ulang maupun merancang perbaikannya sendiri \u2014 itu tetap pekerjaan agen fase.`;
12906
+ };
12722
12907
  writesCode = (flow) => PIPELINES[flow].some((p3) => WORK_PHASES.includes(p3));
12723
12908
  scopeClause = (flow, scope) => scope && writesCode(flow) ? verifyScopeClause(scope) : "";
12724
12909
  codeStyleClause = (flow) => writesCode(flow) ? CODE_STYLE_CLAUSE : "";
@@ -12732,6 +12917,57 @@ BACA semuanya SEBELUM mengerjakan fase pertama \u2014 dokumen teks (.md/.txt/.lo
12732
12917
  Lampiran bisa BERTAMBAH atau BERKURANG selagi sesi berjalan. Di AWAL SETIAP FASE, baca ulang manifest \`${ctx.dir}/INDEX.md\` lalu baca lampiran yang belum pernah kamu baca \u2014 daftar di atas adalah keadaan saat sesi ini lahir, bukan keadaan tetap.`;
12733
12918
  };
12734
12919
  PROJECT_METHOD = resolveMethod();
12920
+ specContext = (spec) => {
12921
+ const detail = spec.payload ? `
12922
+ Detail: ${JSON.stringify(spec.payload)}` : "";
12923
+ return `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
12924
+ Judul: ${spec.title}
12925
+ Objective: ${spec.objective}${detail}`;
12926
+ };
12927
+ goalDetail = (spec) => {
12928
+ const g = readGoalPayload(spec.payload);
12929
+ return [
12930
+ `Goal: ${g?.goal ?? spec.objective}`,
12931
+ g?.done ? `Selesai bila: ${g.done}` : "",
12932
+ g?.constraints ? `Batasan: ${g.constraints}` : ""
12933
+ ].filter(Boolean).join("\n");
12934
+ };
12935
+ goalBlock = (spec) => `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
12936
+ Judul: ${spec.title}`;
12937
+ goalContext = (spec) => `${goalDetail(spec)}
12938
+
12939
+ ${goalBlock(spec)}`;
12940
+ projectContext = (project) => `Project ${project.id} \xB7 ${project.name}
12941
+ Deskripsi: ${project.desc || "\u2014"}
12942
+ Stack: ${project.stack || "\u2014"}`;
12943
+ scaffoldContext = (project) => `Project ${project.id} \xB7 ${project.name}
12944
+ Ide awal: ${project.desc || "\u2014"}
12945
+ Stack: ${project.stack || "\u2014"}`;
12946
+ prdBriefBlock = (project, brief) => `Project ${project.id} \xB7 ${project.name}
12947
+ Brief \u2014 Judul: ${brief.title}
12948
+ Konteks: ${brief.context}
12949
+ Outcome: ${brief.outcome}${brief.constraints ? `
12950
+ Batasan: ${brief.constraints}` : ""}`;
12951
+ prdAuditBlock = (audit) => audit ? `=== DOKUMEN AUDIT ${audit.id} (${audit.path}) ===
12952
+ PRD ini adalah TINDAK LANJUT audit di bawah. Pakai temuannya sebagai bahan brainstorm \u2014 jangan menginvestigasi ulang, dan jangan pula menyalinnya mentah-mentah ke PRD.
12953
+
12954
+ ${audit.content}` : "";
12955
+ prdContext = (project, brief, audit) => [prdBriefBlock(project, brief), prdAuditBlock(audit)].filter(Boolean).join("\n\n");
12956
+ breakdownContext = (project, prd) => `Project ${project.id} \xB7 ${project.name}
12957
+ === PRD: ${prd.title} (${prd.path}) ===
12958
+ ${prd.content}`;
12959
+ prdPhaseLines = (slug) => ({
12960
+ Brainstorm: `- Brainstorm: pandu PM secara interaktif. Ajukan SATU pertanyaan per giliran ke manusia di terminal ini, tunggu jawabannya, perdalam brief sampai jelas (masalah, pengguna, scope, metrik sukses). Jangan mengarang; topik yang PM belum jawab tandai sebagai open question.`,
12961
+ PRD: `- PRD: tulis dokumen ke \`docs/prd/${slug}.md\`. Awali dengan heading \`# <judul PRD>\`, lalu bagian: Ringkasan \xB7 Masalah & konteks \xB7 Persona/pengguna \xB7 Goals & non-goals \xB7 Scope (in/out) \xB7 User stories \xB7 Acceptance criteria (gaya EARS) \xB7 Metrik sukses \xB7 Open questions. Isi lengkap dan spesifik dari hasil brainstorm, bukan kerangka kosong.`
12962
+ });
12963
+ breakdownPhaseLines = (slug, title) => ({
12964
+ Analisis: `- Analisis: baca PRD (di bawah) sampai paham SELURUH scope in-PRD. Petakan pekerjaan menjadi unit-unit yang: (a) kecil & terukur \u2014 tiap unit tuntas dalam satu sesi; (b) non-overlapping \u2014 cakupan tak tumpang tindih; (c) TANPA cross-dependency \u2014 urutan bebas, bisa jalan bersamaan; (d) gabungannya MENUTUP seluruh scope PRD. Bila dua unit terpaksa berurutan, gabung jadi satu.`,
12965
+ Breakdown: `- Breakdown: tulis manifest ke \`docs/prd/${slug}.breakdown.md\`. Awali heading \`# Breakdown: ${title}\`, lalu prosa: ringkasan + untuk TIAP backlog satu paragraf (judul, cakupan, dan SATU kalimat kenapa aman-paralel / tak bergantung yang lain). Di AKHIR dokumen sertakan TEPAT SATU blok kode berpagar json berisi kontrak mesin PERSIS bentuk ini (tanpa komentar, priority \u2208 tinggi|sedang|rendah):
12966
+ \`\`\`json
12967
+ { "items": [ { "title": "\u2026", "context": "\u2026", "outcome": "\u2026", "priority": "sedang" } ] }
12968
+ \`\`\`
12969
+ \`context\` = bagian PRD yang dicakup; \`outcome\` = kondisi selesai terukur; \`title\` ringkas. Minimal 2 item bila PRD memang kompleks; bila PRD ternyata sekecil 1 unit, katakan itu di prosa dan tetap tulis 1 item.`
12970
+ });
12735
12971
  resumeClause = (r, branchTo, planDir, hasPlan = true) => {
12736
12972
  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.";
12737
12973
  const lanjut = r.next ? `Lanjutkan dari fase: ${r.next}.` : hasPlan ? `Semua fase sudah tercatat. Periksa apakah plan di \`${planDir}/**\` 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.";
@@ -12919,7 +13155,7 @@ var init_settings2 = __esm({
12919
13155
  '${HANOMAN_EVENT_HOST:+-H "host: $HANOMAN_EVENT_HOST"}',
12920
13156
  "--data-binary @- >/dev/null 2>&1; fi; exit 0"
12921
13157
  ].join(" ");
12922
- guardSettings = (decisionFile, goal, eventHook) => {
13158
+ guardSettings = (decisionFile, goal, eventHook, subagentStatusLine) => {
12923
13159
  const hooks2 = {};
12924
13160
  if (decisionFile) {
12925
13161
  const f = `'${decisionFile.split("'").join("'\\''")}'`;
@@ -12938,7 +13174,10 @@ var init_settings2 = __esm({
12938
13174
  hooks2.SubagentStop = [{ hooks: [{ type: "command", command: EVENT_HOOK_COMMAND }] }];
12939
13175
  }
12940
13176
  if (goal) hooks2.Stop = [{ hooks: [{ type: "prompt", prompt: goal }] }];
12941
- return { hooks: hooks2 };
13177
+ return {
13178
+ hooks: hooks2,
13179
+ ...subagentStatusLine ? { subagentStatusLine: { type: "command", command: subagentStatusLine } } : {}
13180
+ };
12942
13181
  };
12943
13182
  }
12944
13183
  });
@@ -13100,7 +13339,7 @@ function agentFlags(o) {
13100
13339
  ...o.effort ? ["--effort", o.effort] : [],
13101
13340
  "--dangerously-skip-permissions",
13102
13341
  "--settings",
13103
- JSON.stringify(guardSettings(o.decisionFile, o.goal, o.eventHook))
13342
+ JSON.stringify(guardSettings(o.decisionFile, o.goal, o.eventHook, o.subagentStatusLine))
13104
13343
  ];
13105
13344
  }
13106
13345
  var init_agent_cli = __esm({
@@ -13149,6 +13388,15 @@ function renderAgentsJson(defs, options2 = {}) {
13149
13388
  if (defs.length === 0) return "";
13150
13389
  const out4 = {};
13151
13390
  for (const d of defs) {
13391
+ if (d.kind === "phase") {
13392
+ out4[d.name] = {
13393
+ description: d.description,
13394
+ prompt: d.instructions,
13395
+ ...d.model ? { model: d.model } : {},
13396
+ ...d.effort ? { effort: d.effort } : {}
13397
+ };
13398
+ continue;
13399
+ }
13152
13400
  const resolvedTools = resolveTools({ tools: d.tools, mentions: d.mentions });
13153
13401
  const readOnly = d.workspacePolicy === "read-only";
13154
13402
  out4[d.name] = {
@@ -13176,7 +13424,7 @@ function renderAgentsJson(defs, options2 = {}) {
13176
13424
  return JSON.stringify(out4);
13177
13425
  }
13178
13426
  function agentDelegationClause(defs, runtime = "claude") {
13179
- if (defs.length === 0) return "";
13427
+ if (defs.every((def2) => def2.kind === "phase")) return "";
13180
13428
  return [
13181
13429
  "",
13182
13430
  "",
@@ -13268,10 +13516,6 @@ var init_runtime_profile = __esm({
13268
13516
  // ../runner/src/codex-agent-config.ts
13269
13517
  import { chmodSync, writeFileSync } from "node:fs";
13270
13518
  import { join } from "node:path";
13271
- function codexNativeAgentsSupported(version) {
13272
- const parsed = version ? /(\d+)\.(\d+)\.(\d+)/.exec(version)?.[0] : null;
13273
- return parsed ? cmpVersion(parsed, CODEX_NATIVE_AGENTS_MIN_CLIENT) >= 0 : false;
13274
- }
13275
13519
  function codexNativeVersionProbe(env, codexBin3 = env.HANOMAN_CODEX_BIN ?? "codex") {
13276
13520
  const sandbox = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
13277
13521
  if (sandbox !== "podman") return { bin: codexBin3, args: ["--version"] };
@@ -13296,7 +13540,7 @@ function renderCodexAgentToml(def2, roster, options2 = {}) {
13296
13540
  const lines2 = [
13297
13541
  `name = ${tomlString(def2.name)}`,
13298
13542
  `description = ${tomlString(def2.description)}`,
13299
- `developer_instructions = ${tomlString(agentPromptOf(def2, roster, "codex") + (options2.promptSuffix ?? ""))}`,
13543
+ `developer_instructions = ${tomlString(def2.kind === "phase" ? def2.instructions : agentPromptOf(def2, roster, "codex") + (options2.promptSuffix ?? ""))}`,
13300
13544
  ...def2.model ? [`model = ${tomlString(def2.model)}`] : [],
13301
13545
  ...def2.effort ? [`model_reasoning_effort = ${tomlString(def2.effort)}`] : [],
13302
13546
  ...def2.workspacePolicy === "read-only" ? ['sandbox_mode = "read-only"'] : []
@@ -13365,6 +13609,7 @@ function materializeCodexAgents(defs, tempDir, options2 = {}) {
13365
13609
  "-c",
13366
13610
  "agents.max_concurrent_threads_per_session=3"
13367
13611
  ];
13612
+ if (options2.maxDepth) args.push("-c", `agents.max_depth=${options2.maxDepth}`);
13368
13613
  for (const { def: def2, path } of successful) {
13369
13614
  const key = `agents.${tomlKey(def2.name)}`;
13370
13615
  args.push("-c", `${key}.description=${tomlString(def2.description)}`);
@@ -13379,14 +13624,13 @@ function materializeCodexAgents(defs, tempDir, options2 = {}) {
13379
13624
  liveDefs
13380
13625
  };
13381
13626
  }
13382
- var CODEX_NATIVE_AGENTS_MIN_CLIENT, shellQuote, tomlString, tomlKey, safeFilename;
13627
+ var shellQuote, tomlString, tomlKey, safeFilename;
13383
13628
  var init_codex_agent_config = __esm({
13384
13629
  "../runner/src/codex-agent-config.ts"() {
13385
13630
  "use strict";
13386
13631
  init_src();
13387
13632
  init_custom_agents();
13388
13633
  init_runtime_profile();
13389
- CODEX_NATIVE_AGENTS_MIN_CLIENT = "0.151.0";
13390
13634
  shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
13391
13635
  tomlString = (value) => JSON.stringify(value);
13392
13636
  tomlKey = (value) => JSON.stringify(value);
@@ -13547,7 +13791,7 @@ function agentDefinitionHash(def2, roster, runtime, inherited = {}) {
13547
13791
  const options2 = { readOnlyHookCommand: "<hanoman-read-only-hook>", promptSuffix: inherited.promptSuffix };
13548
13792
  const liveRoster = [def2, ...roster.filter((entry) => entry.name !== def2.name)];
13549
13793
  const native = runtime === "codex" ? renderCodexAgentToml(def2, liveRoster, options2) : JSON.parse(renderAgentsJson(liveRoster, options2))[def2.name];
13550
- if (typeof native !== "string") native.tools.sort();
13794
+ if (typeof native !== "string" && native.tools) native.tools.sort();
13551
13795
  return createHash("sha256").update(JSON.stringify({
13552
13796
  version: 1,
13553
13797
  runtime,
@@ -13567,6 +13811,112 @@ var init_agent_definition = __esm({
13567
13811
  }
13568
13812
  });
13569
13813
 
13814
+ // ../runner/src/phase-agents.ts
13815
+ function fromAuditOf(payload) {
13816
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return void 0;
13817
+ const value = payload.fromAudit;
13818
+ return typeof value === "string" && value ? value : void 0;
13819
+ }
13820
+ function backlogGuide(flow, phase, ctx) {
13821
+ const m = ctx.method;
13822
+ switch (phase) {
13823
+ case "Brainstorm":
13824
+ return `- Brainstorm: gali konteks, alternatif, dan keputusan untuk backlog ini dari Source of Truth dan kode. Tulis hasilnya sebagai bagian \`## Konteks & keputusan\` di dokumen spec baru di \`${m.specDir}/\` (nama berkas \`<YYYY-MM-DD>-<spec-id>-<slug>-design.md\`) dan laporkan path-nya.` + (ctx.fromAudit ? ` Backlog ini LANJUTAN audit ${ctx.fromAudit}: baca \`internal/docs/research/audit-${ctx.fromAudit.toLowerCase()}-*.md\` lebih dulu dan pakai temuannya sebagai bahan \u2014 jangan menginvestigasi ulang dari nol.` : "");
13825
+ case "Objective":
13826
+ return "- Objective: baca dokumen spec dari fase Brainstorm (path-nya di serah-terima), lalu tambahkan bagian `## Objective` berisi SATU objective terukur beserta kriteria suksesnya.";
13827
+ case "Spec":
13828
+ return flow === "qa" ? `- Spec: tulis dokumen spec perbaikan di \`${m.specDir}/\` dari dokumen audit fase Audit (path-nya di serah-terima): akar masalah, bentuk perbaikan, dan acceptance criteria gaya EARS.` + (ctx.fromAudit ? ` Backlog ini LANJUTAN audit ${ctx.fromAudit} \u2014 fase Audit sengaja DILEWATI orchestrator, jadi baca dokumennya di \`internal/docs/research/audit-${ctx.fromAudit.toLowerCase()}-*.md\` (path juga ada di serah-terima) sebagai temuan; jangan menginvestigasi ulang dari nol.` : "") : "- Spec: lengkapi dokumen spec yang sama \u2014 arsitektur, komponen, kontrak data/API, penanganan galat, dan acceptance criteria gaya EARS. Perbarui docs Source of Truth yang tersentuh.";
13829
+ case "Plan":
13830
+ return `- Plan: tulis plan implementasi berkotak \`- [ ]\` di \`${m.planDir}/\` untuk backlog ini dari dokumen spec-nya (path di serah-terima).`;
13831
+ case "Execute":
13832
+ return `- Execute: kerjakan plan di \`${m.planDir}/**\` untuk backlog ini. Execute BELUM selesai selama plan masih punya task \`- [ ]\`: kerjakan SEMUA task sampai tiap kotak jadi \`- [x]\` sebelum melapor \`Status: selesai\`. hanoman menahan backlog di \`executing\`, bukan \`done\`, selama masih ada \`- [ ]\`.`;
13833
+ case "Audit":
13834
+ return flow === "audit" ? "- Audit: ini audit-only \u2014 investigasi SAJA, JANGAN menulis perbaikan kode apa pun. Telusuri akar masalah / log / jawaban dan nilai apakah issue terdefinisi dengan baik. Laporkan temuan beserta buktinya; dokumennya ditulis fase Laporan." : "- Audit: telusuri akar masalah dengan bukti (reproduksi, log, kode) dan tulis dokumen audit ke `internal/docs/research/audit-<spec-id>-<slug>.md`. Lalu putuskan jalurnya dari hasil Audit, bukan default: bila temuan berconfidence tinggi dan perbaikannya bisa dikerjakan langsung (diff kecil, akar masalah jelas), tulis `Rekomendasi fase: jalur-cepat` beserta alasannya \u2014 orchestrator akan melewati Spec & Plan dan dokumen audit menjadi doc-of-record perbaikan itu. Bila temuan luas, berisiko, atau ambigu, tulis `Rekomendasi fase: penuh`.";
13835
+ case "Laporan":
13836
+ return "- Laporan: tulis DOKUMEN AUDIT ke Source of Truth `internal/docs/research/audit-<spec-id>-<slug>.md` (ikuti konvensi audit yang ada), tautkan di `internal/docs/README.md`, memuat: keluhan/pertanyaan, temuan (dengan bukti/log), apakah issue terdefinisi baik, dan rekomendasi tindak lanjut. Commit dokumen itu. Tak ada kode fitur.\n\n" + ESCALATION_CONTRACT;
13837
+ case "Goal":
13838
+ return "- Goal: kerjakan goal di blok KONTEKS sampai tercapai. TIDAK ada design doc, plan berkotak, maupun backlog baru.";
13839
+ case "Verifikasi":
13840
+ return "- Verifikasi: bukan formalitas \u2014 jalankan perintah yang membuktikan goal-nya tercapai (test/typecheck/benchmark/perintah yang relevan) dan baca outputnya. Klaim tanpa output bukan bukti.";
13841
+ case "Kerjakan":
13842
+ return "- Kerjakan: SATU pekerjaan remeh. Langsung kerjakan dan buktikan seperlunya di fase yang sama. Jangan menulis design doc, plan berkotak, backlog baru, atau fase tambahan.";
13843
+ default:
13844
+ return "";
13845
+ }
13846
+ }
13847
+ function projectGuide(flow, phase, ctx) {
13848
+ if (flow === "reverse") return guideLine(REVERSE_PHASE_GUIDE, phase);
13849
+ if (flow === "scaffold") return guideLine(SCAFFOLD_PHASE_GUIDE, phase);
13850
+ const slug = ctx.prd?.slug ?? flow;
13851
+ const lines2 = flow === "prd" ? prdPhaseLines(slug) : breakdownPhaseLines(slug, ctx.prd?.title ?? slug);
13852
+ return lines2[phase] ?? "";
13853
+ }
13854
+ function phaseAgentInstructions(entry, index, plan, ctx) {
13855
+ const { flow, method } = ctx;
13856
+ const phase = entry.phase;
13857
+ const guide = PROJECT_FLOWS.has(flow) ? projectGuide(flow, phase, ctx) : backlogGuide(flow, phase, ctx);
13858
+ const skills = phaseSkillsFor(flow, phase, method);
13859
+ const work = WORK_PHASES.includes(phase);
13860
+ return [
13861
+ `Kamu agen fase "${phase}" (fase ${index + 1}/${plan.phases.length}) flow ${flow} hanoman. Ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.`,
13862
+ guide,
13863
+ DOC_WRITING_PHASES.has(phase) ? `=== STANDAR DOCS ===
13864
+ ${REVERSE_STANDARD}` : "",
13865
+ skills.length ? `Skills ${method.label} WAJIB untuk fase ini \u2014 muat & ikuti dengan mekanisme yang tersedia di agenmu: ` + skills.join(", ") : "",
13866
+ work ? scopeClause(flow, ctx.verifyScope) : "",
13867
+ work ? codeStyleClause(flow) : "",
13868
+ methodClause(method),
13869
+ ATTACHMENT_NOTE,
13870
+ PHASE_AGENT_AUTONOMY,
13871
+ PHASE_AGENT_RULES,
13872
+ PHASE_AGENT_REPORT,
13873
+ `=== KONTEKS ===
13874
+ ${ctx.context}`
13875
+ ].filter(Boolean).join("\n\n");
13876
+ }
13877
+ function buildPhaseAgents(plan, ctx) {
13878
+ return plan.phases.map((entry, index) => ({
13879
+ kind: "phase",
13880
+ phase: entry.phase,
13881
+ name: entry.agentName,
13882
+ description: `Fase ${entry.phase} flow ${plan.flow} hanoman \u2014 hanya dipanggil orchestrator sesi ini.`,
13883
+ instructions: phaseAgentInstructions(entry, index, plan, ctx),
13884
+ tools: null,
13885
+ model: entry.model,
13886
+ effort: entry.effort,
13887
+ mentions: []
13888
+ }));
13889
+ }
13890
+ var PROJECT_FLOWS, DOC_WRITING_PHASES, PHASE_AGENT_AUTONOMY, PHASE_AGENT_RULES, PHASE_AGENT_REPORT, ATTACHMENT_NOTE;
13891
+ var init_phase_agents = __esm({
13892
+ "../runner/src/phase-agents.ts"() {
13893
+ "use strict";
13894
+ init_src();
13895
+ init_prompt();
13896
+ init_reverse_standard();
13897
+ PROJECT_FLOWS = /* @__PURE__ */ new Set(["reverse", "scaffold", "prd", "breakdown"]);
13898
+ DOC_WRITING_PHASES = /* @__PURE__ */ new Set(["Docs teknis", "Konvensi & index", "Doc index"]);
13899
+ PHASE_AGENT_AUTONOMY = 'Kamu dipanggil orchestrator sesi hanoman, bukan manusia. Checkpoint "review"/"approval" milik skill BUKAN titik berhenti \u2014 lanjut sampai fase ini tuntas. JANGAN bertanya ke manusia secara langsung: bila butuh keputusan yang mengubah bentuk kerja (data model, kontrak API, scope), atau panduan fase menyuruhmu bertanya ke manusia di terminal, tulis SATU pertanyaan di bagian `Pertanyaan untuk manusia:` laporanmu lalu berhenti. Jawabannya datang sebagai pesan susulan ke agen yang sama \u2014 lanjutkan dari sana dengan konteks yang sudah kamu punya.';
13900
+ PHASE_AGENT_RULES = [
13901
+ "Batas peran fase:",
13902
+ "- JANGAN menulis `$HANOMAN_PHASE_FILE` \u2014 orchestrator satu-satunya penulis marker fase.",
13903
+ "- JANGAN `git push` \u2014 push milik orchestrator. Commit artefak fasemu sendiri sebelum melapor.",
13904
+ `- JANGAN memanggil agen berawalan \`${PHASE_AGENT_PREFIX}\`. Custom agent lain boleh dipanggil bila relevan.`,
13905
+ "- Kerjakan HANYA fase ini; fase lain dikerjakan agen fasenya sendiri."
13906
+ ].join("\n");
13907
+ PHASE_AGENT_REPORT = [
13908
+ "Kontrak laporan (wajib, di akhir):",
13909
+ "- Baris pertama: `Status: selesai | sebagian | terhalang`.",
13910
+ "- `Artefak:` path berkas yang kamu tulis/ubah.",
13911
+ "- `Bukti:` perintah yang dijalankan beserta hasil yang benar-benar kamu baca.",
13912
+ "- `Pertanyaan untuk manusia:` (opsional) SATU pertanyaan; bila ada, berhenti di situ.",
13913
+ "- `Rekomendasi fase:` (opsional) mis. `jalur-cepat` sesudah Audit qa.",
13914
+ "Klaim tanpa bukti bukan bukti. Maksimal 1200 kata."
13915
+ ].join("\n");
13916
+ ATTACHMENT_NOTE = "Bila serah-terima menyebut manifest lampiran (`INDEX.md`), baca manifest itu dan lampiran yang relevan di awal fase.";
13917
+ }
13918
+ });
13919
+
13570
13920
  // ../runner/src/custom-agent-eval-evidence.ts
13571
13921
  var init_custom_agent_eval_evidence = __esm({
13572
13922
  "../runner/src/custom-agent-eval-evidence.ts"() {
@@ -14093,6 +14443,50 @@ var init_sandbox_probe = __esm({
14093
14443
  }
14094
14444
  });
14095
14445
 
14446
+ // ../runner/src/subagent-statusline.ts
14447
+ import { writeFileSync as writeFileSync4 } from "node:fs";
14448
+ import { join as join7 } from "node:path";
14449
+ function writeSubagentStatusline(dir2) {
14450
+ const script2 = join7(dir2, "subagent-statusline.cjs");
14451
+ const labels = join7(dir2, "subagent-models.json");
14452
+ writeFileSync4(script2, SUBAGENT_STATUSLINE_SCRIPT, { mode: 384 });
14453
+ writeFileSync4(labels, JSON.stringify(Object.fromEntries(MODELS.map((m) => [m.id, m.label]))), { mode: 384 });
14454
+ return `node ${JSON.stringify(script2)} ${JSON.stringify(labels)}`;
14455
+ }
14456
+ var SUBAGENT_STATUSLINE_SCRIPT;
14457
+ var init_subagent_statusline = __esm({
14458
+ "../runner/src/subagent-statusline.ts"() {
14459
+ "use strict";
14460
+ init_src();
14461
+ SUBAGENT_STATUSLINE_SCRIPT = [
14462
+ 'const fs = require("node:fs");',
14463
+ "const labelsPath = process.argv[2];",
14464
+ 'let raw = "";',
14465
+ 'process.stdin.setEncoding("utf8");',
14466
+ 'process.stdin.on("data", (c) => { raw += c; });',
14467
+ 'process.stdin.on("end", () => {',
14468
+ " try {",
14469
+ ' const labels = JSON.parse(fs.readFileSync(labelsPath, "utf8"));',
14470
+ " const tasks = JSON.parse(raw).tasks || [];",
14471
+ " const rows = [];",
14472
+ " for (const t of tasks) {",
14473
+ ' if (!t || typeof t.id !== "string" || typeof t.model !== "string") continue;',
14474
+ ' const parts = [t.label || t.description || "subagent", labels[t.model] || t.model];',
14475
+ ' if (typeof t.effort === "string") parts.push(t.effort);',
14476
+ ' if (typeof t.startTime === "number") {',
14477
+ " const s = Math.max(0, Math.round((Date.now() - t.startTime) / 1000));",
14478
+ ' parts.push(s >= 60 ? Math.floor(s / 60) + "m" + String(s % 60).padStart(2, "0") + "s" : s + "s");',
14479
+ " }",
14480
+ ' if (typeof t.tokenCount === "number" && t.tokenCount > 0) parts.push(Math.round(t.tokenCount / 1000) + "k tok");',
14481
+ ' rows.push(JSON.stringify({ id: t.id, content: parts.join(" \xB7 ") }));',
14482
+ " }",
14483
+ ' if (rows.length) process.stdout.write(rows.join("\\n") + "\\n");',
14484
+ " } catch {}",
14485
+ "});"
14486
+ ].join("\n");
14487
+ }
14488
+ });
14489
+
14096
14490
  // ../runner/src/index.ts
14097
14491
  var init_src2 = __esm({
14098
14492
  "../runner/src/index.ts"() {
@@ -14108,6 +14502,7 @@ var init_src2 = __esm({
14108
14502
  init_agent_cli();
14109
14503
  init_custom_agents();
14110
14504
  init_agent_definition();
14505
+ init_phase_agents();
14111
14506
  init_agent_readonly();
14112
14507
  init_codex_agent_config();
14113
14508
  init_custom_agent_eval();
@@ -14120,6 +14515,7 @@ var init_src2 = __esm({
14120
14515
  init_config_env();
14121
14516
  init_runtime_profile();
14122
14517
  init_sandbox_probe();
14518
+ init_subagent_statusline();
14123
14519
  }
14124
14520
  });
14125
14521
 
@@ -14318,6 +14714,44 @@ function readPhases(file, flow) {
14318
14714
  return { name: name2, state: "active" };
14319
14715
  });
14320
14716
  }
14717
+ function enrichPhases(phases, roster, invocations, doneSeenAt, now, bornAt2, doneAtBirth = /* @__PURE__ */ new Set()) {
14718
+ return phases.map((p3) => {
14719
+ const r = roster.find((entry) => entry.phase === p3.name);
14720
+ if (!r) return p3;
14721
+ const all = invocations.filter((i) => i.phase === p3.name);
14722
+ const mine = all.filter((i) => bornAt2 === 0 || Date.parse(i.startedAt) >= bornAt2).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
14723
+ const last = mine[mine.length - 1];
14724
+ const bornDone = doneAtBirth.has(p3.name);
14725
+ const seen2 = doneSeenAt.get(p3.name);
14726
+ const evidence = mine.length > 0 ? "ok" : bornDone ? all.length > 0 ? "ok" : "pending" : p3.state === "done" && seen2 !== void 0 && now - seen2 >= PHASE_EVIDENCE_GRACE_MS ? "missing" : "pending";
14727
+ return {
14728
+ ...p3,
14729
+ agent: {
14730
+ name: r.name,
14731
+ ...r.model ? { model: r.model } : {},
14732
+ ...r.effort ? { effort: r.effort } : {},
14733
+ attempts: new Set(mine.map((i) => i.runtimeInvocationId)).size,
14734
+ ...last ? {
14735
+ status: last.status,
14736
+ startedAt: last.startedAt,
14737
+ durationMs: last.durationMs,
14738
+ inputTokens: last.inputTokens,
14739
+ outputTokens: last.outputTokens,
14740
+ cachedTokens: last.cachedTokens,
14741
+ resultExcerpt: last.resultExcerpt
14742
+ } : {},
14743
+ evidence
14744
+ }
14745
+ };
14746
+ });
14747
+ }
14748
+ function trackDoneSeen(phases, doneSeenAt, now) {
14749
+ const byName = new Map(phases.map((p3) => [p3.name, p3.state]));
14750
+ for (const name2 of [...doneSeenAt.keys()])
14751
+ if (byName.get(name2) !== "done") doneSeenAt.delete(name2);
14752
+ for (const p3 of phases)
14753
+ if (p3.state === "done" && !doneSeenAt.has(p3.name)) doneSeenAt.set(p3.name, now);
14754
+ }
14321
14755
  function stageFor(phases) {
14322
14756
  let best = -1;
14323
14757
  for (const p3 of phases) {
@@ -14397,9 +14831,9 @@ var init_session_id = __esm({
14397
14831
  });
14398
14832
 
14399
14833
  // src/services/secret-box.ts
14400
- import { chmodSync as chmodSync5, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
14834
+ import { chmodSync as chmodSync5, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
14401
14835
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
14402
- import { dirname as dirname6, join as join7 } from "node:path";
14836
+ import { dirname as dirname6, join as join8 } from "node:path";
14403
14837
  function isEncrypted(value) {
14404
14838
  return value.startsWith(ENC_PREFIX);
14405
14839
  }
@@ -14431,7 +14865,7 @@ function fromEnv(raw) {
14431
14865
  return null;
14432
14866
  }
14433
14867
  function secretKeyPath() {
14434
- return join7(resolveHome(), "secret.key");
14868
+ return join8(resolveHome(), "secret.key");
14435
14869
  }
14436
14870
  function secretKey() {
14437
14871
  if (cached) return cached;
@@ -14451,7 +14885,7 @@ function secretKey() {
14451
14885
  }
14452
14886
  const key = randomBytes(KEY_BYTES);
14453
14887
  mkdirSync4(dirname6(path), { recursive: true });
14454
- writeFileSync4(path, key.toString("base64url"), { mode: 384 });
14888
+ writeFileSync5(path, key.toString("base64url"), { mode: 384 });
14455
14889
  chmodSync5(path, 384);
14456
14890
  cached = key;
14457
14891
  return key;
@@ -14733,7 +15167,7 @@ var init_safe_outbound_request = __esm({
14733
15167
  // src/services/uploads.ts
14734
15168
  import { randomUUID } from "node:crypto";
14735
15169
  import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
14736
- import { join as join8, resolve as resolve8 } from "node:path";
15170
+ import { join as join9, resolve as resolve8 } from "node:path";
14737
15171
  function extFor(mimeType) {
14738
15172
  return EXT[mimeType] ?? ".bin";
14739
15173
  }
@@ -14742,12 +15176,12 @@ function uploadDir() {
14742
15176
  }
14743
15177
  function sessionUploadDir(sessionId2) {
14744
15178
  if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
14745
- return join8(uploadDir(), "terminal", sessionId2);
15179
+ return join9(uploadDir(), "terminal", sessionId2);
14746
15180
  }
14747
15181
  async function saveSessionUpload(sessionId2, buf, mimeType) {
14748
15182
  const dir2 = sessionUploadDir(sessionId2);
14749
15183
  await mkdir(dir2, { recursive: true, mode: 448 });
14750
- const path = join8(dir2, `${randomUUID()}${extFor(mimeType)}`);
15184
+ const path = join9(dir2, `${randomUUID()}${extFor(mimeType)}`);
14751
15185
  await writeFile(path, buf, { mode: 384 });
14752
15186
  return { path, size: buf.length };
14753
15187
  }
@@ -14763,11 +15197,11 @@ async function dropSessionUploads(sessionId2) {
14763
15197
  }
14764
15198
  async function readUpload(storageKey) {
14765
15199
  const safe = storageKey.replace(/[/\\]/g, "");
14766
- return readFile2(join8(uploadDir(), safe));
15200
+ return readFile2(join9(uploadDir(), safe));
14767
15201
  }
14768
15202
  async function readUploadOrFetch(storageKey) {
14769
15203
  const safe = storageKey.replace(/[/\\]/g, "");
14770
- const target2 = join8(uploadDir(), safe);
15204
+ const target2 = join9(uploadDir(), safe);
14771
15205
  try {
14772
15206
  return await readFile2(target2);
14773
15207
  } catch {
@@ -14794,7 +15228,7 @@ async function readUploadOrFetch(storageKey) {
14794
15228
  }
14795
15229
  async function deleteUpload(storageKey) {
14796
15230
  const safe = storageKey.replace(/[/\\]/g, "");
14797
- await unlink(join8(uploadDir(), safe)).catch(() => {
15231
+ await unlink(join9(uploadDir(), safe)).catch(() => {
14798
15232
  });
14799
15233
  }
14800
15234
  var EXT, SESSION_ID;
@@ -15224,14 +15658,14 @@ var init_session_sandbox = __esm({
15224
15658
  });
15225
15659
 
15226
15660
  // src/services/session-event-spool.ts
15227
- import { join as join9 } from "node:path";
15661
+ import { join as join10 } from "node:path";
15228
15662
  var sessionEventSpoolRoot, sessionEventDir;
15229
15663
  var init_session_event_spool = __esm({
15230
15664
  "src/services/session-event-spool.ts"() {
15231
15665
  "use strict";
15232
15666
  init_src2();
15233
- sessionEventSpoolRoot = (env = process.env) => join9(resolveHome(env), "session-events");
15234
- sessionEventDir = (sessionId2, env = process.env) => join9(sessionEventSpoolRoot(env), sessionId2);
15667
+ sessionEventSpoolRoot = (env = process.env) => join10(resolveHome(env), "session-events");
15668
+ sessionEventDir = (sessionId2, env = process.env) => join10(sessionEventSpoolRoot(env), sessionId2);
15235
15669
  }
15236
15670
  });
15237
15671
 
@@ -15240,13 +15674,28 @@ import { spawn } from "node-pty";
15240
15674
  import { execFile, execFileSync as execFileSync2 } from "node:child_process";
15241
15675
  import { createRequire } from "node:module";
15242
15676
  import { randomUUID as randomUUID2 } from "node:crypto";
15243
- import { chmodSync as chmodSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync5 } from "node:fs";
15677
+ import { chmodSync as chmodSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync6 } from "node:fs";
15244
15678
  import { dirname as dirname7 } from "node:path";
15245
15679
  import { tmpdir } from "node:os";
15680
+ function phaseView(p3, a) {
15681
+ const phases = readPhases(p3.phaseFile, p3.flow);
15682
+ const now = Date.now();
15683
+ trackDoneSeen(phases, a.doneSeenAt, now);
15684
+ const roster = (p3.agentRoster ?? []).flatMap((r) => r.phase ? [{ name: r.name, phase: r.phase, model: r.model, effort: r.effort }] : []);
15685
+ return roster.length ? enrichPhases(
15686
+ phases,
15687
+ roster,
15688
+ phaseInvocations.get(p3.id) ?? [],
15689
+ a.doneSeenAt,
15690
+ now,
15691
+ p3.startedAt * 1e3,
15692
+ new Set(p3.doneAtBirth ?? [])
15693
+ ) : phases;
15694
+ }
15246
15695
  function noTtyPromptEnv() {
15247
15696
  const path = askpassDenyPath();
15248
15697
  mkdirSync5(dirname7(path), { recursive: true, mode: 448 });
15249
- writeFileSync5(path, ASKPASS_DENY, { mode: 448 });
15698
+ writeFileSync6(path, ASKPASS_DENY, { mode: 448 });
15250
15699
  return { SSH_ASKPASS: path, SSH_ASKPASS_REQUIRE: "force", GIT_TERMINAL_PROMPT: "0" };
15251
15700
  }
15252
15701
  function tmux(...args) {
@@ -15325,7 +15774,11 @@ function parsePanes(out4) {
15325
15774
  eventHook,
15326
15775
  created,
15327
15776
  agentRoster,
15328
- launchClass
15777
+ launchClass,
15778
+ model,
15779
+ effort2,
15780
+ orchestrated,
15781
+ doneAtBirth
15329
15782
  ] = line.split(" ");
15330
15783
  if (!n2?.startsWith(PREFIX)) return [];
15331
15784
  const exited = dead === "1";
@@ -15357,7 +15810,13 @@ function parsePanes(out4) {
15357
15810
  // SPEC-909 · ADR-0146 · sesi yang lahir sebelum pembaruan tak punya opsi ini → false.
15358
15811
  eventHook: eventHook === "1",
15359
15812
  agentRoster: parseAgentRoster(agentRoster),
15360
- launchClass: launchClass === "agent" || launchClass === "terminal" ? launchClass : void 0
15813
+ launchClass: launchClass === "agent" || launchClass === "terminal" ? launchClass : void 0,
15814
+ model: model || void 0,
15815
+ effort: effort2 || void 0,
15816
+ orchestrated: orchestrated === "1",
15817
+ // M-2 · ADR-0164 · daftar kosong/tak ada opsi → undefined, bukan [] (sesi lama atau sesi
15818
+ // tanpa satu pun fase done saat lahir tak perlu membawa array kosong di DTO internal).
15819
+ doneAtBirth: doneAtBirth ? doneAtBirth.split(",").filter(Boolean) : void 0
15361
15820
  }];
15362
15821
  });
15363
15822
  }
@@ -15374,7 +15833,9 @@ function parseAgentRoster(value) {
15374
15833
  ...typeof row.id === "string" ? { id: row.id } : {},
15375
15834
  ...typeof row.model === "string" ? { model: row.model } : {},
15376
15835
  ...typeof row.definitionHash === "string" && /^[a-f0-9]{64}$/.test(row.definitionHash) ? { definitionHash: row.definitionHash } : {},
15377
- ...typeof row.timeoutSeconds === "number" ? { timeoutSeconds: row.timeoutSeconds } : {}
15836
+ ...typeof row.timeoutSeconds === "number" ? { timeoutSeconds: row.timeoutSeconds } : {},
15837
+ ...typeof row.phase === "string" ? { phase: row.phase } : {},
15838
+ ...typeof row.effort === "string" ? { effort: row.effort } : {}
15378
15839
  }];
15379
15840
  });
15380
15841
  } catch {
@@ -15444,6 +15905,7 @@ function createSession(projectId, cwd, opts = {}) {
15444
15905
  chmodSync6(eventDir, 448);
15445
15906
  }
15446
15907
  const agentForDefs = opts.agent ?? "claude";
15908
+ const sessionEffort = agentForDefs === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort;
15447
15909
  const selectionContext = {
15448
15910
  projectId,
15449
15911
  runtime: agentForDefs,
@@ -15453,36 +15915,85 @@ function createSession(projectId, cwd, opts = {}) {
15453
15915
  prompt: opts.prompt,
15454
15916
  changedFiles: opts.command ? [] : collectChangedFiles(cwd, opts.env?.HANOMAN_BASE_SHA)
15455
15917
  };
15456
- const customDefs = opts.command ? [] : customAgentsFor(selectionContext);
15918
+ const rawCustomDefs = opts.command ? [] : customAgentsFor(selectionContext);
15919
+ const customDefs = rawCustomDefs.filter((def2) => !isPhaseAgentName(def2.name));
15920
+ for (const def2 of rawCustomDefs) {
15921
+ if (isPhaseAgentName(def2.name)) {
15922
+ process.stderr.write(
15923
+ `hanoman: custom agent ${def2.name} diabaikan \u2014 awalan hanoman-fase- dicadangkan (ADR-0164)
15924
+ `
15925
+ );
15926
+ }
15927
+ }
15928
+ const requestedPhaseDefs = opts.command ? [] : opts.phaseAgents ?? [];
15457
15929
  let rosterBlock = "";
15458
15930
  let codexAgentArgs = [];
15459
15931
  let agentsFile;
15460
15932
  let agentConfigDir;
15461
15933
  let liveAgentDefs = [];
15462
- if (customDefs.length > 0) {
15934
+ let renderedDefs = [];
15935
+ let orchestrated = false;
15936
+ let statusLineCommand;
15937
+ if (customDefs.length > 0 || requestedPhaseDefs.length > 0) {
15463
15938
  const tempDir = agentTempDir(id2);
15464
15939
  agentConfigDir = tempDir;
15465
15940
  mkdirSync5(tempDir, { recursive: true, mode: 448 });
15466
15941
  const readOnlyHook = customDefs.some((def2) => def2.workspacePolicy === "read-only") ? writeReadOnlyHook(tempDir) : void 0;
15467
- if (agentForDefs === "claude") {
15468
- const json = renderAgentsJson(customDefs, { readOnlyHookCommand: readOnlyHook?.command });
15469
- if (json) {
15470
- agentsFile = agentsFilePath(id2);
15471
- writeFileSync5(agentsFile, json, { mode: 384 });
15942
+ const attempt = (phaseDefs) => {
15943
+ const defs = [...phaseDefs, ...customDefs];
15944
+ if (defs.length === 0) return [];
15945
+ if (agentForDefs === "claude") {
15946
+ const file = agentsFilePath(id2);
15947
+ try {
15948
+ writeFileSync6(file, renderAgentsJson(defs, { readOnlyHookCommand: readOnlyHook?.command }), { mode: 384 });
15949
+ } catch (error) {
15950
+ if (phaseDefs.length === 0) throw error;
15951
+ return [error instanceof Error ? error.message : String(error)];
15952
+ }
15953
+ agentsFile = file;
15472
15954
  rosterBlock = agentDelegationClause(customDefs, "claude");
15473
- liveAgentDefs = customDefs;
15955
+ liveAgentDefs = defs;
15956
+ renderedDefs = defs;
15957
+ return [];
15474
15958
  }
15475
- } else {
15476
- const materialized = materializeCodexAgents(customDefs, tempDir, {
15959
+ const materialized = materializeCodexAgents(defs, tempDir, {
15477
15960
  readOnlyHookCommand: readOnlyHook?.command,
15478
- clientVersion: codexNativeAgentSupport().version
15961
+ clientVersion: codexNativeAgentSupport().version,
15962
+ ...phaseDefs.length > 0 ? { maxDepth: 3 } : {}
15479
15963
  });
15964
+ const phaseFailures = materialized.warnings.filter((w) => phaseDefs.some((d) => d.name === w.agentName));
15965
+ if (phaseFailures.length > 0) return phaseFailures.map((w) => `${w.agentName}: ${w.reason}`);
15480
15966
  codexAgentArgs = materialized.args;
15481
15967
  rosterBlock = materialized.delegationClause;
15482
15968
  liveAgentDefs = materialized.liveDefs;
15969
+ renderedDefs = defs;
15483
15970
  for (const warning of materialized.warnings) {
15484
15971
  process.stderr.write(
15485
15972
  `hanoman: custom agent ${warning.agentName} tidak dimaterialisasi: ${warning.reason}
15973
+ `
15974
+ );
15975
+ }
15976
+ return [];
15977
+ };
15978
+ let phaseFailReasons = [];
15979
+ if (requestedPhaseDefs.length > 0) {
15980
+ phaseFailReasons = attempt(requestedPhaseDefs);
15981
+ orchestrated = phaseFailReasons.length === 0;
15982
+ }
15983
+ if (!orchestrated) {
15984
+ if (requestedPhaseDefs.length > 0)
15985
+ process.stderr.write(
15986
+ `hanoman: agen fase sesi ${id2} gagal dimaterialisasi \u2014 sesi lahir mode tunggal: ${phaseFailReasons.join("; ")}
15987
+ `
15988
+ );
15989
+ attempt([]);
15990
+ }
15991
+ if (orchestrated && agentForDefs === "claude") {
15992
+ try {
15993
+ statusLineCommand = writeSubagentStatusline(tempDir);
15994
+ } catch (error) {
15995
+ process.stderr.write(
15996
+ `hanoman: gagal menulis subagentStatusline sesi ${id2}: ${error instanceof Error ? error.message : String(error)}
15486
15997
  `
15487
15998
  );
15488
15999
  }
@@ -15490,10 +16001,11 @@ function createSession(projectId, cwd, opts = {}) {
15490
16001
  }
15491
16002
  let promptArg = "";
15492
16003
  let promptFile;
15493
- if (!opts.command && opts.prompt) {
16004
+ const sessionPrompt = orchestrated ? opts.prompt : opts.legacyPrompt ?? opts.prompt;
16005
+ if (!opts.command && sessionPrompt) {
15494
16006
  promptFile = promptFilePath(id2);
15495
16007
  mkdirSync5(dirname7(promptFile), { recursive: true, mode: 448 });
15496
- writeFileSync5(promptFile, opts.prompt + rosterBlock, { mode: 384 });
16008
+ writeFileSync6(promptFile, sessionPrompt + rosterBlock, { mode: 384 });
15497
16009
  promptArg = `"$(cat ${sq(promptFile)})"`;
15498
16010
  }
15499
16011
  const agent = opts.agent ?? "claude";
@@ -15505,7 +16017,7 @@ function createSession(projectId, cwd, opts = {}) {
15505
16017
  if (agent === "codex" && opts.goal && opts.flow && opts.specId) {
15506
16018
  goalGate = goalGatePath(id2);
15507
16019
  mkdirSync5(dirname7(goalGate), { recursive: true, mode: 448 });
15508
- writeFileSync5(goalGate, codexGoalScript({
16020
+ writeFileSync6(goalGate, codexGoalScript({
15509
16021
  flow: opts.flow,
15510
16022
  specId: opts.specId,
15511
16023
  condition: opts.goal,
@@ -15514,17 +16026,17 @@ function createSession(projectId, cwd, opts = {}) {
15514
16026
  stateFile: goalStatePath(id2)
15515
16027
  }), { mode: 448 });
15516
16028
  }
15517
- const effort2 = agent === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort;
15518
16029
  const flags = agentFlags({
15519
16030
  agent,
15520
16031
  model: opts.model,
15521
- effort: effort2,
16032
+ effort: sessionEffort,
15522
16033
  decisionFile: opts.decisionFile,
15523
16034
  goal: opts.goal,
15524
16035
  goalGate,
15525
16036
  // SPEC-909 · ADR-0146 · sesi ber-`opts.command` tak pernah sampai ke sini (cabang shell
15526
16037
  // mentah di atas), jadi hook event hanya pernah terpasang di sesi agen.
15527
- eventHook: true
16038
+ eventHook: true,
16039
+ subagentStatusLine: statusLineCommand
15528
16040
  }).map(sq).join(" ");
15529
16041
  const agentsArg = agentsFile ? `--agents "$(cat ${sq(agentsFile)})"` : "";
15530
16042
  const nativeAgentArgs = agent === "codex" ? codexAgentArgs.map(sq).join(" ") : "";
@@ -15639,22 +16151,29 @@ function createSession(projectId, cwd, opts = {}) {
15639
16151
  "@hanoman_launch_class",
15640
16152
  opts.command || kind === "terminal" ? "terminal" : "agent"
15641
16153
  );
16154
+ if (!opts.command && opts.model) tmux("set-option", "-t", name(id2), "@hanoman_model", opts.model);
16155
+ if (!opts.command && sessionEffort) tmux("set-option", "-t", name(id2), "@hanoman_effort", sessionEffort);
16156
+ if (orchestrated) tmux("set-option", "-t", name(id2), "@hanoman_orchestrated", "1");
15642
16157
  if (liveAgentDefs.length > 0) {
15643
- const inherited = {
15644
- model: opts.model,
15645
- effort: agent === "codex" && opts.model && opts.effort ? coerceCodexEffort(opts.model, opts.effort) : opts.effort
15646
- };
16158
+ const inherited = { model: opts.model, effort: sessionEffort };
15647
16159
  const roster = liveAgentDefs.map((def2) => ({
15648
16160
  ...def2.id ? { id: def2.id } : {},
15649
16161
  name: def2.name,
15650
16162
  ...def2.model ?? inherited.model ? { model: def2.model ?? inherited.model } : {},
15651
- // Native files were rendered with customDefs, even if another Codex file failed to write.
15652
- definitionHash: agentDefinitionHash(def2, customDefs, agent, inherited),
16163
+ ...def2.phase ? { phase: def2.phase } : {},
16164
+ ...def2.effort ?? inherited.effort ? { effort: def2.effort ?? inherited.effort } : {},
16165
+ // Berkas native dirender dengan `renderedDefs`, walau satu berkas Codex lain gagal ditulis.
16166
+ definitionHash: agentDefinitionHash(def2, renderedDefs, agent, inherited),
15653
16167
  ...def2.timeoutSeconds ? { timeoutSeconds: def2.timeoutSeconds } : {}
15654
16168
  }));
15655
16169
  tmux("set-option", "-t", name(id2), "@hanoman_agent_roster", JSON.stringify(roster));
15656
16170
  }
15657
16171
  if (opts.phaseFile) tmux("set-option", "-t", name(id2), "@hanoman_phase_file", opts.phaseFile);
16172
+ if (opts.phaseFile && opts.flow) {
16173
+ const doneAtBirth = readPhases(opts.phaseFile, opts.flow).filter((p3) => p3.state === "done" || p3.state === "skipped").map((p3) => p3.name);
16174
+ if (doneAtBirth.length > 0)
16175
+ tmux("set-option", "-t", name(id2), "@hanoman_done_at_birth", doneAtBirth.join(","));
16176
+ }
15658
16177
  if (opts.decisionFile) tmux("set-option", "-t", name(id2), "@hanoman_decision_file", opts.decisionFile);
15659
16178
  if (!opts.command) tmux("set-option", "-t", name(id2), "@hanoman_event_hook", "1");
15660
16179
  if (opts.goal && !opts.command) void armGoalInTui(id2, opts.goal, { agent }).catch(() => {
@@ -15799,7 +16318,14 @@ function flushOutput(a) {
15799
16318
  }
15800
16319
  function open(id2) {
15801
16320
  const pty = spawnPty("attach-session", "-d", "-t", name(id2));
15802
- const a = { pty, scrollback: "", clients: /* @__PURE__ */ new Set(), lastPhases: "", pending: "" };
16321
+ const a = {
16322
+ pty,
16323
+ scrollback: "",
16324
+ clients: /* @__PURE__ */ new Set(),
16325
+ lastPhases: "",
16326
+ pending: "",
16327
+ doneSeenAt: /* @__PURE__ */ new Map()
16328
+ };
15803
16329
  pty.onData((d) => {
15804
16330
  a.pending += d;
15805
16331
  if (a.pending.length >= COALESCE_MAX_BYTES) flushOutput(a);
@@ -15817,6 +16343,7 @@ function drop(id2) {
15817
16343
  if (!a) return;
15818
16344
  flushOutput(a);
15819
16345
  attached.delete(id2);
16346
+ phaseInvocations.delete(id2);
15820
16347
  a.pty.kill();
15821
16348
  for (const c of a.clients) c.close();
15822
16349
  a.clients.clear();
@@ -15830,13 +16357,16 @@ function end(id2, code) {
15830
16357
  }
15831
16358
  function pollPhases(p3, a) {
15832
16359
  if (!p3.flow || !p3.phaseFile) return;
15833
- const phases = readPhases(p3.phaseFile, p3.flow);
16360
+ const phases = phaseView(p3, a);
15834
16361
  const complete = paneComplete(p3);
15835
16362
  const json = phaseKey(phases, complete);
15836
16363
  if (json === a.lastPhases) return;
15837
16364
  a.lastPhases = json;
15838
16365
  broadcast(a, { t: "phase", phases, complete });
15839
16366
  }
16367
+ function setPhaseInvocations(sessionId2, rows) {
16368
+ if (attached.has(sessionId2)) phaseInvocations.set(sessionId2, rows);
16369
+ }
15840
16370
  function pollAlt(p3, a) {
15841
16371
  if (p3.altScreen === a.lastAlt) return;
15842
16372
  a.lastAlt = p3.altScreen;
@@ -15892,7 +16422,7 @@ function attach(id2, c) {
15892
16422
  a.lastAlt = p3.altScreen;
15893
16423
  c.send(frame({ t: "alt", on: p3.altScreen }));
15894
16424
  if (p3.flow && p3.phaseFile) {
15895
- const phases = readPhases(p3.phaseFile, p3.flow);
16425
+ const phases = phaseView(p3, a);
15896
16426
  const complete = paneComplete(p3);
15897
16427
  a.lastPhases = phaseKey(phases, complete);
15898
16428
  c.send(frame({ t: "phase", phases, complete }));
@@ -15908,6 +16438,7 @@ function resize(id2, cols, rows) {
15908
16438
  attached.get(id2)?.pty.resize(cols, rows);
15909
16439
  }
15910
16440
  function killSession(id2) {
16441
+ phaseInvocations.delete(id2);
15911
16442
  const p3 = getSession(id2);
15912
16443
  if (!p3) return false;
15913
16444
  const transcript = captureTranscript(id2);
@@ -15946,7 +16477,7 @@ function spawnPty(...args) {
15946
16477
  );
15947
16478
  }
15948
16479
  }
15949
- var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, clearMarker, PANE_QUIET_MS, paneQuiet, markerOnset, decisionOnset, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentTempDir, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, liveDecisions, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, codexNativeAgentSupport, customAgentsFor, runGitDiff, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, polling, TERMINAL_QUERY, stripTerminalQueries, detach;
16480
+ var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, clearMarker, PANE_QUIET_MS, paneQuiet, markerOnset, decisionOnset, attached, phaseInvocations, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentTempDir, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, liveDecisions, getSession, getSessionAsync, hooks, emitBirth, emitDeath, customAgentSource, codexNativeAgentSupport, nativeAgentsAvailable, customAgentsFor, runGitDiff, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, polling, TERMINAL_QUERY, stripTerminalQueries, detach;
15950
16481
  var init_pty = __esm({
15951
16482
  "src/services/pty.ts"() {
15952
16483
  "use strict";
@@ -15977,7 +16508,7 @@ var init_pty = __esm({
15977
16508
  };
15978
16509
  clearMarker = (f) => {
15979
16510
  try {
15980
- writeFileSync5(f, "");
16511
+ writeFileSync6(f, "");
15981
16512
  } catch {
15982
16513
  }
15983
16514
  };
@@ -15998,6 +16529,7 @@ var init_pty = __esm({
15998
16529
  return secs > 0 ? new Date(secs * 1e3).toISOString() : void 0;
15999
16530
  };
16000
16531
  attached = /* @__PURE__ */ new Map();
16532
+ phaseInvocations = /* @__PURE__ */ new Map();
16001
16533
  claudeBin = () => effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
16002
16534
  shellBin = () => effectiveStr("HANOMAN_SHELL") ?? process.env.SHELL ?? "/bin/bash";
16003
16535
  codexBin = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
@@ -16044,7 +16576,13 @@ exit 1
16044
16576
  "#{@hanoman_event_hook}",
16045
16577
  "#{session_created}",
16046
16578
  "#{@hanoman_agent_roster}",
16047
- "#{@hanoman_launch_class}"
16579
+ "#{@hanoman_launch_class}",
16580
+ // ADR-0164 · di UJUNG juga, alasan yang sama dengan SPEC-919.
16581
+ "#{@hanoman_model}",
16582
+ "#{@hanoman_effort}",
16583
+ "#{@hanoman_orchestrated}",
16584
+ // M-2 · ADR-0164 · nama fase done|skipped SAAT LAHIR, dipisah koma — di UJUNG juga.
16585
+ "#{@hanoman_done_at_birth}"
16048
16586
  ].join(" ");
16049
16587
  toSessionInfo = ({
16050
16588
  id: id2,
@@ -16058,7 +16596,10 @@ exit 1
16058
16596
  decision,
16059
16597
  agent,
16060
16598
  decisionFile,
16061
- activityAt
16599
+ activityAt,
16600
+ model,
16601
+ effort: effort2,
16602
+ orchestrated
16062
16603
  }) => ({
16063
16604
  id: id2,
16064
16605
  projectId,
@@ -16072,7 +16613,10 @@ exit 1
16072
16613
  // Hanya untuk pane mati: `pane_dead_status` kosong pada pane hidup, dan `exitCode: 0` di sana
16073
16614
  // akan terbaca sebagai "sudah berakhir sukses".
16074
16615
  ...exited ? { exitCode: code } : {},
16075
- ...decision && decisionFile ? { decisionAt: decisionOnset(decisionFile, activityAt) } : {}
16616
+ ...decision && decisionFile ? { decisionAt: decisionOnset(decisionFile, activityAt) } : {},
16617
+ ...model ? { model } : {},
16618
+ ...effort2 ? { effort: effort2 } : {},
16619
+ ...orchestrated ? { orchestrated: true } : {}
16076
16620
  });
16077
16621
  listSessions = () => listPanes().map(toSessionInfo);
16078
16622
  listSessionsAsync = async () => (await listPanesAsync()).map(toSessionInfo);
@@ -16103,6 +16647,7 @@ exit 1
16103
16647
  };
16104
16648
  customAgentSource = () => [];
16105
16649
  codexNativeAgentSupport = () => ({ version: "0.151.0", ok: true });
16650
+ nativeAgentsAvailable = (agent) => agent === "claude" || codexNativeAgentSupport().ok;
16106
16651
  customAgentsFor = (context) => {
16107
16652
  try {
16108
16653
  return customAgentSource(context);
@@ -16906,6 +17451,8 @@ var init_settings3 = __esm({
16906
17451
  // SPEC-518 · agen pembuat changelog (opt-in, mati)
16907
17452
  portalChat: PORTAL_CHAT_DEFAULTS,
16908
17453
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
17454
+ orchestration: ORCHESTRATION_DEFAULTS,
17455
+ // ADR-0164 · orkestrasi subagent per fase (default aktif)
16909
17456
  builtinAgents: {},
16910
17457
  // SPEC-881 · ADR-0136 · sidik jari seed (lokal, tak disync)
16911
17458
  builtinAgentPolicies: {}
@@ -23585,15 +24132,15 @@ var init_sync_client = __esm({
23585
24132
  // src/services/agent-tool-catalog.ts
23586
24133
  import { readFileSync as readFileSync17 } from "node:fs";
23587
24134
  import { homedir as homedir10 } from "node:os";
23588
- import { join as join32 } from "node:path";
24135
+ import { join as join33 } from "node:path";
23589
24136
  function mcpServerNames(repoDir) {
23590
24137
  const names = [];
23591
- const claudeJson = readJson2(join32(home(), ".claude.json"));
24138
+ const claudeJson = readJson2(join33(home(), ".claude.json"));
23592
24139
  names.push(...serversOf(claudeJson));
23593
24140
  if (repoDir) {
23594
24141
  const projects = claudeJson?.projects;
23595
24142
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
23596
- names.push(...serversOf(readJson2(join32(repoDir, ".mcp.json"))));
24143
+ names.push(...serversOf(readJson2(join33(repoDir, ".mcp.json"))));
23597
24144
  }
23598
24145
  names.push(...codexServers());
23599
24146
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -23622,7 +24169,7 @@ var init_agent_tool_catalog = __esm({
23622
24169
  codexServers = () => {
23623
24170
  let text;
23624
24171
  try {
23625
- text = readFileSync17(join32(home(), ".codex", "config.toml"), "utf8");
24172
+ text = readFileSync17(join33(home(), ".codex", "config.toml"), "utf8");
23626
24173
  } catch {
23627
24174
  return [];
23628
24175
  }
@@ -27273,9 +27820,9 @@ import { resolve as resolve10 } from "node:path";
27273
27820
  // src/services/safe-repo-path.ts
27274
27821
  import { constants as constants2 } from "node:fs";
27275
27822
  import { lstat, mkdir as mkdir2, open as open2, realpath, rename, unlink as unlink2 } from "node:fs/promises";
27276
- import { isAbsolute as isAbsolute4, join as join10, relative, resolve as resolve9, sep as sep2 } from "node:path";
27823
+ import { isAbsolute as isAbsolute4, join as join11, relative, resolve as resolve9, sep as sep2 } from "node:path";
27277
27824
  import { randomUUID as randomUUID3 } from "node:crypto";
27278
- import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
27825
+ import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
27279
27826
  var PathContainmentError = class extends Error {
27280
27827
  code = "PATH_CONTAINMENT";
27281
27828
  };
@@ -27303,7 +27850,7 @@ async function ensureRepoParents(root, rel) {
27303
27850
  const parts = components(rel).slice(0, -1);
27304
27851
  let current = base2;
27305
27852
  for (const part of parts) {
27306
- current = join10(current, part);
27853
+ current = join11(current, part);
27307
27854
  try {
27308
27855
  await mkdir2(current, { mode: 448 });
27309
27856
  } catch (error) {
@@ -27320,7 +27867,7 @@ function ensureRepoParentsSync(root, rel) {
27320
27867
  const parts = components(rel).slice(0, -1);
27321
27868
  let current = base2;
27322
27869
  for (const part of parts) {
27323
- current = join10(current, part);
27870
+ current = join11(current, part);
27324
27871
  try {
27325
27872
  mkdirSync6(current, { mode: 448 });
27326
27873
  } catch (error) {
@@ -27336,7 +27883,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
27336
27883
  const parts = components(rel);
27337
27884
  let current = base2;
27338
27885
  for (let i = 0; i < parts.length; i++) {
27339
- current = join10(current, parts[i]);
27886
+ current = join11(current, parts[i]);
27340
27887
  const stat4 = await lstat(current).catch((error) => {
27341
27888
  if (error.code === "ENOENT" && opts.allowMissingTail) return null;
27342
27889
  if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
@@ -27357,7 +27904,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
27357
27904
  const parts = components(rel);
27358
27905
  let current = base2;
27359
27906
  for (let i = 0; i < parts.length; i++) {
27360
- current = join10(current, parts[i]);
27907
+ current = join11(current, parts[i]);
27361
27908
  try {
27362
27909
  const stat4 = lstatSync(current);
27363
27910
  if (stat4.isSymbolicLink()) denied("symlink");
@@ -27387,10 +27934,10 @@ function writeRepoFileAtomicSync(root, rel, data) {
27387
27934
  const parent = resolve9(path, "..");
27388
27935
  const parentRel = relative(realpathSync2(root), parent);
27389
27936
  if (parentRel) assertSafeRepoPathSync(root, parentRel);
27390
- const temp = join10(parent, `.hanoman-${randomUUID3()}.tmp`);
27937
+ const temp = join11(parent, `.hanoman-${randomUUID3()}.tmp`);
27391
27938
  const fd = openSync(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
27392
27939
  try {
27393
- writeFileSync6(fd, data);
27940
+ writeFileSync7(fd, data);
27394
27941
  } finally {
27395
27942
  closeSync(fd);
27396
27943
  }
@@ -27434,7 +27981,7 @@ async function writeRepoFileAtomic(root, rel, data) {
27434
27981
  await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
27435
27982
  if (error.code !== "EEXIST") throw error;
27436
27983
  });
27437
- const temp = join10(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
27984
+ const temp = join11(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
27438
27985
  const handle = await open2(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
27439
27986
  try {
27440
27987
  await handle.writeFile(data);
@@ -27876,7 +28423,7 @@ import { existsSync as existsSync9 } from "node:fs";
27876
28423
  import { execFile as execFile4 } from "node:child_process";
27877
28424
  import { promisify as promisify3 } from "node:util";
27878
28425
  import { rmSync as rmSync4 } from "node:fs";
27879
- import { join as join11 } from "node:path";
28426
+ import { join as join12 } from "node:path";
27880
28427
  var sanitize = (id2) => id2.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
27881
28428
  var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
27882
28429
  var exec3 = promisify3(execFile4);
@@ -27920,7 +28467,7 @@ async function integrateBranch(repoDir, src, op, target2) {
27920
28467
  const tgt = await resolveTarget(repoDir, target2);
27921
28468
  if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
27922
28469
  await sh(repoDir, ["fetch", "origin"]);
27923
- const wt = join11(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
28470
+ const wt = join12(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
27924
28471
  await reclaim(repoDir, wt);
27925
28472
  const baseRef = op === "merge" ? tgt.ref : source;
27926
28473
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
@@ -27981,7 +28528,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
27981
28528
  const src = await resolveGraphSource(repoDir, source);
27982
28529
  if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
27983
28530
  await sh(repoDir, ["fetch", "origin"]);
27984
- const wt = join11(repoDir, ".worktrees", `merge-${sanitize(current)}`);
28531
+ const wt = join12(repoDir, ".worktrees", `merge-${sanitize(current)}`);
27985
28532
  await reclaim(repoDir, wt);
27986
28533
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
27987
28534
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -28019,7 +28566,7 @@ async function replayCurrent(repoDir, source, cmd) {
28019
28566
  const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
28020
28567
  if (!current || current === "HEAD")
28021
28568
  return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
28022
- const wt = join11(repoDir, ".worktrees", `merge-${sanitize(current)}`);
28569
+ const wt = join12(repoDir, ".worktrees", `merge-${sanitize(current)}`);
28023
28570
  await reclaim(repoDir, wt);
28024
28571
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
28025
28572
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -28069,15 +28616,15 @@ import { execFile as execFile5 } from "node:child_process";
28069
28616
  import { promisify as promisify4 } from "node:util";
28070
28617
  import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
28071
28618
  import { tmpdir as tmpdir2 } from "node:os";
28072
- import { join as join12, resolve as resolve11 } from "node:path";
28619
+ import { join as join13, resolve as resolve11 } from "node:path";
28073
28620
  var exec4 = promisify4(execFile5);
28074
28621
  var GIT3 = { maxBuffer: 1 << 24 };
28075
28622
  var MAX = 256 * 1024;
28076
- var worktreeDir = (repoDir, specId) => join12(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
28623
+ var worktreeDir = (repoDir, specId) => join13(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
28077
28624
  async function withTempIndex(wt, fn) {
28078
28625
  const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
28079
- const dir2 = await mkdtemp(join12(tmpdir2(), "hanoman-idx-"));
28080
- const tmp = join12(dir2, "index");
28626
+ const dir2 = await mkdtemp(join13(tmpdir2(), "hanoman-idx-"));
28627
+ const tmp = join13(dir2, "index");
28081
28628
  await copyFile(resolve11(wt, idx), tmp);
28082
28629
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
28083
28630
  try {
@@ -28276,7 +28823,7 @@ function appendSourceHistory(current, entry) {
28276
28823
  // src/services/spec-reset.ts
28277
28824
  import { execFile as execFile8 } from "node:child_process";
28278
28825
  import { existsSync as existsSync8 } from "node:fs";
28279
- import { join as join14 } from "node:path";
28826
+ import { join as join15 } from "node:path";
28280
28827
  import { promisify as promisify7 } from "node:util";
28281
28828
 
28282
28829
  // src/services/stage-artifacts.ts
@@ -28325,7 +28872,7 @@ init_db();
28325
28872
  import { execFile as execFile6 } from "node:child_process";
28326
28873
  import { readdir, rm as rm3 } from "node:fs/promises";
28327
28874
  import { promisify as promisify5 } from "node:util";
28328
- import { join as join13, resolve as resolve12 } from "node:path";
28875
+ import { join as join14, resolve as resolve12 } from "node:path";
28329
28876
  init_notifications2();
28330
28877
  var TICK_MS = 6e4;
28331
28878
  var trashDirOf = (repoDir) => resolve12(repoDir, ".worktrees", ".trash");
@@ -28387,7 +28934,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
28387
28934
  }
28388
28935
  let removed = 0;
28389
28936
  for (const entry of entries3) {
28390
- const path = join13(dir2, entry);
28937
+ const path = join14(dir2, entry);
28391
28938
  const known = pending.get(path);
28392
28939
  const row = known ?? { path, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
28393
28940
  pending.set(path, row);
@@ -28943,7 +29490,7 @@ async function planSpecReset(spec) {
28943
29490
  const repoDir = await resolveRepoDir(spec.projectId);
28944
29491
  if (!repoDir) return EMPTY;
28945
29492
  const sid = sessionIdForSpec(spec.id);
28946
- const wt = join14(repoDir, ".worktrees", sid);
29493
+ const wt = join15(repoDir, ".worktrees", sid);
28947
29494
  const branch = `hanoman/${sid}`;
28948
29495
  const hasBranch = await shaResolvable(repoDir, `refs/heads/${branch}`);
28949
29496
  return {
@@ -29044,7 +29591,7 @@ import { extname } from "node:path";
29044
29591
  import { randomUUID as randomUUID5 } from "node:crypto";
29045
29592
  import { spawn as spawn2 } from "node:child_process";
29046
29593
  import { mkdir as mkdir3, rename as rename2, unlink as unlink3, writeFile as writeFile2 } from "node:fs/promises";
29047
- import { basename as basename3, isAbsolute as isAbsolute5, join as join15 } from "node:path";
29594
+ import { basename as basename3, isAbsolute as isAbsolute5, join as join16 } from "node:path";
29048
29595
 
29049
29596
  // ../node_modules/.pnpm/strtok3@10.3.5/node_modules/strtok3/lib/stream/Errors.js
29050
29597
  var defaultMessages = "End-Of-Stream";
@@ -32986,16 +33533,16 @@ function safeFilename2(input, extension2) {
32986
33533
  }
32987
33534
  async function commitToStorage(buffer, extension2, deps, beforePromote) {
32988
33535
  const storageDir = deps.storageDir ?? uploadDir();
32989
- const quarantineDir = join15(storageDir, ".quarantine");
33536
+ const quarantineDir = join16(storageDir, ".quarantine");
32990
33537
  await mkdir3(quarantineDir, { recursive: true, mode: 448 });
32991
33538
  await mkdir3(storageDir, { recursive: true, mode: 448 });
32992
- const quarantine = join15(quarantineDir, `${randomUUID5()}.upload`);
33539
+ const quarantine = join16(quarantineDir, `${randomUUID5()}.upload`);
32993
33540
  const storageKey = `${randomUUID5()}${extension2}`;
32994
33541
  await writeFile2(quarantine, buffer, { mode: 384, flag: "wx" });
32995
33542
  try {
32996
33543
  await timeout((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
32997
33544
  await beforePromote?.();
32998
- await rename2(quarantine, join15(storageDir, storageKey));
33545
+ await rename2(quarantine, join16(storageDir, storageKey));
32999
33546
  } catch (error) {
33000
33547
  await unlink3(quarantine).catch(() => {
33001
33548
  });
@@ -33196,10 +33743,10 @@ async function dropSpecAttachments(specId) {
33196
33743
  // src/services/spec-attachment-dir.ts
33197
33744
  init_db();
33198
33745
  import { mkdir as mkdir4, readdir as readdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
33199
- import { join as join16 } from "node:path";
33746
+ import { join as join17 } from "node:path";
33200
33747
  init_session_id();
33201
33748
  init_uploads();
33202
- var specAttachmentsDir = (repoDir, sessionId2) => join16(repoDir, ".worktrees", ".attachments", sessionId2);
33749
+ var specAttachmentsDir = (repoDir, sessionId2) => join17(repoDir, ".worktrees", ".attachments", sessionId2);
33203
33750
  var INDEX = "INDEX.md";
33204
33751
  var humanSize = (n2) => n2 >= 1024 * 1024 ? `${(n2 / 1024 / 1024).toFixed(1)} MB` : n2 >= 1024 ? `${Math.round(n2 / 1024)} KB` : `${n2} B`;
33205
33752
  function uniqueName(taken, filename) {
@@ -33249,19 +33796,19 @@ async function syncSpecAttachmentsDir(specId, projectId) {
33249
33796
  for (const a of rows) {
33250
33797
  let bytes;
33251
33798
  try {
33252
- bytes = await readFile3(join16(uploadDir(), a.storageKey));
33799
+ bytes = await readFile3(join17(uploadDir(), a.storageKey));
33253
33800
  } catch {
33254
33801
  continue;
33255
33802
  }
33256
33803
  const filename = uniqueName(taken, a.filename);
33257
- const path = join16(dir2, filename);
33804
+ const path = join17(dir2, filename);
33258
33805
  await writeFile3(path, bytes, { mode: 384 });
33259
33806
  items.push({ filename, mimeType: a.mimeType, size: a.size, path });
33260
33807
  }
33261
- await writeFile3(join16(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
33808
+ await writeFile3(join17(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
33262
33809
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
33263
33810
  for (const name2 of await readdir3(dir2)) {
33264
- if (!keep.has(name2)) await rm4(join16(dir2, name2), { recursive: true, force: true }).catch(() => {
33811
+ if (!keep.has(name2)) await rm4(join17(dir2, name2), { recursive: true, force: true }).catch(() => {
33265
33812
  });
33266
33813
  }
33267
33814
  return items;
@@ -36805,7 +37352,7 @@ async function deleteBranches(repoDir, names, opts) {
36805
37352
  import { execFile as execFile11 } from "node:child_process";
36806
37353
  import { realpathSync as realpathSync4 } from "node:fs";
36807
37354
  import { stat } from "node:fs/promises";
36808
- import { basename as basename4, dirname as dirname9, join as join17, resolve as resolve15, sep as sep4 } from "node:path";
37355
+ import { basename as basename4, dirname as dirname9, join as join18, resolve as resolve15, sep as sep4 } from "node:path";
36809
37356
  import { promisify as promisify10 } from "node:util";
36810
37357
  var exec10 = promisify10(execFile11);
36811
37358
  var GIT7 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
@@ -36847,7 +37394,7 @@ var real = (p3) => {
36847
37394
  } catch {
36848
37395
  const path = resolve15(p3);
36849
37396
  const parent = dirname9(path);
36850
- return parent === path ? path : join17(real(parent), basename4(path));
37397
+ return parent === path ? path : join18(real(parent), basename4(path));
36851
37398
  }
36852
37399
  };
36853
37400
  function hasWorktreeSession(path, sessionId2, sessions) {
@@ -36859,7 +37406,7 @@ function hasWorktreeSession(path, sessionId2, sessions) {
36859
37406
  }
36860
37407
  async function bornAt(path) {
36861
37408
  try {
36862
- const st = await stat(join17(path, ".git"));
37409
+ const st = await stat(join18(path, ".git"));
36863
37410
  const ms = st.birthtimeMs > 0 ? st.birthtimeMs : st.mtimeMs;
36864
37411
  return new Date(ms).toISOString();
36865
37412
  } catch {
@@ -37067,7 +37614,7 @@ init_config2();
37067
37614
  init_src2();
37068
37615
  import { randomUUID as randomUUID6 } from "node:crypto";
37069
37616
  import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir4, stat as stat2 } from "node:fs/promises";
37070
- import { join as join18, resolve as resolve16, basename as basename5 } from "node:path";
37617
+ import { join as join19, resolve as resolve16, basename as basename5 } from "node:path";
37071
37618
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
37072
37619
  function transcriptDir() {
37073
37620
  return resolve16(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
@@ -37088,13 +37635,13 @@ async function saveTranscript(text) {
37088
37635
  const dir2 = transcriptDir();
37089
37636
  await mkdir5(dir2, { recursive: true, mode: 448 });
37090
37637
  const key = `${randomUUID6()}.log`;
37091
- await writeFile4(join18(dir2, key), body, { encoding: "utf8", mode: 384 });
37638
+ await writeFile4(join19(dir2, key), body, { encoding: "utf8", mode: 384 });
37092
37639
  return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
37093
37640
  }
37094
37641
  async function readTranscript(key) {
37095
37642
  if (!key) return null;
37096
37643
  try {
37097
- return await readFile4(join18(transcriptDir(), basename5(key)), "utf8");
37644
+ return await readFile4(join19(transcriptDir(), basename5(key)), "utf8");
37098
37645
  } catch {
37099
37646
  return null;
37100
37647
  }
@@ -37102,7 +37649,7 @@ async function readTranscript(key) {
37102
37649
  async function deleteTranscript(key) {
37103
37650
  if (!key) return;
37104
37651
  try {
37105
- await unlink4(join18(transcriptDir(), basename5(key)));
37652
+ await unlink4(join19(transcriptDir(), basename5(key)));
37106
37653
  } catch (e) {
37107
37654
  if (e.code !== "ENOENT") throw e;
37108
37655
  }
@@ -37120,7 +37667,7 @@ async function listTranscripts() {
37120
37667
  for (const name2 of names) {
37121
37668
  if (!name2.endsWith(".log")) continue;
37122
37669
  try {
37123
- rows.push({ key: name2, mtimeMs: (await stat2(join18(dir2, name2))).mtimeMs });
37670
+ rows.push({ key: name2, mtimeMs: (await stat2(join19(dir2, name2))).mtimeMs });
37124
37671
  } catch {
37125
37672
  }
37126
37673
  }
@@ -37367,7 +37914,7 @@ async function detectOrphanWorktrees(deps = { repos: prodReaperDeps.repos, input
37367
37914
  // src/services/repo-fs.ts
37368
37915
  import { lstat as lstat2, rename as rename3, rm as rm5 } from "node:fs/promises";
37369
37916
  import { createWriteStream } from "node:fs";
37370
- import { join as join19 } from "node:path";
37917
+ import { join as join20 } from "node:path";
37371
37918
  import { randomUUID as randomUUID8 } from "node:crypto";
37372
37919
  import { pipeline } from "node:stream/promises";
37373
37920
  var EntryExistsError = class extends Error {
@@ -37428,7 +37975,7 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
37428
37975
  if (current && !opts.overwrite) return { status: "exists" };
37429
37976
  if (current && !current.isFile())
37430
37977
  throw new PathContainmentError("repository path ditolak: target bukan file regular");
37431
- const temp = join19(entry.parent, `.hanoman-${randomUUID8()}.tmp`);
37978
+ const temp = join20(entry.parent, `.hanoman-${randomUUID8()}.tmp`);
37432
37979
  try {
37433
37980
  await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
37434
37981
  if (opts.isTruncated?.()) {
@@ -37949,14 +38496,14 @@ async function finishGraphOp(reply, id2, repoDir, r, verb) {
37949
38496
  // src/routes/fs.ts
37950
38497
  import { readdir as readdir5 } from "node:fs/promises";
37951
38498
  import { homedir as homedir4 } from "node:os";
37952
- import { resolve as resolve18, dirname as dirname10, join as join20 } from "node:path";
38499
+ import { resolve as resolve18, dirname as dirname10, join as join21 } from "node:path";
37953
38500
  async function fs_default(app2) {
37954
38501
  app2.get("/fs/browse", async (req, reply) => {
37955
38502
  const q = req.query.path;
37956
38503
  const dir2 = q && q.trim() ? resolve18(q.trim()) : homedir4();
37957
38504
  try {
37958
38505
  const ents = await readdir5(dir2, { withFileTypes: true });
37959
- const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join20(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
38506
+ const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join21(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
37960
38507
  const parent = dirname10(dir2);
37961
38508
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries3 };
37962
38509
  } catch {
@@ -37981,6 +38528,21 @@ init_src();
37981
38528
  init_settings3();
37982
38529
  init_codex_trust();
37983
38530
  init_pty();
38531
+
38532
+ // src/services/orchestration.ts
38533
+ init_src();
38534
+ init_pty();
38535
+ function sessionPhasePlan(setting, flow, agent, orchestrator) {
38536
+ return resolvePhasePlan({
38537
+ flow,
38538
+ runtime: agent,
38539
+ orchestration: setting.orchestration,
38540
+ orchestrator,
38541
+ nativeAgents: nativeAgentsAvailable(agent)
38542
+ });
38543
+ }
38544
+
38545
+ // src/services/session-launch.ts
37984
38546
  init_session_phases();
37985
38547
  init_session_launch_gate();
37986
38548
  init_pty();
@@ -38075,22 +38637,36 @@ async function startSpecSession(spec, opts) {
38075
38637
  payload: spec.payload ?? void 0
38076
38638
  };
38077
38639
  const resumeCtx = resume ? buildResumeCtx(repoDir, id2, opts.flow, resume.worktreeKept) : void 0;
38078
- let prompt;
38079
- if (isGoalFlow) {
38080
- prompt = startGoalPrompt(opts.flow, brief, branchTo, {
38081
- autonomy: opts.autonomy,
38082
- verifyScope,
38083
- resume: resumeCtx,
38084
- method: method.id,
38085
- attachments
38086
- });
38087
- } else if (isContinue) {
38088
- prompt = continuePrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments);
38089
- } else if (resumeCtx) {
38090
- prompt = resumePrompt(opts.flow, brief, branchTo, resumeCtx, opts.autonomy, verifyScope, method.id, attachments);
38091
- } else {
38092
- prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments);
38093
- }
38640
+ const fullPlan = sessionPhasePlan(setting, opts.flow, agent, { model, effort: effort2 });
38641
+ let plan = fullPlan;
38642
+ if (fullPlan && isContinue && !isGoalFlow) {
38643
+ const phases = fullPlan.phases.filter((p3) => p3.phase === "Execute");
38644
+ plan = phases.length ? { ...fullPlan, phases } : null;
38645
+ }
38646
+ const buildPrompt = (p3) => {
38647
+ if (isGoalFlow) {
38648
+ return startGoalPrompt(opts.flow, brief, branchTo, {
38649
+ autonomy: opts.autonomy,
38650
+ verifyScope,
38651
+ resume: resumeCtx,
38652
+ method: method.id,
38653
+ attachments,
38654
+ plan: p3
38655
+ });
38656
+ }
38657
+ if (isContinue) return continuePrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments, p3);
38658
+ if (resumeCtx) return resumePrompt(opts.flow, brief, branchTo, resumeCtx, opts.autonomy, verifyScope, method.id, attachments, p3);
38659
+ return startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id, attachments, p3);
38660
+ };
38661
+ const legacyPrompt = buildPrompt(null);
38662
+ const prompt = plan ? buildPrompt(plan) : legacyPrompt;
38663
+ const phaseAgents = plan ? buildPhaseAgents(plan, {
38664
+ flow: opts.flow,
38665
+ method,
38666
+ verifyScope,
38667
+ context: isGoalFlow ? goalContext(brief) : specContext(brief),
38668
+ fromAudit: fromAuditOf(spec.payload)
38669
+ }) : [];
38094
38670
  const scopeEnv = { HANOMAN_BASE_SHA: baseSha, HANOMAN_VERIFY_SCOPE: verifyScope };
38095
38671
  const s2 = createSession(spec.projectId, worktree, {
38096
38672
  specId: spec.id,
@@ -38103,6 +38679,8 @@ async function startSpecSession(spec, opts) {
38103
38679
  decisionFile: decisionFilePath(repoDir, id2),
38104
38680
  attachmentsDir: attachments.items.length ? attachments.dir : void 0,
38105
38681
  prompt,
38682
+ legacyPrompt,
38683
+ phaseAgents,
38106
38684
  env: scopeEnv
38107
38685
  });
38108
38686
  return resume ? { id: s2.id, resumed: true } : { id: s2.id };
@@ -38444,13 +39022,13 @@ function installCommand(m, agent, shell = shellBin()) {
38444
39022
 
38445
39023
  // src/services/terminal-diag.ts
38446
39024
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync5 } from "node:fs";
38447
- import { join as join21 } from "node:path";
39025
+ import { join as join22 } from "node:path";
38448
39026
  var DIAG_MAX_BYTES = 2 * 1024 * 1024;
38449
39027
  var KINDS = /* @__PURE__ */ new Set(["key", "comp", "data", "ack", "pred"]);
38450
39028
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
38451
39029
  function diagFile(home3, sessionId2) {
38452
39030
  if (!ID.test(sessionId2)) throw new Error(`id sesi tak sah untuk diag: ${sessionId2}`);
38453
- return join21(home3, "diag", `${sessionId2}.jsonl`);
39031
+ return join22(home3, "diag", `${sessionId2}.jsonl`);
38454
39032
  }
38455
39033
  function usable(ev) {
38456
39034
  if (!ev || typeof ev !== "object") return false;
@@ -38461,7 +39039,7 @@ function appendDiag(home3, sessionId2, events) {
38461
39039
  const file = diagFile(home3, sessionId2);
38462
39040
  const rows = events.filter(usable);
38463
39041
  if (!rows.length) return;
38464
- mkdirSync9(join21(home3, "diag"), { recursive: true });
39042
+ mkdirSync9(join22(home3, "diag"), { recursive: true });
38465
39043
  try {
38466
39044
  if (statSync3(file).size > DIAG_MAX_BYTES) rmSync5(file);
38467
39045
  } catch {
@@ -38475,12 +39053,505 @@ init_stage_machine();
38475
39053
  init_pty();
38476
39054
  init_uploads();
38477
39055
 
39056
+ // src/services/agent-invocations.ts
39057
+ init_src();
39058
+ init_db();
39059
+ import { createHash as createHash8, randomUUID as randomUUID9 } from "node:crypto";
39060
+ import { execFileSync as execFileSync3 } from "node:child_process";
39061
+ import { homedir as homedir5 } from "node:os";
39062
+ import { readFileSync as readFileSync9, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
39063
+ import { isAbsolute as isAbsolute6, relative as relative2, resolve as resolve19 } from "node:path";
39064
+
39065
+ // src/services/session-event-relay.ts
39066
+ init_session_event_token();
39067
+ init_session_event_spool();
39068
+ import { constants as constants3 } from "node:fs";
39069
+ import { mkdir as mkdir6, open as open3, readdir as readdir6, rm as rm6 } from "node:fs/promises";
39070
+ import { join as join23 } from "node:path";
39071
+ var MAX_EVENT_BYTES = 1e6;
39072
+ var MAX_FILES_PER_DRAIN = 1e3;
39073
+ var SESSION_ID_RE = /^[a-z0-9_-]+$/;
39074
+ var observations = /* @__PURE__ */ new Map();
39075
+ function sessionEventRelayStatus(root = sessionEventSpoolRoot()) {
39076
+ return { ...observations.get(root) ?? {
39077
+ state: "unobserved",
39078
+ checkedAt: null,
39079
+ lastDeliveryAt: null,
39080
+ lastIssueAt: null,
39081
+ retryPending: 0,
39082
+ retryAttempts: 0,
39083
+ droppedEvents: 0
39084
+ } };
39085
+ }
39086
+ async function drainSessionEventSpool(app2, root = sessionEventSpoolRoot()) {
39087
+ const previous = sessionEventRelayStatus(root);
39088
+ const current = { retries: 0, dropped: 0 };
39089
+ let delivered = 0;
39090
+ let failed = false;
39091
+ try {
39092
+ delivered = await drainSpool(app2, root, current);
39093
+ return delivered;
39094
+ } catch (error) {
39095
+ failed = true;
39096
+ throw error;
39097
+ } finally {
39098
+ const now = (/* @__PURE__ */ new Date()).toISOString();
39099
+ observations.set(root, {
39100
+ state: failed || current.retries > 0 ? "degraded" : "ready",
39101
+ checkedAt: now,
39102
+ lastDeliveryAt: delivered ? now : previous.lastDeliveryAt,
39103
+ lastIssueAt: failed || current.retries || current.dropped ? now : previous.lastIssueAt,
39104
+ retryPending: current.retries,
39105
+ retryAttempts: previous.retryAttempts + current.retries,
39106
+ droppedEvents: previous.droppedEvents + current.dropped
39107
+ });
39108
+ }
39109
+ }
39110
+ async function drainSpool(app2, root, observation) {
39111
+ await mkdir6(root, { recursive: true, mode: 448 });
39112
+ let delivered = 0;
39113
+ let examined = 0;
39114
+ let readBuffer;
39115
+ for (const session of await readdir6(root, { withFileTypes: true })) {
39116
+ if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
39117
+ const dir2 = join23(root, session.name);
39118
+ let entries3;
39119
+ try {
39120
+ entries3 = await readdir6(dir2, { withFileTypes: true });
39121
+ } catch {
39122
+ continue;
39123
+ }
39124
+ for (const entry of entries3) {
39125
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
39126
+ if (++examined > MAX_FILES_PER_DRAIN) return delivered;
39127
+ const path = join23(dir2, entry.name);
39128
+ let payload;
39129
+ let handle;
39130
+ try {
39131
+ handle = await open3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
39132
+ const stat4 = await handle.stat();
39133
+ if (!stat4.isFile() || stat4.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
39134
+ readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
39135
+ const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
39136
+ if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
39137
+ const parsed = JSON.parse(readBuffer.subarray(0, bytesRead).toString("utf8"));
39138
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
39139
+ throw new Error("payload event bukan object");
39140
+ }
39141
+ payload = parsed;
39142
+ } catch {
39143
+ observation.dropped++;
39144
+ await rm6(path, { force: true }).catch(() => {
39145
+ });
39146
+ continue;
39147
+ } finally {
39148
+ await handle?.close().catch(() => {
39149
+ });
39150
+ }
39151
+ try {
39152
+ const response = await app2.inject({
39153
+ method: "POST",
39154
+ url: "/api/session-events",
39155
+ headers: {
39156
+ authorization: `Bearer ${sessionEventToken(session.name)}`,
39157
+ "x-hanoman-session": session.name
39158
+ },
39159
+ payload
39160
+ });
39161
+ if (response.statusCode === 429 || response.statusCode >= 500) {
39162
+ observation.retries++;
39163
+ continue;
39164
+ }
39165
+ await rm6(path, { force: true }).catch(() => {
39166
+ });
39167
+ if (response.statusCode >= 200 && response.statusCode < 300) delivered++;
39168
+ else observation.dropped++;
39169
+ } catch {
39170
+ observation.retries++;
39171
+ }
39172
+ }
39173
+ }
39174
+ return delivered;
39175
+ }
39176
+ function startSessionEventRelay(app2, options2 = {}) {
39177
+ const root = options2.root ?? sessionEventSpoolRoot();
39178
+ let running = false;
39179
+ const tick6 = async () => {
39180
+ if (running) return;
39181
+ running = true;
39182
+ try {
39183
+ await drainSessionEventSpool({ inject: async (request) => app2.inject(request) }, root);
39184
+ } catch (error) {
39185
+ console.error("session event relay gagal:", error);
39186
+ } finally {
39187
+ running = false;
39188
+ }
39189
+ };
39190
+ const timer9 = setInterval(() => {
39191
+ void tick6();
39192
+ }, options2.intervalMs ?? 250);
39193
+ timer9.unref();
39194
+ app2.addHook("onClose", async () => {
39195
+ clearInterval(timer9);
39196
+ });
39197
+ void tick6();
39198
+ }
39199
+
39200
+ // src/services/agent-invocations.ts
39201
+ var MAX_EXCERPT_BYTES = 4096;
39202
+ var MAX_TRANSCRIPT_BYTES2 = 10 * 1024 * 1024;
39203
+ var ANSI2 = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
39204
+ var snapshotHashes = /* @__PURE__ */ new Map();
39205
+ var keyOf = (x) => `${x.sessionId}\0${x.runtimeInvocationId}`;
39206
+ var hash2 = (value) => createHash8("sha256").update(value).digest("hex");
39207
+ var defaultGitStatus = (cwd) => {
39208
+ try {
39209
+ return execFileSync3("git", ["-C", cwd, "status", "--porcelain=v1", "-z"], {
39210
+ encoding: "utf8",
39211
+ stdio: ["ignore", "pipe", "ignore"]
39212
+ });
39213
+ } catch {
39214
+ return null;
39215
+ }
39216
+ };
39217
+ function snapshot2(cwd, run4 = defaultGitStatus) {
39218
+ try {
39219
+ const value = run4(cwd);
39220
+ return value === null ? null : hash2(value);
39221
+ } catch {
39222
+ return null;
39223
+ }
39224
+ }
39225
+ var stripAnsi = (value) => value.replace(ANSI2, "");
39226
+ var utf8Prefix = (value, maxBytes) => {
39227
+ let out4 = "", bytes = 0;
39228
+ for (const char of value) {
39229
+ const next = Buffer.byteLength(char, "utf8");
39230
+ if (bytes + next > maxBytes) break;
39231
+ out4 += char;
39232
+ bytes += next;
39233
+ }
39234
+ return out4;
39235
+ };
39236
+ var transcriptRoots = () => [
39237
+ resolve19(process.env.CLAUDE_CONFIG_DIR ?? `${homedir5()}/.claude`),
39238
+ resolve19(process.env.CODEX_HOME ?? `${homedir5()}/.codex`)
39239
+ ];
39240
+ var inside = (path, root) => {
39241
+ const rel = relative2(root, path);
39242
+ return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
39243
+ };
39244
+ var EMPTY_USAGE = { inputTokens: null, outputTokens: null, cachedTokens: null };
39245
+ var nonnegativeInt = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
39246
+ function usageFromText(text) {
39247
+ const found = [];
39248
+ const inspect = (value) => {
39249
+ if (!value || typeof value !== "object") return;
39250
+ const record2 = value;
39251
+ const usage2 = record2.usage;
39252
+ if (usage2 && typeof usage2 === "object") {
39253
+ const u = usage2;
39254
+ found.push([
39255
+ nonnegativeInt(u.input_tokens ?? u.inputTokens) ?? -1,
39256
+ nonnegativeInt(u.output_tokens ?? u.outputTokens) ?? -1,
39257
+ nonnegativeInt(u.cached_tokens ?? u.cachedTokens ?? u.cache_read_input_tokens ?? u.cacheReadInputTokens) ?? -1
39258
+ ]);
39259
+ }
39260
+ };
39261
+ for (const line of text.split("\n")) {
39262
+ if (!line.trim()) continue;
39263
+ try {
39264
+ inspect(JSON.parse(line));
39265
+ } catch {
39266
+ }
39267
+ }
39268
+ if (found.length === 0) return EMPTY_USAGE;
39269
+ const max = (index) => {
39270
+ const values = found.map((entry) => entry[index]).filter((n2) => n2 >= 0);
39271
+ return values.length ? Math.max(...values) : null;
39272
+ };
39273
+ return { inputTokens: max(0), outputTokens: max(1), cachedTokens: max(2) };
39274
+ }
39275
+ function transcriptUsage(path, roots = transcriptRoots()) {
39276
+ if (!path) return EMPTY_USAGE;
39277
+ try {
39278
+ const real2 = realpathSync5(path);
39279
+ const safeRoots = roots.map((root) => realpathSync5(root));
39280
+ if (!safeRoots.some((root) => inside(real2, root))) return EMPTY_USAGE;
39281
+ const info = statSync4(real2);
39282
+ if (!info.isFile() || info.size > MAX_TRANSCRIPT_BYTES2) return EMPTY_USAGE;
39283
+ return usageFromText(readFileSync9(real2, "utf8"));
39284
+ } catch {
39285
+ return EMPTY_USAGE;
39286
+ }
39287
+ }
39288
+ async function startAgentInvocation(input, io = {}) {
39289
+ const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
39290
+ const existing = await prisma.agentInvocation.findUnique({
39291
+ where: { sessionId_runtimeInvocationId: {
39292
+ sessionId: input.sessionId,
39293
+ runtimeInvocationId: input.runtimeInvocationId
39294
+ } }
39295
+ });
39296
+ const row = await prisma.agentInvocation.upsert({
39297
+ where: { sessionId_runtimeInvocationId: {
39298
+ sessionId: input.sessionId,
39299
+ runtimeInvocationId: input.runtimeInvocationId
39300
+ } },
39301
+ update: {},
39302
+ create: {
39303
+ id: randomUUID9(),
39304
+ sessionId: input.sessionId,
39305
+ projectId: input.projectId,
39306
+ specId: input.specId ?? null,
39307
+ runtime: input.runtime,
39308
+ runtimeInvocationId: input.runtimeInvocationId,
39309
+ customAgentId: input.customAgentId ?? null,
39310
+ agentName: input.agentName,
39311
+ model: input.model ?? null,
39312
+ definitionHash: input.definitionHash ?? null,
39313
+ phase: input.phase ?? null,
39314
+ effort: input.effort ?? null,
39315
+ status: "running",
39316
+ startedAt
39317
+ }
39318
+ });
39319
+ const before2 = snapshot2(input.cwd, io.gitStatus);
39320
+ if (before2 !== null) snapshotHashes.set(keyOf(input), before2);
39321
+ return { row, duplicate: existing !== null };
39322
+ }
39323
+ async function stopAgentInvocation(input, io = {}) {
39324
+ const unique = { sessionId_runtimeInvocationId: {
39325
+ sessionId: input.sessionId,
39326
+ runtimeInvocationId: input.runtimeInvocationId
39327
+ } };
39328
+ const existing = await prisma.agentInvocation.findUnique({ where: unique });
39329
+ if (existing?.endedAt) return { row: existing, duplicate: true };
39330
+ const endedAt = input.endedAt ?? /* @__PURE__ */ new Date();
39331
+ const startedAt = existing?.startedAt ?? endedAt;
39332
+ const cleanResult = input.result === void 0 ? null : stripAnsi(input.result);
39333
+ const usage2 = transcriptUsage(input.transcriptPath, io.transcriptRoots);
39334
+ const before2 = snapshotHashes.get(keyOf(input));
39335
+ const after = snapshot2(input.cwd, io.gitStatus);
39336
+ snapshotHashes.delete(keyOf(input));
39337
+ const evidence = {
39338
+ // Stop tanpa start lazim setelah restart server di tengah invocation. Waktu start dan status
39339
+ // runtime sudah hilang; simpan baris sintetis yang dapat diaudit tanpa mengarang durasi 0 ms.
39340
+ status: existing ? input.status ?? "completed" : "completed",
39341
+ endedAt,
39342
+ durationMs: existing ? Math.max(0, endedAt.getTime() - startedAt.getTime()) : null,
39343
+ ...usage2,
39344
+ resultExcerpt: cleanResult === null ? null : utf8Prefix(cleanResult, MAX_EXCERPT_BYTES),
39345
+ resultHash: cleanResult === null ? null : hash2(cleanResult),
39346
+ workspaceChanged: before2 !== void 0 && after !== null && before2 !== after,
39347
+ // ADR-0164 · effort yang BENAR-BENAR dipakai runtime menang atas nilai start-time.
39348
+ ...input.effort ? { effort: input.effort } : {}
39349
+ };
39350
+ if (existing) {
39351
+ const row2 = await prisma.agentInvocation.update({ where: { id: existing.id }, data: evidence });
39352
+ return { row: row2, duplicate: false };
39353
+ }
39354
+ const row = await prisma.agentInvocation.create({
39355
+ data: {
39356
+ id: randomUUID9(),
39357
+ sessionId: input.sessionId,
39358
+ projectId: input.projectId,
39359
+ specId: input.specId ?? null,
39360
+ runtime: input.runtime,
39361
+ runtimeInvocationId: input.runtimeInvocationId,
39362
+ customAgentId: input.customAgentId ?? null,
39363
+ agentName: input.agentName,
39364
+ model: input.model ?? null,
39365
+ definitionHash: input.definitionHash ?? null,
39366
+ phase: input.phase ?? null,
39367
+ startedAt,
39368
+ ...evidence
39369
+ }
39370
+ });
39371
+ return { row, duplicate: false };
39372
+ }
39373
+ async function reconcileAgentInvocations(liveSessionIds) {
39374
+ const live = new Set(liveSessionIds);
39375
+ const open5 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
39376
+ let changed = 0;
39377
+ const endedAt = /* @__PURE__ */ new Date();
39378
+ for (const row of open5) {
39379
+ if (live.has(row.sessionId)) continue;
39380
+ await prisma.agentInvocation.update({
39381
+ where: { id: row.id },
39382
+ data: {
39383
+ status: "abandoned",
39384
+ endedAt,
39385
+ durationMs: Math.max(0, endedAt.getTime() - row.startedAt.getTime())
39386
+ }
39387
+ });
39388
+ changed++;
39389
+ }
39390
+ return changed;
39391
+ }
39392
+ var agentInvocationView = (row) => ({
39393
+ id: row.id,
39394
+ sessionId: row.sessionId,
39395
+ projectId: row.projectId,
39396
+ specId: row.specId,
39397
+ runtime: row.runtime === "codex" ? "codex" : "claude",
39398
+ customAgentId: row.customAgentId,
39399
+ agentName: row.agentName,
39400
+ model: row.model,
39401
+ definitionHash: row.definitionHash,
39402
+ status: row.status,
39403
+ startedAt: row.startedAt.toISOString(),
39404
+ endedAt: row.endedAt?.toISOString() ?? null,
39405
+ durationMs: row.durationMs,
39406
+ inputTokens: row.inputTokens,
39407
+ outputTokens: row.outputTokens,
39408
+ cachedTokens: row.cachedTokens,
39409
+ resultExcerpt: row.resultExcerpt,
39410
+ resultHash: row.resultHash,
39411
+ workspaceChanged: row.workspaceChanged,
39412
+ disposition: AGENT_DISPOSITIONS.includes(row.disposition) ? row.disposition : "pending",
39413
+ dispositionNote: row.dispositionNote,
39414
+ evaluatedAt: row.evaluatedAt?.toISOString() ?? null,
39415
+ reworkRequired: row.reworkRequired
39416
+ });
39417
+ var median = (values) => {
39418
+ if (values.length === 0) return null;
39419
+ values.sort((a, b) => a - b);
39420
+ const middle = Math.floor(values.length / 2);
39421
+ return values.length % 2 === 1 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
39422
+ };
39423
+ var availableSum = (values) => {
39424
+ const known = values.filter((value) => value !== null);
39425
+ return known.length ? known.reduce((sum, value) => sum + value, 0) : null;
39426
+ };
39427
+ async function agentMetrics(query2) {
39428
+ const where = {
39429
+ // ADR-0164 · telemetri agen fase milik terminal, bukan presisi katalog custom agent.
39430
+ phase: null,
39431
+ ...query2.projectId ? { projectId: query2.projectId } : {},
39432
+ ...query2.from || query2.to ? { startedAt: {
39433
+ ...query2.from ? { gte: query2.from } : {},
39434
+ ...query2.to ? { lte: query2.to } : {}
39435
+ } } : {}
39436
+ };
39437
+ const rows = await prisma.agentInvocation.findMany({ where, orderBy: { startedAt: "desc" } });
39438
+ const groups = /* @__PURE__ */ new Map();
39439
+ const variantGroups = /* @__PURE__ */ new Map();
39440
+ const add = (map, key, row) => {
39441
+ const group2 = map.get(key);
39442
+ if (group2) group2.push(row);
39443
+ else map.set(key, [row]);
39444
+ };
39445
+ for (const row of rows) {
39446
+ add(groups, row.agentName, row);
39447
+ add(variantGroups, JSON.stringify([row.agentName, row.runtime, row.model, row.definitionHash]), row);
39448
+ }
39449
+ const summarize = (agentName, invocations) => {
39450
+ const dispositions = { pending: 0, accepted: 0, partial: 0, rejected: 0, falsePositive: 0 };
39451
+ for (const row of invocations) {
39452
+ if (row.disposition === "accepted") dispositions.accepted++;
39453
+ else if (row.disposition === "partial") dispositions.partial++;
39454
+ else if (row.disposition === "rejected") dispositions.rejected++;
39455
+ else if (row.disposition === "false-positive") dispositions.falsePositive++;
39456
+ else dispositions.pending++;
39457
+ }
39458
+ const evaluated = dispositions.accepted + dispositions.partial + dispositions.rejected + dispositions.falsePositive;
39459
+ return {
39460
+ agentName,
39461
+ invocationCount: invocations.length,
39462
+ medianDurationMs: median(invocations.flatMap((row) => row.durationMs === null ? [] : [row.durationMs])),
39463
+ inputTokens: availableSum(invocations.map((row) => row.inputTokens)),
39464
+ outputTokens: availableSum(invocations.map((row) => row.outputTokens)),
39465
+ cachedTokens: availableSum(invocations.map((row) => row.cachedTokens)),
39466
+ dispositions,
39467
+ operationalPrecision: evaluated ? (dispositions.accepted + dispositions.partial) / evaluated : null,
39468
+ workspaceChanged: invocations.some((row) => row.workspaceChanged),
39469
+ evaluatedCount: evaluated,
39470
+ rework: {
39471
+ required: invocations.filter((row) => row.reworkRequired === true).length,
39472
+ notRequired: invocations.filter((row) => row.reworkRequired === false).length,
39473
+ unknown: invocations.filter((row) => row.reworkRequired === null).length
39474
+ }
39475
+ };
39476
+ };
39477
+ const agents = [...groups.entries()].map(([name2, group2]) => summarize(name2, group2)).sort((a, b) => a.agentName.localeCompare(b.agentName));
39478
+ const variants = [...variantGroups.values()].map((group2) => ({
39479
+ ...summarize(group2[0].agentName, group2),
39480
+ runtime: group2[0].runtime === "codex" ? "codex" : "claude",
39481
+ model: group2[0].model,
39482
+ definitionHash: group2[0].definitionHash
39483
+ })).sort((a, b) => JSON.stringify([a.agentName, a.runtime, a.model, a.definitionHash]).localeCompare(JSON.stringify([b.agentName, b.runtime, b.model, b.definitionHash])));
39484
+ const lastEventAt = rows.reduce((latest, row) => Math.max(latest, row.startedAt.getTime(), row.endedAt?.getTime() ?? 0), 0);
39485
+ const sampleRows = /* @__PURE__ */ new Map();
39486
+ for (const group2 of groups.values()) {
39487
+ for (const category of [
39488
+ (row) => row.disposition === "pending",
39489
+ (row) => row.disposition !== "pending",
39490
+ (row) => row.reworkRequired === true
39491
+ ]) {
39492
+ for (const row of group2.filter(category).slice(0, 2)) sampleRows.set(row.id, row);
39493
+ }
39494
+ }
39495
+ return {
39496
+ agents,
39497
+ variants,
39498
+ recent: rows.slice(0, 100).map(agentInvocationView),
39499
+ samples: [...sampleRows.values()].map(agentInvocationView),
39500
+ telemetry: {
39501
+ state: rows.length ? "observed" : "unobserved",
39502
+ lastEventAt: rows.length ? new Date(lastEventAt).toISOString() : null,
39503
+ incompleteCount: rows.filter((row) => !row.endedAt || row.resultHash === null).length,
39504
+ relay: sessionEventRelayStatus()
39505
+ }
39506
+ };
39507
+ }
39508
+ async function updateAgentInvocationDisposition(id2, disposition, note, reworkRequired) {
39509
+ const exists = await prisma.agentInvocation.findUnique({ where: { id: id2 } });
39510
+ if (!exists) return null;
39511
+ const row = await prisma.agentInvocation.update({
39512
+ where: { id: id2 },
39513
+ data: {
39514
+ disposition,
39515
+ dispositionNote: note?.trim() || null,
39516
+ evaluatedAt: /* @__PURE__ */ new Date(),
39517
+ ...reworkRequired !== void 0 ? { reworkRequired } : {}
39518
+ }
39519
+ });
39520
+ return agentInvocationView(row);
39521
+ }
39522
+ async function listPhaseInvocations(sessionId2) {
39523
+ const rows = await prisma.agentInvocation.findMany({
39524
+ where: { sessionId: sessionId2, phase: { not: null } },
39525
+ orderBy: { startedAt: "asc" }
39526
+ });
39527
+ return rows.map((row) => ({
39528
+ phase: row.phase,
39529
+ runtimeInvocationId: row.runtimeInvocationId,
39530
+ status: row.status,
39531
+ startedAt: row.startedAt.toISOString(),
39532
+ durationMs: row.durationMs,
39533
+ inputTokens: row.inputTokens,
39534
+ outputTokens: row.outputTokens,
39535
+ cachedTokens: row.cachedTokens,
39536
+ resultExcerpt: row.resultExcerpt
39537
+ }));
39538
+ }
39539
+
39540
+ // src/services/phase-invocations.ts
39541
+ init_pty();
39542
+ async function refreshPhaseInvocations(sessionId2) {
39543
+ try {
39544
+ setPhaseInvocations(sessionId2, await listPhaseInvocations(sessionId2));
39545
+ } catch {
39546
+ }
39547
+ }
39548
+
38478
39549
  // src/services/session-dialog.ts
38479
39550
  init_tui_dialog();
38480
39551
  init_pty();
38481
- import { createHash as createHash8 } from "node:crypto";
39552
+ import { createHash as createHash9 } from "node:crypto";
38482
39553
  var DIALOG_CHUNK_MS = 50;
38483
- var screenHashOf = (paneText2) => createHash8("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
39554
+ var screenHashOf = (paneText2) => createHash9("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
38484
39555
  var answerable = (paneText2) => {
38485
39556
  const s2 = readDialogScreen(paneText2);
38486
39557
  if (s2?.kind !== "question") return null;
@@ -38843,7 +39914,7 @@ function leadPrompt(q, c) {
38843
39914
  // src/services/lead/verdict.ts
38844
39915
  init_src();
38845
39916
  import { existsSync as existsSync11 } from "node:fs";
38846
- import { isAbsolute as isAbsolute6, resolve as resolvePath, relative as relative2 } from "node:path";
39917
+ import { isAbsolute as isAbsolute7, resolve as resolvePath, relative as relative3 } from "node:path";
38847
39918
  function extractJsonBlock(raw) {
38848
39919
  const re = /```json\s*\n([\s\S]*?)```/g;
38849
39920
  let last = null;
@@ -38876,9 +39947,9 @@ function keepExistingRefs(refs2, repoDir) {
38876
39947
  continue;
38877
39948
  }
38878
39949
  if (!repoDir) continue;
38879
- if (isAbsolute6(ref)) continue;
39950
+ if (isAbsolute7(ref)) continue;
38880
39951
  const full = resolvePath(repoDir, ref);
38881
- const rel = relative2(repoDir, full);
39952
+ const rel = relative3(repoDir, full);
38882
39953
  if (rel.startsWith("..")) continue;
38883
39954
  if (existsSync11(full)) out4.push(ref);
38884
39955
  }
@@ -39017,10 +40088,10 @@ init_config2();
39017
40088
  init_pty();
39018
40089
  init_session_sandbox();
39019
40090
  import { execFile as execFile13 } from "node:child_process";
39020
- import { randomUUID as randomUUID9 } from "node:crypto";
39021
- import { mkdirSync as mkdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
40091
+ import { randomUUID as randomUUID10 } from "node:crypto";
40092
+ import { mkdirSync as mkdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
39022
40093
  import { tmpdir as tmpdir3 } from "node:os";
39023
- import { join as join22 } from "node:path";
40094
+ import { join as join24 } from "node:path";
39024
40095
  var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
39025
40096
  function leadArgv(o) {
39026
40097
  if (o.agent === "codex") {
@@ -39048,11 +40119,11 @@ function leadProcess(prompt, o, env = process.env) {
39048
40119
  const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
39049
40120
  if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
39050
40121
  } };
39051
- const promptDir = join22(tmpdir3(), "hanoman-prompts");
40122
+ const promptDir = join24(tmpdir3(), "hanoman-prompts");
39052
40123
  mkdirSync10(promptDir, { recursive: true, mode: 448 });
39053
- const promptFile = join22(promptDir, `oneshot-${randomUUID9()}`);
39054
- writeFileSync7(promptFile, prompt, { flag: "wx", mode: 384 });
39055
- const workspace = o.cwd ?? join22(tmpdir3(), `hanoman-oneshot-${randomUUID9()}`);
40124
+ const promptFile = join24(promptDir, `oneshot-${randomUUID10()}`);
40125
+ writeFileSync8(promptFile, prompt, { flag: "wx", mode: 384 });
40126
+ const workspace = o.cwd ?? join24(tmpdir3(), `hanoman-oneshot-${randomUUID10()}`);
39056
40127
  if (!o.cwd) mkdirSync10(workspace, { recursive: false, mode: 448 });
39057
40128
  try {
39058
40129
  const argsWithoutPrompt = directArgs.slice(0, -1);
@@ -39935,6 +41006,9 @@ async function terminal_default(app2, opts) {
39935
41006
  } catch (e) {
39936
41007
  return { code: 422, body: { error: `gagal membuat worktree: ${e.message}` } };
39937
41008
  }
41009
+ const brief = { id: project.id, name: project.name, desc: project.desc, stack: project.stack };
41010
+ const plan = sessionPhasePlan(await getSetting(), "reverse", agent2, { model: model2, effort: effort3 });
41011
+ const legacyPrompt = startProjectPrompt("reverse", brief, "reverse-docs") + resumeNote(reused);
39938
41012
  const s3 = createSession(project.id, wt, {
39939
41013
  id: id2,
39940
41014
  flow: "reverse",
@@ -39943,12 +41017,9 @@ async function terminal_default(app2, opts) {
39943
41017
  agent: agent2,
39944
41018
  phaseFile: phaseFilePath(repoDir, id2),
39945
41019
  decisionFile: decisionFilePath(repoDir, id2),
39946
- prompt: startProjectPrompt("reverse", {
39947
- id: project.id,
39948
- name: project.name,
39949
- desc: project.desc,
39950
- stack: project.stack
39951
- }, "reverse-docs") + resumeNote(reused)
41020
+ prompt: plan ? startProjectPrompt("reverse", brief, "reverse-docs", plan) + resumeNote(reused) : legacyPrompt,
41021
+ legacyPrompt,
41022
+ phaseAgents: plan ? buildPhaseAgents(plan, { flow: "reverse", method: PROJECT_METHOD, context: projectContext(brief) }) : []
39952
41023
  });
39953
41024
  return { code: 201, body: { id: s3.id } };
39954
41025
  }, (pane) => ({ code: 201, body: { id: pane.id } }));
@@ -39967,6 +41038,9 @@ async function terminal_default(app2, opts) {
39967
41038
  } catch (e) {
39968
41039
  return { code: 422, body: { error: `gagal membuat worktree: ${e.message}` } };
39969
41040
  }
41041
+ const brief = { id: project.id, name: project.name, desc: project.desc, stack: project.stack };
41042
+ const plan = sessionPhasePlan(await getSetting(), "scaffold", agent2, { model: model2, effort: effort3 });
41043
+ const legacyPrompt = startScaffoldPrompt(brief, "scaffold-docs") + resumeNote(reused);
39970
41044
  const s3 = createSession(project.id, wt, {
39971
41045
  id: id2,
39972
41046
  flow: "scaffold",
@@ -39975,10 +41049,9 @@ async function terminal_default(app2, opts) {
39975
41049
  agent: agent2,
39976
41050
  phaseFile: phaseFilePath(repoDir, id2),
39977
41051
  decisionFile: decisionFilePath(repoDir, id2),
39978
- prompt: startScaffoldPrompt(
39979
- { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
39980
- "scaffold-docs"
39981
- ) + resumeNote(reused)
41052
+ prompt: plan ? startScaffoldPrompt(brief, "scaffold-docs", plan) + resumeNote(reused) : legacyPrompt,
41053
+ legacyPrompt,
41054
+ phaseAgents: plan ? buildPhaseAgents(plan, { flow: "scaffold", method: PROJECT_METHOD, context: scaffoldContext(brief) }) : []
39982
41055
  });
39983
41056
  return { code: 201, body: { id: s3.id } };
39984
41057
  }, (pane) => ({ code: 201, body: { id: pane.id } }));
@@ -40000,6 +41073,10 @@ async function terminal_default(app2, opts) {
40000
41073
  return { code: 422, body: { error: `gagal membuat worktree: ${e.message}` } };
40001
41074
  }
40002
41075
  const auditDoc = fromAudit ? await readAuditDoc(fromAudit) : null;
41076
+ const project_ = { id: project.id, name: project.name, desc: project.desc, stack: project.stack };
41077
+ const audit = auditDoc ? { id: fromAudit, path: auditDoc.path, content: auditDoc.content } : void 0;
41078
+ const plan = sessionPhasePlan(await getSetting(), "prd", agent2, { model: model2, effort: effort3 });
41079
+ const legacyPrompt = startPrdPrompt(project_, brief, `prd/${slug}`, audit) + resumeNote(reused);
40003
41080
  const s3 = createSession(project.id, wt, {
40004
41081
  id: id2,
40005
41082
  flow: "prd",
@@ -40009,12 +41086,14 @@ async function terminal_default(app2, opts) {
40009
41086
  agent: agent2,
40010
41087
  phaseFile: phaseFilePath(repoDir, id2),
40011
41088
  decisionFile: decisionFilePath(repoDir, id2),
40012
- prompt: startPrdPrompt(
40013
- { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
40014
- brief,
40015
- `prd/${slug}`,
40016
- auditDoc ? { id: fromAudit, path: auditDoc.path, content: auditDoc.content } : void 0
40017
- ) + resumeNote(reused)
41089
+ prompt: plan ? startPrdPrompt(project_, brief, `prd/${slug}`, audit, plan) + resumeNote(reused) : legacyPrompt,
41090
+ legacyPrompt,
41091
+ phaseAgents: plan ? buildPhaseAgents(plan, {
41092
+ flow: "prd",
41093
+ method: PROJECT_METHOD,
41094
+ context: prdContext(project_, brief, audit),
41095
+ prd: { slug }
41096
+ }) : []
40018
41097
  });
40019
41098
  return { code: 201, body: { id: s3.id } };
40020
41099
  }, (pane) => ({ code: 201, body: { id: pane.id } }));
@@ -40040,6 +41119,10 @@ async function terminal_default(app2, opts) {
40040
41119
  }
40041
41120
  const titleM = content.match(/^#\s+(.+)$/m);
40042
41121
  const title = titleM ? titleM[1].trim() : slug;
41122
+ const project_ = { id: project.id, name: project.name, desc: project.desc, stack: project.stack };
41123
+ const prd = { title, path: prdPath, content };
41124
+ const plan = sessionPhasePlan(await getSetting(), "breakdown", agent2, { model: model2, effort: effort3 });
41125
+ const legacyPrompt = startBreakdownPrompt(project_, prd, `breakdown/${slug}`) + resumeNote(reused);
40043
41126
  const s3 = createSession(project.id, wt, {
40044
41127
  id: id2,
40045
41128
  flow: "breakdown",
@@ -40049,11 +41132,14 @@ async function terminal_default(app2, opts) {
40049
41132
  agent: agent2,
40050
41133
  phaseFile: phaseFilePath(repoDir, id2),
40051
41134
  decisionFile: decisionFilePath(repoDir, id2),
40052
- prompt: startBreakdownPrompt(
40053
- { id: project.id, name: project.name, desc: project.desc, stack: project.stack },
40054
- { title, path: prdPath, content },
40055
- `breakdown/${slug}`
40056
- ) + resumeNote(reused)
41135
+ prompt: plan ? startBreakdownPrompt(project_, prd, `breakdown/${slug}`, plan) + resumeNote(reused) : legacyPrompt,
41136
+ legacyPrompt,
41137
+ phaseAgents: plan ? buildPhaseAgents(plan, {
41138
+ flow: "breakdown",
41139
+ method: PROJECT_METHOD,
41140
+ context: breakdownContext(project_, prd),
41141
+ prd: { slug, title }
41142
+ }) : []
40057
41143
  });
40058
41144
  return { code: 201, body: { id: s3.id } };
40059
41145
  }, (pane) => ({ code: 201, body: { id: pane.id } }));
@@ -40218,6 +41304,7 @@ async function terminal_default(app2, opts) {
40218
41304
  const guard = new WsMessageGuard({ perWindow: TERMINAL_WS_MESSAGES_PER_MINUTE });
40219
41305
  const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
40220
41306
  attach(id2, client);
41307
+ void refreshPhaseInvocations(id2);
40221
41308
  const watch = createPrincipalWatch({
40222
41309
  check: () => revalidateWsPrincipal(req, principal),
40223
41310
  onRevoked: () => socket2.close(1008, "session revoked")
@@ -40347,8 +41434,8 @@ async function terminalWorkspaceRoutes(app2) {
40347
41434
  // src/routes/vps.ts
40348
41435
  init_db();
40349
41436
  init_src();
40350
- import { readFileSync as readFileSync13, existsSync as existsSync16 } from "node:fs";
40351
- import { homedir as homedir6 } from "node:os";
41437
+ import { readFileSync as readFileSync14, existsSync as existsSync16 } from "node:fs";
41438
+ import { homedir as homedir7 } from "node:os";
40352
41439
 
40353
41440
  // src/vps/catalog/catalog.data.ts
40354
41441
  var RAW_SECTIONS = [{ "id": "ssh", "title": "SSH Hardening", "icon": "\u{1F511}", "count": 14 }, { "id": "firewall", "title": "Firewall & Network", "icon": "\u{1F525}", "count": 14 }, { "id": "aapanel", "title": "aaPanel Security", "icon": "\u{1F5A5}\uFE0F", "count": 14 }, { "id": "user", "title": "User & Permission Management", "icon": "\u{1F464}", "count": 14 }, { "id": "system", "title": "System Hardening (OS Level)", "icon": "\u2699\uFE0F", "count": 16 }, { "id": "webserver", "title": "Web Server Hardening", "icon": "\u{1F310}", "count": 14 }, { "id": "database", "title": "Database Security", "icon": "\u{1F5C4}\uFE0F", "count": 15 }, { "id": "ssl", "title": "SSL/TLS & Enkripsi", "icon": "\u{1F512}", "count": 13 }, { "id": "ids", "title": "Intrusion Detection & Prevention", "icon": "\u{1F6A8}", "count": 9 }, { "id": "logging", "title": "Logging & Monitoring", "icon": "\u{1F4CA}", "count": 13 }, { "id": "backup", "title": "Backup & Disaster Recovery", "icon": "\u{1F4BE}", "count": 12 }, { "id": "ddos", "title": "DDoS Protection", "icon": "\u{1F30A}", "count": 11 }, { "id": "kernel", "title": "Kernel Hardening", "icon": "\u{1F9E0}", "count": 12 }, { "id": "malware", "title": "File Integrity & Malware Detection", "icon": "\u{1F9A0}", "count": 7 }, { "id": "enterprise", "title": "Advanced Enterprise & Government Level", "icon": "\u{1F3E2}", "count": 31 }, { "id": "checklist", "title": "Checklist Rutin", "icon": "\u2705", "count": 23 }];
@@ -40545,14 +41632,14 @@ function resolveComponents(ids, profile) {
40545
41632
  }
40546
41633
 
40547
41634
  // src/services/vps-provision.ts
40548
- import { readFileSync as readFileSync10 } from "node:fs";
41635
+ import { readFileSync as readFileSync11 } from "node:fs";
40549
41636
 
40550
41637
  // src/services/vps-ssh.ts
40551
41638
  init_config2();
40552
41639
  import { spawn as spawn4 } from "node:child_process";
40553
- import { mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
41640
+ import { mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
40554
41641
  import { tmpdir as tmpdir4 } from "node:os";
40555
- import { dirname as dirname11, join as join23 } from "node:path";
41642
+ import { dirname as dirname11, join as join25 } from "node:path";
40556
41643
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
40557
41644
  function consoleArgv(t) {
40558
41645
  return [
@@ -40567,9 +41654,9 @@ function consoleArgv(t) {
40567
41654
  ];
40568
41655
  }
40569
41656
  function askpassScript() {
40570
- const dir2 = mkdtempSync(join23(tmpdir4(), "hanoman-askpass-"));
40571
- const path = join23(dir2, "askpass.sh");
40572
- writeFileSync8(path, `#!/bin/sh
41657
+ const dir2 = mkdtempSync(join25(tmpdir4(), "hanoman-askpass-"));
41658
+ const path = join25(dir2, "askpass.sh");
41659
+ writeFileSync9(path, `#!/bin/sh
40573
41660
  printf '%s' "$HANOMAN_SSH_PASSWORD"
40574
41661
  `, { mode: 448 });
40575
41662
  return path;
@@ -40628,17 +41715,17 @@ function sshExec(t, remoteCmd, opts = {}) {
40628
41715
 
40629
41716
  // src/services/vps-audit.ts
40630
41717
  init_db();
40631
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
40632
- import { join as join25 } from "node:path";
41718
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
41719
+ import { join as join27 } from "node:path";
40633
41720
  import { fileURLToPath as fileURLToPath3 } from "node:url";
40634
41721
 
40635
41722
  // src/runner/deps.ts
40636
41723
  import { existsSync as existsSync13 } from "node:fs";
40637
- import { dirname as dirname12, join as join24 } from "node:path";
41724
+ import { dirname as dirname12, join as join26 } from "node:path";
40638
41725
  function repoRootFrom(startDir) {
40639
41726
  let dir2 = startDir;
40640
41727
  for (let i = 0; i < 8; i++) {
40641
- if (existsSync13(join24(dir2, "pnpm-workspace.yaml"))) return dir2;
41728
+ if (existsSync13(join26(dir2, "pnpm-workspace.yaml"))) return dir2;
40642
41729
  const parent = dirname12(dir2);
40643
41730
  if (parent === dir2) break;
40644
41731
  dir2 = parent;
@@ -40751,7 +41838,7 @@ var packagedScript = (f) => fileURLToPath3(new URL(`../scripts/vps/${f}`, import
40751
41838
  var moduleDir = () => fileURLToPath3(new URL(".", import.meta.url));
40752
41839
  var scriptPath = (f) => {
40753
41840
  const packed = packagedScript(f);
40754
- return existsSync14(packed) ? packed : join25(repoRoot(moduleDir()), "server", "scripts", "vps", f);
41841
+ return existsSync14(packed) ? packed : join27(repoRoot(moduleDir()), "server", "scripts", "vps", f);
40755
41842
  };
40756
41843
  async function itemStatesOf(vpsId) {
40757
41844
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
@@ -40761,7 +41848,7 @@ async function runAudit(v) {
40761
41848
  const r = await sshExec(
40762
41849
  target(v),
40763
41850
  "sudo -n bash -s",
40764
- { stdin: readFileSync9(scriptPath("audit.sh"), "utf8"), timeoutMs: 6e4 }
41851
+ { stdin: readFileSync10(scriptPath("audit.sh"), "utf8"), timeoutMs: 6e4 }
40765
41852
  );
40766
41853
  const audit = parseAudit(r.out);
40767
41854
  if (r.code !== 0 || audit.length === 0) return { ok: false, out: r.out };
@@ -40841,7 +41928,7 @@ function parseProvisionSteps(out4) {
40841
41928
  return m ? [{ item: m[1], status: m[2], detail: (m[3] ?? "").trim() }] : [];
40842
41929
  });
40843
41930
  }
40844
- var script = () => readFileSync10(scriptPath("provision.sh"), "utf8");
41931
+ var script = () => readFileSync11(scriptPath("provision.sh"), "utf8");
40845
41932
  async function probeComponents(v) {
40846
41933
  const r = await sshExec(v, "env MODE=probe bash -s", { stdin: script(), timeoutMs: PROBE_TIMEOUT_MS });
40847
41934
  const components2 = parseComponents(r.out);
@@ -40868,7 +41955,7 @@ async function readSetupToken(v, home3 = "/var/lib/hanoman") {
40868
41955
  }
40869
41956
 
40870
41957
  // src/services/vps-remediate.ts
40871
- import { readFileSync as readFileSync11 } from "node:fs";
41958
+ import { readFileSync as readFileSync12 } from "node:fs";
40872
41959
  function parseSteps(out4) {
40873
41960
  return out4.split("\n").flatMap((line) => {
40874
41961
  const m = line.match(/^STEP (\S+) (would|ok|fail)(?: (.*))?$/);
@@ -40879,7 +41966,7 @@ async function remediate(v, items, dryRun) {
40879
41966
  const r = await sshExec(
40880
41967
  v,
40881
41968
  `sudo -n env ITEMS=${items.join(",")} SSH_PORT=${v.port} DRY_RUN=${dryRun ? "1" : ""} bash -s`,
40882
- { stdin: readFileSync11(scriptPath("remediate.sh"), "utf8"), timeoutMs: 3e5 }
41969
+ { stdin: readFileSync12(scriptPath("remediate.sh"), "utf8"), timeoutMs: 3e5 }
40883
41970
  );
40884
41971
  return { ok: r.code === 0, steps: parseSteps(r.out), out: r.out };
40885
41972
  }
@@ -40958,39 +42045,39 @@ async function buildChecklist(vpsId) {
40958
42045
  // src/services/vps-key.ts
40959
42046
  init_src2();
40960
42047
  init_config2();
40961
- import { execFileSync as execFileSync3 } from "node:child_process";
40962
- import { chmodSync as chmodSync7, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
40963
- import { homedir as homedir5 } from "node:os";
40964
- import { join as join26 } from "node:path";
42048
+ import { execFileSync as execFileSync4 } from "node:child_process";
42049
+ import { chmodSync as chmodSync7, copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync13, unlinkSync as unlinkSync2 } from "node:fs";
42050
+ import { homedir as homedir6 } from "node:os";
42051
+ import { join as join28 } from "node:path";
40965
42052
  var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? resolveDataDirs().sshKeys;
40966
42053
  var KEY_FILES = ["id_ed25519", "id_ed25519.pub"];
40967
42054
  function adoptLegacyKey(dir2) {
40968
42055
  if (effectiveStr("HANOMAN_SSH_KEY_DIR")) return;
40969
- const legacy = join26(homedir5(), ".hanoman");
42056
+ const legacy = join28(homedir6(), ".hanoman");
40970
42057
  if (legacy === dir2) return;
40971
- if (!KEY_FILES.every((f) => existsSync15(join26(legacy, f)))) return;
42058
+ if (!KEY_FILES.every((f) => existsSync15(join28(legacy, f)))) return;
40972
42059
  mkdirSync11(dir2, { recursive: true, mode: 448 });
40973
42060
  for (const f of KEY_FILES) {
40974
- copyFileSync(join26(legacy, f), join26(dir2, f));
40975
- chmodSync7(join26(dir2, f), 384);
42061
+ copyFileSync(join28(legacy, f), join28(dir2, f));
42062
+ chmodSync7(join28(dir2, f), 384);
40976
42063
  }
40977
- for (const f of KEY_FILES) unlinkSync2(join26(legacy, f));
42064
+ for (const f of KEY_FILES) unlinkSync2(join28(legacy, f));
40978
42065
  console.log(`vps: key SSH dipindah dari ${legacy} ke ${dir2} (SPEC-846 \u2014 satu batas backup)`);
40979
42066
  }
40980
42067
  function ensureHanomanKey() {
40981
42068
  const dir2 = keyDir();
40982
- const privPath = join26(dir2, KEY_FILES[0]);
42069
+ const privPath = join28(dir2, KEY_FILES[0]);
40983
42070
  const pubPath = `${privPath}.pub`;
40984
42071
  if (!existsSync15(privPath)) adoptLegacyKey(dir2);
40985
42072
  if (!existsSync15(privPath)) {
40986
42073
  mkdirSync11(dir2, { recursive: true, mode: 448 });
40987
- execFileSync3(
42074
+ execFileSync4(
40988
42075
  "ssh-keygen",
40989
42076
  ["-t", "ed25519", "-N", "", "-C", "hanoman", "-f", privPath],
40990
42077
  { stdio: ["ignore", "ignore", "pipe"] }
40991
42078
  );
40992
42079
  }
40993
- return { privPath, pubPath, pub: readFileSync12(pubPath, "utf8").trim() };
42080
+ return { privPath, pubPath, pub: readFileSync13(pubPath, "utf8").trim() };
40994
42081
  }
40995
42082
 
40996
42083
  // src/services/vps-bootstrap.ts
@@ -41238,7 +42325,7 @@ async function vps_default(app2) {
41238
42325
  const r = await sshExec(
41239
42326
  v,
41240
42327
  `sudo -n env SSH_PORT=${v.port} SSH_USER=${v.user} bash -s`,
41241
- { stdin: readFileSync13(scriptPath("harden.sh"), "utf8"), timeoutMs: 3e5 }
42328
+ { stdin: readFileSync14(scriptPath("harden.sh"), "utf8"), timeoutMs: 3e5 }
41242
42329
  );
41243
42330
  if (r.code !== 0) return reply.code(502).send({ error: "harden gagal lewat ssh", transcript: r.out });
41244
42331
  const verify = await sshExec(v, "true", { timeoutMs: 3e4 });
@@ -41263,7 +42350,7 @@ async function vps_default(app2) {
41263
42350
  const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
41264
42351
  if (!v) return reply.code(404).send({ error: "not found" });
41265
42352
  if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
41266
- const s2 = await createOperatorSession(`vps-console:${v.id}`, homedir6(), { id: `vpsc-${v.id}`, command: consoleArgv(v) });
42353
+ const s2 = await createOperatorSession(`vps-console:${v.id}`, homedir7(), { id: `vpsc-${v.id}`, command: consoleArgv(v) });
41267
42354
  return reply.code(201).send({ id: s2.id });
41268
42355
  });
41269
42356
  app2.post("/vps/:id/session", async (req, reply) => {
@@ -41272,7 +42359,7 @@ async function vps_default(app2) {
41272
42359
  if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
41273
42360
  const checks = v.audit ?? [];
41274
42361
  const { model, effort: effort2 } = await sessionModel();
41275
- const s2 = await createAgentSession(`vps:${v.id}`, homedir6(), {
42362
+ const s2 = await createAgentSession(`vps:${v.id}`, homedir7(), {
41276
42363
  model,
41277
42364
  effort: effort2,
41278
42365
  prompt: [
@@ -41289,10 +42376,10 @@ async function vps_default(app2) {
41289
42376
 
41290
42377
  // src/services/limits.ts
41291
42378
  init_config2();
41292
- import { execFileSync as execFileSync4 } from "node:child_process";
41293
- import { readFileSync as readFileSync14 } from "node:fs";
41294
- import { homedir as homedir7 } from "node:os";
41295
- import { join as join27 } from "node:path";
42379
+ import { execFileSync as execFileSync5 } from "node:child_process";
42380
+ import { readFileSync as readFileSync15 } from "node:fs";
42381
+ import { homedir as homedir8 } from "node:os";
42382
+ import { join as join29 } from "node:path";
41296
42383
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
41297
42384
  var TTL_MS2 = 3e4;
41298
42385
  var lastOk = null;
@@ -41305,12 +42392,12 @@ var LABELS = {
41305
42392
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
41306
42393
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
41307
42394
  function credsFile() {
41308
- return join27(effectiveStr("CLAUDE_CONFIG_DIR") ?? join27(homedir7(), ".claude"), ".credentials.json");
42395
+ return join29(effectiveStr("CLAUDE_CONFIG_DIR") ?? join29(homedir8(), ".claude"), ".credentials.json");
41309
42396
  }
41310
42397
  function readAccessToken() {
41311
42398
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
41312
42399
  try {
41313
- const blob = execFileSync4(
42400
+ const blob = execFileSync5(
41314
42401
  "security",
41315
42402
  ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
41316
42403
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -41321,7 +42408,7 @@ function readAccessToken() {
41321
42408
  }
41322
42409
  }
41323
42410
  try {
41324
- return JSON.parse(readFileSync14(credsFile(), "utf8"))?.claudeAiOauth?.accessToken ?? null;
42411
+ return JSON.parse(readFileSync15(credsFile(), "utf8"))?.claudeAiOauth?.accessToken ?? null;
41325
42412
  } catch {
41326
42413
  return null;
41327
42414
  }
@@ -41377,16 +42464,16 @@ async function getLimits() {
41377
42464
  }
41378
42465
 
41379
42466
  // src/services/codex-limits.ts
41380
- import { open as open3, readdir as readdir6, stat as stat3 } from "node:fs/promises";
41381
- import { homedir as homedir8 } from "node:os";
41382
- import { join as join28 } from "node:path";
42467
+ import { open as open4, readdir as readdir7, stat as stat3 } from "node:fs/promises";
42468
+ import { homedir as homedir9 } from "node:os";
42469
+ import { join as join30 } from "node:path";
41383
42470
  var TTL_MS3 = 3e4;
41384
42471
  var STALE_AFTER_MS = 12 * 36e5;
41385
42472
  var TAIL_BYTES = 512 * 1024;
41386
42473
  var MAX_FILES = 8;
41387
42474
  var cache2 = null;
41388
42475
  var freshUntil2 = 0;
41389
- var codexSessionsDir = () => join28(process.env.CODEX_HOME ?? join28(homedir8(), ".codex"), "sessions");
42476
+ var codexSessionsDir = () => join30(process.env.CODEX_HOME ?? join30(homedir9(), ".codex"), "sessions");
41390
42477
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
41391
42478
  function windowLabel(minutes) {
41392
42479
  if (minutes === 300) return "Sesi 5 jam";
@@ -41414,12 +42501,12 @@ function toWindow(slot, raw, reached) {
41414
42501
  async function recentRollouts(dir2) {
41415
42502
  let entries3;
41416
42503
  try {
41417
- entries3 = (await readdir6(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
42504
+ entries3 = (await readdir7(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
41418
42505
  } catch {
41419
42506
  return [];
41420
42507
  }
41421
42508
  const stamped = await Promise.all(entries3.map(async (rel) => {
41422
- const full = join28(dir2, rel);
42509
+ const full = join30(dir2, rel);
41423
42510
  try {
41424
42511
  return { full, mtime: (await stat3(full)).mtimeMs };
41425
42512
  } catch {
@@ -41429,7 +42516,7 @@ async function recentRollouts(dir2) {
41429
42516
  return stamped.filter((x) => x !== null).sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((x) => x.full);
41430
42517
  }
41431
42518
  async function readTail(file) {
41432
- const fh = await open3(file, "r");
42519
+ const fh = await open4(file, "r");
41433
42520
  try {
41434
42521
  const { size } = await fh.stat();
41435
42522
  const len = Math.min(size, TAIL_BYTES);
@@ -41505,8 +42592,8 @@ async function codex(app2) {
41505
42592
  // src/services/model-catalog.ts
41506
42593
  init_src2();
41507
42594
  init_src();
41508
- import { readFile as readFile5, writeFile as writeFile5, rename as rename4, mkdir as mkdir6 } from "node:fs/promises";
41509
- import { dirname as dirname13, join as join30 } from "node:path";
42595
+ import { readFile as readFile5, writeFile as writeFile5, rename as rename4, mkdir as mkdir7 } from "node:fs/promises";
42596
+ import { dirname as dirname13, join as join32 } from "node:path";
41510
42597
 
41511
42598
  // src/services/model-catalog-parser.ts
41512
42599
  init_zod();
@@ -41581,9 +42668,9 @@ function parseCachedCatalog(raw) {
41581
42668
  init_config2();
41582
42669
  init_session_sandbox();
41583
42670
  import { spawn as spawn5 } from "node:child_process";
41584
- import { mkdtemp as mkdtemp2, rm as rm6 } from "node:fs/promises";
42671
+ import { mkdtemp as mkdtemp2, rm as rm7 } from "node:fs/promises";
41585
42672
  import { tmpdir as tmpdir5 } from "node:os";
41586
- import { join as join29 } from "node:path";
42673
+ import { join as join31 } from "node:path";
41587
42674
  var MAX_BYTES = 4 * 1024 * 1024;
41588
42675
  var TIMEOUT_MS = 2e4;
41589
42676
  var quote2 = (s2) => "'" + s2.replaceAll("'", "'\\''") + "'";
@@ -41679,7 +42766,7 @@ function runCatalogCommand(command, agent, cwd, env, timeoutMs = TIMEOUT_MS) {
41679
42766
  });
41680
42767
  }
41681
42768
  async function probeModelCatalog(agent) {
41682
- const cwd = await mkdtemp2(join29(tmpdir5(), "hanoman-models-"));
42769
+ const cwd = await mkdtemp2(join31(tmpdir5(), "hanoman-models-"));
41683
42770
  const env = { ...process.env };
41684
42771
  for (const key of ["HANOMAN_CLAUDE_BIN", "HANOMAN_CODEX_BIN", "HANOMAN_PODMAN_BIN"]) {
41685
42772
  const value = effectiveStr(key);
@@ -41692,7 +42779,7 @@ async function probeModelCatalog(agent) {
41692
42779
  const raw = await runCatalogCommand(catalogCommand(agent, cwd, env), agent, cwd, env);
41693
42780
  return agent === "claude" ? parseClaudeModels(raw) : parseCodexModels(raw);
41694
42781
  } finally {
41695
- await rm6(cwd, { recursive: true, force: true });
42782
+ await rm7(cwd, { recursive: true, force: true });
41696
42783
  }
41697
42784
  }
41698
42785
 
@@ -41756,7 +42843,7 @@ function createModelCatalogService(deps) {
41756
42843
  }
41757
42844
  return { snapshot: () => state, refresh };
41758
42845
  }
41759
- var cacheFile = () => join30(resolveHome(), "model-catalog.json");
42846
+ var cacheFile = () => join32(resolveHome(), "model-catalog.json");
41760
42847
  var modelCatalogService = createModelCatalogService({
41761
42848
  probe: probeModelCatalog,
41762
42849
  read: async () => {
@@ -41766,7 +42853,7 @@ var modelCatalogService = createModelCatalogService({
41766
42853
  },
41767
42854
  write: async (catalog) => {
41768
42855
  const file = cacheFile();
41769
- await mkdir6(dirname13(file), { recursive: true });
42856
+ await mkdir7(dirname13(file), { recursive: true });
41770
42857
  const temp = file + "." + process.pid + ".tmp";
41771
42858
  await writeFile5(temp, JSON.stringify(catalog), { mode: 384 });
41772
42859
  await rename4(temp, file);
@@ -41799,8 +42886,8 @@ init_src();
41799
42886
  // src/services/update.ts
41800
42887
  init_src();
41801
42888
  init_config2();
41802
- import { readFileSync as readFileSync15 } from "node:fs";
41803
- import { resolve as resolve19, dirname as dirname14 } from "node:path";
42889
+ import { readFileSync as readFileSync16 } from "node:fs";
42890
+ import { resolve as resolve20, dirname as dirname14 } from "node:path";
41804
42891
  import { fileURLToPath as fileURLToPath4 } from "node:url";
41805
42892
  var UPDATE_COMMAND = "npm i -g hanoman@latest --prefer-online";
41806
42893
  function composeUpdate(x) {
@@ -41826,9 +42913,9 @@ var lastLatest = null;
41826
42913
  var lastStatus = "unavailable";
41827
42914
  function runningVersion() {
41828
42915
  const here = dirname14(fileURLToPath4(import.meta.url));
41829
- for (const p3 of [resolve19(here, "build-info.json"), resolve19(here, "../package.json")]) {
42916
+ for (const p3 of [resolve20(here, "build-info.json"), resolve20(here, "../package.json")]) {
41830
42917
  try {
41831
- const v = JSON.parse(readFileSync15(p3, "utf8")).version;
42918
+ const v = JSON.parse(readFileSync16(p3, "utf8")).version;
41832
42919
  if (typeof v === "string" && v) return v;
41833
42920
  } catch {
41834
42921
  }
@@ -43443,468 +44530,6 @@ async function session_history_default(app2) {
43443
44530
  init_src();
43444
44531
  init_session_event_token();
43445
44532
  init_pty();
43446
-
43447
- // src/services/agent-invocations.ts
43448
- init_src();
43449
- init_db();
43450
- import { createHash as createHash9, randomUUID as randomUUID10 } from "node:crypto";
43451
- import { execFileSync as execFileSync5 } from "node:child_process";
43452
- import { homedir as homedir9 } from "node:os";
43453
- import { readFileSync as readFileSync16, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
43454
- import { isAbsolute as isAbsolute7, relative as relative3, resolve as resolve20 } from "node:path";
43455
-
43456
- // src/services/session-event-relay.ts
43457
- init_session_event_token();
43458
- init_session_event_spool();
43459
- import { constants as constants3 } from "node:fs";
43460
- import { mkdir as mkdir7, open as open4, readdir as readdir7, rm as rm7 } from "node:fs/promises";
43461
- import { join as join31 } from "node:path";
43462
- var MAX_EVENT_BYTES = 1e6;
43463
- var MAX_FILES_PER_DRAIN = 1e3;
43464
- var SESSION_ID_RE = /^[a-z0-9_-]+$/;
43465
- var observations = /* @__PURE__ */ new Map();
43466
- function sessionEventRelayStatus(root = sessionEventSpoolRoot()) {
43467
- return { ...observations.get(root) ?? {
43468
- state: "unobserved",
43469
- checkedAt: null,
43470
- lastDeliveryAt: null,
43471
- lastIssueAt: null,
43472
- retryPending: 0,
43473
- retryAttempts: 0,
43474
- droppedEvents: 0
43475
- } };
43476
- }
43477
- async function drainSessionEventSpool(app2, root = sessionEventSpoolRoot()) {
43478
- const previous = sessionEventRelayStatus(root);
43479
- const current = { retries: 0, dropped: 0 };
43480
- let delivered = 0;
43481
- let failed = false;
43482
- try {
43483
- delivered = await drainSpool(app2, root, current);
43484
- return delivered;
43485
- } catch (error) {
43486
- failed = true;
43487
- throw error;
43488
- } finally {
43489
- const now = (/* @__PURE__ */ new Date()).toISOString();
43490
- observations.set(root, {
43491
- state: failed || current.retries > 0 ? "degraded" : "ready",
43492
- checkedAt: now,
43493
- lastDeliveryAt: delivered ? now : previous.lastDeliveryAt,
43494
- lastIssueAt: failed || current.retries || current.dropped ? now : previous.lastIssueAt,
43495
- retryPending: current.retries,
43496
- retryAttempts: previous.retryAttempts + current.retries,
43497
- droppedEvents: previous.droppedEvents + current.dropped
43498
- });
43499
- }
43500
- }
43501
- async function drainSpool(app2, root, observation) {
43502
- await mkdir7(root, { recursive: true, mode: 448 });
43503
- let delivered = 0;
43504
- let examined = 0;
43505
- let readBuffer;
43506
- for (const session of await readdir7(root, { withFileTypes: true })) {
43507
- if (!session.isDirectory() || !SESSION_ID_RE.test(session.name)) continue;
43508
- const dir2 = join31(root, session.name);
43509
- let entries3;
43510
- try {
43511
- entries3 = await readdir7(dir2, { withFileTypes: true });
43512
- } catch {
43513
- continue;
43514
- }
43515
- for (const entry of entries3) {
43516
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
43517
- if (++examined > MAX_FILES_PER_DRAIN) return delivered;
43518
- const path = join31(dir2, entry.name);
43519
- let payload;
43520
- let handle;
43521
- try {
43522
- handle = await open4(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
43523
- const stat4 = await handle.stat();
43524
- if (!stat4.isFile() || stat4.size > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
43525
- readBuffer ??= Buffer.allocUnsafe(MAX_EVENT_BYTES + 1);
43526
- const { bytesRead } = await handle.read(readBuffer, 0, readBuffer.length, 0);
43527
- if (bytesRead > MAX_EVENT_BYTES) throw new Error("payload terlalu besar");
43528
- const parsed = JSON.parse(readBuffer.subarray(0, bytesRead).toString("utf8"));
43529
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
43530
- throw new Error("payload event bukan object");
43531
- }
43532
- payload = parsed;
43533
- } catch {
43534
- observation.dropped++;
43535
- await rm7(path, { force: true }).catch(() => {
43536
- });
43537
- continue;
43538
- } finally {
43539
- await handle?.close().catch(() => {
43540
- });
43541
- }
43542
- try {
43543
- const response = await app2.inject({
43544
- method: "POST",
43545
- url: "/api/session-events",
43546
- headers: {
43547
- authorization: `Bearer ${sessionEventToken(session.name)}`,
43548
- "x-hanoman-session": session.name
43549
- },
43550
- payload
43551
- });
43552
- if (response.statusCode === 429 || response.statusCode >= 500) {
43553
- observation.retries++;
43554
- continue;
43555
- }
43556
- await rm7(path, { force: true }).catch(() => {
43557
- });
43558
- if (response.statusCode >= 200 && response.statusCode < 300) delivered++;
43559
- else observation.dropped++;
43560
- } catch {
43561
- observation.retries++;
43562
- }
43563
- }
43564
- }
43565
- return delivered;
43566
- }
43567
- function startSessionEventRelay(app2, options2 = {}) {
43568
- const root = options2.root ?? sessionEventSpoolRoot();
43569
- let running = false;
43570
- const tick6 = async () => {
43571
- if (running) return;
43572
- running = true;
43573
- try {
43574
- await drainSessionEventSpool({ inject: async (request) => app2.inject(request) }, root);
43575
- } catch (error) {
43576
- console.error("session event relay gagal:", error);
43577
- } finally {
43578
- running = false;
43579
- }
43580
- };
43581
- const timer9 = setInterval(() => {
43582
- void tick6();
43583
- }, options2.intervalMs ?? 250);
43584
- timer9.unref();
43585
- app2.addHook("onClose", async () => {
43586
- clearInterval(timer9);
43587
- });
43588
- void tick6();
43589
- }
43590
-
43591
- // src/services/agent-invocations.ts
43592
- var MAX_EXCERPT_BYTES = 4096;
43593
- var MAX_TRANSCRIPT_BYTES2 = 10 * 1024 * 1024;
43594
- var ANSI2 = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
43595
- var snapshotHashes = /* @__PURE__ */ new Map();
43596
- var keyOf = (x) => `${x.sessionId}\0${x.runtimeInvocationId}`;
43597
- var hash2 = (value) => createHash9("sha256").update(value).digest("hex");
43598
- var defaultGitStatus = (cwd) => {
43599
- try {
43600
- return execFileSync5("git", ["-C", cwd, "status", "--porcelain=v1", "-z"], {
43601
- encoding: "utf8",
43602
- stdio: ["ignore", "pipe", "ignore"]
43603
- });
43604
- } catch {
43605
- return null;
43606
- }
43607
- };
43608
- function snapshot2(cwd, run4 = defaultGitStatus) {
43609
- try {
43610
- const value = run4(cwd);
43611
- return value === null ? null : hash2(value);
43612
- } catch {
43613
- return null;
43614
- }
43615
- }
43616
- var stripAnsi = (value) => value.replace(ANSI2, "");
43617
- var utf8Prefix = (value, maxBytes) => {
43618
- let out4 = "", bytes = 0;
43619
- for (const char of value) {
43620
- const next = Buffer.byteLength(char, "utf8");
43621
- if (bytes + next > maxBytes) break;
43622
- out4 += char;
43623
- bytes += next;
43624
- }
43625
- return out4;
43626
- };
43627
- var transcriptRoots = () => [
43628
- resolve20(process.env.CLAUDE_CONFIG_DIR ?? `${homedir9()}/.claude`),
43629
- resolve20(process.env.CODEX_HOME ?? `${homedir9()}/.codex`)
43630
- ];
43631
- var inside = (path, root) => {
43632
- const rel = relative3(root, path);
43633
- return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
43634
- };
43635
- var EMPTY_USAGE = { inputTokens: null, outputTokens: null, cachedTokens: null };
43636
- var nonnegativeInt = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
43637
- function usageFromText(text) {
43638
- const found = [];
43639
- const inspect = (value) => {
43640
- if (!value || typeof value !== "object") return;
43641
- const record2 = value;
43642
- const usage2 = record2.usage;
43643
- if (usage2 && typeof usage2 === "object") {
43644
- const u = usage2;
43645
- found.push([
43646
- nonnegativeInt(u.input_tokens ?? u.inputTokens) ?? -1,
43647
- nonnegativeInt(u.output_tokens ?? u.outputTokens) ?? -1,
43648
- nonnegativeInt(u.cached_tokens ?? u.cachedTokens ?? u.cache_read_input_tokens ?? u.cacheReadInputTokens) ?? -1
43649
- ]);
43650
- }
43651
- };
43652
- for (const line of text.split("\n")) {
43653
- if (!line.trim()) continue;
43654
- try {
43655
- inspect(JSON.parse(line));
43656
- } catch {
43657
- }
43658
- }
43659
- if (found.length === 0) return EMPTY_USAGE;
43660
- const max = (index) => {
43661
- const values = found.map((entry) => entry[index]).filter((n2) => n2 >= 0);
43662
- return values.length ? Math.max(...values) : null;
43663
- };
43664
- return { inputTokens: max(0), outputTokens: max(1), cachedTokens: max(2) };
43665
- }
43666
- function transcriptUsage(path, roots = transcriptRoots()) {
43667
- if (!path) return EMPTY_USAGE;
43668
- try {
43669
- const real2 = realpathSync5(path);
43670
- const safeRoots = roots.map((root) => realpathSync5(root));
43671
- if (!safeRoots.some((root) => inside(real2, root))) return EMPTY_USAGE;
43672
- const info = statSync4(real2);
43673
- if (!info.isFile() || info.size > MAX_TRANSCRIPT_BYTES2) return EMPTY_USAGE;
43674
- return usageFromText(readFileSync16(real2, "utf8"));
43675
- } catch {
43676
- return EMPTY_USAGE;
43677
- }
43678
- }
43679
- async function startAgentInvocation(input, io = {}) {
43680
- const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
43681
- const existing = await prisma.agentInvocation.findUnique({
43682
- where: { sessionId_runtimeInvocationId: {
43683
- sessionId: input.sessionId,
43684
- runtimeInvocationId: input.runtimeInvocationId
43685
- } }
43686
- });
43687
- const row = await prisma.agentInvocation.upsert({
43688
- where: { sessionId_runtimeInvocationId: {
43689
- sessionId: input.sessionId,
43690
- runtimeInvocationId: input.runtimeInvocationId
43691
- } },
43692
- update: {},
43693
- create: {
43694
- id: randomUUID10(),
43695
- sessionId: input.sessionId,
43696
- projectId: input.projectId,
43697
- specId: input.specId ?? null,
43698
- runtime: input.runtime,
43699
- runtimeInvocationId: input.runtimeInvocationId,
43700
- customAgentId: input.customAgentId ?? null,
43701
- agentName: input.agentName,
43702
- model: input.model ?? null,
43703
- definitionHash: input.definitionHash ?? null,
43704
- status: "running",
43705
- startedAt
43706
- }
43707
- });
43708
- const before2 = snapshot2(input.cwd, io.gitStatus);
43709
- if (before2 !== null) snapshotHashes.set(keyOf(input), before2);
43710
- return { row, duplicate: existing !== null };
43711
- }
43712
- async function stopAgentInvocation(input, io = {}) {
43713
- const unique = { sessionId_runtimeInvocationId: {
43714
- sessionId: input.sessionId,
43715
- runtimeInvocationId: input.runtimeInvocationId
43716
- } };
43717
- const existing = await prisma.agentInvocation.findUnique({ where: unique });
43718
- if (existing?.endedAt) return { row: existing, duplicate: true };
43719
- const endedAt = input.endedAt ?? /* @__PURE__ */ new Date();
43720
- const startedAt = existing?.startedAt ?? endedAt;
43721
- const cleanResult = input.result === void 0 ? null : stripAnsi(input.result);
43722
- const usage2 = transcriptUsage(input.transcriptPath, io.transcriptRoots);
43723
- const before2 = snapshotHashes.get(keyOf(input));
43724
- const after = snapshot2(input.cwd, io.gitStatus);
43725
- snapshotHashes.delete(keyOf(input));
43726
- const evidence = {
43727
- // Stop tanpa start lazim setelah restart server di tengah invocation. Waktu start dan status
43728
- // runtime sudah hilang; simpan baris sintetis yang dapat diaudit tanpa mengarang durasi 0 ms.
43729
- status: existing ? input.status ?? "completed" : "completed",
43730
- endedAt,
43731
- durationMs: existing ? Math.max(0, endedAt.getTime() - startedAt.getTime()) : null,
43732
- ...usage2,
43733
- resultExcerpt: cleanResult === null ? null : utf8Prefix(cleanResult, MAX_EXCERPT_BYTES),
43734
- resultHash: cleanResult === null ? null : hash2(cleanResult),
43735
- workspaceChanged: before2 !== void 0 && after !== null && before2 !== after
43736
- };
43737
- if (existing) {
43738
- const row2 = await prisma.agentInvocation.update({ where: { id: existing.id }, data: evidence });
43739
- return { row: row2, duplicate: false };
43740
- }
43741
- const row = await prisma.agentInvocation.create({
43742
- data: {
43743
- id: randomUUID10(),
43744
- sessionId: input.sessionId,
43745
- projectId: input.projectId,
43746
- specId: input.specId ?? null,
43747
- runtime: input.runtime,
43748
- runtimeInvocationId: input.runtimeInvocationId,
43749
- customAgentId: input.customAgentId ?? null,
43750
- agentName: input.agentName,
43751
- model: input.model ?? null,
43752
- definitionHash: input.definitionHash ?? null,
43753
- startedAt,
43754
- ...evidence
43755
- }
43756
- });
43757
- return { row, duplicate: false };
43758
- }
43759
- async function reconcileAgentInvocations(liveSessionIds) {
43760
- const live = new Set(liveSessionIds);
43761
- const open5 = await prisma.agentInvocation.findMany({ where: { status: "running" } });
43762
- let changed = 0;
43763
- const endedAt = /* @__PURE__ */ new Date();
43764
- for (const row of open5) {
43765
- if (live.has(row.sessionId)) continue;
43766
- await prisma.agentInvocation.update({
43767
- where: { id: row.id },
43768
- data: {
43769
- status: "abandoned",
43770
- endedAt,
43771
- durationMs: Math.max(0, endedAt.getTime() - row.startedAt.getTime())
43772
- }
43773
- });
43774
- changed++;
43775
- }
43776
- return changed;
43777
- }
43778
- var agentInvocationView = (row) => ({
43779
- id: row.id,
43780
- sessionId: row.sessionId,
43781
- projectId: row.projectId,
43782
- specId: row.specId,
43783
- runtime: row.runtime === "codex" ? "codex" : "claude",
43784
- customAgentId: row.customAgentId,
43785
- agentName: row.agentName,
43786
- model: row.model,
43787
- definitionHash: row.definitionHash,
43788
- status: row.status,
43789
- startedAt: row.startedAt.toISOString(),
43790
- endedAt: row.endedAt?.toISOString() ?? null,
43791
- durationMs: row.durationMs,
43792
- inputTokens: row.inputTokens,
43793
- outputTokens: row.outputTokens,
43794
- cachedTokens: row.cachedTokens,
43795
- resultExcerpt: row.resultExcerpt,
43796
- resultHash: row.resultHash,
43797
- workspaceChanged: row.workspaceChanged,
43798
- disposition: AGENT_DISPOSITIONS.includes(row.disposition) ? row.disposition : "pending",
43799
- dispositionNote: row.dispositionNote,
43800
- evaluatedAt: row.evaluatedAt?.toISOString() ?? null,
43801
- reworkRequired: row.reworkRequired
43802
- });
43803
- var median = (values) => {
43804
- if (values.length === 0) return null;
43805
- values.sort((a, b) => a - b);
43806
- const middle = Math.floor(values.length / 2);
43807
- return values.length % 2 === 1 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
43808
- };
43809
- var availableSum = (values) => {
43810
- const known = values.filter((value) => value !== null);
43811
- return known.length ? known.reduce((sum, value) => sum + value, 0) : null;
43812
- };
43813
- async function agentMetrics(query2) {
43814
- const where = {
43815
- ...query2.projectId ? { projectId: query2.projectId } : {},
43816
- ...query2.from || query2.to ? { startedAt: {
43817
- ...query2.from ? { gte: query2.from } : {},
43818
- ...query2.to ? { lte: query2.to } : {}
43819
- } } : {}
43820
- };
43821
- const rows = await prisma.agentInvocation.findMany({ where, orderBy: { startedAt: "desc" } });
43822
- const groups = /* @__PURE__ */ new Map();
43823
- const variantGroups = /* @__PURE__ */ new Map();
43824
- const add = (map, key, row) => {
43825
- const group2 = map.get(key);
43826
- if (group2) group2.push(row);
43827
- else map.set(key, [row]);
43828
- };
43829
- for (const row of rows) {
43830
- add(groups, row.agentName, row);
43831
- add(variantGroups, JSON.stringify([row.agentName, row.runtime, row.model, row.definitionHash]), row);
43832
- }
43833
- const summarize = (agentName, invocations) => {
43834
- const dispositions = { pending: 0, accepted: 0, partial: 0, rejected: 0, falsePositive: 0 };
43835
- for (const row of invocations) {
43836
- if (row.disposition === "accepted") dispositions.accepted++;
43837
- else if (row.disposition === "partial") dispositions.partial++;
43838
- else if (row.disposition === "rejected") dispositions.rejected++;
43839
- else if (row.disposition === "false-positive") dispositions.falsePositive++;
43840
- else dispositions.pending++;
43841
- }
43842
- const evaluated = dispositions.accepted + dispositions.partial + dispositions.rejected + dispositions.falsePositive;
43843
- return {
43844
- agentName,
43845
- invocationCount: invocations.length,
43846
- medianDurationMs: median(invocations.flatMap((row) => row.durationMs === null ? [] : [row.durationMs])),
43847
- inputTokens: availableSum(invocations.map((row) => row.inputTokens)),
43848
- outputTokens: availableSum(invocations.map((row) => row.outputTokens)),
43849
- cachedTokens: availableSum(invocations.map((row) => row.cachedTokens)),
43850
- dispositions,
43851
- operationalPrecision: evaluated ? (dispositions.accepted + dispositions.partial) / evaluated : null,
43852
- workspaceChanged: invocations.some((row) => row.workspaceChanged),
43853
- evaluatedCount: evaluated,
43854
- rework: {
43855
- required: invocations.filter((row) => row.reworkRequired === true).length,
43856
- notRequired: invocations.filter((row) => row.reworkRequired === false).length,
43857
- unknown: invocations.filter((row) => row.reworkRequired === null).length
43858
- }
43859
- };
43860
- };
43861
- const agents = [...groups.entries()].map(([name2, group2]) => summarize(name2, group2)).sort((a, b) => a.agentName.localeCompare(b.agentName));
43862
- const variants = [...variantGroups.values()].map((group2) => ({
43863
- ...summarize(group2[0].agentName, group2),
43864
- runtime: group2[0].runtime === "codex" ? "codex" : "claude",
43865
- model: group2[0].model,
43866
- definitionHash: group2[0].definitionHash
43867
- })).sort((a, b) => JSON.stringify([a.agentName, a.runtime, a.model, a.definitionHash]).localeCompare(JSON.stringify([b.agentName, b.runtime, b.model, b.definitionHash])));
43868
- const lastEventAt = rows.reduce((latest, row) => Math.max(latest, row.startedAt.getTime(), row.endedAt?.getTime() ?? 0), 0);
43869
- const sampleRows = /* @__PURE__ */ new Map();
43870
- for (const group2 of groups.values()) {
43871
- for (const category of [
43872
- (row) => row.disposition === "pending",
43873
- (row) => row.disposition !== "pending",
43874
- (row) => row.reworkRequired === true
43875
- ]) {
43876
- for (const row of group2.filter(category).slice(0, 2)) sampleRows.set(row.id, row);
43877
- }
43878
- }
43879
- return {
43880
- agents,
43881
- variants,
43882
- recent: rows.slice(0, 100).map(agentInvocationView),
43883
- samples: [...sampleRows.values()].map(agentInvocationView),
43884
- telemetry: {
43885
- state: rows.length ? "observed" : "unobserved",
43886
- lastEventAt: rows.length ? new Date(lastEventAt).toISOString() : null,
43887
- incompleteCount: rows.filter((row) => !row.endedAt || row.resultHash === null).length,
43888
- relay: sessionEventRelayStatus()
43889
- }
43890
- };
43891
- }
43892
- async function updateAgentInvocationDisposition(id2, disposition, note, reworkRequired) {
43893
- const exists = await prisma.agentInvocation.findUnique({ where: { id: id2 } });
43894
- if (!exists) return null;
43895
- const row = await prisma.agentInvocation.update({
43896
- where: { id: id2 },
43897
- data: {
43898
- disposition,
43899
- dispositionNote: note?.trim() || null,
43900
- evaluatedAt: /* @__PURE__ */ new Date(),
43901
- ...reworkRequired !== void 0 ? { reworkRequired } : {}
43902
- }
43903
- });
43904
- return agentInvocationView(row);
43905
- }
43906
-
43907
- // src/routes/session-events.ts
43908
44533
  var bearer = (h) => /^Bearer (.+)$/.exec(h ?? "")?.[1] ?? "";
43909
44534
  var recordOf = (value) => value && typeof value === "object" ? value : null;
43910
44535
  var boundedString = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max ? value : void 0;
@@ -43936,6 +44561,8 @@ async function session_events_default(app2) {
43936
44561
  agentName: meta.name,
43937
44562
  model: meta.model,
43938
44563
  definitionHash: meta.definitionHash,
44564
+ ...meta.phase ? { phase: meta.phase } : {},
44565
+ ...meta.effort ? { effort: meta.effort } : {},
43939
44566
  cwd: s2.cwd
43940
44567
  };
43941
44568
  const outcome = lifecycle === "SubagentStart" ? await startAgentInvocation(identity) : await stopAgentInvocation({
@@ -43945,8 +44572,17 @@ async function session_events_default(app2) {
43945
44572
  transcriptPath: boundedString(
43946
44573
  body.agent_transcript_path ?? body.transcript_path,
43947
44574
  4096
43948
- )
44575
+ ),
44576
+ // ADR-0164 · effort yang BENAR-BENAR dipakai runtime (claude: `effort.level` di
44577
+ // SubagentStop) — HANYA untuk agen fase. Review Task 9: custom agent bisa membawa
44578
+ // `effort` roster sendiri (mis. katalog method), dan payload runtime bukan miliknya
44579
+ // untuk ditimpa — pertahankan perilaku effort roster custom agent apa adanya.
44580
+ // M-4 · payload hook tak tepercaya: `boundedString` menolak nilai bukan-string, kosong,
44581
+ // atau raksasa — melebihi batas berarti diabaikan, effort roster dipertahankan APA
44582
+ // ADANYA (bukan ditimpa potongan yang menyesatkan).
44583
+ ...meta.phase && boundedString(recordOf(body.effort)?.level, 64) ? { effort: boundedString(recordOf(body.effort)?.level, 64) } : {}
43949
44584
  });
44585
+ if (meta.phase) void refreshPhaseInvocations(sessionId2);
43950
44586
  return reply.code(202).send(outcome.duplicate ? { duplicate: true } : { accepted: true });
43951
44587
  }
43952
44588
  const event = parseHookEvent(req.body);
@@ -46964,9 +47600,9 @@ ${baru}`;
46964
47600
  // src/services/portal-chat/workspace.ts
46965
47601
  init_src();
46966
47602
  init_db();
46967
- import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
47603
+ import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
46968
47604
  import { tmpdir as tmpdir6 } from "node:os";
46969
- import { join as join33 } from "node:path";
47605
+ import { join as join34 } from "node:path";
46970
47606
  var STAGE_LABEL = {
46971
47607
  brainstorming: "Dirumuskan",
46972
47608
  objective: "Dirumuskan",
@@ -47021,7 +47657,7 @@ ${c.body}
47021
47657
  ${baris.join("\n")}`;
47022
47658
  }
47023
47659
  async function buildChatWorkspace(projectId) {
47024
- const dir2 = mkdtempSync2(join33(tmpdir6(), "hanoman-portal-chat-"));
47660
+ const dir2 = mkdtempSync2(join34(tmpdir6(), "hanoman-portal-chat-"));
47025
47661
  try {
47026
47662
  const project = await prisma.project.findUnique({
47027
47663
  where: { id: projectId },
@@ -47042,7 +47678,7 @@ async function buildChatWorkspace(projectId) {
47042
47678
  });
47043
47679
  const files = [];
47044
47680
  const tulis = (rel, isi) => {
47045
- writeFileSync9(join33(dir2, rel), isi, { mode: 384 });
47681
+ writeFileSync10(join34(dir2, rel), isi, { mode: 384 });
47046
47682
  files.push(rel);
47047
47683
  };
47048
47684
  tulis("project.md", renderProjectDoc(project));
@@ -47051,11 +47687,11 @@ async function buildChatWorkspace(projectId) {
47051
47687
  tulis("catatan-rilis.md", renderChangelogDoc(changelogs));
47052
47688
  const prds = await listPrds(projectId);
47053
47689
  if (prds.length) {
47054
- mkdirSync12(join33(dir2, "dokumen"), { mode: 448 });
47690
+ mkdirSync12(join34(dir2, "dokumen"), { mode: 448 });
47055
47691
  for (const prd of prds) {
47056
47692
  const isi = await readPrd(projectId, prd.path);
47057
47693
  if (!isi) continue;
47058
- tulis(join33("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
47694
+ tulis(join34("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
47059
47695
  }
47060
47696
  }
47061
47697
  return { dir: dir2, files, cleanup: () => rmSync8(dir2, { recursive: true, force: true }) };
@@ -47568,13 +48204,13 @@ init_db();
47568
48204
  // src/services/bootstrap.ts
47569
48205
  import { createHash as createHash12, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
47570
48206
  import { chmod, lstat as lstat3, mkdir as mkdir8, readFile as readFile6, unlink as unlink5, writeFile as writeFile6 } from "node:fs/promises";
47571
- import { join as join34 } from "node:path";
48207
+ import { join as join35 } from "node:path";
47572
48208
  var TTL_MS5 = 15 * 6e4;
47573
48209
  var SETUP_TOKEN_FILE = "setup.token";
47574
48210
  var BootstrapError = class extends Error {
47575
48211
  code = "BOOTSTRAP_PROOF";
47576
48212
  };
47577
- var tokenPath = (home3) => join34(home3, SETUP_TOKEN_FILE);
48213
+ var tokenPath = (home3) => join35(home3, SETUP_TOKEN_FILE);
47578
48214
  async function readStored(home3) {
47579
48215
  try {
47580
48216
  const info = await lstat3(tokenPath(home3));