hanoman 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -4256,6 +4256,130 @@ var init_spec_source = __esm({
4256
4256
  }
4257
4257
  });
4258
4258
 
4259
+ // ../shared/src/method-catalog.ts
4260
+ function resolveMethod(id) {
4261
+ return (id ? METHODS[id] : void 0) ?? METHODS[DEFAULT_METHOD];
4262
+ }
4263
+ function readSpecMethod(payload) {
4264
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
4265
+ const m = payload.method;
4266
+ return typeof m === "string" && m.trim() !== "" ? m.trim() : null;
4267
+ }
4268
+ function stampSpecMethod(payload, methodId) {
4269
+ if (payload === null || payload === void 0) return { method: methodId };
4270
+ if (typeof payload !== "object" || Array.isArray(payload)) return null;
4271
+ return { ...payload, method: methodId };
4272
+ }
4273
+ var DEFAULT_METHOD, VERIFICATION_GATE, MATT_CLAUSE, METHODS, METHOD_IDS, uniq, PLAN_DIRS, SPEC_DIRS;
4274
+ var init_method_catalog = __esm({
4275
+ "../shared/src/method-catalog.ts"() {
4276
+ "use strict";
4277
+ DEFAULT_METHOD = "superpowers";
4278
+ VERIFICATION_GATE = "superpowers:verification-before-completion";
4279
+ MATT_CLAUSE = "Sesi ini TAK BERPENUNGGU \u2014 tak ada manusia yang menonton terminal untuk menjawab. Skill mattpocock yang kontraknya mewawancarai manusia (`/grill-me`, `/to-spec`, `/triage`) JANGAN dipakai: pakai primitif model-invoked-nya dan putuskan sendiri. `to-tickets` di fase Plan dipakai HANYA sebagai penghasil berkas plan berkotak `- [ ]` di `docs/matt/plans/`, BUKAN penerbit tiket \u2014 hanoman sendiri adalah issue tracker-nya, jadi jangan menulis ke tracker eksternal mana pun.";
4280
+ METHODS = {
4281
+ // Isi `PHASE_SKILLS` (SPEC-166) dipindah APA ADANYA. Objective & Spec adalah keluaran skill
4282
+ // brainstorming yang di-invoke di fase Brainstorm — sengaja tak punya entri sendiri. Fase reverse
4283
+ // dipandu standar docs di prompt-nya, bukan skill.
4284
+ superpowers: {
4285
+ id: "superpowers",
4286
+ label: "superpowers",
4287
+ planDir: "docs/superpowers/plans",
4288
+ specDir: "docs/superpowers/specs",
4289
+ phaseSkills: {
4290
+ Brainstorm: ["superpowers:brainstorming"],
4291
+ Audit: ["superpowers:systematic-debugging"],
4292
+ Plan: ["superpowers:writing-plans"],
4293
+ Execute: [
4294
+ "superpowers:executing-plans",
4295
+ "superpowers:test-driven-development",
4296
+ VERIFICATION_GATE
4297
+ ],
4298
+ // SPEC-407 · fase `Goal` sengaja TANPA skill: seluruh inti flow itu membebaskan sesi dari
4299
+ // proses kaku. Yang tetap dijaga cuma pintu keluarnya.
4300
+ Verifikasi: [VERIFICATION_GATE]
4301
+ },
4302
+ exitSkills: [VERIFICATION_GATE],
4303
+ requires: ["superpowers"],
4304
+ install: {
4305
+ claude: [
4306
+ "claude plugin marketplace add obra/superpowers-marketplace",
4307
+ "claude plugin install superpowers@superpowers-marketplace"
4308
+ ],
4309
+ codex: ["codex plugin add superpowers@openai-curated"]
4310
+ }
4311
+ },
4312
+ // Plugin `mattpocock-skills` (`/plugin install mattpocock-skills`); skill plugin di Claude Code
4313
+ // beralamat `plugin:skill`. `superpowers` tetap ikut di `requires` karena gerbang verifikasinya
4314
+ // dipinjam dari sana — mattpocock tak punya padanannya.
4315
+ matt: {
4316
+ id: "matt",
4317
+ label: "mattpocock",
4318
+ planDir: "docs/matt/plans",
4319
+ specDir: "docs/matt/specs",
4320
+ phaseSkills: {
4321
+ Brainstorm: ["mattpocock-skills:grilling"],
4322
+ Audit: ["mattpocock-skills:diagnosing-bugs"],
4323
+ Plan: ["mattpocock-skills:to-tickets"],
4324
+ Execute: [
4325
+ "mattpocock-skills:implement",
4326
+ "mattpocock-skills:tdd",
4327
+ "mattpocock-skills:code-review"
4328
+ ],
4329
+ Verifikasi: [VERIFICATION_GATE]
4330
+ },
4331
+ exitSkills: [VERIFICATION_GATE],
4332
+ extraClause: MATT_CLAUSE,
4333
+ requires: ["mattpocock-skills", "superpowers"],
4334
+ // `mattpocock-skills` ada di marketplace resmi Claude Code → tanpa `marketplace add`.
4335
+ // superpowers ikut dipasang karena `requires` menyebutnya: gerbang verifikasinya dipinjam
4336
+ // dari sana (INVARIAN 2). codex belum punya plugin native mattpocock — jalur resminya
4337
+ // `npx skills`, dan ia INTERAKTIF; itu justru alasan pemasangan hidup di pane yang ditonton.
4338
+ install: {
4339
+ claude: [
4340
+ "claude plugin install mattpocock-skills",
4341
+ "claude plugin marketplace add obra/superpowers-marketplace",
4342
+ "claude plugin install superpowers@superpowers-marketplace"
4343
+ ],
4344
+ codex: [
4345
+ "npx skills@latest add mattpocock/skills",
4346
+ "codex plugin add superpowers@openai-curated"
4347
+ ]
4348
+ }
4349
+ }
4350
+ };
4351
+ METHOD_IDS = Object.keys(METHODS);
4352
+ uniq = (xs) => [...new Set(xs)];
4353
+ PLAN_DIRS = uniq(METHOD_IDS.map((id) => METHODS[id].planDir));
4354
+ SPEC_DIRS = uniq(METHOD_IDS.map((id) => METHODS[id].specDir));
4355
+ }
4356
+ });
4357
+
4358
+ // ../shared/src/method-status.ts
4359
+ function methodSkills(m) {
4360
+ return [.../* @__PURE__ */ new Set([...Object.values(m.phaseSkills).flat(), ...m.exitSkills])];
4361
+ }
4362
+ function methodStatus(m, agent, installed) {
4363
+ const haveSkills = new Set(installed.skills);
4364
+ const havePackages = new Set(installed.packages);
4365
+ const missingPackages = m.requires.filter((p) => !havePackages.has(p));
4366
+ const missingSkills = methodSkills(m).filter((id) => !haveSkills.has(id));
4367
+ return {
4368
+ method: m.id,
4369
+ label: m.label,
4370
+ agent,
4371
+ ready: missingPackages.length === 0 && missingSkills.length === 0,
4372
+ missingPackages,
4373
+ missingSkills,
4374
+ install: [...m.install[agent]]
4375
+ };
4376
+ }
4377
+ var init_method_status = __esm({
4378
+ "../shared/src/method-status.ts"() {
4379
+ "use strict";
4380
+ }
4381
+ });
4382
+
4259
4383
  // ../shared/src/agent-engine.ts
4260
4384
  var zAgentEngine;
4261
4385
  var init_agent_engine = __esm({
@@ -4442,6 +4566,7 @@ var init_entities = __esm({
4442
4566
  init_telegram();
4443
4567
  init_agent_engine();
4444
4568
  init_auto_merge();
4569
+ init_method_catalog();
4445
4570
  zProject = external_exports.object({
4446
4571
  id: external_exports.string(),
4447
4572
  name: external_exports.string(),
@@ -4662,6 +4787,12 @@ var init_entities = __esm({
4662
4787
  // SPEC-338 · ADR-0074 · model/effort codex
4663
4788
  verifyScope: zVerifyScope.default("changed"),
4664
4789
  // SPEC-376 · ADR-0080 · scope verifikasi sesi
4790
+ // SPEC-734 · ADR-0113 · metode workflow default. LENIENT (`z.string()`, bukan `z.enum`):
4791
+ // instance yang di-sync dari hub bisa membawa id metode yang belum ada di build ini, dan itu
4792
+ // harus jadi fallback diam di titik pakai (`resolveMethod`) — bukan baris Setting yang gagal
4793
+ // parse lalu mengosongkan layar Settings. Alasan yang sama dengan model/effort di sini.
4794
+ method: external_exports.string().default(DEFAULT_METHOD),
4795
+ // SPEC-734 · ADR-0113 · metode workflow default
4665
4796
  conflict: zConflict.default(CONFLICT_DEFAULTS),
4666
4797
  // SPEC-383 · ADR-0081 · default sesi konflik rebase/merge
4667
4798
  lead: zLead.default(LEAD_DEFAULTS),
@@ -5651,7 +5782,15 @@ var init_dto = __esm({
5651
5782
  // SPEC-236 · terminal biasa NON-claude: shell mentah di repoDir project. Tanpa flow (bukan
5652
5783
  // pipeline claude). DIDAHULUKAN: z.object non-strict membuang key asing, jadi bila varian
5653
5784
  // longgar {project,flow?} lebih dulu, {project,shell:true} akan lolos sbg plain (shell dibuang).
5654
- external_exports.object({ project: external_exports.string(), shell: external_exports.literal(true) }),
5785
+ // SPEC-739 · ADR-0114 · pemasangan skill metode: klien mengirim METODE + AGEN, bukan teks
5786
+ // perintah — perintahnya diturunkan server dari katalog `METHODS`. Dengan begitu "perintah
5787
+ // instalasi adalah data katalog" berlaku ujung ke ujung, dan endpoint ini tak pernah menjadi
5788
+ // "jalankan shell arbitrer". Tanpa field ini, shell mentah persis seperti SPEC-236.
5789
+ external_exports.object({
5790
+ project: external_exports.string(),
5791
+ shell: external_exports.literal(true),
5792
+ install: external_exports.object({ method: external_exports.string(), agent: zAgent }).optional()
5793
+ }),
5655
5794
  // SPEC-166 · "reverse" = sesi project-level di worktree-nya sendiri, menyusun Source of Truth
5656
5795
  // dari kode. TANPA override runtime: sesi project-level mengikuti Setting.agent (ADR-0074).
5657
5796
  // Terminal biasa (tanpa flow) kini punya variannya SENDIRI di bawah — lihat SPEC-517.
@@ -5698,6 +5837,10 @@ var init_dto = __esm({
5698
5837
  goalCondition: external_exports.string().max(4e3).optional(),
5699
5838
  agent: zAgent.optional(),
5700
5839
  verifyScope: zVerifyScope.optional(),
5840
+ // SPEC-734 · ADR-0113 — metode workflow per SESI: undefined → Spec.payload.method →
5841
+ // Setting.method → "superpowers". LENIENT (`z.string()`, bukan `z.enum`): id yang tak dikenal
5842
+ // jatuh ke default di `resolveMethod`, bukan ditolak 400 — alasan yang sama dengan zSetting.
5843
+ method: external_exports.string().optional(),
5701
5844
  // SPEC-447 · ADR-0093 — lewati gerbang dependency. Hanya jalur manusia; UI hanya
5702
5845
  // mengirimkannya sesudah operator melihat daftar pemblokirnya.
5703
5846
  force: external_exports.boolean().optional()
@@ -5891,6 +6034,8 @@ var init_api = __esm({
5891
6034
  limits: `${API}/limits`,
5892
6035
  // SPEC-339 · versi codex CLI (peringatan lunak model GPT-5.6).
5893
6036
  codexVersion: `${API}/codex/version`,
6037
+ // SPEC-739 · ADR-0114 · kesiapan skill metode per agen. LOCAL-only, diturunkan live dari disk.
6038
+ methodStatus: `${API}/methods/status`,
5894
6039
  // SPEC-489 · panduan AI agent — markdown MENTAH & PUBLIC (tanpa auth). Satu definisi URL untuk
5895
6040
  // klien web dan untuk tautan yang disalin operator ke agennya.
5896
6041
  agentDoc: `${API}/agent-integration.md`,
@@ -7557,6 +7702,8 @@ var init_src = __esm({
7557
7702
  "use strict";
7558
7703
  init_enums();
7559
7704
  init_spec_source();
7705
+ init_method_catalog();
7706
+ init_method_status();
7560
7707
  init_agent_engine();
7561
7708
  init_entities();
7562
7709
  init_agent();
@@ -7811,54 +7958,60 @@ var init_goal_spec = __esm({
7811
7958
  });
7812
7959
 
7813
7960
  // ../runner/src/prompt.ts
7814
- function startPrompt(flow, spec, branchTo, autonomy, verifyScope) {
7961
+ function startPrompt(flow, spec, branchTo, autonomy, verifyScope, method) {
7962
+ const m = resolveMethod(method);
7815
7963
  const detail = spec.payload ? `
7816
7964
  Detail: ${JSON.stringify(spec.payload)}` : "";
7817
7965
  return [
7818
7966
  `hanoman ${flow}. Ikuti internal/docs sebagai Source of Truth; perbarui docs yang tersentuh dan link-nya di index, dalam commit yang sama.`,
7819
- phaseInstruction(PIPELINES[flow]),
7967
+ phaseInstruction(PIPELINES[flow], m),
7820
7968
  auditDecisionInstruction(flow),
7821
7969
  auditContinuationInstruction(flow, spec),
7822
7970
  auditOnlyInstruction(flow),
7823
7971
  autonomyClause(autonomy),
7824
7972
  scopeClause(flow, verifyScope),
7825
7973
  codeStyleClause(flow),
7826
- skillInstruction(PIPELINES[flow]),
7974
+ methodClause(m),
7975
+ skillInstruction(PIPELINES[flow], m, writesCode(flow)),
7827
7976
  `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
7828
7977
  `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
7829
7978
  Judul: ${spec.title}
7830
7979
  Objective: ${spec.objective}${detail}`
7831
7980
  ].filter(Boolean).join("\n\n");
7832
7981
  }
7833
- function continuePrompt(flow, spec, branchTo, autonomy, verifyScope) {
7982
+ function continuePrompt(flow, spec, branchTo, autonomy, verifyScope, method) {
7983
+ const m = resolveMethod(method);
7834
7984
  const detail = spec.payload ? `
7835
7985
  Detail: ${JSON.stringify(spec.payload)}` : "";
7836
7986
  return [
7837
7987
  `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.`,
7838
- `JANGAN mengulang fase awal \u2014 spec & plan sudah ada. Lanjut di fase Execute: baca plan di docs/superpowers/plans/** untuk backlog item ini, periksa task yang sudah \`[x]\` dan selesaikan yang masih \`[ ]\`. Verifikasi nyata sebelum klaim selesai.`,
7988
+ `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.`,
7839
7989
  autonomyClause(autonomy),
7840
7990
  scopeClause(flow, verifyScope),
7841
7991
  codeStyleClause(flow),
7842
- skillInstruction(["Execute"]),
7992
+ methodClause(m),
7993
+ skillInstruction(["Execute"], m, writesCode(flow)),
7843
7994
  `Setelah selesai: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
7844
7995
  `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
7845
7996
  Judul: ${spec.title}
7846
7997
  Objective: ${spec.objective}${detail}`
7847
7998
  ].filter(Boolean).join("\n\n");
7848
7999
  }
7849
- function resumePrompt(flow, spec, branchTo, resume, autonomy, verifyScope) {
8000
+ function resumePrompt(flow, spec, branchTo, resume, autonomy, verifyScope, method) {
8001
+ const m = resolveMethod(method);
7850
8002
  const detail = spec.payload ? `
7851
8003
  Detail: ${JSON.stringify(spec.payload)}` : "";
7852
8004
  const auditDecided = resume.recorded.some((line) => line.startsWith("Audit "));
7853
8005
  return [
7854
8006
  `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.`,
7855
- resumeClause(resume, branchTo, PIPELINES[flow].includes("Plan")),
7856
- phaseInstruction(PIPELINES[flow]),
8007
+ resumeClause(resume, branchTo, m.planDir, PIPELINES[flow].includes("Plan")),
8008
+ phaseInstruction(PIPELINES[flow], m),
7857
8009
  auditDecided ? "" : auditDecisionInstruction(flow),
7858
8010
  autonomyClause(autonomy),
7859
8011
  scopeClause(flow, verifyScope),
7860
8012
  codeStyleClause(flow),
7861
- skillInstruction(PIPELINES[flow]),
8013
+ methodClause(m),
8014
+ skillInstruction(PIPELINES[flow], m, writesCode(flow)),
7862
8015
  `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
7863
8016
  `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
7864
8017
  Judul: ${spec.title}
@@ -7866,6 +8019,7 @@ Objective: ${spec.objective}${detail}`
7866
8019
  ].filter(Boolean).join("\n\n");
7867
8020
  }
7868
8021
  function startGoalPrompt(spec, branchTo, opts = {}) {
8022
+ const m = resolveMethod(opts.method);
7869
8023
  const g = readGoalPayload(spec.payload);
7870
8024
  const detail = [
7871
8025
  `Goal: ${g?.goal ?? spec.objective}`,
@@ -7874,14 +8028,15 @@ function startGoalPrompt(spec, branchTo, opts = {}) {
7874
8028
  ].filter(Boolean).join("\n");
7875
8029
  return [
7876
8030
  "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.",
7877
- opts.resume ? resumeClause(opts.resume, branchTo, false) : "",
8031
+ opts.resume ? resumeClause(opts.resume, branchTo, m.planDir, false) : "",
7878
8032
  detail,
7879
- phaseInstruction(PIPELINES.goal),
8033
+ phaseInstruction(PIPELINES.goal, m),
7880
8034
  "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.",
7881
8035
  autonomyClause(opts.autonomy),
7882
8036
  scopeClause("goal", opts.verifyScope),
7883
8037
  codeStyleClause("goal"),
7884
- skillInstruction(PIPELINES.goal),
8038
+ methodClause(m),
8039
+ skillInstruction(PIPELINES.goal, m, writesCode("goal")),
7885
8040
  `Setelah fase terakhir: commit, lalu \`git push origin HEAD:refs/heads/${branchTo}\`. Worktree ini detached HEAD \u2014 itu memang disengaja.`,
7886
8041
  `Backlog item ${spec.id} \xB7 sumber ${spec.source} \xB7 prioritas ${spec.priority}
7887
8042
  Judul: ${spec.title}`
@@ -7890,7 +8045,7 @@ Judul: ${spec.title}`
7890
8045
  function startProjectPrompt(flow, project, branchTo) {
7891
8046
  return [
7892
8047
  `hanoman ${flow}. Susun Source of Truth repo ini dari kodenya di internal/docs/**, mengikuti STANDAR DOCS di bagian bawah prompt ini.`,
7893
- phaseInstruction(PIPELINES[flow]),
8048
+ phaseInstruction(PIPELINES[flow], PROJECT_METHOD),
7894
8049
  REVERSE_PHASE_GUIDE,
7895
8050
  `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}.`,
7896
8051
  `Project ${project.id} \xB7 ${project.name}
@@ -7908,10 +8063,10 @@ PRD ini adalah TINDAK LANJUT audit di bawah. Pakai temuannya sebagai bahan brain
7908
8063
  ${audit.content}` : "";
7909
8064
  return [
7910
8065
  `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.`,
7911
- phaseInstruction(PIPELINES.prd),
8066
+ phaseInstruction(PIPELINES.prd, PROJECT_METHOD),
7912
8067
  `- 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.`,
7913
8068
  `- 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.`,
7914
- skillInstruction(PIPELINES.prd),
8069
+ skillInstruction(PIPELINES.prd, PROJECT_METHOD, false),
7915
8070
  `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}.`,
7916
8071
  `Project ${project.id} \xB7 ${project.name}
7917
8072
  Brief \u2014 Judul: ${brief.title}
@@ -7925,7 +8080,7 @@ function startBreakdownPrompt(project, prd, branchTo) {
7925
8080
  const slug = branchTo.slice(branchTo.lastIndexOf("/") + 1);
7926
8081
  return [
7927
8082
  `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.`,
7928
- phaseInstruction(PIPELINES.breakdown),
8083
+ phaseInstruction(PIPELINES.breakdown, PROJECT_METHOD),
7929
8084
  `- 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.`,
7930
8085
  `- 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):
7931
8086
  \`\`\`json
@@ -7942,10 +8097,10 @@ ${prd.content}`
7942
8097
  function startScaffoldPrompt(project, branchTo) {
7943
8098
  return [
7944
8099
  `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.`,
7945
- phaseInstruction(PIPELINES.scaffold),
8100
+ phaseInstruction(PIPELINES.scaffold, PROJECT_METHOD),
7946
8101
  SCAFFOLD_PHASE_GUIDE,
7947
8102
  `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}.`,
7948
- skillInstruction(PIPELINES.scaffold),
8103
+ skillInstruction(PIPELINES.scaffold, PROJECT_METHOD, false),
7949
8104
  `Project ${project.id} \xB7 ${project.name}
7950
8105
  Ide awal: ${project.desc || "\u2014"}
7951
8106
  Stack: ${project.stack || "\u2014"}`,
@@ -7967,10 +8122,11 @@ Stack: ${project.stack || "\u2014"}`,
7967
8122
  ${cron.prompt}`
7968
8123
  ].join("\n\n");
7969
8124
  }
7970
- var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, PHASE_SKILLS, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, codeStyleClause, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
8125
+ var PIPELINES, AUTONOMY_CLAUSE, AUTONOMY_CLAUSE_FULL, autonomyClause, phaseInstruction, skillInstruction, auditDecisionInstruction, ESCALATION_CONTRACT, auditOnlyInstruction, auditContinuationInstruction, writesCode, scopeClause, codeStyleClause, methodClause, PROJECT_METHOD, resumeClause, RESUMED_WORKTREE_NOTE, REVERSE_PHASE_GUIDE, SCAFFOLD_PHASE_GUIDE;
7971
8126
  var init_prompt = __esm({
7972
8127
  "../runner/src/prompt.ts"() {
7973
8128
  "use strict";
8129
+ init_src();
7974
8130
  init_reverse_standard();
7975
8131
  init_verify_scope();
7976
8132
  init_code_style();
@@ -7993,29 +8149,21 @@ var init_prompt = __esm({
7993
8149
  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.';
7994
8150
  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.";
7995
8151
  autonomyClause = (mode) => mode === "full-control" ? AUTONOMY_CLAUSE_FULL : AUTONOMY_CLAUSE;
7996
- phaseInstruction = (phases) => {
8152
+ phaseInstruction = (phases, method) => {
7997
8153
  const base2 = `Kerjakan fase berurutan: ${phases.join(" \u2192 ")}.
7998
8154
  Setiap kali sebuah fase selesai (atau kamu putuskan dilewati), append satu baris ke berkas di $HANOMAN_PHASE_FILE \u2014 persis: \`echo "<Nama Fase> done" >> "$HANOMAN_PHASE_FILE"\`, atau \`skipped\` sebagai ganti \`done\`. Nama fase ditulis apa adanya seperti di atas.`;
7999
8155
  if (!phases.includes("Plan") || !phases.includes("Execute")) return base2;
8000
8156
  return base2 + `
8001
- Execute BELUM selesai selama plan (\`docs/superpowers/plans/**\`) 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 \`- [ ]\`.`;
8002
- };
8003
- PHASE_SKILLS = {
8004
- Brainstorm: ["superpowers:brainstorming"],
8005
- Audit: ["superpowers:systematic-debugging"],
8006
- Plan: ["superpowers:writing-plans"],
8007
- Execute: [
8008
- "superpowers:executing-plans",
8009
- "superpowers:test-driven-development",
8010
- "superpowers:verification-before-completion"
8011
- ],
8012
- // SPEC-407 · fase `Goal` sengaja TANPA skill: seluruh inti flow ini adalah membebaskan sesi
8013
- // dari proses kaku. Yang tetap dijaga cuma pintu keluarnya.
8014
- Verifikasi: ["superpowers:verification-before-completion"]
8157
+ 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 \`- [ ]\`.`;
8015
8158
  };
8016
- skillInstruction = (phases) => {
8017
- const lines2 = phases.filter((p) => PHASE_SKILLS[p]).map((p) => `- ${p}: ${PHASE_SKILLS[p].join(", ")}`);
8018
- return lines2.length ? `Skills superpowers WAJIB: sebelum mengerjakan fase di bawah, muat & ikuti skill-nya dengan mekanisme yang tersedia di agenmu \u2014 bila skill relevan tersedia, pakai.
8159
+ skillInstruction = (phases, method, withExit) => {
8160
+ const last = phases[phases.length - 1];
8161
+ const lines2 = phases.map((p) => {
8162
+ const own = method.phaseSkills[p] ?? [];
8163
+ const skills = withExit && p === last ? [.../* @__PURE__ */ new Set([...own, ...method.exitSkills])] : own;
8164
+ return skills.length ? `- ${p}: ${skills.join(", ")}` : "";
8165
+ }).filter(Boolean);
8166
+ 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.
8019
8167
  ${lines2.join("\n")}` : "";
8020
8168
  };
8021
8169
  auditDecisionInstruction = (flow) => flow !== "qa" ? "" : 'Keputusan pasca-Audit (qa): bila temuan berconfidence tinggi dan perbaikannya bisa dikerjakan langsung (diff kecil, akar masalah jelas), LEWATI Spec dan Plan \u2014 tandai keduanya `skipped` (`echo "Spec skipped" >> "$HANOMAN_PHASE_FILE"` lalu `echo "Plan skipped" >> "$HANOMAN_PHASE_FILE"`) dan langsung ke Execute; dokumen audit menjadi doc-of-record perbaikan itu. Bila temuan luas, berisiko, atau ambigu, jalankan Spec \u2192 Plan \u2192 Execute penuh. Keputusan ini milikmu berdasarkan hasil Audit, bukan default \u2014 jangan bayar perencanaan yang tak perlu untuk perbaikan sepele.';
@@ -8054,11 +8202,13 @@ ${lines2.join("\n")}` : "";
8054
8202
  writesCode = (flow) => PIPELINES[flow].includes("Execute") || PIPELINES[flow].includes("Goal");
8055
8203
  scopeClause = (flow, scope) => scope && writesCode(flow) ? verifyScopeClause(scope) : "";
8056
8204
  codeStyleClause = (flow) => writesCode(flow) ? CODE_STYLE_CLAUSE : "";
8057
- resumeClause = (r, branchTo, hasPlan = true) => {
8205
+ methodClause = (method) => method.extraClause ?? "";
8206
+ PROJECT_METHOD = resolveMethod();
8207
+ resumeClause = (r, branchTo, planDir, hasPlan = true) => {
8058
8208
  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.";
8059
- const lanjut = r.next ? `Lanjutkan dari fase: ${r.next}.` : hasPlan ? "Semua fase sudah tercatat. Periksa apakah plan di `docs/superpowers/plans/**` masih menyisakan task `- [ ]` dan selesaikan sisanya; bila sudah bersih, tinggal commit & push." : "Semua fase sudah tercatat. Buktikan sekali lagi goal-nya benar-benar tercapai, lalu commit & push.";
8209
+ 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.";
8060
8210
  const worktree = r.worktreeKept ? "Worktree ini adalah worktree sesi sebelumnya apa adanya \u2014 termasuk perubahan yang belum di-commit." : `Worktree ini DIBANGUN ULANG dari tip branch sesi \`${branchTo}\`: commit sesi sebelumnya ada, tetapi perubahan yang belum sempat di-commit TIDAK ada.`;
8061
- const baca = hasPlan ? "Sebelum menulis apa pun: baca `git log --oneline` dan `git status`, lalu plan di `docs/superpowers/plans/**` untuk backlog item ini (`- [x]` sudah selesai, `- [ ]` belum). Jangan menulis ulang yang sudah ada." : "Sebelum menulis apa pun: baca `git log --oneline` dan `git status` untuk melihat apa yang sudah dikerjakan. Jangan menulis ulang yang sudah ada.";
8211
+ const baca = hasPlan ? `Sebelum menulis apa pun: baca \`git log --oneline\` dan \`git status\`, lalu plan di \`${planDir}/**\` untuk backlog item ini (\`- [x]\` sudah selesai, \`- [ ]\` belum). Jangan menulis ulang yang sudah ada.` : "Sebelum menulis apa pun: baca `git log --oneline` dan `git status` untuk melihat apa yang sudah dikerjakan. Jangan menulis ulang yang sudah ada.";
8062
8212
  return [
8063
8213
  "Sesi ini MELANJUTKAN pekerjaan sesi sebelumnya untuk backlog item yang sama \u2014 bukan memulai dari nol.",
8064
8214
  fase,
@@ -8243,8 +8393,9 @@ function defaultGoalCondition({ flow, specId, branchTo, spec }) {
8243
8393
  `1. output \`cat "$HANOMAN_PHASE_FILE"\` yang memuat satu baris untuk SETIAP fase ${phases.join(" \u2192 ")}, masing-masing berakhiran \`done\` atau \`skipped\`;`
8244
8394
  ];
8245
8395
  if (planGate) {
8396
+ const dirs = PLAN_DIRS.map((d) => `${d}/`).join(" ");
8246
8397
  clauses.push(
8247
- `2. output \`grep -rn -- "- \\[ \\]" docs/superpowers/plans/\` yang KOSONG untuk plan backlog ini \u2014 tak ada task yang masih \`- [ ]\` (atau bukti bahwa backlog ini memang tak berplan);`
8398
+ `2. output \`grep -rn -- "- \\[ \\]" ${dirs}\` yang KOSONG untuk plan backlog ini \u2014 tak ada task yang masih \`- [ ]\` (atau bukti bahwa backlog ini memang tak berplan);`
8248
8399
  );
8249
8400
  }
8250
8401
  clauses.push(
@@ -8269,6 +8420,7 @@ var GOAL_MAX, goalOneLine, GOAL_CHUNK;
8269
8420
  var init_goal = __esm({
8270
8421
  "../runner/src/goal.ts"() {
8271
8422
  "use strict";
8423
+ init_src();
8272
8424
  init_prompt();
8273
8425
  init_goal_spec();
8274
8426
  GOAL_MAX = 4e3;
@@ -8309,12 +8461,14 @@ function codexGoalScript(o) {
8309
8461
  );
8310
8462
  }
8311
8463
  if (planGate) {
8312
- lines2.push(
8313
- `for f in ${shq(o.worktree)}/docs/superpowers/plans/*${o.specId.toLowerCase()}*; do`,
8314
- ` [ -f "$f" ] || continue`,
8315
- ` grep -qE '^[ ]*- \\[ \\]' "$f" && missing="$missing\\n- plan $f masih punya task - [ ] yang belum selesai"`,
8316
- "done"
8317
- );
8464
+ for (const dir2 of PLAN_DIRS) {
8465
+ lines2.push(
8466
+ `for f in ${shq(`${o.worktree}/${dir2}`)}/*${o.specId.toLowerCase()}*; do`,
8467
+ ` [ -f "$f" ] || continue`,
8468
+ ` grep -qE '^[ ]*- \\[ \\]' "$f" && missing="$missing\\n- plan $f masih punya task - [ ] yang belum selesai"`,
8469
+ "done"
8470
+ );
8471
+ }
8318
8472
  }
8319
8473
  lines2.push(
8320
8474
  'if [ -z "$missing" ]; then exit 0; fi',
@@ -8339,6 +8493,7 @@ var tq, shq, group, GOAL_MAX_BLOCKS;
8339
8493
  var init_codex_settings = __esm({
8340
8494
  "../runner/src/codex-settings.ts"() {
8341
8495
  "use strict";
8496
+ init_src();
8342
8497
  init_prompt();
8343
8498
  tq = (s2) => `"${s2.split("\\").join("\\\\").split('"').join('\\"')}"`;
8344
8499
  shq = (s2) => `'${s2.split("'").join("'\\''")}'`;
@@ -8500,6 +8655,128 @@ var init_paths = __esm({
8500
8655
  }
8501
8656
  });
8502
8657
 
8658
+ // ../runner/src/skills.ts
8659
+ import { homedir as homedir2 } from "node:os";
8660
+ import { join as join2 } from "node:path";
8661
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
8662
+ function agentSkillHome(agent, env = process.env, osHome = homedir2()) {
8663
+ const own = (agent === "codex" ? env.HANOMAN_CODEX_HOME : env.HANOMAN_CLAUDE_HOME)?.trim();
8664
+ if (own) return own;
8665
+ if (agent === "codex") {
8666
+ const cx = env.CODEX_HOME?.trim();
8667
+ if (cx) return cx;
8668
+ }
8669
+ return join2(osHome, agent === "codex" ? ".codex" : ".claude");
8670
+ }
8671
+ function dirsIn(dir2) {
8672
+ try {
8673
+ return readdirSync(dir2, { withFileTypes: true }).filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith(".")).map((e) => e.name);
8674
+ } catch {
8675
+ return [];
8676
+ }
8677
+ }
8678
+ function readJson(file) {
8679
+ try {
8680
+ return JSON.parse(readFileSync2(file, "utf8"));
8681
+ } catch {
8682
+ return null;
8683
+ }
8684
+ }
8685
+ function skillsUnder(dir2, pkg) {
8686
+ return dirsIn(dir2).filter((name2) => existsSync2(join2(dir2, name2, "SKILL.md"))).map((name2) => ({ id: pkg ? `${pkg}:${name2}` : name2, name: name2, pkg, dir: join2(dir2, name2) }));
8687
+ }
8688
+ function splitPluginKey(key) {
8689
+ const at = key.lastIndexOf("@");
8690
+ return at > 0 ? { pkg: key.slice(0, at), marketplace: key.slice(at + 1) } : { pkg: key, marketplace: "" };
8691
+ }
8692
+ function manifestRoots(home2) {
8693
+ const j = readJson(join2(home2, "plugins", "installed_plugins.json"));
8694
+ const plugins = j?.plugins;
8695
+ if (!plugins || typeof plugins !== "object") return [];
8696
+ const out3 = [];
8697
+ for (const [key, entries] of Object.entries(plugins)) {
8698
+ const { pkg, marketplace } = splitPluginKey(key);
8699
+ for (const e of Array.isArray(entries) ? entries : []) {
8700
+ const p = e?.installPath;
8701
+ if (typeof p === "string" && p) out3.push({ pkg, marketplace, dir: p });
8702
+ }
8703
+ }
8704
+ return out3;
8705
+ }
8706
+ function cacheRoots(home2) {
8707
+ const cache6 = join2(home2, "plugins", "cache");
8708
+ const out3 = [];
8709
+ for (const marketplace of dirsIn(cache6))
8710
+ for (const pkg of dirsIn(join2(cache6, marketplace)))
8711
+ for (const version of dirsIn(join2(cache6, marketplace, pkg)))
8712
+ out3.push({ pkg, marketplace, dir: join2(cache6, marketplace, pkg, version) });
8713
+ return out3;
8714
+ }
8715
+ function disabledPlugins(agent, home2) {
8716
+ const out3 = /* @__PURE__ */ new Set();
8717
+ if (agent === "claude") {
8718
+ const en = readJson(join2(home2, "settings.json"))?.enabledPlugins;
8719
+ if (en && typeof en === "object") {
8720
+ for (const [k, v] of Object.entries(en)) if (v === false) out3.add(k);
8721
+ }
8722
+ return out3;
8723
+ }
8724
+ let toml;
8725
+ try {
8726
+ toml = readFileSync2(join2(home2, "config.toml"), "utf8");
8727
+ } catch {
8728
+ return out3;
8729
+ }
8730
+ let current = null;
8731
+ for (const raw of toml.split("\n")) {
8732
+ const line = raw.trim();
8733
+ const head = /^\[([^\]]*)\]$/.exec(line);
8734
+ if (head) {
8735
+ const m = /^plugins\."(.+)"$/.exec(head[1] ?? "");
8736
+ current = m ? m[1] : null;
8737
+ continue;
8738
+ }
8739
+ if (current && /^enabled\s*=\s*false\b/.test(line)) out3.add(current);
8740
+ }
8741
+ return out3;
8742
+ }
8743
+ function scanAgentSkills(agent, env = process.env, osHome = homedir2()) {
8744
+ const home2 = agentSkillHome(agent, env, osHome);
8745
+ const disabled = disabledPlugins(agent, home2);
8746
+ const roots = [];
8747
+ const skills = [];
8748
+ const packages = /* @__PURE__ */ new Set();
8749
+ const seenSkill = /* @__PURE__ */ new Set();
8750
+ const seenRoot = /* @__PURE__ */ new Set();
8751
+ const add = (dir2, found) => {
8752
+ if (!found.length) return;
8753
+ if (!seenRoot.has(dir2)) {
8754
+ seenRoot.add(dir2);
8755
+ roots.push(dir2);
8756
+ }
8757
+ for (const s2 of found) if (!seenSkill.has(s2.id)) {
8758
+ seenSkill.add(s2.id);
8759
+ skills.push(s2);
8760
+ }
8761
+ };
8762
+ const userDir = join2(home2, "skills");
8763
+ add(userDir, skillsUnder(userDir, null));
8764
+ for (const r of [...manifestRoots(home2), ...cacheRoots(home2)]) {
8765
+ if (disabled.has(`${r.pkg}@${r.marketplace}`)) continue;
8766
+ const dir2 = join2(r.dir, "skills");
8767
+ const found = skillsUnder(dir2, r.pkg);
8768
+ if (!found.length) continue;
8769
+ packages.add(r.pkg);
8770
+ add(dir2, found);
8771
+ }
8772
+ return { agent, home: home2, roots, skills, packages: [...packages] };
8773
+ }
8774
+ var init_skills = __esm({
8775
+ "../runner/src/skills.ts"() {
8776
+ "use strict";
8777
+ }
8778
+ });
8779
+
8503
8780
  // ../runner/src/telegram-operator.ts
8504
8781
  function buildTelegramOperatorPrompt(input) {
8505
8782
  const personality = input.personality ? [
@@ -8588,6 +8865,7 @@ var init_src2 = __esm({
8588
8865
  init_verify_scope();
8589
8866
  init_code_style();
8590
8867
  init_paths();
8868
+ init_skills();
8591
8869
  init_telegram_operator();
8592
8870
  }
8593
8871
  });
@@ -8755,12 +9033,12 @@ var init_stage_machine = __esm({
8755
9033
  });
8756
9034
 
8757
9035
  // src/services/session-phases.ts
8758
- import { readFileSync as readFileSync2, readdirSync } from "node:fs";
9036
+ import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
8759
9037
  function recorded(file) {
8760
9038
  const out3 = /* @__PURE__ */ new Map();
8761
9039
  let raw;
8762
9040
  try {
8763
- raw = readFileSync2(file, "utf8");
9041
+ raw = readFileSync3(file, "utf8");
8764
9042
  } catch {
8765
9043
  return out3;
8766
9044
  }
@@ -8798,19 +9076,21 @@ function stageFor(phases) {
8798
9076
  return best < 0 ? null : STAGES[best];
8799
9077
  }
8800
9078
  function planComplete(worktree, specId) {
8801
- const dir2 = `${worktree}/docs/superpowers/plans`;
8802
9079
  const re = new RegExp(`(^|[^a-z0-9])${specId.toLowerCase()}([^0-9]|$)`);
8803
- let names;
8804
- try {
8805
- names = readdirSync(dir2);
8806
- } catch {
8807
- return true;
8808
- }
8809
- for (const n2 of names) {
8810
- if (!re.test(n2.toLowerCase())) continue;
9080
+ for (const rel of PLAN_DIRS) {
9081
+ const dir2 = `${worktree}/${rel}`;
9082
+ let names;
8811
9083
  try {
8812
- if (/^[ \t]*- \[ \]/m.test(readFileSync2(`${dir2}/${n2}`, "utf8"))) return false;
9084
+ names = readdirSync2(dir2);
8813
9085
  } catch {
9086
+ continue;
9087
+ }
9088
+ for (const n2 of names) {
9089
+ if (!re.test(n2.toLowerCase())) continue;
9090
+ try {
9091
+ if (/^[ \t]*- \[ \]/m.test(readFileSync3(`${dir2}/${n2}`, "utf8"))) return false;
9092
+ } catch {
9093
+ }
8814
9094
  }
8815
9095
  }
8816
9096
  return true;
@@ -8829,6 +9109,7 @@ var init_session_phases = __esm({
8829
9109
  "src/services/session-phases.ts"() {
8830
9110
  "use strict";
8831
9111
  init_src2();
9112
+ init_src();
8832
9113
  init_stage_machine();
8833
9114
  phaseFilePath = (repoDir, sessionId2) => `${repoDir}/.worktrees/.phases/${sessionId2}`;
8834
9115
  decisionFilePath = (repoDir, sessionId2) => `${repoDir}/.worktrees/.decisions/${sessionId2}`;
@@ -9090,9 +9371,9 @@ var init_tui_dialog = __esm({
9090
9371
  });
9091
9372
 
9092
9373
  // src/services/secret-box.ts
9093
- import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync } from "node:fs";
9374
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
9094
9375
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
9095
- import { dirname as dirname4, join as join2 } from "node:path";
9376
+ import { dirname as dirname4, join as join3 } from "node:path";
9096
9377
  function isEncrypted(value) {
9097
9378
  return value.startsWith(ENC_PREFIX);
9098
9379
  }
@@ -9124,7 +9405,7 @@ function fromEnv(raw) {
9124
9405
  return null;
9125
9406
  }
9126
9407
  function secretKeyPath() {
9127
- return join2(resolveHome(), "secret.key");
9408
+ return join3(resolveHome(), "secret.key");
9128
9409
  }
9129
9410
  function secretKey() {
9130
9411
  if (cached) return cached;
@@ -9136,8 +9417,8 @@ function secretKey() {
9136
9417
  return key2;
9137
9418
  }
9138
9419
  const path = secretKeyPath();
9139
- if (existsSync2(path)) {
9140
- const key2 = Buffer.from(readFileSync3(path, "utf8").trim(), "base64url");
9420
+ if (existsSync3(path)) {
9421
+ const key2 = Buffer.from(readFileSync4(path, "utf8").trim(), "base64url");
9141
9422
  if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path} rusak \u2014 panjangnya bukan 32 byte`);
9142
9423
  cached = key2;
9143
9424
  return key2;
@@ -10199,6 +10480,8 @@ var init_settings2 = __esm({
10199
10480
  // SPEC-338 · ADR-0074 · model/effort codex
10200
10481
  verifyScope: "changed",
10201
10482
  // SPEC-376 · ADR-0080 · uji hanya yang berubah
10483
+ method: DEFAULT_METHOD,
10484
+ // SPEC-734 · ADR-0113 · metode workflow default
10202
10485
  conflict: CONFLICT_DEFAULTS,
10203
10486
  // SPEC-383 · ADR-0081 · default sesi konflik (opt-in, mati)
10204
10487
  lead: LEAD_DEFAULTS,
@@ -10213,8 +10496,8 @@ var init_settings2 = __esm({
10213
10496
  });
10214
10497
 
10215
10498
  // src/services/codex-trust.ts
10216
- import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "node:fs";
10217
- import { homedir as homedir2 } from "node:os";
10499
+ import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
10500
+ import { homedir as homedir3 } from "node:os";
10218
10501
  function ensureCodexTrust(repoDir, home2) {
10219
10502
  const path = codexConfigPath(home2);
10220
10503
  let canonical = repoDir;
@@ -10226,7 +10509,7 @@ function ensureCodexTrust(repoDir, home2) {
10226
10509
  try {
10227
10510
  let existing = "";
10228
10511
  try {
10229
- existing = readFileSync5(path, "utf8");
10512
+ existing = readFileSync6(path, "utf8");
10230
10513
  } catch {
10231
10514
  }
10232
10515
  if (existing.includes(header2)) return;
@@ -10243,7 +10526,7 @@ var codexHome, codexConfigPath;
10243
10526
  var init_codex_trust = __esm({
10244
10527
  "src/services/codex-trust.ts"() {
10245
10528
  "use strict";
10246
- codexHome = (home2) => home2 ?? process.env.CODEX_HOME ?? `${homedir2()}/.codex`;
10529
+ codexHome = (home2) => home2 ?? process.env.CODEX_HOME ?? `${homedir3()}/.codex`;
10247
10530
  codexConfigPath = (home2) => `${codexHome(home2)}/config.toml`;
10248
10531
  }
10249
10532
  });
@@ -18861,7 +19144,7 @@ import websocket from "@fastify/websocket";
18861
19144
  import cookie from "@fastify/cookie";
18862
19145
  import { fileURLToPath as fileURLToPath4 } from "node:url";
18863
19146
  import { dirname as dirname13 } from "node:path";
18864
- import { existsSync as existsSync13 } from "node:fs";
19147
+ import { existsSync as existsSync14 } from "node:fs";
18865
19148
 
18866
19149
  // src/web-dir.ts
18867
19150
  import { resolve } from "node:path";
@@ -18923,7 +19206,7 @@ init_stage_machine();
18923
19206
  init_src();
18924
19207
  import { execFile } from "node:child_process";
18925
19208
  import { promisify } from "node:util";
18926
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync5 } from "node:fs";
19209
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, rmSync as rmSync2, mkdirSync as mkdirSync5 } from "node:fs";
18927
19210
  import { resolve as resolve7, dirname as dirname6, sep } from "node:path";
18928
19211
  var exec = promisify(execFile);
18929
19212
  async function listRepoDocs(repoDir) {
@@ -18940,7 +19223,7 @@ async function listRepoDocs(repoDir) {
18940
19223
  }
18941
19224
  function docsDirOf(repoDir) {
18942
19225
  try {
18943
- const raw = readFileSync4(resolve7(repoDir, "hanoman.config.json"), "utf8");
19226
+ const raw = readFileSync5(resolve7(repoDir, "hanoman.config.json"), "utf8");
18944
19227
  return zHanomanConfig.parse(JSON.parse(raw)).docsDir;
18945
19228
  } catch {
18946
19229
  return zHanomanConfig.parse({}).docsDir;
@@ -18948,18 +19231,18 @@ function docsDirOf(repoDir) {
18948
19231
  }
18949
19232
  function resolveIndex(repoDir, docsDir) {
18950
19233
  const rel = `${docsDir}/README.md`;
18951
- return existsSync3(resolve7(repoDir, rel)) ? rel : "";
19234
+ return existsSync4(resolve7(repoDir, rel)) ? rel : "";
18952
19235
  }
18953
19236
  var catOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ".";
18954
19237
  var nameOf = (rel) => rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
18955
19238
  async function scanRepoDocs(repoDir) {
18956
- if (!repoDir || !existsSync3(repoDir)) return { coverage: 0, tree: [] };
19239
+ if (!repoDir || !existsSync4(repoDir)) return { coverage: 0, tree: [] };
18957
19240
  const files = await listRepoDocs(repoDir);
18958
19241
  const docsDir = docsDirOf(repoDir);
18959
19242
  const index = resolveIndex(repoDir, docsDir);
18960
19243
  const read = (rel) => {
18961
19244
  try {
18962
- return readFileSync4(resolve7(repoDir, rel), "utf8");
19245
+ return readFileSync5(resolve7(repoDir, rel), "utf8");
18963
19246
  } catch {
18964
19247
  return null;
18965
19248
  }
@@ -18988,7 +19271,7 @@ function docAbsPath(repoDir, rel) {
18988
19271
  }
18989
19272
  function readDocFile(repoDir, rel) {
18990
19273
  try {
18991
- return readFileSync4(docAbsPath(repoDir, rel), "utf8");
19274
+ return readFileSync5(docAbsPath(repoDir, rel), "utf8");
18992
19275
  } catch {
18993
19276
  return null;
18994
19277
  }
@@ -19000,7 +19283,7 @@ function writeDocFile(repoDir, rel, content) {
19000
19283
  }
19001
19284
  function deleteDocFile(repoDir, rel) {
19002
19285
  const abs = docAbsPath(repoDir, rel);
19003
- if (!existsSync3(abs)) return false;
19286
+ if (!existsSync4(abs)) return false;
19004
19287
  rmSync2(abs);
19005
19288
  return true;
19006
19289
  }
@@ -19287,13 +19570,13 @@ async function projects_default(app2) {
19287
19570
  // src/routes/specs.ts
19288
19571
  init_src();
19289
19572
  init_src2();
19290
- import { existsSync as existsSync4 } from "node:fs";
19573
+ import { existsSync as existsSync5 } from "node:fs";
19291
19574
 
19292
19575
  // src/services/integrate.ts
19293
19576
  import { execFile as execFile3 } from "node:child_process";
19294
19577
  import { promisify as promisify3 } from "node:util";
19295
19578
  import { rmSync as rmSync3 } from "node:fs";
19296
- import { join as join3 } from "node:path";
19579
+ import { join as join4 } from "node:path";
19297
19580
  var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
19298
19581
  var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
19299
19582
  var exec3 = promisify3(execFile3);
@@ -19337,7 +19620,7 @@ async function integrateBranch(repoDir, src, op, target2) {
19337
19620
  const tgt = await resolveTarget(repoDir, target2);
19338
19621
  if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
19339
19622
  await sh(repoDir, ["fetch", "origin"]);
19340
- const wt = join3(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
19623
+ const wt = join4(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
19341
19624
  await reclaim(repoDir, wt);
19342
19625
  const baseRef = op === "merge" ? tgt.ref : source;
19343
19626
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
@@ -19398,7 +19681,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
19398
19681
  const src = await resolveGraphSource(repoDir, source);
19399
19682
  if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
19400
19683
  await sh(repoDir, ["fetch", "origin"]);
19401
- const wt = join3(repoDir, ".worktrees", `merge-${sanitize(current)}`);
19684
+ const wt = join4(repoDir, ".worktrees", `merge-${sanitize(current)}`);
19402
19685
  await reclaim(repoDir, wt);
19403
19686
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
19404
19687
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -19436,7 +19719,7 @@ async function replayCurrent(repoDir, source, cmd) {
19436
19719
  const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
19437
19720
  if (!current || current === "HEAD")
19438
19721
  return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
19439
- const wt = join3(repoDir, ".worktrees", `merge-${sanitize(current)}`);
19722
+ const wt = join4(repoDir, ".worktrees", `merge-${sanitize(current)}`);
19440
19723
  await reclaim(repoDir, wt);
19441
19724
  const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
19442
19725
  if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
@@ -19485,15 +19768,15 @@ import { execFile as execFile4 } from "node:child_process";
19485
19768
  import { promisify as promisify4 } from "node:util";
19486
19769
  import { readFile as readFile2, mkdtemp, copyFile, rm } from "node:fs/promises";
19487
19770
  import { tmpdir as tmpdir2 } from "node:os";
19488
- import { join as join4, resolve as resolve8 } from "node:path";
19771
+ import { join as join5, resolve as resolve8 } from "node:path";
19489
19772
  var exec4 = promisify4(execFile4);
19490
19773
  var GIT3 = { maxBuffer: 1 << 24 };
19491
19774
  var MAX = 256 * 1024;
19492
- var worktreeDir = (repoDir, specId) => join4(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
19775
+ var worktreeDir = (repoDir, specId) => join5(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
19493
19776
  async function withTempIndex(wt, fn) {
19494
19777
  const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
19495
- const dir2 = await mkdtemp(join4(tmpdir2(), "hanoman-idx-"));
19496
- const tmp = join4(dir2, "index");
19778
+ const dir2 = await mkdtemp(join5(tmpdir2(), "hanoman-idx-"));
19779
+ const tmp = join5(dir2, "index");
19497
19780
  await copyFile(resolve8(wt, idx), tmp);
19498
19781
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
19499
19782
  try {
@@ -19618,7 +19901,7 @@ async function reviewFile(repoDir, specId, baseSha, branchFrom, path) {
19618
19901
  let contentRaw = null;
19619
19902
  if (status !== "D") {
19620
19903
  try {
19621
- contentRaw = await readFile2(join4(wt, path), "utf8");
19904
+ contentRaw = await readFile2(join5(wt, path), "utf8");
19622
19905
  } catch {
19623
19906
  contentRaw = null;
19624
19907
  }
@@ -19803,14 +20086,15 @@ async function notificationsFeed(p = {}) {
19803
20086
  init_stage_machine();
19804
20087
 
19805
20088
  // src/services/stage-artifacts.ts
20089
+ init_src();
19806
20090
  init_stage_machine();
19807
20091
  var ARTIFACT_DIR = {
19808
- "spec-ready": "docs/superpowers/specs/",
19809
- planned: "docs/superpowers/plans/"
20092
+ "spec-ready": SPEC_DIRS.map((d) => `${d}/`),
20093
+ planned: PLAN_DIRS.map((d) => `${d}/`)
19810
20094
  };
19811
20095
  async function artifactsToRemove(projectId, specId, target2, current) {
19812
20096
  const ti = STAGES.indexOf(target2), ci = STAGES.indexOf(current);
19813
- const dirs = STAGES.filter((_, i) => i > ti && i <= ci).map((s2) => ARTIFACT_DIR[s2]).filter((d) => !!d);
20097
+ const dirs = STAGES.filter((_, i) => i > ti && i <= ci).flatMap((s2) => ARTIFACT_DIR[s2] ?? []);
19814
20098
  if (!dirs.length) return [];
19815
20099
  const repoDir = await resolveRepoDir(projectId);
19816
20100
  if (!repoDir) return [];
@@ -19842,6 +20126,7 @@ async function deleteDoc(projectId, path) {
19842
20126
  }
19843
20127
 
19844
20128
  // src/services/spec-docs.ts
20129
+ init_src();
19845
20130
  init_db();
19846
20131
  init_pty();
19847
20132
  var ORDER = ["audit", "spec", "plan", "objective", "brainstorm", "other"];
@@ -19850,8 +20135,8 @@ function kindOf(path) {
19850
20135
  if (p.endsWith("-audit.md") || p.includes("/research/audit-")) return "audit";
19851
20136
  if (p.endsWith("-objective.md")) return "objective";
19852
20137
  if (p.endsWith("-brainstorm.md")) return "brainstorm";
19853
- if (p.endsWith("-design.md") || p.endsWith("-spec.md") || p.startsWith("docs/superpowers/specs/")) return "spec";
19854
- if (p.startsWith("docs/superpowers/plans/") || p.endsWith("-plan.md")) return "plan";
20138
+ if (p.endsWith("-design.md") || p.endsWith("-spec.md") || SPEC_DIRS.some((d) => p.startsWith(`${d}/`))) return "spec";
20139
+ if (PLAN_DIRS.some((d) => p.startsWith(`${d}/`)) || p.endsWith("-plan.md")) return "plan";
19855
20140
  return "other";
19856
20141
  }
19857
20142
  async function resolveDir(specId, sessions = listSessions()) {
@@ -22763,7 +23048,7 @@ ${item.context}` : item.context;
22763
23048
  });
22764
23049
  const specWithProject = (id) => prisma.spec.findUnique({ where: { id }, include: { project: true } });
22765
23050
  const resolveReview = async (repoDir, spec) => {
22766
- if (existsSync4(worktreeDir(repoDir, spec.id))) return { wt: true };
23051
+ if (existsSync5(worktreeDir(repoDir, spec.id))) return { wt: true };
22767
23052
  if (spec.baseSha && spec.headSha && await shaResolvable(repoDir, spec.baseSha) && await shaResolvable(repoDir, spec.headSha))
22768
23053
  return { wt: false, base: spec.baseSha, head: spec.headSha };
22769
23054
  const r = await specCommitRange(repoDir, spec.id);
@@ -23004,7 +23289,7 @@ import { spawn as spawn2 } from "node:child_process";
23004
23289
  // src/services/git-remotes.ts
23005
23290
  import { execFile as execFile5 } from "node:child_process";
23006
23291
  import { promisify as promisify5 } from "node:util";
23007
- import { existsSync as existsSync5 } from "node:fs";
23292
+ import { existsSync as existsSync6 } from "node:fs";
23008
23293
  var exec5 = promisify5(execFile5);
23009
23294
  var GIT4 = { maxBuffer: 1 << 24, encoding: "utf8" };
23010
23295
  function parseRemote(url2) {
@@ -23023,7 +23308,7 @@ function prUrl(remoteUrl, branch, base2 = "main") {
23023
23308
  return null;
23024
23309
  }
23025
23310
  async function listRemotes(repoDir) {
23026
- if (!repoDir || !existsSync5(repoDir)) return [];
23311
+ if (!repoDir || !existsSync6(repoDir)) return [];
23027
23312
  try {
23028
23313
  const { stdout } = await exec5("git", ["remote", "-v"], { cwd: repoDir, ...GIT4 });
23029
23314
  const map = /* @__PURE__ */ new Map();
@@ -23065,7 +23350,7 @@ import { promisify as promisify7 } from "node:util";
23065
23350
  import { execFile as execFile6 } from "node:child_process";
23066
23351
  import { promisify as promisify6 } from "node:util";
23067
23352
  import { readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
23068
- import { existsSync as existsSync6 } from "node:fs";
23353
+ import { existsSync as existsSync7 } from "node:fs";
23069
23354
  import { resolve as resolve9, sep as sep2, dirname as dirname8 } from "node:path";
23070
23355
  var exec6 = promisify6(execFile6);
23071
23356
  var GIT5 = { maxBuffer: 1 << 24 };
@@ -23078,7 +23363,7 @@ function repoAbsPath(repoDir, rel) {
23078
23363
  return abs;
23079
23364
  }
23080
23365
  async function listRepoTree(repoDir, ref = "") {
23081
- if (!repoDir || !existsSync6(repoDir)) return [];
23366
+ if (!repoDir || !existsSync7(repoDir)) return [];
23082
23367
  try {
23083
23368
  const { stdout } = ref ? await exec6("git", ["ls-tree", "-r", "--name-only", "-z", ref], { cwd: repoDir, ...GIT5 }) : await exec6("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { cwd: repoDir, ...GIT5 });
23084
23369
  return [...new Set(splitZ2(stdout))].sort();
@@ -23104,7 +23389,7 @@ async function currentBranch(repoDir) {
23104
23389
  }
23105
23390
  async function repoStatus(repoDir) {
23106
23391
  const empty = { branch: "", ahead: 0, behind: 0, staged: [], unstaged: [], untracked: [], clean: true };
23107
- if (!repoDir || !existsSync6(repoDir)) return empty;
23392
+ if (!repoDir || !existsSync7(repoDir)) return empty;
23108
23393
  try {
23109
23394
  const { stdout } = await exec6("git", ["status", "--porcelain=v1", "-z", "-b"], { cwd: repoDir, ...GIT5 });
23110
23395
  const parts = stdout.split("\0");
@@ -23137,7 +23422,7 @@ async function repoStatus(repoDir) {
23137
23422
  }
23138
23423
  }
23139
23424
  async function listStashes(repoDir) {
23140
- if (!repoDir || !existsSync6(repoDir)) return [];
23425
+ if (!repoDir || !existsSync7(repoDir)) return [];
23141
23426
  try {
23142
23427
  const { stdout } = await exec6("git", ["stash", "list", `--format=%gd${US}%s${US}%aI`], { cwd: repoDir, ...GIT5 });
23143
23428
  return stdout.split("\n").filter(Boolean).map((line) => {
@@ -23149,7 +23434,7 @@ async function listStashes(repoDir) {
23149
23434
  }
23150
23435
  }
23151
23436
  async function listGraph(repoDir, limit = 200, opts = {}) {
23152
- if (!repoDir || !existsSync6(repoDir)) return { commits: [], current: "", total: 0 };
23437
+ if (!repoDir || !existsSync7(repoDir)) return { commits: [], current: "", total: 0 };
23153
23438
  try {
23154
23439
  const fmt = ["%H", "%P", "%an", "%aI", "%s", "%D"].join(US);
23155
23440
  const refArgs = opts.branches?.length ? ["--end-of-options", ...opts.branches] : [
@@ -23219,7 +23504,7 @@ async function changedOf(repoDir, sha) {
23219
23504
  }
23220
23505
  async function compareCommits(repoDir, from, to) {
23221
23506
  const empty = { from, to, changed: [] };
23222
- if (!repoDir || !existsSync6(repoDir)) return empty;
23507
+ if (!repoDir || !existsSync7(repoDir)) return empty;
23223
23508
  try {
23224
23509
  const [num, name2] = await Promise.all([
23225
23510
  exec6("git", ["diff", "--numstat", "-z", "--no-renames", "--end-of-options", from, to], { cwd: repoDir, ...GIT5 }),
@@ -23231,7 +23516,7 @@ async function compareCommits(repoDir, from, to) {
23231
23516
  }
23232
23517
  }
23233
23518
  async function searchCommits(repoDir, q, by = "all") {
23234
- if (!repoDir || !existsSync6(repoDir) || !q) return [];
23519
+ if (!repoDir || !existsSync7(repoDir) || !q) return [];
23235
23520
  const run2 = async (extra) => {
23236
23521
  try {
23237
23522
  return (await exec6("git", ["log", "--all", "--format=%H", ...extra], { cwd: repoDir, ...GIT5 })).stdout.split("\n").filter(Boolean);
@@ -23480,7 +23765,7 @@ async function runGitOp(repoDir, op) {
23480
23765
  }
23481
23766
  }
23482
23767
  async function workingStatus(repoDir) {
23483
- if (!repoDir || !existsSync6(repoDir)) return { branch: "", staged: [], unstaged: [] };
23768
+ if (!repoDir || !existsSync7(repoDir)) return { branch: "", staged: [], unstaged: [] };
23484
23769
  const [branch, staged, unstaged] = await Promise.all([
23485
23770
  currentBranch(repoDir),
23486
23771
  changedFiles(repoDir, ["--cached"]),
@@ -23489,7 +23774,7 @@ async function workingStatus(repoDir) {
23489
23774
  return { branch, staged, unstaged };
23490
23775
  }
23491
23776
  async function workingFileDiff(repoDir, path, staged) {
23492
- if (!repoDir || !existsSync6(repoDir)) return null;
23777
+ if (!repoDir || !existsSync7(repoDir)) return null;
23493
23778
  repoAbsPath(repoDir, path);
23494
23779
  const changed = staged ? await changedFiles(repoDir, ["--cached"]) : await withTempIndex(repoDir, (env) => changedFiles(repoDir, [], env));
23495
23780
  const cf = changed.find((c) => c.path === path);
@@ -23966,15 +24251,15 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
23966
24251
 
23967
24252
  // src/routes/fs.ts
23968
24253
  import { readdir } from "node:fs/promises";
23969
- import { homedir as homedir3 } from "node:os";
23970
- import { resolve as resolve10, dirname as dirname9, join as join5 } from "node:path";
24254
+ import { homedir as homedir4 } from "node:os";
24255
+ import { resolve as resolve10, dirname as dirname9, join as join6 } from "node:path";
23971
24256
  async function fs_default(app2) {
23972
24257
  app2.get("/fs/browse", async (req, reply) => {
23973
24258
  const q = req.query.path;
23974
- const dir2 = q && q.trim() ? resolve10(q.trim()) : homedir3();
24259
+ const dir2 = q && q.trim() ? resolve10(q.trim()) : homedir4();
23975
24260
  try {
23976
24261
  const ents = await readdir(dir2, { withFileTypes: true });
23977
- const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join5(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
24262
+ const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join6(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
23978
24263
  const parent = dirname9(dir2);
23979
24264
  return { path: dir2, parent: parent === dir2 ? null : parent, entries };
23980
24265
  } catch {
@@ -23988,13 +24273,14 @@ init_db();
23988
24273
  init_src();
23989
24274
  init_src2();
23990
24275
  init_session_phases();
23991
- import { existsSync as existsSync7 } from "node:fs";
24276
+ import { existsSync as existsSync8 } from "node:fs";
23992
24277
  init_settings2();
23993
24278
  init_codex_trust();
23994
24279
 
23995
24280
  // src/services/session-launch.ts
23996
24281
  init_db();
23997
24282
  init_src2();
24283
+ init_src();
23998
24284
  init_settings2();
23999
24285
  init_codex_trust();
24000
24286
  init_pty();
@@ -24051,6 +24337,8 @@ async function startSpecSession(spec, opts) {
24051
24337
  };
24052
24338
  const goal = isGoalFlow || (opts.goal ?? setting.goal.enabled) ? resolveGoalCondition(goalArgs, opts.goalCondition, isGoalFlow ? null : setting.goal.condition) : void 0;
24053
24339
  const verifyScope = opts.verifyScope ?? setting.verifyScope;
24340
+ const recordedMethod = readSpecMethod(spec.payload);
24341
+ const method = resolveMethod(opts.method ?? recordedMethod ?? setting.method);
24054
24342
  if (agent === "codex") ensureCodexTrust(repoDir);
24055
24343
  let baseSha;
24056
24344
  if (resume?.worktreeKept) {
@@ -24067,6 +24355,10 @@ async function startSpecSession(spec, opts) {
24067
24355
  data: { baseSha, headSha: null, startedAt: /* @__PURE__ */ new Date() }
24068
24356
  });
24069
24357
  }
24358
+ if (!recordedMethod) {
24359
+ const stamped = stampSpecMethod(spec.payload, method.id);
24360
+ if (stamped) await prisma.spec.update({ where: { id: spec.id }, data: { payload: stamped } });
24361
+ }
24070
24362
  const brief = {
24071
24363
  id: spec.id,
24072
24364
  title: spec.title,
@@ -24081,14 +24373,15 @@ async function startSpecSession(spec, opts) {
24081
24373
  prompt = startGoalPrompt(brief, branchTo, {
24082
24374
  autonomy: opts.autonomy,
24083
24375
  verifyScope,
24084
- resume: resumeCtx
24376
+ resume: resumeCtx,
24377
+ method: method.id
24085
24378
  });
24086
24379
  } else if (isContinue) {
24087
- prompt = continuePrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope);
24380
+ prompt = continuePrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id);
24088
24381
  } else if (resumeCtx) {
24089
- prompt = resumePrompt(opts.flow, brief, branchTo, resumeCtx, opts.autonomy, verifyScope);
24382
+ prompt = resumePrompt(opts.flow, brief, branchTo, resumeCtx, opts.autonomy, verifyScope, method.id);
24090
24383
  } else {
24091
- prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope);
24384
+ prompt = startPrompt(opts.flow, brief, branchTo, opts.autonomy, verifyScope, method.id);
24092
24385
  }
24093
24386
  const scopeEnv = { HANOMAN_BASE_SHA: baseSha, HANOMAN_VERIFY_SCOPE: verifyScope };
24094
24387
  const s2 = createSession(spec.projectId, worktree, {
@@ -24106,6 +24399,24 @@ async function startSpecSession(spec, opts) {
24106
24399
  return resume ? { id: s2.id, resumed: true } : { id: s2.id };
24107
24400
  }
24108
24401
 
24402
+ // src/services/method-status.ts
24403
+ init_src();
24404
+ init_src2();
24405
+ init_pty();
24406
+ function methodStatusReport(env = process.env) {
24407
+ const scans = zAgent.options.map((a) => scanAgentSkills(a, env));
24408
+ return {
24409
+ agents: scans.map((s2) => ({ agent: s2.agent, home: s2.home, roots: s2.roots, skills: s2.skills.length })),
24410
+ methods: scans.flatMap((s2) => {
24411
+ const installed = { skills: s2.skills.map((k) => k.id), packages: s2.packages };
24412
+ return METHOD_IDS.map((id) => methodStatus(METHODS[id], s2.agent, installed));
24413
+ })
24414
+ };
24415
+ }
24416
+ function installCommand(m, agent, shell = shellBin()) {
24417
+ return [shell, "-lc", `${m.install[agent].join(" && ")}; exec '${shell}' -l`];
24418
+ }
24419
+
24109
24420
  // src/services/session-worktree.ts
24110
24421
  import { resolve as resolve11, sep as sep3 } from "node:path";
24111
24422
  function ownsWorktree(repoDir, cwd) {
@@ -24196,6 +24507,8 @@ async function terminal_default(app2) {
24196
24507
  // SPEC-338 · ADR-0074
24197
24508
  verifyScope: parsed.data.verifyScope,
24198
24509
  // SPEC-376 · ADR-0080
24510
+ method: parsed.data.method,
24511
+ // SPEC-734 · ADR-0113
24199
24512
  force: parsed.data.force
24200
24513
  // SPEC-447 · ADR-0093
24201
24514
  });
@@ -24214,6 +24527,13 @@ async function terminal_default(app2) {
24214
24527
  if ("shell" in parsed.data) {
24215
24528
  const repoDir2 = await resolveRepoDir(project.id);
24216
24529
  if (!repoDir2) return reply.code(400).send({ error: `project "${project.id}" belum di-bind ke checkout lokal`, needsBind: true });
24530
+ const inst = parsed.data.install;
24531
+ if (inst) {
24532
+ const m = METHODS[inst.method];
24533
+ if (!m) return reply.code(400).send({ error: `metode "${inst.method}" tak dikenal` });
24534
+ const s4 = createSession(project.id, repoDir2, { command: installCommand(m, inst.agent) });
24535
+ return reply.code(201).send({ id: s4.id });
24536
+ }
24217
24537
  const s3 = createSession(project.id, repoDir2, { command: [shellBin()] });
24218
24538
  return reply.code(201).send({ id: s3.id });
24219
24539
  }
@@ -24382,7 +24702,7 @@ async function terminal_default(app2) {
24382
24702
  if (!s2) return { ok: false, code: 404, msg: "not found" };
24383
24703
  const repoDir = await resolveRepoDir(s2.projectId);
24384
24704
  if (!repoDir) return { ok: false, code: 409, msg: "project belum punya repoDir" };
24385
- if (!s2.cwd.includes("/.worktrees/") || !existsSync7(s2.cwd))
24705
+ if (!s2.cwd.includes("/.worktrees/") || !existsSync8(s2.cwd))
24386
24706
  return { ok: false, code: 409, msg: "belum ada worktree untuk di-review \u2014 jalankan/lanjutkan sesi dulu" };
24387
24707
  return { ok: true, id: s2.id, repoDir };
24388
24708
  };
@@ -24474,8 +24794,8 @@ async function terminal_default(app2) {
24474
24794
  // src/routes/vps.ts
24475
24795
  init_db();
24476
24796
  init_src();
24477
- import { readFileSync as readFileSync9, existsSync as existsSync10 } from "node:fs";
24478
- import { homedir as homedir5 } from "node:os";
24797
+ import { readFileSync as readFileSync10, existsSync as existsSync11 } from "node:fs";
24798
+ import { homedir as homedir6 } from "node:os";
24479
24799
 
24480
24800
  // src/vps/catalog/catalog.data.ts
24481
24801
  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 }];
@@ -24557,14 +24877,14 @@ var _map = new Map(CATALOG.map((c) => [c.id, c]));
24557
24877
  var byId = (id) => _map.get(id);
24558
24878
 
24559
24879
  // src/services/vps-remediate.ts
24560
- import { readFileSync as readFileSync7 } from "node:fs";
24880
+ import { readFileSync as readFileSync8 } from "node:fs";
24561
24881
 
24562
24882
  // src/services/vps-ssh.ts
24563
24883
  init_config2();
24564
24884
  import { spawn as spawn3 } from "node:child_process";
24565
24885
  import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
24566
24886
  import { tmpdir as tmpdir3 } from "node:os";
24567
- import { dirname as dirname10, join as join6 } from "node:path";
24887
+ import { dirname as dirname10, join as join7 } from "node:path";
24568
24888
  var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
24569
24889
  function consoleArgv(t) {
24570
24890
  return [
@@ -24579,8 +24899,8 @@ function consoleArgv(t) {
24579
24899
  ];
24580
24900
  }
24581
24901
  function askpassScript() {
24582
- const dir2 = mkdtempSync(join6(tmpdir3(), "hanoman-askpass-"));
24583
- const path = join6(dir2, "askpass.sh");
24902
+ const dir2 = mkdtempSync(join7(tmpdir3(), "hanoman-askpass-"));
24903
+ const path = join7(dir2, "askpass.sh");
24584
24904
  writeFileSync4(path, `#!/bin/sh
24585
24905
  printf '%s' "$HANOMAN_SSH_PASSWORD"
24586
24906
  `, { mode: 448 });
@@ -24640,16 +24960,16 @@ function sshExec(t, remoteCmd, opts = {}) {
24640
24960
 
24641
24961
  // src/services/vps-audit.ts
24642
24962
  init_db();
24643
- import { readFileSync as readFileSync6 } from "node:fs";
24644
- import { join as join8 } from "node:path";
24963
+ import { readFileSync as readFileSync7 } from "node:fs";
24964
+ import { join as join9 } from "node:path";
24645
24965
 
24646
24966
  // src/runner/deps.ts
24647
- import { existsSync as existsSync8 } from "node:fs";
24648
- import { dirname as dirname11, join as join7 } from "node:path";
24967
+ import { existsSync as existsSync9 } from "node:fs";
24968
+ import { dirname as dirname11, join as join8 } from "node:path";
24649
24969
  function repoRootFrom(startDir) {
24650
24970
  let dir2 = startDir;
24651
24971
  for (let i = 0; i < 8; i++) {
24652
- if (existsSync8(join7(dir2, "pnpm-workspace.yaml"))) return dir2;
24972
+ if (existsSync9(join8(dir2, "pnpm-workspace.yaml"))) return dir2;
24653
24973
  const parent = dirname11(dir2);
24654
24974
  if (parent === dir2) break;
24655
24975
  dir2 = parent;
@@ -24754,7 +25074,7 @@ function parseHealth(out3) {
24754
25074
  return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
24755
25075
  }
24756
25076
  var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
24757
- var scriptPath = (f) => join8(repoRoot(), "server", "scripts", "vps", f);
25077
+ var scriptPath = (f) => join9(repoRoot(), "server", "scripts", "vps", f);
24758
25078
  async function itemStatesOf(vpsId) {
24759
25079
  const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
24760
25080
  return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
@@ -24763,7 +25083,7 @@ async function runAudit(v) {
24763
25083
  const r = await sshExec(
24764
25084
  target(v),
24765
25085
  "sudo -n bash -s",
24766
- { stdin: readFileSync6(scriptPath("audit.sh"), "utf8"), timeoutMs: 6e4 }
25086
+ { stdin: readFileSync7(scriptPath("audit.sh"), "utf8"), timeoutMs: 6e4 }
24767
25087
  );
24768
25088
  const audit = parseAudit(r.out);
24769
25089
  if (r.code !== 0 || audit.length === 0) return { ok: false, out: r.out };
@@ -24822,7 +25142,7 @@ async function remediate(v, items, dryRun) {
24822
25142
  const r = await sshExec(
24823
25143
  v,
24824
25144
  `sudo -n env ITEMS=${items.join(",")} SSH_PORT=${v.port} DRY_RUN=${dryRun ? "1" : ""} bash -s`,
24825
- { stdin: readFileSync7(scriptPath("remediate.sh"), "utf8"), timeoutMs: 3e5 }
25145
+ { stdin: readFileSync8(scriptPath("remediate.sh"), "utf8"), timeoutMs: 3e5 }
24826
25146
  );
24827
25147
  return { ok: r.code === 0, steps: parseSteps(r.out), out: r.out };
24828
25148
  }
@@ -24901,15 +25221,15 @@ async function buildChecklist(vpsId) {
24901
25221
  // src/services/vps-key.ts
24902
25222
  init_config2();
24903
25223
  import { execFileSync as execFileSync2 } from "node:child_process";
24904
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync8 } from "node:fs";
24905
- import { homedir as homedir4 } from "node:os";
24906
- import { join as join9 } from "node:path";
24907
- var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join9(homedir4(), ".hanoman");
25224
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
25225
+ import { homedir as homedir5 } from "node:os";
25226
+ import { join as join10 } from "node:path";
25227
+ var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? join10(homedir5(), ".hanoman");
24908
25228
  function ensureHanomanKey() {
24909
25229
  const dir2 = keyDir();
24910
- const privPath = join9(dir2, "id_ed25519");
25230
+ const privPath = join10(dir2, "id_ed25519");
24911
25231
  const pubPath = `${privPath}.pub`;
24912
- if (!existsSync9(privPath)) {
25232
+ if (!existsSync10(privPath)) {
24913
25233
  mkdirSync8(dir2, { recursive: true, mode: 448 });
24914
25234
  execFileSync2(
24915
25235
  "ssh-keygen",
@@ -24917,7 +25237,7 @@ function ensureHanomanKey() {
24917
25237
  { stdio: ["ignore", "ignore", "pipe"] }
24918
25238
  );
24919
25239
  }
24920
- return { privPath, pubPath, pub: readFileSync8(pubPath, "utf8").trim() };
25240
+ return { privPath, pubPath, pub: readFileSync9(pubPath, "utf8").trim() };
24921
25241
  }
24922
25242
 
24923
25243
  // src/services/vps-bootstrap.ts
@@ -24936,7 +25256,7 @@ async function bootstrapKey(t, password) {
24936
25256
  init_pty();
24937
25257
  init_settings2();
24938
25258
  function keyMissing(v) {
24939
- return !!v.keyPath && !existsSync10(v.keyPath);
25259
+ return !!v.keyPath && !existsSync11(v.keyPath);
24940
25260
  }
24941
25261
  async function vps_default(app2) {
24942
25262
  app2.get("/vps", async () => prisma.vps.findMany({ orderBy: { createdAt: "asc" } }));
@@ -25094,7 +25414,7 @@ async function vps_default(app2) {
25094
25414
  const r = await sshExec(
25095
25415
  v,
25096
25416
  `sudo -n env SSH_PORT=${v.port} SSH_USER=${v.user} bash -s`,
25097
- { stdin: readFileSync9(scriptPath("harden.sh"), "utf8"), timeoutMs: 3e5 }
25417
+ { stdin: readFileSync10(scriptPath("harden.sh"), "utf8"), timeoutMs: 3e5 }
25098
25418
  );
25099
25419
  if (r.code !== 0) return reply.code(502).send({ error: "harden gagal lewat ssh", transcript: r.out });
25100
25420
  const verify = await sshExec(v, "true", { timeoutMs: 3e4 });
@@ -25119,7 +25439,7 @@ async function vps_default(app2) {
25119
25439
  const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
25120
25440
  if (!v) return reply.code(404).send({ error: "not found" });
25121
25441
  if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
25122
- const s2 = createSession(`vps-console:${v.id}`, homedir5(), { id: `vpsc-${v.id}`, command: consoleArgv(v) });
25442
+ const s2 = createSession(`vps-console:${v.id}`, homedir6(), { id: `vpsc-${v.id}`, command: consoleArgv(v) });
25123
25443
  return reply.code(201).send({ id: s2.id });
25124
25444
  });
25125
25445
  app2.post("/vps/:id/session", async (req, reply) => {
@@ -25128,7 +25448,7 @@ async function vps_default(app2) {
25128
25448
  if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
25129
25449
  const checks = v.audit ?? [];
25130
25450
  const { model, effort } = await sessionModel();
25131
- const s2 = createSession(`vps:${v.id}`, homedir5(), {
25451
+ const s2 = createSession(`vps:${v.id}`, homedir6(), {
25132
25452
  model,
25133
25453
  effort,
25134
25454
  prompt: [
@@ -25146,9 +25466,9 @@ async function vps_default(app2) {
25146
25466
  // src/services/limits.ts
25147
25467
  init_config2();
25148
25468
  import { execFileSync as execFileSync3 } from "node:child_process";
25149
- import { readFileSync as readFileSync10 } from "node:fs";
25150
- import { homedir as homedir6 } from "node:os";
25151
- import { join as join10 } from "node:path";
25469
+ import { readFileSync as readFileSync11 } from "node:fs";
25470
+ import { homedir as homedir7 } from "node:os";
25471
+ import { join as join11 } from "node:path";
25152
25472
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
25153
25473
  var TTL_MS2 = 3e4;
25154
25474
  var lastOk = null;
@@ -25161,7 +25481,7 @@ var LABELS = {
25161
25481
  var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
25162
25482
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
25163
25483
  function credsFile() {
25164
- return join10(effectiveStr("CLAUDE_CONFIG_DIR") ?? join10(homedir6(), ".claude"), ".credentials.json");
25484
+ return join11(effectiveStr("CLAUDE_CONFIG_DIR") ?? join11(homedir7(), ".claude"), ".credentials.json");
25165
25485
  }
25166
25486
  function readAccessToken() {
25167
25487
  if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
@@ -25177,7 +25497,7 @@ function readAccessToken() {
25177
25497
  }
25178
25498
  }
25179
25499
  try {
25180
- return JSON.parse(readFileSync10(credsFile(), "utf8"))?.claudeAiOauth?.accessToken ?? null;
25500
+ return JSON.parse(readFileSync11(credsFile(), "utf8"))?.claudeAiOauth?.accessToken ?? null;
25181
25501
  } catch {
25182
25502
  return null;
25183
25503
  }
@@ -25234,15 +25554,15 @@ async function getLimits() {
25234
25554
 
25235
25555
  // src/services/codex-limits.ts
25236
25556
  import { open as open2, readdir as readdir2, stat } from "node:fs/promises";
25237
- import { homedir as homedir7 } from "node:os";
25238
- import { join as join11 } from "node:path";
25557
+ import { homedir as homedir8 } from "node:os";
25558
+ import { join as join12 } from "node:path";
25239
25559
  var TTL_MS3 = 3e4;
25240
25560
  var STALE_AFTER_MS = 12 * 36e5;
25241
25561
  var TAIL_BYTES = 512 * 1024;
25242
25562
  var MAX_FILES = 8;
25243
25563
  var cache2 = null;
25244
25564
  var freshUntil2 = 0;
25245
- var codexSessionsDir = () => join11(process.env.CODEX_HOME ?? join11(homedir7(), ".codex"), "sessions");
25565
+ var codexSessionsDir = () => join12(process.env.CODEX_HOME ?? join12(homedir8(), ".codex"), "sessions");
25246
25566
  var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
25247
25567
  function windowLabel(minutes) {
25248
25568
  if (minutes === 300) return "Sesi 5 jam";
@@ -25275,7 +25595,7 @@ async function recentRollouts(dir2) {
25275
25595
  return [];
25276
25596
  }
25277
25597
  const stamped = await Promise.all(entries.map(async (rel) => {
25278
- const full = join11(dir2, rel);
25598
+ const full = join12(dir2, rel);
25279
25599
  try {
25280
25600
  return { full, mtime: (await stat(full)).mtimeMs };
25281
25601
  } catch {
@@ -25391,13 +25711,18 @@ async function codex(app2) {
25391
25711
  app2.get("/codex/version", async () => codexVersionInfo());
25392
25712
  }
25393
25713
 
25714
+ // src/routes/methods.ts
25715
+ async function methods(app2) {
25716
+ app2.get("/methods/status", async () => methodStatusReport());
25717
+ }
25718
+
25394
25719
  // src/routes/update.ts
25395
25720
  init_src();
25396
25721
 
25397
25722
  // src/services/update.ts
25398
25723
  init_src();
25399
25724
  init_config2();
25400
- import { readFileSync as readFileSync11 } from "node:fs";
25725
+ import { readFileSync as readFileSync12 } from "node:fs";
25401
25726
  import { resolve as resolve12, dirname as dirname12 } from "node:path";
25402
25727
  import { fileURLToPath as fileURLToPath3 } from "node:url";
25403
25728
  var UPDATE_COMMAND = "npm i -g hanoman@latest";
@@ -25426,7 +25751,7 @@ function runningVersion() {
25426
25751
  const here = dirname12(fileURLToPath3(import.meta.url));
25427
25752
  for (const p of [resolve12(here, "build-info.json"), resolve12(here, "../package.json")]) {
25428
25753
  try {
25429
- const v = JSON.parse(readFileSync11(p, "utf8")).version;
25754
+ const v = JSON.parse(readFileSync12(p, "utf8")).version;
25430
25755
  if (typeof v === "string" && v) return v;
25431
25756
  } catch {
25432
25757
  }
@@ -25774,7 +26099,7 @@ init_conflicts();
25774
26099
  init_config2();
25775
26100
  import { randomUUID as randomUUID3 } from "node:crypto";
25776
26101
  import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile4, unlink } from "node:fs/promises";
25777
- import { join as join12, resolve as resolve13 } from "node:path";
26102
+ import { join as join13, resolve as resolve13 } from "node:path";
25778
26103
  var EXT = {
25779
26104
  "image/png": ".png",
25780
26105
  "image/jpeg": ".jpg",
@@ -25784,22 +26109,22 @@ function extFor(mimeType) {
25784
26109
  return EXT[mimeType] ?? ".bin";
25785
26110
  }
25786
26111
  function uploadDir() {
25787
- return resolve13(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join12(process.cwd(), "data", "uploads"));
26112
+ return resolve13(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join13(process.cwd(), "data", "uploads"));
25788
26113
  }
25789
26114
  async function saveUpload(buf, mimeType) {
25790
26115
  const dir2 = uploadDir();
25791
26116
  await mkdir2(dir2, { recursive: true });
25792
26117
  const storageKey = `${randomUUID3()}${extFor(mimeType)}`;
25793
- await writeFile2(join12(dir2, storageKey), buf);
26118
+ await writeFile2(join13(dir2, storageKey), buf);
25794
26119
  return { storageKey, size: buf.length };
25795
26120
  }
25796
26121
  async function readUpload(storageKey) {
25797
26122
  const safe = storageKey.replace(/[/\\]/g, "");
25798
- return readFile4(join12(uploadDir(), safe));
26123
+ return readFile4(join13(uploadDir(), safe));
25799
26124
  }
25800
26125
  async function readUploadOrFetch(storageKey) {
25801
26126
  const safe = storageKey.replace(/[/\\]/g, "");
25802
- const target2 = join12(uploadDir(), safe);
26127
+ const target2 = join13(uploadDir(), safe);
25803
26128
  try {
25804
26129
  return await readFile4(target2);
25805
26130
  } catch {
@@ -25818,7 +26143,7 @@ async function readUploadOrFetch(storageKey) {
25818
26143
  }
25819
26144
  async function deleteUpload(storageKey) {
25820
26145
  const safe = storageKey.replace(/[/\\]/g, "");
25821
- await unlink(join12(uploadDir(), safe)).catch(() => {
26146
+ await unlink(join13(uploadDir(), safe)).catch(() => {
25822
26147
  });
25823
26148
  }
25824
26149
 
@@ -25949,10 +26274,10 @@ import { randomUUID as randomUUID5 } from "node:crypto";
25949
26274
  init_config2();
25950
26275
  import { randomUUID as randomUUID4 } from "node:crypto";
25951
26276
  import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile5, unlink as unlink2 } from "node:fs/promises";
25952
- import { join as join13, resolve as resolve14, basename as basename2 } from "node:path";
26277
+ import { join as join14, resolve as resolve14, basename as basename2 } from "node:path";
25953
26278
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
25954
26279
  function transcriptDir() {
25955
- return resolve14(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join13(process.cwd(), "data", "transcripts"));
26280
+ return resolve14(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join14(process.cwd(), "data", "transcripts"));
25956
26281
  }
25957
26282
  function clamp(text) {
25958
26283
  const buf = Buffer.from(text, "utf8");
@@ -25970,20 +26295,20 @@ async function saveTranscript(text) {
25970
26295
  const dir2 = transcriptDir();
25971
26296
  await mkdir3(dir2, { recursive: true });
25972
26297
  const key = `${randomUUID4()}.log`;
25973
- await writeFile3(join13(dir2, key), body, "utf8");
26298
+ await writeFile3(join14(dir2, key), body, "utf8");
25974
26299
  return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
25975
26300
  }
25976
26301
  async function readTranscript(key) {
25977
26302
  if (!key) return null;
25978
26303
  try {
25979
- return await readFile5(join13(transcriptDir(), basename2(key)), "utf8");
26304
+ return await readFile5(join14(transcriptDir(), basename2(key)), "utf8");
25980
26305
  } catch {
25981
26306
  return null;
25982
26307
  }
25983
26308
  }
25984
26309
  async function deleteTranscript(key) {
25985
26310
  if (!key) return;
25986
- await unlink2(join13(transcriptDir(), basename2(key))).catch(() => {
26311
+ await unlink2(join14(transcriptDir(), basename2(key))).catch(() => {
25987
26312
  });
25988
26313
  }
25989
26314
 
@@ -26406,7 +26731,7 @@ init_db();
26406
26731
 
26407
26732
  // src/services/ticket-accept.ts
26408
26733
  init_db();
26409
- import { join as join14 } from "node:path";
26734
+ import { join as join15 } from "node:path";
26410
26735
  async function materializeAttachments(atts) {
26411
26736
  const ready = /* @__PURE__ */ new Set();
26412
26737
  for (const a of atts) {
@@ -26421,7 +26746,7 @@ async function materializeAttachments(atts) {
26421
26746
  var attachmentInstruction = (t, atts, ready) => {
26422
26747
  if (atts.length === 0) return "Tanpa lampiran.";
26423
26748
  const list2 = atts.map(
26424
- (a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${join14(uploadDir(), a.storageKey)}` : `- ${a.filename} (${a.mimeType}) \u2192 BELUM TERUNDUH, ambil via GET /api/tickets/${t.id}/attachments/${a.id}`
26749
+ (a) => ready.has(a.id) ? `- ${a.filename} (${a.mimeType}) \u2192 ${join15(uploadDir(), a.storageKey)}` : `- ${a.filename} (${a.mimeType}) \u2192 BELUM TERUNDUH, ambil via GET /api/tickets/${t.id}/attachments/${a.id}`
26425
26750
  ).join("\n");
26426
26751
  return `LAMPIRAN (${atts.length}) dari pelapor \u2014 biasanya screenshot yang menunjukkan masalah. PERIKSA setiap lampiran untuk memahami konteks keluhan sebelum bekerja; jangan berasumsi dari teks saja. Berkas ada di direktori upload server (baca langsung dengan tool Read):
26427
26752
  ${list2}
@@ -27112,7 +27437,7 @@ function leadPrompt(q, c) {
27112
27437
  lines2.push(`Anggarkan begini: pakai paling banyak separuh waktu untuk mengumpulkan bukti, lalu **berhenti membaca** dan tulis jawabannya dengan apa yang sudah kamu punya. Kalau buktinya jadi tipis karena itu, turunkan \`confidence\`-nya \u2014 jangan menambah waktu baca.`);
27113
27438
  lines2.push("");
27114
27439
  lines2.push("## Cara kerja");
27115
- lines2.push("1. KUMPULKAN BUKTI DULU sebelum memutuskan, DI DALAM anggaran waktu di atas: `internal/docs/**` (Source of Truth) dan index-nya, ADR yang relevan, plan `docs/superpowers/plans/**`, kode yang bersangkutan, dan riwayat git. Baca, jangan mengingat \u2014 tapi baca seperlunya, bukan sehabisnya.");
27440
+ lines2.push(`1. KUMPULKAN BUKTI DULU sebelum memutuskan, DI DALAM anggaran waktu di atas: \`internal/docs/**\` (Source of Truth) dan index-nya, ADR yang relevan, plan ${PLAN_DIRS.map((d) => `\`${d}/**\``).join(" / ")}, kode yang bersangkutan, dan riwayat git. Baca, jangan mengingat \u2014 tapi baca seperlunya, bukan sehabisnya.`);
27116
27441
  lines2.push('2. Putuskan. Kalau setelah membaca kamu masih ragu, TETAP putuskan: pilih opsi yang PALING MUDAH DIBATALKAN, lalu tandai `confidence: "ragu"`. "Tidak tahu" bukan jawaban, dan meminta manusia memutuskan adalah persis keadaan yang kamu ada untuk menghapusnya. SATU pengecualian: bila jawabannya menuntut fakta konkret yang memang TIDAK ADA di repo maupun di konteks ini, isi `missing` dengan daftar pendek hal yang kurang \u2014 hal yang bisa disediakan seseorang, bukan keluhan \u2014 dan tetap tulis `decision` sebagai langkah paling aman sementara. `missing` yang terisi memaksa `confidence` jadi `ragu` dan memanggil operator; jangan memakainya untuk bukti yang cuma tipis.');
27117
27442
  lines2.push("3. Rujuk bukti yang BENAR-BENAR kamu baca. Rujukan berupa path berkas relatif terhadap checkout, nomor ADR (`ADR-0091`), atau sha commit. Rujukan yang tak ada di repo akan dibuang server dan membuat jawabanmu tampak tanpa dasar.");
27118
27443
  lines2.push("4. JANGAN membaca atau mengutip kredensial (isi `.env*`, token, kunci privat). Jejak keputusan disimpan di basis data; rahasia tak boleh mendarat di sana.");
@@ -27147,7 +27472,7 @@ function leadPrompt(q, c) {
27147
27472
 
27148
27473
  // src/services/lead/verdict.ts
27149
27474
  init_src();
27150
- import { existsSync as existsSync11 } from "node:fs";
27475
+ import { existsSync as existsSync12 } from "node:fs";
27151
27476
  import { isAbsolute as isAbsolute3, resolve as resolvePath, relative } from "node:path";
27152
27477
  function extractJsonBlock(raw) {
27153
27478
  const re = /```json\s*\n([\s\S]*?)```/g;
@@ -27185,7 +27510,7 @@ function keepExistingRefs(refs2, repoDir) {
27185
27510
  const full = resolvePath(repoDir, ref);
27186
27511
  const rel = relative(repoDir, full);
27187
27512
  if (rel.startsWith("..")) continue;
27188
- if (existsSync11(full)) out3.push(ref);
27513
+ if (existsSync12(full)) out3.push(ref);
27189
27514
  }
27190
27515
  return [...new Set(out3)];
27191
27516
  }
@@ -28783,7 +29108,7 @@ init_db();
28783
29108
  init_db();
28784
29109
  import { execFile as execFile10 } from "node:child_process";
28785
29110
  import { promisify as promisify9 } from "node:util";
28786
- import { existsSync as existsSync12 } from "node:fs";
29111
+ import { existsSync as existsSync13 } from "node:fs";
28787
29112
 
28788
29113
  // src/services/changelog/scrub.ts
28789
29114
  var MERGE = /^merge\s+(branch|pull request|remote-tracking branch|tag)\b/i;
@@ -28850,7 +29175,7 @@ var US2 = "";
28850
29175
  var RS = "";
28851
29176
  var NO_REPO = "project ini belum ditautkan ke repo di mesin ini";
28852
29177
  var NO_TAG = "repo project ini belum punya tag rilis";
28853
- var usable = (repoDir) => !!repoDir && existsSync12(repoDir);
29178
+ var usable = (repoDir) => !!repoDir && existsSync13(repoDir);
28854
29179
  async function listTags(repoDir) {
28855
29180
  if (!usable(repoDir)) return { tags: [], head: null, reason: NO_REPO };
28856
29181
  try {
@@ -29111,13 +29436,13 @@ init_db();
29111
29436
 
29112
29437
  // src/services/agent-tool-catalog.ts
29113
29438
  init_src();
29114
- import { readFileSync as readFileSync12 } from "node:fs";
29115
- import { homedir as homedir8 } from "node:os";
29116
- import { join as join15 } from "node:path";
29117
- var home = () => process.env.HOME || homedir8();
29118
- var readJson = (path) => {
29439
+ import { readFileSync as readFileSync13 } from "node:fs";
29440
+ import { homedir as homedir9 } from "node:os";
29441
+ import { join as join16 } from "node:path";
29442
+ var home = () => process.env.HOME || homedir9();
29443
+ var readJson2 = (path) => {
29119
29444
  try {
29120
- return JSON.parse(readFileSync12(path, "utf8"));
29445
+ return JSON.parse(readFileSync13(path, "utf8"));
29121
29446
  } catch {
29122
29447
  return null;
29123
29448
  }
@@ -29130,7 +29455,7 @@ var serversOf = (node) => {
29130
29455
  var codexServers = () => {
29131
29456
  let text;
29132
29457
  try {
29133
- text = readFileSync12(join15(home(), ".codex", "config.toml"), "utf8");
29458
+ text = readFileSync13(join16(home(), ".codex", "config.toml"), "utf8");
29134
29459
  } catch {
29135
29460
  return [];
29136
29461
  }
@@ -29143,12 +29468,12 @@ var codexServers = () => {
29143
29468
  };
29144
29469
  function mcpServerNames(repoDir) {
29145
29470
  const names = [];
29146
- const claudeJson = readJson(join15(home(), ".claude.json"));
29471
+ const claudeJson = readJson2(join16(home(), ".claude.json"));
29147
29472
  names.push(...serversOf(claudeJson));
29148
29473
  if (repoDir) {
29149
29474
  const projects = claudeJson?.projects;
29150
29475
  if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
29151
- names.push(...serversOf(readJson(join15(repoDir, ".mcp.json"))));
29476
+ names.push(...serversOf(readJson2(join16(repoDir, ".mcp.json"))));
29152
29477
  }
29153
29478
  names.push(...codexServers());
29154
29479
  return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
@@ -31034,7 +31359,7 @@ var PUBLIC = /* @__PURE__ */ new Set([
31034
31359
  "GET /api/agent-integration.md"
31035
31360
  ]);
31036
31361
  function buildApp({ requireAuth = true, agentDocFile } = {}) {
31037
- const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync13);
31362
+ const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync14);
31038
31363
  const app2 = Fastify({ logger: false, trustProxy: true });
31039
31364
  app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
31040
31365
  if (!body) return done(null, void 0);
@@ -31114,6 +31439,7 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
31114
31439
  await api.register(tickets_default);
31115
31440
  await api.register(scheduler_default);
31116
31441
  await api.register(codex);
31442
+ await api.register(methods);
31117
31443
  await api.register(lead_default);
31118
31444
  await api.register(custom_agents_default);
31119
31445
  await api.register(githubIssues);
@@ -31124,7 +31450,7 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
31124
31450
  await api.register(client_accounts_default);
31125
31451
  }, { prefix: "/api" });
31126
31452
  if (process.env.NODE_ENV === "production") {
31127
- const dist = pickWebDir(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync13);
31453
+ const dist = pickWebDir(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync14);
31128
31454
  if (dist) {
31129
31455
  app2.register(fastifyStatic, { root: dist });
31130
31456
  app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));