hanoman 0.9.18 → 0.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +63 -167
  3. package/dist/server.js +418 -639
  4. package/docs/agent-integration.md +9 -53
  5. package/package.json +1 -1
  6. package/prisma/migrations/20261007120000_project_memory/migration.sql +54 -0
  7. package/prisma/migrations/20261007130000_memory_sync/migration.sql +32 -0
  8. package/prisma/migrations/20261010070000_remove_memory_changelog/migration.sql +40 -0
  9. package/prisma/schema.prisma +3 -25
  10. package/web/assets/{DalangHanomanScreen-C1Ivdj4r.js → DalangHanomanScreen-DqzYitwv.js} +1 -1
  11. package/web/assets/{DocPreviewModal-Cw69hQk1.js → DocPreviewModal-Chk-jQ51.js} +1 -1
  12. package/web/assets/{DocsWorkspace--9rvckps.js → DocsWorkspace-DuNVIjTg.js} +1 -1
  13. package/web/assets/{IdeScreen-DxNG9eLb.js → IdeScreen-BZGVyFT5.js} +1 -1
  14. package/web/assets/{LeadScreen-CV3Lgbyd.js → LeadScreen-35VgddUo.js} +1 -1
  15. package/web/assets/{ReviewScreen-JLPOS52f.js → ReviewScreen-EkG8ywTU.js} +1 -1
  16. package/web/assets/{SchedulerScreen-UhxWcCtY.js → SchedulerScreen-ClaLLB8Y.js} +1 -1
  17. package/web/assets/SettingsScreen-liOlVCeT.js +62 -0
  18. package/web/assets/{TeamScreen-D24DsEwn.js → TeamScreen-Dzl6Ruju.js} +1 -1
  19. package/web/assets/{TerminalScreen-3nSnkwTy.js → TerminalScreen-CK3SzzgP.js} +1 -1
  20. package/web/assets/{TriageScreen-Dw6EqWar.js → TriageScreen-EtBRNg-t.js} +1 -1
  21. package/web/assets/{VpsScreen-C_rNhMi5.js → VpsScreen-9qeTHo1w.js} +1 -1
  22. package/web/assets/index-D5w059cP.js +1063 -0
  23. package/web/assets/{live-DHIFZVo0.js → live-Bz-jmiWI.js} +1 -1
  24. package/web/index.html +1 -1
  25. package/web/assets/ChangelogScreen-Bj1KreGh.js +0 -1
  26. package/web/assets/SettingsScreen-D3nVVnsZ.js +0 -62
  27. package/web/assets/index-DHVdBKua.js +0 -1053
package/dist/server.js CHANGED
@@ -4273,13 +4273,14 @@ function stampSpecMethod(payload, methodId) {
4273
4273
  if (typeof payload !== "object" || Array.isArray(payload)) return null;
4274
4274
  return { ...payload, method: methodId };
4275
4275
  }
4276
- var DEFAULT_METHOD, VERIFICATION_GATE, MATT_CLAUSE, METHODS, METHOD_IDS, uniq, PLAN_DIRS, SPEC_DIRS;
4276
+ var DEFAULT_METHOD, VERIFICATION_GATE, MATT_CLAUSE, AGENT_SKILLS_CLAUSE, METHODS, METHOD_IDS, uniq, PLAN_DIRS, SPEC_DIRS;
4277
4277
  var init_method_catalog = __esm({
4278
4278
  "../shared/src/method-catalog.ts"() {
4279
4279
  "use strict";
4280
4280
  DEFAULT_METHOD = "superpowers";
4281
4281
  VERIFICATION_GATE = "superpowers:verification-before-completion";
4282
4282
  MATT_CLAUSE = "Skill mattpocock yang kontraknya mewawancarai manusia (`/grill-me`, `/to-spec`, `/triage`) JANGAN dipakai: pakai primitif model-invoked-nya untuk menguji rancanganmu sendiri, bukan untuk mewawancarai siapa pun. Hal yang masih ambigu dan akan mempengaruhi hasil tetap JANGAN diputuskan sendiri \u2014 bila kamu agen fase, daftarkan di `Keputusan terbuka:` laporanmu; bila tidak, ajukan lewat jalur tanya di klausa otonomi prompt ini. `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.";
4283
+ AGENT_SKILLS_CLAUSE = "Pakai skill Agent Skills langsung sesuai fase, bukan slash-command atau meta-router `using-agent-skills`. Kontrak fase, instruksi project/pengguna, dan jalur keputusan/review hanoman tetap berlaku: `spec-driven-development` hanya menyusun spec pada fase Spec, jangan menjalankan Plan atau Execute dari fase itu. Simpan spec di `docs/agent-skills/specs/` dan plan beserta SELURUH checklist task `- [ ]` di `docs/agent-skills/plans/`, dengan id backlog pada nama berkas; ini menggantikan default `SPEC.md`, `tasks/plan.md`, dan `tasks/todo.md` upstream. Jangan menimpa plan lain yang belum tuntas atau membuat tiket di tracker eksternal: hanoman adalah issue tracker-nya. Centang task di berkas plan yang sama setelah implementasi dan verifikasi. Hal ambigu yang mempengaruhi hasil mengikuti jalur tanya prompt ini; bila kamu agen fase, daftarkan di `Keputusan terbuka:` laporanmu.";
4283
4284
  METHODS = {
4284
4285
  // Isi `PHASE_SKILLS` (SPEC-166) dipindah APA ADANYA. Objective & Spec adalah keluaran skill
4285
4286
  // brainstorming yang di-invoke di fase Brainstorm — sengaja tak punya entri sendiri. Fase reverse
@@ -4360,6 +4361,44 @@ var init_method_catalog = __esm({
4360
4361
  "codex plugin add superpowers@openai-curated"
4361
4362
  ]
4362
4363
  }
4364
+ },
4365
+ "agent-skills": {
4366
+ id: "agent-skills",
4367
+ label: "Agent Skills (Addy Osmani)",
4368
+ planDir: "docs/agent-skills/plans",
4369
+ specDir: "docs/agent-skills/specs",
4370
+ phaseSkills: {
4371
+ Brainstorm: ["agent-skills:idea-refine"],
4372
+ Spec: ["agent-skills:spec-driven-development"],
4373
+ Audit: [
4374
+ "agent-skills:debugging-and-error-recovery",
4375
+ "agent-skills:code-review-and-quality"
4376
+ ],
4377
+ Plan: ["agent-skills:planning-and-task-breakdown"],
4378
+ Execute: [
4379
+ "agent-skills:incremental-implementation",
4380
+ "agent-skills:test-driven-development",
4381
+ "agent-skills:code-review-and-quality"
4382
+ ],
4383
+ Verifikasi: [VERIFICATION_GATE]
4384
+ },
4385
+ exitSkills: [VERIFICATION_GATE],
4386
+ extraClause: AGENT_SKILLS_CLAUSE,
4387
+ requires: ["agent-skills", "superpowers"],
4388
+ // Manifest upstream: marketplace Claude = addy-agent-skills, Codex = agent-skills.
4389
+ install: {
4390
+ claude: [
4391
+ "claude plugin marketplace add addyosmani/agent-skills",
4392
+ "claude plugin install agent-skills@addy-agent-skills",
4393
+ "claude plugin marketplace add obra/superpowers-marketplace",
4394
+ "claude plugin install superpowers@superpowers-marketplace"
4395
+ ],
4396
+ codex: [
4397
+ "codex plugin marketplace add addyosmani/agent-skills",
4398
+ "codex plugin add agent-skills@agent-skills",
4399
+ "codex plugin add superpowers@openai-curated"
4400
+ ]
4401
+ }
4363
4402
  }
4364
4403
  };
4365
4404
  METHOD_IDS = Object.keys(METHODS);
@@ -5055,7 +5094,7 @@ function cmpVersion(a, b) {
5055
5094
  }
5056
5095
  return 0;
5057
5096
  }
5058
- var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, zSpec, zSpecListItem, zSpecSlim, NOTIFY_SOUNDS, EFFORTS, MODELS, CLAUDE_DEFAULT_ALIAS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zPortalChat, PORTAL_CHAT_DEFAULTS, zPhaseCell, PHASE_OVERRIDE_LIMITS, zPhaseOverride, zPhaseOverrides, zFlowOrchestration, zOrchestration, ORCHESTRATION_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
5097
+ var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, zSpec, zSpecListItem, zSpecSlim, NOTIFY_SOUNDS, EFFORTS, MODELS, CLAUDE_DEFAULT_ALIAS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zPortalChat, PORTAL_CHAT_DEFAULTS, zPhaseCell, PHASE_OVERRIDE_LIMITS, zPhaseOverride, zPhaseOverrides, zFlowOrchestration, zOrchestration, ORCHESTRATION_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
5059
5098
  var init_entities = __esm({
5060
5099
  "../shared/src/entities.ts"() {
5061
5100
  "use strict";
@@ -5247,7 +5286,6 @@ var init_entities = __esm({
5247
5286
  effort: external_exports.string().default("xhigh")
5248
5287
  });
5249
5288
  CONFLICT_DEFAULTS = zConflict.parse({});
5250
- CHANGELOG_ENGINE_DEFAULTS = zAgentEngine.parse({});
5251
5289
  zLeadEngine = zAgentEngine;
5252
5290
  zLead = external_exports.object({
5253
5291
  enabled: external_exports.boolean().default(false),
@@ -5368,8 +5406,6 @@ var init_entities = __esm({
5368
5406
  // SPEC-409 · ADR-0091 · hanoman-lead (default mati)
5369
5407
  telegram: zTelegramSettings.default(TELEGRAM_DEFAULTS),
5370
5408
  // SPEC-476 · ADR-0096 · gateway Telegram (default mati)
5371
- changelog: zAgentEngine.default(CHANGELOG_ENGINE_DEFAULTS),
5372
- // SPEC-518 · agen pembuat changelog (opt-in, mati)
5373
5409
  portalChat: zPortalChat.default(PORTAL_CHAT_DEFAULTS),
5374
5410
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
5375
5411
  orchestration: zOrchestration.default(ORCHESTRATION_DEFAULTS),
@@ -8191,6 +8227,7 @@ var init_api = __esm({
8191
8227
  qaAttachments: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/attachments`,
8192
8228
  qaAttachment: (pid, rid, aid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/attachments/${encodeURIComponent(aid)}`,
8193
8229
  qaFindingBacklog: (pid, rid, fid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/findings/${encodeURIComponent(fid)}/backlog`,
8230
+ qaFindingSession: (pid, rid, fid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/findings/${encodeURIComponent(fid)}/session`,
8194
8231
  qaReportBacklog: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/backlog`,
8195
8232
  qaCasesImport: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/cases/import`,
8196
8233
  qaImport: (pid) => `${API}/projects/${encodeURIComponent(pid)}/qa/import`,
@@ -8241,10 +8278,6 @@ var init_api = __esm({
8241
8278
  // SPEC-471 · ADR-0095 · tarik & triase issue GitHub (di belakang gate cookie, capability `support`).
8242
8279
  // hanoman TIDAK PERNAH menulis ke GitHub — tak ada path komentar/close di sini, dan itu disengaja.
8243
8280
  githubPull: (id2) => `${API}/projects/${encodeURIComponent(id2)}/github/pull`,
8244
- // SPEC-516 · ADR-0105 · changelog per project
8245
- changelog: (id2) => `${API}/projects/${encodeURIComponent(id2)}/changelog`,
8246
- changelogSources: (id2) => `${API}/projects/${encodeURIComponent(id2)}/changelog/sources`,
8247
- changelogItem: (id2, cid) => `${API}/projects/${encodeURIComponent(id2)}/changelog/${encodeURIComponent(cid)}`,
8248
8281
  githubIssues: (id2) => `${API}/projects/${encodeURIComponent(id2)}/github/issues`,
8249
8282
  githubIssuesAccept: `${API}/github-issues/accept`,
8250
8283
  githubIssueAccept: (id2) => `${API}/github-issues/${encodeURIComponent(id2)}/accept`,
@@ -10819,7 +10852,7 @@ var init_docs = __esm({
10819
10852
  required: ["project", "prd"]
10820
10853
  }),
10821
10854
  // `breakdown` hidup di berkas route docs.ts, TAPI `capabilityForRoute` memetakan sub-path
10822
- // `projects/:id/*` yang tak terdaftar ke `projects:*` — hanya `docs`, `prds`, dan `changelog`
10855
+ // `projects/:id/*` yang tak terdaftar ke `projects:*` — hanya `docs` dan `prds`
10823
10856
  // yang dipetakan ke `docs:*`. Menuliskannya `docs:read` membuat uji kontrak merah, dan itulah
10824
10857
  // yang terjadi saat tool ini pertama ditulis.
10825
10858
  mode: "read",
@@ -10832,116 +10865,6 @@ var init_docs = __esm({
10832
10865
  query: query({ prd: s(a.prd) })
10833
10866
  }),
10834
10867
  shape: (raw) => raw
10835
- },
10836
- {
10837
- name: "hanoman_changelog_sources",
10838
- title: "Sumber changelog yang tersedia",
10839
- description: "Bahan mentah yang bisa dipakai membuat changelog: daftar tag git, HEAD, dan rentang backlog yang sudah selesai. Panggil ini SEBELUM hanoman_changelog_create \u2014 ia memberitahu mode mana yang mungkin (`hasRepo: false` berarti mode commit & version tak akan jalan) dan `defaultRange` yang masuk akal.",
10840
- inputSchema: obj({ properties: { project: PROJECT }, required: ["project"] }),
10841
- mode: "read",
10842
- capability: "docs:read",
10843
- samplePath: "/projects/hanoman/changelog/sources",
10844
- sampleMethod: "GET",
10845
- build: (a) => ({ method: "GET", path: `/projects/${enc(String(a.project))}/changelog/sources` }),
10846
- shape: (raw) => raw
10847
- },
10848
- {
10849
- name: "hanoman_changelog_list",
10850
- title: "Daftar changelog project",
10851
- description: "Changelog yang sudah dibuat untuk sebuah project, terbaru dulu, berhalaman. `q` menyaring judul & isi.",
10852
- inputSchema: obj({
10853
- properties: {
10854
- project: PROJECT,
10855
- q: str("Kata kunci penyaring judul & isi changelog. Kosongkan untuk semua."),
10856
- page: int("Halaman, mulai 1.", { minimum: 1 }),
10857
- limit: int("Item per halaman.", { minimum: 1, maximum: 200 })
10858
- },
10859
- required: ["project"]
10860
- }),
10861
- mode: "read",
10862
- capability: "docs:read",
10863
- samplePath: "/projects/hanoman/changelog",
10864
- sampleMethod: "GET",
10865
- build: (a) => ({
10866
- method: "GET",
10867
- path: `/projects/${enc(String(a.project))}/changelog`,
10868
- query: query({ q: s(a.q), page: n(a.page)?.toString(), limit: n(a.limit)?.toString() })
10869
- }),
10870
- shape: (raw) => raw
10871
- },
10872
- {
10873
- name: "hanoman_changelog_get",
10874
- title: "Baca satu changelog",
10875
- description: "Satu entri changelog beserta isinya, dicari menurut id di dalam project tersebut.",
10876
- inputSchema: obj({
10877
- properties: { project: PROJECT, changelog: str("Id changelog, dari hanoman_changelog_list.") },
10878
- required: ["project", "changelog"]
10879
- }),
10880
- mode: "read",
10881
- capability: "docs:read",
10882
- samplePath: "/projects/hanoman/changelog/c1",
10883
- sampleMethod: "GET",
10884
- build: (a) => ({ method: "GET", path: `/projects/${enc(String(a.project))}/changelog/${enc(String(a.changelog))}` }),
10885
- shape: (raw) => raw
10886
- },
10887
- {
10888
- name: "hanoman_changelog_create",
10889
- title: "Buat changelog",
10890
- description: "Membuat changelog baru dari salah satu dari tiga sumber. `mode` menentukan field mana yang WAJIB menyertainya: `backlog` memakai rentang tanggal (`from`/`to`, boleh kosong = seluruhnya), `commit` memakai dua sha, `version` memakai tag. Server menjawab 422 bila sumbernya tak tersedia \u2014 panggil hanoman_changelog_sources lebih dulu.",
10891
- inputSchema: obj({
10892
- properties: {
10893
- project: PROJECT,
10894
- mode: enumStr(["backlog", "commit", "version"], "Sumber changelog. Field yang menyertainya ditentukan nilai ini."),
10895
- from: str("Mode `backlog`: tanggal awal `YYYY-MM-DD`. Kosongkan untuk tanpa batas awal."),
10896
- to: str("Mode `backlog`: tanggal akhir `YYYY-MM-DD`. Harus >= `from`."),
10897
- fromSha: str("Mode `commit`: sha awal, minimal 4 karakter."),
10898
- toSha: str("Mode `commit`: sha akhir, minimal 4 karakter."),
10899
- fromTag: str("Mode `version`: tag awal. Kosongkan untuk mulai dari tag terawal."),
10900
- toTag: str("Mode `version`: tag akhir. WAJIB.")
10901
- },
10902
- required: ["project", "mode"],
10903
- // Skema ketat DI KLIEN: validator SDK menolak pasangan yang salah sebelum permintaan lahir,
10904
- // jadi agen dibimbing ke panggilan yang sah alih-alih menemukannya lewat 400 (ADR-0099 #2).
10905
- allOf: [
10906
- {
10907
- if: { properties: { mode: { const: "backlog" } }, required: ["mode"] },
10908
- then: { properties: { from: { type: "string" }, to: { type: "string" } } }
10909
- },
10910
- {
10911
- if: { properties: { mode: { const: "commit" } }, required: ["mode"] },
10912
- then: { required: ["fromSha", "toSha"] }
10913
- },
10914
- {
10915
- if: { properties: { mode: { const: "version" } }, required: ["mode"] },
10916
- then: { required: ["toTag"] }
10917
- }
10918
- ]
10919
- }),
10920
- mode: "write",
10921
- capability: "docs:write",
10922
- samplePath: "/projects/hanoman/changelog",
10923
- sampleMethod: "POST",
10924
- build: (a) => {
10925
- const mode = String(a.mode);
10926
- const body = mode === "commit" ? { mode, fromSha: String(a.fromSha), toSha: String(a.toSha) } : mode === "version" ? { mode, ...s(a.fromTag) ? { fromTag: s(a.fromTag) } : {}, toTag: String(a.toTag) } : { mode, ...s(a.from) ? { from: s(a.from) } : {}, ...s(a.to) ? { to: s(a.to) } : {} };
10927
- return { method: "POST", path: `/projects/${enc(String(a.project))}/changelog`, body };
10928
- },
10929
- shape: (raw) => raw
10930
- },
10931
- {
10932
- name: "hanoman_changelog_delete",
10933
- title: "Hapus changelog (BERBAHAYA)",
10934
- description: "BERBAHAYA \u2014 menghapus satu entri changelog secara permanen. Isinya tak tersimpan di tempat lain: changelog lahir dari generator dan tak bisa dipulihkan tanpa membuatnya ulang. Hanya muncul saat tingkat `--danger` menyala.",
10935
- inputSchema: obj({
10936
- properties: { project: PROJECT, changelog: str("Id changelog yang akan dihapus.") },
10937
- required: ["project", "changelog"]
10938
- }),
10939
- mode: "danger",
10940
- capability: "docs:write",
10941
- samplePath: "/projects/hanoman/changelog/c1",
10942
- sampleMethod: "DELETE",
10943
- build: (a) => ({ method: "DELETE", path: `/projects/${enc(String(a.project))}/changelog/${enc(String(a.changelog))}` }),
10944
- shape: (raw) => raw
10945
10868
  }
10946
10869
  ];
10947
10870
  }
@@ -13679,6 +13602,18 @@ var init_qa2 = __esm({
13679
13602
  build: (a) => ({ method: "PATCH", path: `${one(a)}/findings/${enc(String(a.finding))}`, body: findingBody(a) }),
13680
13603
  shape: (raw) => raw
13681
13604
  },
13605
+ {
13606
+ name: "hanoman_qa_finding_start_session",
13607
+ title: "Kerjakan temuan QA langsung",
13608
+ description: "Membuka sesi agen terisolasi dari temuan QA tanpa membuat backlog, membawa langkah uji dan lampiran. Memerlukan qa:write DAN sessions:write. Klik ulang memakai sesi aktif yang sama. Temuan wontfix atau sudah terkait backlog ditolak; laporan closed boleh.",
13609
+ inputSchema: obj({ properties: { project: PROJECT3, report: REPORT, finding: FINDING }, required: ["project", "report", "finding"] }),
13610
+ mode: "write",
13611
+ capability: "qa:write",
13612
+ samplePath: "/projects/p1/qa/reports/r1/findings/f1/session",
13613
+ sampleMethod: "POST",
13614
+ build: (a) => ({ method: "POST", path: `${one(a)}/findings/${enc(String(a.finding))}/session`, body: {} }),
13615
+ shape: (raw) => raw
13616
+ },
13682
13617
  {
13683
13618
  name: "hanoman_qa_finding_to_backlog",
13684
13619
  title: "Kirim temuan QA ke backlog",
@@ -13839,7 +13774,7 @@ var init_mcp = __esm({
13839
13774
  init_mcp_shape();
13840
13775
  init_mcp_catalog();
13841
13776
  init_mcp_catalog();
13842
- MCP_TOOL_SCHEMA_VERSION = 1;
13777
+ MCP_TOOL_SCHEMA_VERSION = 2;
13843
13778
  MCP_INSTRUCTIONS = [
13844
13779
  `hanoman \u2014 orchestrator backlog + dashboard. Skema tool versi ${MCP_TOOL_SCHEMA_VERSION}.`,
13845
13780
  "",
@@ -13852,56 +13787,6 @@ var init_mcp = __esm({
13852
13787
  }
13853
13788
  });
13854
13789
 
13855
- // ../shared/src/changelog.ts
13856
- function dayString(d) {
13857
- const p3 = (n2) => String(n2).padStart(2, "0");
13858
- return `${d.getFullYear()}-${p3(d.getMonth() + 1)}-${p3(d.getDate())}`;
13859
- }
13860
- function defaultRange(today) {
13861
- const from = new Date(today.getFullYear(), today.getMonth(), today.getDate() - (DEFAULT_RANGE_DAYS - 1));
13862
- return { from: dayString(from), to: dayString(today) };
13863
- }
13864
- function changelogMatches(row, q2) {
13865
- const needle = (q2 ?? "").trim().toLowerCase();
13866
- if (!needle) return true;
13867
- return row.title.toLowerCase().includes(needle) || row.body.toLowerCase().includes(needle) || row.mode.toLowerCase().includes(needle);
13868
- }
13869
- var CHANGELOG_MODES, zChangelogMode, zDay, zChangelogRequest, zChangelogView, DEFAULT_RANGE_DAYS;
13870
- var init_changelog = __esm({
13871
- "../shared/src/changelog.ts"() {
13872
- "use strict";
13873
- init_zod();
13874
- CHANGELOG_MODES = ["backlog", "commit", "version"];
13875
- zChangelogMode = external_exports.enum(CHANGELOG_MODES);
13876
- zDay = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "format tanggal harus YYYY-MM-DD");
13877
- zChangelogRequest = external_exports.discriminatedUnion("mode", [
13878
- external_exports.object({ mode: external_exports.literal("backlog"), from: zDay.optional(), to: zDay.optional() }),
13879
- external_exports.object({ mode: external_exports.literal("commit"), fromSha: external_exports.string().trim().min(4), toSha: external_exports.string().trim().min(4) }),
13880
- external_exports.object({ mode: external_exports.literal("version"), fromTag: external_exports.string().trim().min(1).optional(), toTag: external_exports.string().trim().min(1) })
13881
- ]).superRefine((v, ctx) => {
13882
- if (v.mode === "backlog" && v.from && v.to && v.from > v.to)
13883
- ctx.addIssue({
13884
- code: external_exports.ZodIssueCode.custom,
13885
- path: ["from"],
13886
- message: "rentang tanggal terbalik \u2014 `from` harus lebih awal atau sama dengan `to`"
13887
- });
13888
- });
13889
- zChangelogView = external_exports.object({
13890
- id: external_exports.string(),
13891
- projectId: external_exports.string(),
13892
- mode: zChangelogMode,
13893
- title: external_exports.string(),
13894
- params: external_exports.unknown(),
13895
- body: external_exports.string(),
13896
- generator: external_exports.enum(["agent", "fallback"]),
13897
- warning: external_exports.string().nullable(),
13898
- itemCount: external_exports.number().int(),
13899
- createdAt: external_exports.string()
13900
- });
13901
- DEFAULT_RANGE_DAYS = 30;
13902
- }
13903
- });
13904
-
13905
13790
  // ../shared/src/terminal-workspace.ts
13906
13791
  var zSessionId, zTrackSizes, zTerminalGroup, zTerminalWorkspaceV1, zTerminalWorkspaceWrite, zTerminalWorkspaceSnapshot;
13907
13792
  var init_terminal_workspace = __esm({
@@ -14350,7 +14235,6 @@ var init_src = __esm({
14350
14235
  init_mcp();
14351
14236
  init_auto_merge();
14352
14237
  init_handled_by();
14353
- init_changelog();
14354
14238
  init_cron_expr();
14355
14239
  init_terminal_workspace();
14356
14240
  init_terminal_io();
@@ -23445,8 +23329,8 @@ var require_webidl = __commonJS({
23445
23329
  return new Uint8Array(0);
23446
23330
  }
23447
23331
  const bytes = new Uint8Array(length);
23448
- const view13 = new Uint8Array(jsArrayBuffer, offset, length);
23449
- bytes.set(view13);
23332
+ const view12 = new Uint8Array(jsArrayBuffer, offset, length);
23333
+ bytes.set(view12);
23450
23334
  return bytes;
23451
23335
  };
23452
23336
  webidl.converters.DOMString = function(V, prefix, argument, flags) {
@@ -31855,7 +31739,7 @@ var require_mock_interceptor = __commonJS({
31855
31739
  var require_mock_client = __commonJS({
31856
31740
  "../node_modules/.pnpm/undici@7.29.1/node_modules/undici/lib/mock/mock-client.js"(exports, module) {
31857
31741
  "use strict";
31858
- var { promisify: promisify18 } = __require("node:util");
31742
+ var { promisify: promisify17 } = __require("node:util");
31859
31743
  var Client = require_client();
31860
31744
  var { buildMockDispatch } = require_mock_utils();
31861
31745
  var {
@@ -31903,7 +31787,7 @@ var require_mock_client = __commonJS({
31903
31787
  this[kDispatches] = [];
31904
31788
  }
31905
31789
  async [kClose]() {
31906
- await promisify18(this[kOriginalClose])();
31790
+ await promisify17(this[kOriginalClose])();
31907
31791
  this[kConnected] = 0;
31908
31792
  this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
31909
31793
  }
@@ -32116,7 +32000,7 @@ var require_mock_call_history = __commonJS({
32116
32000
  var require_mock_pool = __commonJS({
32117
32001
  "../node_modules/.pnpm/undici@7.29.1/node_modules/undici/lib/mock/mock-pool.js"(exports, module) {
32118
32002
  "use strict";
32119
- var { promisify: promisify18 } = __require("node:util");
32003
+ var { promisify: promisify17 } = __require("node:util");
32120
32004
  var Pool = require_pool();
32121
32005
  var { buildMockDispatch } = require_mock_utils();
32122
32006
  var {
@@ -32164,7 +32048,7 @@ var require_mock_pool = __commonJS({
32164
32048
  this[kDispatches] = [];
32165
32049
  }
32166
32050
  async [kClose]() {
32167
- await promisify18(this[kOriginalClose])();
32051
+ await promisify17(this[kOriginalClose])();
32168
32052
  this[kConnected] = 0;
32169
32053
  this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
32170
32054
  }
@@ -48932,25 +48816,25 @@ var init_lib4 = __esm({
48932
48816
  });
48933
48817
 
48934
48818
  // ../node_modules/.pnpm/uint8array-extras@1.5.0/node_modules/uint8array-extras/index.js
48935
- function getUintBE(view13) {
48936
- const { byteLength } = view13;
48819
+ function getUintBE(view12) {
48820
+ const { byteLength } = view12;
48937
48821
  if (byteLength === 6) {
48938
- return view13.getUint16(0) * 2 ** 32 + view13.getUint32(2);
48822
+ return view12.getUint16(0) * 2 ** 32 + view12.getUint32(2);
48939
48823
  }
48940
48824
  if (byteLength === 5) {
48941
- return view13.getUint8(0) * 2 ** 32 + view13.getUint32(1);
48825
+ return view12.getUint8(0) * 2 ** 32 + view12.getUint32(1);
48942
48826
  }
48943
48827
  if (byteLength === 4) {
48944
- return view13.getUint32(0);
48828
+ return view12.getUint32(0);
48945
48829
  }
48946
48830
  if (byteLength === 3) {
48947
- return view13.getUint8(0) * 2 ** 16 + view13.getUint16(1);
48831
+ return view12.getUint8(0) * 2 ** 16 + view12.getUint16(1);
48948
48832
  }
48949
48833
  if (byteLength === 2) {
48950
- return view13.getUint16(0);
48834
+ return view12.getUint16(0);
48951
48835
  }
48952
48836
  if (byteLength === 1) {
48953
- return view13.getUint8(0);
48837
+ return view12.getUint8(0);
48954
48838
  }
48955
48839
  }
48956
48840
  var cachedDecoders, cachedEncoder, byteToHexLookupTable;
@@ -51784,6 +51668,7 @@ var init_qa_attachment_sync = __esm({
51784
51668
  var sync_exports = {};
51785
51669
  __export(sync_exports, {
51786
51670
  BOOTSTRAP_ORDER: () => BOOTSTRAP_ORDER,
51671
+ OPTIONAL_ENTITIES: () => OPTIONAL_ENTITIES,
51787
51672
  PARENTS: () => PARENTS,
51788
51673
  PULL_MAX_BYTES: () => PULL_MAX_BYTES,
51789
51674
  QA_SYNC_MIMES: () => QA_SYNC_MIMES,
@@ -51792,12 +51677,14 @@ __export(sync_exports, {
51792
51677
  __FIELDS: () => __FIELDS,
51793
51678
  __FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
51794
51679
  __JSON_FIELDS: () => __JSON_FIELDS,
51680
+ acceptedOptional: () => acceptedOptional,
51795
51681
  applyPush: () => applyPush,
51796
51682
  backfillFeed: () => backfillFeed,
51797
51683
  bootstrapSnapshot: () => bootstrapSnapshot,
51798
51684
  consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
51799
51685
  deleteRow: () => deleteRow,
51800
51686
  isEntity: () => isEntity,
51687
+ isOptionalEntity: () => isOptionalEntity,
51801
51688
  publishDelete: () => publishDelete,
51802
51689
  publishLocal: () => publishLocal,
51803
51690
  pull: () => pull,
@@ -51807,6 +51694,10 @@ __export(sync_exports, {
51807
51694
  upsertLocal: () => upsertLocal,
51808
51695
  validateSyncData: () => validateSyncData
51809
51696
  });
51697
+ function acceptedOptional(raw) {
51698
+ const want = typeof raw === "string" ? raw.split(",").map((x) => x.trim()) : [];
51699
+ return new Set(OPTIONAL_ENTITIES.filter((e) => want.includes(e)));
51700
+ }
51810
51701
  function validateSyncData(entity, data, options2 = {}) {
51811
51702
  const allowed = new Set(FIELDS[entity]);
51812
51703
  if (entity === "project" && options2.allowProjectRename) allowed.add("renamedFrom");
@@ -51953,17 +51844,23 @@ async function applyPush(entity, id2, baseVersion, data, deviceId, op = "upsert"
51953
51844
  function recordBytes(rec) {
51954
51845
  return Buffer.byteLength(JSON.stringify(rec));
51955
51846
  }
51956
- async function pull(sinceCursor, limit = 500, maxBytes = PULL_MAX_BYTES) {
51847
+ async function pull(sinceCursor, limit = 500, maxBytes = PULL_MAX_BYTES, opts2 = {}) {
51957
51848
  const since = Number(sinceCursor || "0");
51958
51849
  const rows = await prisma.syncLog.findMany({
51959
51850
  where: { seq: { gt: since } },
51960
51851
  orderBy: { seq: "asc" },
51961
51852
  take: limit
51962
51853
  });
51854
+ const accept = opts2.accept ?? /* @__PURE__ */ new Set();
51963
51855
  const records = [];
51964
51856
  let bytes = 0;
51965
51857
  let trimmed = false;
51858
+ let consumed = null;
51966
51859
  for (const r of rows) {
51860
+ if (isOptionalEntity(r.entity) && !accept.has(r.entity)) {
51861
+ consumed = r.seq;
51862
+ continue;
51863
+ }
51967
51864
  const rec = {
51968
51865
  entity: r.entity,
51969
51866
  recordId: r.recordId,
@@ -51978,11 +51875,12 @@ async function pull(sinceCursor, limit = 500, maxBytes = PULL_MAX_BYTES) {
51978
51875
  }
51979
51876
  bytes += size;
51980
51877
  records.push(rec);
51878
+ consumed = r.seq;
51981
51879
  }
51982
- const cursor = records.length ? String(rows[records.length - 1].seq) : sinceCursor || "0";
51983
- return { cursor, records, hasMore: trimmed || rows.length === limit };
51880
+ const cursor = consumed !== null ? String(consumed) : sinceCursor || "0";
51881
+ return { cursor, records, hasMore: trimmed || rows.length === limit, entities: [...OPTIONAL_ENTITIES] };
51984
51882
  }
51985
- async function bootstrapSnapshot(after, maxBytes = PULL_MAX_BYTES) {
51883
+ async function bootstrapSnapshot(after, maxBytes = PULL_MAX_BYTES, opts2 = {}) {
51986
51884
  const tip = await prisma.syncLog.findFirst({ orderBy: { seq: "desc" }, select: { seq: true } });
51987
51885
  const cursor = tip ? String(tip.seq) : "0";
51988
51886
  const sep9 = after ? after.indexOf(":") : -1;
@@ -51995,6 +51893,8 @@ async function bootstrapSnapshot(after, maxBytes = PULL_MAX_BYTES) {
51995
51893
  let last2 = null;
51996
51894
  for (let i = startAt; i < BOOTSTRAP_ORDER.length; i++) {
51997
51895
  const entity = BOOTSTRAP_ORDER[i];
51896
+ if (isOptionalEntity(entity) && !(opts2.accept ?? /* @__PURE__ */ new Set()).has(entity)) continue;
51897
+ if (opts2.only && !opts2.only.has(entity)) continue;
51998
51898
  const gt = i === startAt && afterId ? afterId : null;
51999
51899
  const rows = await DELEGATE[entity].findMany({
52000
51900
  ...gt ? { where: { id: { gt } } } : {},
@@ -52013,14 +51913,14 @@ async function bootstrapSnapshot(after, maxBytes = PULL_MAX_BYTES) {
52013
51913
  };
52014
51914
  const size = recordBytes(rec);
52015
51915
  if (records.length && bytes + size > maxBytes) {
52016
- return { cursor, records, hasMore: true, next: last2 };
51916
+ return { cursor, records, hasMore: true, next: last2, entities: [...OPTIONAL_ENTITIES] };
52017
51917
  }
52018
51918
  bytes += size;
52019
51919
  records.push(rec);
52020
51920
  last2 = `${entity}:${row.id}`;
52021
51921
  }
52022
51922
  }
52023
- return { cursor, records, hasMore: false, next: null };
51923
+ return { cursor, records, hasMore: false, next: null, entities: [...OPTIONAL_ENTITIES] };
52024
51924
  }
52025
51925
  async function publishLocal(entity, id2) {
52026
51926
  const snap = await snapshot(entity, id2);
@@ -52114,7 +52014,7 @@ async function upsertLocal(entity, id2, version, data) {
52114
52014
  function setAcceptedHook(hook) {
52115
52015
  onAccepted = hook;
52116
52016
  }
52117
- var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, NULLABLE_NUMBER_FIELDS, FLOAT_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, QA_SYNC_MIMES, PULL_MAX_BYTES, BOOTSTRAP_ORDER, onAccepted, __FIELDS_FOR_TEST;
52017
+ var SYNCED, OPTIONAL_ENTITIES, isOptionalEntity, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, NULLABLE_NUMBER_FIELDS, FLOAT_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, QA_SYNC_MIMES, PULL_MAX_BYTES, BOOTSTRAP_ORDER, onAccepted, __FIELDS_FOR_TEST;
52118
52018
  var init_sync = __esm({
52119
52019
  "src/services/sync.ts"() {
52120
52020
  "use strict";
@@ -52123,6 +52023,8 @@ var init_sync = __esm({
52123
52023
  init_tombstone();
52124
52024
  init_qa_attachment_sync();
52125
52025
  SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue", "member", "task", "qaReport", "qaCase", "qaFinding", "qaAttachment"];
52026
+ OPTIONAL_ENTITIES = [];
52027
+ isOptionalEntity = (e) => OPTIONAL_ENTITIES.includes(e);
52126
52028
  DELEGATE = {
52127
52029
  project: prisma.project,
52128
52030
  spec: prisma.spec,
@@ -52151,7 +52053,7 @@ var init_sync = __esm({
52151
52053
  // SPEC-447 · ADR-0093 · dependsOn ikut juga: tanpa itu client tak tahu urutannya dan akan
52152
52054
  // meluncurkan pekerjaan yang di hub terblokir. Bukan DATE_FIELDS — nilainya array string.
52153
52055
  // SPEC-516 · ADR-0105 · doneAt ikut menyeberang — cermin createdAt/startedAt. Tanpa ini spec
52154
- // asal-hub mendarat di tiap client dengan doneAt null tanpa satu pun error, dan changelog
52056
+ // asal-hub mendarat di tiap client dengan doneAt null tanpa satu pun error, dan riwayat selesai
52155
52057
  // mode backlog di client itu selamanya kosong.
52156
52058
  // SPEC-546 · ADR-0109 · sourceHistory ikut menyeberang: jejak konversi type adalah bagian
52157
52059
  // keadaan yang harus dilihat sama oleh semua mesin. `upsert` yang tak menyebut sebuah kolom
@@ -52467,8 +52369,6 @@ var init_settings3 = __esm({
52467
52369
  // SPEC-409 · ADR-0091 · hanoman-lead (master switch mati)
52468
52370
  telegram: TELEGRAM_DEFAULTS,
52469
52371
  // SPEC-476 · ADR-0096 · gateway Telegram opt-in
52470
- changelog: CHANGELOG_ENGINE_DEFAULTS,
52471
- // SPEC-518 · agen pembuat changelog (opt-in, mati)
52472
52372
  portalChat: PORTAL_CHAT_DEFAULTS,
52473
52373
  // SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
52474
52374
  orchestration: ORCHESTRATION_DEFAULTS,
@@ -53494,7 +53394,10 @@ async function verifyAgentToken(token) {
53494
53394
  if (a.length !== b.length || !timingSafeEqual2(a, b)) return null;
53495
53395
  await prisma.agentToken.update({ where: { id: row.id }, data: { lastUsedAt: /* @__PURE__ */ new Date() } }).catch(() => {
53496
53396
  });
53497
- return { id: row.id, capabilities: Array.isArray(row.capabilities) ? row.capabilities : [] };
53397
+ return {
53398
+ id: row.id,
53399
+ capabilities: Array.isArray(row.capabilities) ? row.capabilities : []
53400
+ };
53498
53401
  }
53499
53402
  async function listAgentTokens() {
53500
53403
  const rows = await prisma.agentToken.findMany({ orderBy: { createdAt: "desc" } });
@@ -53651,8 +53554,8 @@ async function telegramEngineContext() {
53651
53554
  };
53652
53555
  }
53653
53556
  function telegramReloadNeeded(before2, after) {
53654
- const strip2 = ({ engine: _engine, ...rest }) => rest;
53655
- return JSON.stringify(strip2(before2)) !== JSON.stringify(strip2(after));
53557
+ const strip = ({ engine: _engine, ...rest }) => rest;
53558
+ return JSON.stringify(strip(before2)) !== JSON.stringify(strip(after));
53656
53559
  }
53657
53560
  var init_config4 = __esm({
53658
53561
  "src/services/telegram/config.ts"() {
@@ -54907,8 +54810,18 @@ async function uploadPendingQaBytes() {
54907
54810
  const hub = hubOf();
54908
54811
  if (!hub) return out4;
54909
54812
  const queued3 = new Set((await listOutbox()).filter((o) => o.entity === "qaAttachment").map((o) => o.recordId));
54910
- const rows = await prisma.qaAttachment.findMany({ where: { syncState: "local-only" }, orderBy: [{ createdAt: "asc" }, { id: "asc" }], take: UPLOAD_BATCH + queued3.size });
54813
+ const pendingRows = (after) => prisma.qaAttachment.findMany({
54814
+ where: { syncState: "local-only", id: { gt: after } },
54815
+ orderBy: { id: "asc" },
54816
+ take: UPLOAD_BATCH + queued3.size
54817
+ });
54818
+ let rows = await pendingRows(uploadCursor);
54819
+ if (!rows.length && uploadCursor) {
54820
+ uploadCursor = "";
54821
+ rows = await pendingRows("");
54822
+ }
54911
54823
  for (const a of rows.filter((r) => !queued3.has(r.id)).slice(0, UPLOAD_BATCH)) {
54824
+ uploadCursor = a.id;
54912
54825
  const bytes = QA_STORAGE_KEY.test(a.storageKey) ? await readUpload(a.storageKey).catch(() => null) : null;
54913
54826
  if (!bytes) {
54914
54827
  await prisma.qaAttachment.update({ where: { id: a.id }, data: { syncState: "failed" } });
@@ -54939,7 +54852,28 @@ async function uploadPendingQaBytes() {
54939
54852
  }
54940
54853
  return out4;
54941
54854
  }
54942
- var hubOf, endpoint, isLoopback, inflight2, UPLOAD_BATCH;
54855
+ async function downloadPendingQaBytes() {
54856
+ if (!hubOf()) return { downloaded: 0, pending: 0 };
54857
+ let rows = await prisma.qaAttachment.findMany({
54858
+ where: { syncState: "remote", id: { gt: downloadCursor } },
54859
+ orderBy: { id: "asc" },
54860
+ take: 5
54861
+ });
54862
+ if (!rows.length && downloadCursor) {
54863
+ downloadCursor = "";
54864
+ rows = await prisma.qaAttachment.findMany({ where: { syncState: "remote" }, orderBy: { id: "asc" }, take: 5 });
54865
+ }
54866
+ let downloaded = 0;
54867
+ for (const row of rows) {
54868
+ downloadCursor = row.id;
54869
+ if (await readQaAttachmentBytes(row.id)) {
54870
+ await prisma.qaAttachment.updateMany({ where: { id: row.id, syncState: "remote" }, data: { syncState: "available" } });
54871
+ downloaded++;
54872
+ }
54873
+ }
54874
+ return { downloaded, pending: await prisma.qaAttachment.count({ where: { syncState: "remote" } }) };
54875
+ }
54876
+ var hubOf, endpoint, isLoopback, inflight2, UPLOAD_BATCH, uploadCursor, downloadCursor;
54943
54877
  var init_qa_attachment_transfer = __esm({
54944
54878
  "src/services/qa-attachment-transfer.ts"() {
54945
54879
  "use strict";
@@ -54958,6 +54892,8 @@ var init_qa_attachment_transfer = __esm({
54958
54892
  isLoopback = (u) => ["localhost", "127.0.0.1", "::1"].includes(u.hostname);
54959
54893
  inflight2 = /* @__PURE__ */ new Map();
54960
54894
  UPLOAD_BATCH = 5;
54895
+ uploadCursor = "";
54896
+ downloadCursor = "";
54961
54897
  }
54962
54898
  });
54963
54899
 
@@ -55165,7 +55101,7 @@ function capabilityForRoute(method, path) {
55165
55101
  if (top === "projects") {
55166
55102
  const sub = seg[2];
55167
55103
  if (sub === "qa") return rw("qa");
55168
- if (sub === "docs" || sub === "prds" || sub === "changelog") return rw("docs");
55104
+ if (sub === "docs" || sub === "prds") return rw("docs");
55169
55105
  if (sub === "github") return rw("support");
55170
55106
  if (!read2 && sub === "git") return "ide:git";
55171
55107
  if (!read2 && (sub === "branches" || sub === "worktrees") && seg[3] === "delete") return "ide:git";
@@ -55690,7 +55626,7 @@ var require_permessage_deflate2 = __commonJS({
55690
55626
  var Limiter = require_limiter();
55691
55627
  var { kStatusCode } = require_constants6();
55692
55628
  var FastBuffer = Buffer[Symbol.species];
55693
- var TRAILER2 = Buffer.from([0, 0, 255, 255]);
55629
+ var TRAILER = Buffer.from([0, 0, 255, 255]);
55694
55630
  var kPerMessageDeflate = Symbol("permessage-deflate");
55695
55631
  var kTotalLength = Symbol("total-length");
55696
55632
  var kCallback = Symbol("callback");
@@ -55965,7 +55901,7 @@ var require_permessage_deflate2 = __commonJS({
55965
55901
  }
55966
55902
  this._inflate[kCallback] = callback;
55967
55903
  this._inflate.write(data);
55968
- if (fin) this._inflate.write(TRAILER2);
55904
+ if (fin) this._inflate.write(TRAILER);
55969
55905
  this._inflate.flush(() => {
55970
55906
  const err = this._inflate[kError];
55971
55907
  if (err) {
@@ -57781,7 +57717,7 @@ var require_websocket2 = __commonJS({
57781
57717
  var http = __require("http");
57782
57718
  var net = __require("net");
57783
57719
  var tls = __require("tls");
57784
- var { randomBytes: randomBytes12, createHash: createHash19 } = __require("crypto");
57720
+ var { randomBytes: randomBytes12, createHash: createHash20 } = __require("crypto");
57785
57721
  var { Duplex, Readable } = __require("stream");
57786
57722
  var { URL: URL2 } = __require("url");
57787
57723
  var PerMessageDeflate2 = require_permessage_deflate2();
@@ -58449,7 +58385,7 @@ var require_websocket2 = __commonJS({
58449
58385
  abortHandshake(websocket2, socket2, "Invalid Upgrade header");
58450
58386
  return;
58451
58387
  }
58452
- const digest2 = createHash19("sha1").update(key + GUID).digest("base64");
58388
+ const digest2 = createHash20("sha1").update(key + GUID).digest("base64");
58453
58389
  if (res.headers["sec-websocket-accept"] !== digest2) {
58454
58390
  abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
58455
58391
  return;
@@ -58818,7 +58754,7 @@ var require_websocket_server = __commonJS({
58818
58754
  var EventEmitter = __require("events");
58819
58755
  var http = __require("http");
58820
58756
  var { Duplex } = __require("stream");
58821
- var { createHash: createHash19 } = __require("crypto");
58757
+ var { createHash: createHash20 } = __require("crypto");
58822
58758
  var extension2 = require_extension();
58823
58759
  var PerMessageDeflate2 = require_permessage_deflate2();
58824
58760
  var subprotocol2 = require_subprotocol();
@@ -59125,7 +59061,7 @@ var require_websocket_server = __commonJS({
59125
59061
  );
59126
59062
  }
59127
59063
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
59128
- const digest2 = createHash19("sha1").update(key + GUID).digest("base64");
59064
+ const digest2 = createHash20("sha1").update(key + GUID).digest("base64");
59129
59065
  const headers = [
59130
59066
  "HTTP/1.1 101 Switching Protocols",
59131
59067
  "Upgrade: websocket",
@@ -59571,7 +59507,7 @@ var init_shipper = __esm({
59571
59507
  function validateIncomingRecord(input) {
59572
59508
  if (!input || typeof input !== "object") throw new Error("sync record harus object");
59573
59509
  const row = input;
59574
- if (typeof row.entity !== "string" || !isEntity(row.entity)) throw new Error("sync entity tak dikenal");
59510
+ if (typeof row.entity !== "string" || !isEntity(row.entity)) throw new UnknownEntityError(row.entity);
59575
59511
  if (typeof row.recordId !== "string" || !row.recordId || row.recordId.length > 256) throw new Error("sync recordId invalid");
59576
59512
  if (!Number.isSafeInteger(row.version) || Number(row.version) < 0) throw new Error("sync version invalid");
59577
59513
  if (!row.data || typeof row.data !== "object" || Array.isArray(row.data)) throw new Error("sync data invalid");
@@ -59639,7 +59575,11 @@ async function applyFeedFrame(msg) {
59639
59575
  try {
59640
59576
  const record2 = validateIncomingRecord({ ...msg, version: Number(msg.version ?? 0), data: msg.data ?? {} });
59641
59577
  if (record2.op) await applyRemote(record2.entity, record2.recordId, record2.version, record2.data, record2.op);
59642
- } catch {
59578
+ } catch (e) {
59579
+ if (e instanceof UnknownEntityError) {
59580
+ if (msg.seq && !feedHole) await setCursor(String(msg.seq));
59581
+ return true;
59582
+ }
59643
59583
  feedHole = true;
59644
59584
  return false;
59645
59585
  }
@@ -59671,11 +59611,18 @@ async function bootstrapOnce(transport) {
59671
59611
  let cursor = "0";
59672
59612
  let applied = 0;
59673
59613
  for (let page = 0; page < MAX_DRAIN_PAGES; page++) {
59674
- const q2 = after ? `?after=${encodeURIComponent(after)}` : "";
59614
+ const q2 = `?${OPTIONAL_QUERY}${after ? `&after=${encodeURIComponent(after)}` : ""}`;
59675
59615
  const res = await transport("GET", `/api/sync/bootstrap${q2}`);
59676
59616
  if (res.status !== 200 || !Array.isArray(res.body?.records)) return null;
59617
+ if (page === 0) hubOptional = Array.isArray(res.body.entities) ? new Set(res.body.entities) : null;
59677
59618
  for (const raw of res.body.records) {
59678
- const rec = validateIncomingRecord(raw);
59619
+ let rec;
59620
+ try {
59621
+ rec = validateIncomingRecord(raw);
59622
+ } catch (e) {
59623
+ if (e instanceof UnknownEntityError) continue;
59624
+ throw e;
59625
+ }
59679
59626
  if (!rec.op) continue;
59680
59627
  await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
59681
59628
  applied++;
@@ -59686,6 +59633,46 @@ async function bootstrapOnce(transport) {
59686
59633
  after = next;
59687
59634
  }
59688
59635
  await setCursor(cursor);
59636
+ if (hubOptional) await setOptionalMarker([...hubOptional]);
59637
+ return applied;
59638
+ }
59639
+ async function getOptionalMarker() {
59640
+ const st = await prisma.syncState.findUnique({ where: { id: 1 } });
59641
+ return new Set((st?.entities ?? "").split(",").filter(Boolean));
59642
+ }
59643
+ async function setOptionalMarker(entities) {
59644
+ const have = await getOptionalMarker();
59645
+ for (const e of entities) if (isOptionalEntity(String(e))) have.add(String(e));
59646
+ const value = OPTIONAL_ENTITIES.filter((e) => have.has(e)).join(",");
59647
+ await prisma.syncState.upsert({ where: { id: 1 }, create: { id: 1, entities: value }, update: { entities: value } });
59648
+ }
59649
+ async function catchUpOptional(transport) {
59650
+ const have = await getOptionalMarker();
59651
+ const missing = OPTIONAL_ENTITIES.filter((e) => !have.has(e) && hubOptional?.has(e));
59652
+ if (!missing.length) return 0;
59653
+ if (await getCursor() === "0") {
59654
+ await setOptionalMarker(missing);
59655
+ return 0;
59656
+ }
59657
+ const only = missing.join(",");
59658
+ let after = null;
59659
+ let applied = 0;
59660
+ for (let page = 0; page < MAX_DRAIN_PAGES; page++) {
59661
+ const q2 = `${OPTIONAL_QUERY}&only=${only}${after ? `&after=${encodeURIComponent(after)}` : ""}`;
59662
+ const res = await transport("GET", `/api/sync/bootstrap?${q2}`);
59663
+ if (res.status !== 200 || !Array.isArray(res.body?.records)) return applied;
59664
+ for (const raw of res.body.records) {
59665
+ const rec = validateIncomingRecord(raw);
59666
+ if (rec.op) {
59667
+ await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
59668
+ applied++;
59669
+ }
59670
+ }
59671
+ const next = res.body.next ? String(res.body.next) : null;
59672
+ if (!res.body.hasMore || !next) break;
59673
+ after = next;
59674
+ }
59675
+ await setOptionalMarker(missing);
59689
59676
  return applied;
59690
59677
  }
59691
59678
  async function syncOnce(transport) {
@@ -59704,9 +59691,18 @@ async function syncOnce(transport) {
59704
59691
  let deferred = [];
59705
59692
  for (let page = 0; page < MAX_DRAIN_PAGES; page++) {
59706
59693
  const cursor = await getCursor();
59707
- const pullRes = await transport("GET", `/api/sync/pull?since=${cursor}`);
59694
+ const pullRes = await transport("GET", `/api/sync/pull?since=${cursor}&${OPTIONAL_QUERY}`);
59708
59695
  const rawRecords = Array.isArray(pullRes.body?.records) ? pullRes.body.records : [];
59709
- const records = rawRecords.map(validateIncomingRecord);
59696
+ hubOptional = Array.isArray(pullRes.body?.entities) ? new Set(pullRes.body.entities) : null;
59697
+ const records = [];
59698
+ for (const raw of rawRecords) {
59699
+ try {
59700
+ records.push(validateIncomingRecord(raw));
59701
+ } catch (e) {
59702
+ if (!(e instanceof UnknownEntityError)) throw e;
59703
+ stats.dropped++;
59704
+ }
59705
+ }
59710
59706
  for (const rec of records) {
59711
59707
  if (!isEntity(rec.entity)) continue;
59712
59708
  if (!rec.op) {
@@ -59745,6 +59741,7 @@ async function syncOnce(transport) {
59745
59741
  console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan \u2014 dilewati`);
59746
59742
  stats.dropped++;
59747
59743
  }
59744
+ await catchUpOptional(transport).catch((e) => console.warn("sync: catch-up entitas opsional gagal \u2014", e.message));
59748
59745
  for (const item of outbox) {
59749
59746
  if (item.entity === "projectRename") {
59750
59747
  const [oldId, newId] = item.recordId.split(RENAME_SEP);
@@ -59769,6 +59766,7 @@ async function syncOnce(transport) {
59769
59766
  await clearOutbox(item.entity, item.recordId);
59770
59767
  continue;
59771
59768
  }
59769
+ if (isOptionalEntity(item.entity) && !hubOptional?.has(item.entity)) continue;
59772
59770
  const snap = await snapshot(item.entity, item.recordId);
59773
59771
  if (!snap) {
59774
59772
  const tomb = await findTombstone(item.entity, item.recordId);
@@ -59838,6 +59836,11 @@ async function syncOnce(transport) {
59838
59836
  } catch (e) {
59839
59837
  console.warn("sync: unggah byte lampiran QA gagal \u2014", e.message);
59840
59838
  }
59839
+ try {
59840
+ await downloadPendingQaBytes();
59841
+ } catch (e) {
59842
+ console.warn("sync: unduh byte lampiran QA gagal \u2014", e.message);
59843
+ }
59841
59844
  return stats;
59842
59845
  }
59843
59846
  function fetchTransport(base3, token) {
@@ -59916,7 +59919,7 @@ async function startSyncClient(base3, token, tickMs2) {
59916
59919
  const tick6 = () => syncTick(transport);
59917
59920
  const connectWs = async () => {
59918
59921
  const { WebSocket: WebSocket2 } = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
59919
- const wsUrl = base3.replace(/^http/, "ws").replace(/\/$/, "") + "/api/sync/ws";
59922
+ const wsUrl = base3.replace(/^http/, "ws").replace(/\/$/, "") + "/api/sync/ws?" + OPTIONAL_QUERY;
59920
59923
  const sock2 = new WebSocket2(wsUrl, { headers: { authorization: `Bearer ${token}` } });
59921
59924
  ws = sock2;
59922
59925
  sock2.on("open", () => {
@@ -59984,7 +59987,7 @@ async function applySyncConfig() {
59984
59987
  const token = effectiveStr("SYNC_DEVICE_TOKEN");
59985
59988
  if (base3 && token) await startSyncClient(base3, token, effectiveInt("SYNC_TICK_MS"));
59986
59989
  }
59987
- var MAX_SYNC_RECORD_BYTES, feedHole, MAX_DRAIN_PAGES, timer5, ws, started, reconnectTimer, reconnectDelay, presence, pullSehat;
59990
+ var MAX_SYNC_RECORD_BYTES, OPTIONAL_QUERY, hubOptional, UnknownEntityError, feedHole, MAX_DRAIN_PAGES, timer5, ws, started, reconnectTimer, reconnectDelay, presence, pullSehat;
59988
59991
  var init_sync_client = __esm({
59989
59992
  "src/services/sync-client.ts"() {
59990
59993
  "use strict";
@@ -60003,6 +60006,13 @@ var init_sync_client = __esm({
60003
60006
  init_client2();
60004
60007
  init_shipper();
60005
60008
  MAX_SYNC_RECORD_BYTES = 1024 * 1024;
60009
+ OPTIONAL_QUERY = `entities=${OPTIONAL_ENTITIES.join(",")}`;
60010
+ hubOptional = null;
60011
+ UnknownEntityError = class extends Error {
60012
+ constructor(entity) {
60013
+ super(`sync entity tak dikenal: ${String(entity)}`);
60014
+ }
60015
+ };
60006
60016
  feedHole = false;
60007
60017
  MAX_DRAIN_PAGES = 500;
60008
60018
  started = false;
@@ -61980,7 +61990,7 @@ var require_Decoder = __commonJS({
61980
61990
  "../node_modules/.pnpm/@fastify+busboy@3.2.0/node_modules/@fastify/busboy/lib/utils/Decoder.js"(exports, module) {
61981
61991
  "use strict";
61982
61992
  var RE_PLUS = /\+/g;
61983
- var HEX2 = [
61993
+ var HEX = [
61984
61994
  0,
61985
61995
  0,
61986
61996
  0,
@@ -62121,7 +62131,7 @@ var require_Decoder = __commonJS({
62121
62131
  const len = str5.length;
62122
62132
  for (; i < len; ++i) {
62123
62133
  if (this.buffer !== void 0) {
62124
- if (!HEX2[str5.charCodeAt(i)]) {
62134
+ if (!HEX[str5.charCodeAt(i)]) {
62125
62135
  res += "%" + this.buffer;
62126
62136
  this.buffer = void 0;
62127
62137
  --i;
@@ -62542,7 +62552,7 @@ var require_fastify_plugin = __commonJS({
62542
62552
  var require_generateId = __commonJS({
62543
62553
  "../node_modules/.pnpm/@fastify+multipart@10.1.0/node_modules/@fastify/multipart/lib/generateId.js"(exports, module) {
62544
62554
  "use strict";
62545
- var HEX2 = [
62555
+ var HEX = [
62546
62556
  "00",
62547
62557
  "01",
62548
62558
  "02",
@@ -62802,13 +62812,13 @@ var require_generateId = __commonJS({
62802
62812
  ];
62803
62813
  var random = Math.random;
62804
62814
  function seed() {
62805
- return HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0];
62815
+ return HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0];
62806
62816
  }
62807
62817
  module.exports.generateId = function generateIdFn() {
62808
62818
  let num = 0;
62809
62819
  let str5 = seed();
62810
62820
  return function generateId() {
62811
- return num === 255 ? (str5 = seed()) + HEX2[num = 0] : str5 + HEX2[++num];
62821
+ return num === 255 ? (str5 = seed()) + HEX[num = 0] : str5 + HEX[++num];
62812
62822
  };
62813
62823
  }();
62814
62824
  }
@@ -63667,7 +63677,7 @@ import websocket from "@fastify/websocket";
63667
63677
  import cookie from "@fastify/cookie";
63668
63678
  import { fileURLToPath as fileURLToPath5 } from "node:url";
63669
63679
  import { dirname as dirname17 } from "node:path";
63670
- import { existsSync as existsSync23 } from "node:fs";
63680
+ import { existsSync as existsSync22 } from "node:fs";
63671
63681
 
63672
63682
  // src/web-dir.ts
63673
63683
  import { resolve as resolve10 } from "node:path";
@@ -69235,11 +69245,11 @@ ${item.context}` : item.context;
69235
69245
  if (live && parsed.data.confirm !== true)
69236
69246
  return reply.code(409).send({ error: "confirm-required", session: { id: live.id, agent: live.agent } });
69237
69247
  if (!live) {
69238
- const view13 = await presenceView();
69248
+ const view12 = await presenceView();
69239
69249
  const verdict = remoteSessionVerdict({
69240
69250
  specId: id2,
69241
69251
  now: Date.now(),
69242
- devices: view13.devices,
69252
+ devices: view12.devices,
69243
69253
  recentlyOffline: recentlyOffline(id2),
69244
69254
  lastResultDeviceId: null
69245
69255
  });
@@ -71083,7 +71093,7 @@ async function startSpecSession(spec, opts2) {
71083
71093
  return withSessionAdmission({ id: id2, force: opts2.force }, async () => {
71084
71094
  const pane = await getSessionAsync(id2);
71085
71095
  if (!pane) {
71086
- const view13 = await presenceView();
71096
+ const view12 = await presenceView();
71087
71097
  const lastResult = await prisma.sessionResult.findFirst({
71088
71098
  where: { specId: spec.id },
71089
71099
  orderBy: { createdAt: "desc" },
@@ -71093,7 +71103,7 @@ async function startSpecSession(spec, opts2) {
71093
71103
  const verdict = remoteSessionVerdict({
71094
71104
  specId: spec.id,
71095
71105
  now: Date.now(),
71096
- devices: view13.devices,
71106
+ devices: view12.devices,
71097
71107
  recentlyOffline: recentlyOffline(spec.id),
71098
71108
  lastResultDeviceId
71099
71109
  });
@@ -77016,6 +77026,7 @@ function detachSync(c) {
77016
77026
  function broadcastSyncLog(row) {
77017
77027
  const s2 = JSON.stringify({ t: "sync", ...row });
77018
77028
  for (const c of clients2) {
77029
+ if (isOptionalEntity(row.entity) && !c.accept?.has(row.entity)) continue;
77019
77030
  try {
77020
77031
  c.send(s2);
77021
77032
  } catch {
@@ -77156,12 +77167,16 @@ async function requireDeviceWs(req, reply) {
77156
77167
  }
77157
77168
  async function sync_default(app3) {
77158
77169
  app3.get("/sync/pull", { preHandler: requireDeviceToken }, async (req, reply) => {
77159
- const since = req.query.since ?? "0";
77160
- return maybeGzip(req, reply, await pull(since));
77170
+ const q2 = req.query;
77171
+ return maybeGzip(req, reply, await pull(q2.since ?? "0", void 0, void 0, { accept: acceptedOptional(q2.entities) }));
77161
77172
  });
77162
77173
  app3.get("/sync/bootstrap", { preHandler: requireDeviceToken }, async (req, reply) => {
77163
- const after = req.query.after ?? null;
77164
- return maybeGzip(req, reply, await bootstrapSnapshot(after));
77174
+ const q2 = req.query;
77175
+ return maybeGzip(req, reply, await bootstrapSnapshot(q2.after ?? null, void 0, {
77176
+ accept: acceptedOptional(q2.entities),
77177
+ // ADR-0180 · catch-up: hanya entitas opsional yang diminta, tanpa memindahkan kursor client.
77178
+ ...q2.only ? { only: acceptedOptional(q2.only) } : {}
77179
+ }));
77165
77180
  });
77166
77181
  app3.get("/sync/attachments/:storageKey", { preHandler: requireDeviceToken }, async (req, reply) => {
77167
77182
  const { storageKey } = req.params;
@@ -77314,7 +77329,11 @@ async function sync_default(app3) {
77314
77329
  socket2.close(1008, "connection limit");
77315
77330
  return;
77316
77331
  }
77317
- const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
77332
+ const client = {
77333
+ send: (m) => socket2.send(m),
77334
+ close: () => socket2.close(),
77335
+ accept: acceptedOptional(req.query.entities)
77336
+ };
77318
77337
  attachSync(client);
77319
77338
  const unregisterSocket = registerDeviceSocket(principal.id, "sync", socket2);
77320
77339
  const guard = new WsMessageGuard({ perWindow: PRESENCE_MAX_FRAMES_PER_MIN });
@@ -78414,338 +78433,6 @@ async function lead_default(app3) {
78414
78433
  });
78415
78434
  }
78416
78435
 
78417
- // src/routes/changelog.ts
78418
- init_src();
78419
- init_db();
78420
- init_local_binding();
78421
-
78422
- // src/services/changelog/collect.ts
78423
- init_db();
78424
- import { execFile as execFile17 } from "node:child_process";
78425
- import { promisify as promisify16 } from "node:util";
78426
- import { existsSync as existsSync22 } from "node:fs";
78427
-
78428
- // src/services/changelog/scrub.ts
78429
- var MERGE = /^merge\s+(branch|pull request|remote-tracking branch|tag)\b/i;
78430
- var CONVENTIONAL = /^(feat|fix|chore|docs|refactor|test|perf|build|ci|style|revert)(\([^)]*\))?!?:\s*/i;
78431
- var PATH_LIKE = /(?:[\w.@-]+\/)+[\w.-]+\.\w{1,6}/g;
78432
- var HEX = /\b(?=[0-9a-f]{7,40}\b)(?=[0-9a-f]*\d)(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\b/gi;
78433
- var INTERNAL_REF = /\b(?:SPEC|ADR|PR|ISSUE)[-\s]?\d+\b/gi;
78434
- var CAMEL = /\b[a-z]{2,}(?:[A-Z][a-z]{2,})+\b/g;
78435
- var SNAKE = /\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/gi;
78436
- var CALL = /\b[A-Za-z_][\w.]*\(\s*\)/g;
78437
- var INLINE_CODE = /`[^`\n]*`/g;
78438
- var FENCE = /^```[\s\S]*?^```$/gm;
78439
- var TRAILER = /^[A-Za-z-]+:\s.*$/gm;
78440
- var tidy = (s2) => s2.replace(/\(\s*\)/g, " ").replace(/[ \t]{2,}/g, " ").replace(/\s+([,.;:!?])/g, "$1").replace(/^[\s:;,·—–-]+/, "").replace(/[\s:;,·—–-]+$/, "").trim();
78441
- function strip(s2) {
78442
- return s2.replace(INLINE_CODE, " ").replace(PATH_LIKE, " ").replace(CALL, " ").replace(HEX, " ").replace(INTERNAL_REF, " ").replace(CAMEL, " ").replace(SNAKE, " ");
78443
- }
78444
- function scrubSubject(s2) {
78445
- const one2 = (s2 ?? "").replace(/\s+/g, " ").trim();
78446
- if (!one2 || MERGE.test(one2)) return "";
78447
- return tidy(strip(one2.replace(CONVENTIONAL, "")));
78448
- }
78449
- function scrubBody(s2) {
78450
- const first = (s2 ?? "").replace(TRAILER, "").split(/\n\s*\n/)[0] ?? "";
78451
- return tidy(strip(first.replace(/\s+/g, " ").trim()));
78452
- }
78453
- function scrubOutput(md) {
78454
- const noFence = (md ?? "").replace(FENCE, "");
78455
- const lines2 = noFence.split("\n").map((line) => {
78456
- const m = /^(\s*(?:[-*+]|\d+\.)\s+|\s*#{1,6}\s+|\s*>\s*)?(.*)$/.exec(line);
78457
- const lead = m?.[1] ?? "";
78458
- const rest = m?.[2] ?? line;
78459
- const cleaned = tidy(strip(rest));
78460
- return cleaned ? `${lead}${cleaned}` : "";
78461
- });
78462
- return lines2.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
78463
- }
78464
-
78465
- // src/services/changelog/collect.ts
78466
- var toItem = (label2, detail) => {
78467
- const l = scrubSubject(label2);
78468
- return l ? { label: l, detail: scrubBody(detail) } : null;
78469
- };
78470
- async function collectBacklog(projectId, from, to) {
78471
- const f = dayStart(from), t = dayEnd(to);
78472
- const rows = await prisma.spec.findMany({
78473
- where: { projectId, stage: "done" },
78474
- select: { title: true, objective: true, doneAt: true },
78475
- orderBy: [{ doneAt: "asc" }, { id: "asc" }]
78476
- });
78477
- const stampless = rows.filter((r) => r.doneAt === null).length;
78478
- const hit = rows.filter((r) => inDayRange(r.doneAt, f, t));
78479
- const items = hit.map((r) => toItem(r.title, r.objective ?? "")).filter((x) => x !== null);
78480
- if (items.length === 0)
78481
- return { ok: false, reason: `tak ada backlog yang selesai antara ${from} dan ${to}` };
78482
- const notes = [];
78483
- if (stampless > 0)
78484
- notes.push(`${stampless} item selesai tanpa stempel waktu (selesai sebelum stempel ini ada) dan tak ikut dihitung.`);
78485
- return { ok: true, input: { mode: "backlog", title: `${from} \u2013 ${to}`, items, notes } };
78486
- }
78487
- var exec11 = promisify16(execFile17);
78488
- var GIT8 = { maxBuffer: 1 << 24, encoding: "utf8" };
78489
- var US2 = "";
78490
- var RS = "";
78491
- var NO_REPO = "project ini belum ditautkan ke repo di mesin ini";
78492
- var NO_TAG = "repo project ini belum punya tag rilis";
78493
- var usable2 = (repoDir) => !!repoDir && existsSync22(repoDir);
78494
- async function listTags(repoDir) {
78495
- if (!usable2(repoDir)) return { tags: [], head: null, reason: NO_REPO };
78496
- try {
78497
- const { stdout } = await exec11("git", ["tag", "--list", "--sort=-creatordate"], { cwd: repoDir, ...GIT8 });
78498
- const tags = stdout.split("\n").map((s2) => s2.trim()).filter(Boolean);
78499
- const head = await exec11("git", ["rev-parse", "--short", "HEAD"], { cwd: repoDir, ...GIT8 }).then((r) => r.stdout.trim()).catch(() => null);
78500
- return { tags, head, reason: tags.length ? null : NO_TAG };
78501
- } catch {
78502
- return { tags: [], head: null, reason: NO_TAG };
78503
- }
78504
- }
78505
- async function previousTag(repoDir, tag2) {
78506
- return exec11("git", ["describe", "--tags", "--abbrev=0", "--end-of-options", `${tag2}^`], { cwd: repoDir, ...GIT8 }).then((r) => r.stdout.trim() || null).catch(() => null);
78507
- }
78508
- async function revExists(repoDir, rev) {
78509
- return exec11("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${rev}^{commit}`], { cwd: repoDir, ...GIT8 }).then(() => true).catch(() => false);
78510
- }
78511
- async function logRange(repoDir, range) {
78512
- const { stdout } = await exec11(
78513
- "git",
78514
- ["log", "--no-merges", `--pretty=format:%s${US2}%b${RS}`, ...range],
78515
- { cwd: repoDir, ...GIT8 }
78516
- );
78517
- return stdout.split(RS).map((rec) => rec.replace(/^\n+/, "")).filter((rec) => rec.trim()).map((rec) => {
78518
- const [subject, body] = rec.split(US2);
78519
- return toItem(subject ?? "", body ?? "");
78520
- }).filter((x) => x !== null);
78521
- }
78522
- async function collectCommits(repoDir, fromSha, toSha) {
78523
- if (!usable2(repoDir)) return { ok: false, reason: NO_REPO };
78524
- for (const rev of [fromSha, toSha])
78525
- if (!await revExists(repoDir, rev)) return { ok: false, reason: `revisi "${rev}" tak dikenal di repo project` };
78526
- try {
78527
- const items = await logRange(repoDir, ["--end-of-options", `${fromSha}..${toSha}`]);
78528
- if (!items.length) return { ok: false, reason: `tak ada perubahan antara "${fromSha}" dan "${toSha}"` };
78529
- const short = (s2) => s2.slice(0, 7);
78530
- return { ok: true, input: { mode: "commit", title: `${short(fromSha)} \u2192 ${short(toSha)}`, items, notes: [] } };
78531
- } catch (e) {
78532
- return { ok: false, reason: `git menolak rentang itu: ${e.message.split("\n")[0]}` };
78533
- }
78534
- }
78535
- async function collectVersions(repoDir, fromTag, toTag) {
78536
- const { tags, reason } = await listTags(repoDir);
78537
- if (reason) return { ok: false, reason };
78538
- if (!usable2(repoDir)) return { ok: false, reason: NO_REPO };
78539
- if (!tags.includes(toTag)) return { ok: false, reason: `tag "${toTag}" tak ada di repo project` };
78540
- if (fromTag && !tags.includes(fromTag)) return { ok: false, reason: `tag "${fromTag}" tak ada di repo project` };
78541
- const prev = fromTag ?? await previousTag(repoDir, toTag);
78542
- try {
78543
- const items = await logRange(repoDir, ["--end-of-options", prev ? `${prev}..${toTag}` : toTag]);
78544
- if (!items.length) return { ok: false, reason: `tak ada perubahan yang masuk ke "${toTag}"` };
78545
- const title = fromTag ? `${fromTag} \u2192 ${toTag}` : toTag;
78546
- return { ok: true, input: { mode: "version", title, items, notes: [] } };
78547
- } catch (e) {
78548
- return { ok: false, reason: `git menolak rentang itu: ${e.message.split("\n")[0]}` };
78549
- }
78550
- }
78551
-
78552
- // src/services/changelog/generate.ts
78553
- init_src();
78554
- init_db();
78555
- init_local_binding();
78556
-
78557
- // src/services/changelog/config.ts
78558
- init_src();
78559
- init_settings3();
78560
- async function changelogAgentDefaults() {
78561
- const e = (await getSetting()).changelog;
78562
- if (!e.enabled) return sessionAgentDefaults();
78563
- return e.agent === "codex" ? { agent: "codex", model: e.model, effort: coerceCodexEffort(e.model, e.effort) } : { agent: "claude", model: e.model, effort: e.effort };
78564
- }
78565
-
78566
- // src/services/changelog/render.ts
78567
- init_src2();
78568
- var MODE_LABEL = {
78569
- backlog: "backlog yang selesai dalam rentang tanggal",
78570
- commit: "riwayat perubahan repo dalam rentang yang dipilih",
78571
- version: "perubahan yang masuk ke versi/rilis itu"
78572
- };
78573
- var DETAIL_MAX = 240;
78574
- var clip3 = (s2) => s2.length > DETAIL_MAX ? `${s2.slice(0, DETAIL_MAX - 1).trimEnd()}\u2026` : s2;
78575
- function fallbackMarkdown(input) {
78576
- const lines2 = [`# Changelog \u2014 ${input.title}`, ""];
78577
- if (input.items.length === 0) {
78578
- lines2.push("_Tak ada perubahan yang bisa diringkas untuk rentang ini._", "");
78579
- } else {
78580
- lines2.push(`Ringkasan ${input.items.length} perubahan untuk pemakai.`, "");
78581
- for (const it of input.items) {
78582
- const d = clip3(it.detail.trim());
78583
- lines2.push(d ? `- **${it.label}** \u2014 ${d}` : `- **${it.label}**`);
78584
- }
78585
- lines2.push("");
78586
- }
78587
- if (input.notes.length) {
78588
- lines2.push("---", "");
78589
- for (const n2 of input.notes) lines2.push(`> ${n2}`);
78590
- lines2.push("");
78591
- }
78592
- return lines2.join("\n");
78593
- }
78594
- function changelogPrompt(input, budgetMs) {
78595
- const budget = Math.max(1, Math.round(budgetMs / 1e3));
78596
- const items = input.items.map((it, i) => `${i + 1}. ${it.label}${it.detail ? ` \u2014 ${it.detail}` : ""}`).join("\n");
78597
- return [
78598
- "Kamu menulis changelog untuk PEMAKAI sebuah produk, bukan untuk developer.",
78599
- "",
78600
- `Anggaran waktumu ${budget} detik. Jawab langsung \u2014 jangan membaca berkas, jangan memakai tool,`,
78601
- "jangan menyelidiki apa pun. Seluruh bahan sudah ada di bawah.",
78602
- "",
78603
- `Judul rentang: ${input.title}`,
78604
- `Bahan berasal dari ${MODE_LABEL[input.mode]}.`,
78605
- "",
78606
- "Bahan:",
78607
- items || "(tidak ada)",
78608
- "",
78609
- "Aturan:",
78610
- "- Bahasa Indonesia, gaya editorial: tenang, ringkas, kalimat penuh.",
78611
- "- Tulis apa yang berubah BAGI PEMAKAI, bukan apa yang disentuh di dalam kode.",
78612
- "- DILARANG menyebut nama berkas, nama fungsi, nama variabel, hash commit, nomor SPEC/ADR,",
78613
- " nama branch, atau istilah internal apa pun.",
78614
- "- Gabungkan bahan yang bicara hal yang sama jadi satu butir. 3\u201310 butir; kurangi bila memang sedikit.",
78615
- "- Jangan mengarang perubahan yang tak ada di bahan.",
78616
- "",
78617
- // SPEC-543 · ADR-0108 · dipasang di SEMUA prompt agen, bukan hanya yang menulis kode; gerbang
78618
- // di baris pertama klausa yang membuatnya diam untuk narator ini.
78619
- CODE_STYLE_CLAUSE,
78620
- "",
78621
- "Bentuk keluaran \u2014 HANYA markdown ini, tanpa kalimat pembuka atau penutup di luarnya,",
78622
- "tanpa blok kode:",
78623
- "",
78624
- `# Changelog \u2014 ${input.title}`,
78625
- "",
78626
- "<satu paragraf pembuka, maksimal dua kalimat>",
78627
- "",
78628
- "- **<judul singkat perubahan>** \u2014 <satu kalimat manfaatnya bagi pemakai>"
78629
- ].join("\n");
78630
- }
78631
-
78632
- // src/services/changelog/generate.ts
78633
- var CHANGELOG_TIMEOUT_MS = 18e4;
78634
- async function collect(projectId, req) {
78635
- if (req.mode === "backlog") {
78636
- const d = defaultRange(/* @__PURE__ */ new Date());
78637
- return collectBacklog(projectId, req.from ?? d.from, req.to ?? d.to);
78638
- }
78639
- const repoDir = await resolveRepoDir(projectId);
78640
- return req.mode === "commit" ? collectCommits(repoDir, req.fromSha, req.toSha) : collectVersions(repoDir, req.fromTag, req.toTag);
78641
- }
78642
- async function generateChangelog(projectId, req, deps = {}) {
78643
- const got = await collect(projectId, req);
78644
- if (!got.ok) return got;
78645
- const input = got.input;
78646
- const prompt = changelogPrompt(input, CHANGELOG_TIMEOUT_MS);
78647
- const { agent, model, effort: effort2 } = await changelogAgentDefaults();
78648
- const run5 = deps.think ?? think;
78649
- let body = "";
78650
- let generator = "agent";
78651
- const warnings = [...input.notes];
78652
- try {
78653
- const raw = await run5(prompt, { agent, model, effort: effort2, timeoutMs: CHANGELOG_TIMEOUT_MS });
78654
- body = scrubOutput(raw ?? "");
78655
- if (!body.trim()) throw new Error("agen tak memulangkan teks apa pun");
78656
- } catch (e) {
78657
- generator = "fallback";
78658
- body = fallbackMarkdown(input);
78659
- warnings.push(`Narasi otomatis tak tersedia \u2014 ${e.message}. Yang tampil adalah draf ringkas.`);
78660
- }
78661
- const row = await prisma.changelog.create({
78662
- data: {
78663
- projectId,
78664
- mode: input.mode,
78665
- title: input.title,
78666
- params: req,
78667
- body,
78668
- generator,
78669
- itemCount: input.items.length,
78670
- warning: warnings.length ? warnings.join(" ") : null
78671
- }
78672
- });
78673
- return { ok: true, row };
78674
- }
78675
-
78676
- // src/routes/changelog.ts
78677
- var view7 = (c) => ({
78678
- id: c.id,
78679
- projectId: c.projectId,
78680
- mode: c.mode,
78681
- title: c.title,
78682
- params: c.params,
78683
- body: c.body,
78684
- generator: c.generator,
78685
- warning: c.warning,
78686
- itemCount: c.itemCount,
78687
- createdAt: c.createdAt.toISOString()
78688
- });
78689
- async function changelog_default(app3) {
78690
- app3.get("/projects/:id/changelog/sources", async (req, reply) => {
78691
- const { id: id2 } = req.params;
78692
- if (!await prisma.project.findUnique({ where: { id: id2 } })) return reply.code(404).send({ error: "not found" });
78693
- const { tags, head, reason } = await listTags(await resolveRepoDir(id2));
78694
- const done = await prisma.spec.findMany({
78695
- where: { projectId: id2, stage: "done", doneAt: { not: null } },
78696
- select: { doneAt: true },
78697
- orderBy: { doneAt: "asc" }
78698
- });
78699
- return {
78700
- hasRepo: reason === null || tags.length > 0,
78701
- tags,
78702
- head,
78703
- reason,
78704
- backlog: {
78705
- doneCount: done.length,
78706
- earliest: done[0]?.doneAt?.toISOString() ?? null,
78707
- latest: done[done.length - 1]?.doneAt?.toISOString() ?? null
78708
- },
78709
- defaultRange: defaultRange(/* @__PURE__ */ new Date())
78710
- };
78711
- });
78712
- app3.get("/projects/:id/changelog", async (req, reply) => {
78713
- const { id: id2 } = req.params;
78714
- if (!await prisma.project.findUnique({ where: { id: id2 } })) return reply.code(404).send({ error: "not found" });
78715
- const { page, limit, q: q2 } = req.query;
78716
- const rows = await prisma.changelog.findMany({ where: { projectId: id2 }, orderBy: { createdAt: "desc" } });
78717
- return paginate(rows.filter((r) => changelogMatches(r, q2 ?? "")).map(view7), page, limit);
78718
- });
78719
- app3.get("/projects/:id/changelog/:cid", async (req, reply) => {
78720
- const { id: id2, cid } = req.params;
78721
- const row = await prisma.changelog.findFirst({ where: { id: cid, projectId: id2 } });
78722
- if (!row) return reply.code(404).send({ error: "not found" });
78723
- const fmt = downloadFormat(req.query);
78724
- if (fmt) return sendDocDownload(reply, fmt, {
78725
- content: row.body,
78726
- name: `${row.title}.md`,
78727
- prefix: `${id2}-changelog`,
78728
- eyebrow: `hanoman \xB7 ${id2} \xB7 changelog`,
78729
- path: row.title
78730
- });
78731
- return view7(row);
78732
- });
78733
- app3.post("/projects/:id/changelog", async (req, reply) => {
78734
- const { id: id2 } = req.params;
78735
- if (!await prisma.project.findUnique({ where: { id: id2 } })) return reply.code(404).send({ error: "not found" });
78736
- const parsed = zChangelogRequest.safeParse(req.body);
78737
- if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
78738
- const r = await generateChangelog(id2, parsed.data);
78739
- if (!r.ok) return reply.code(422).send({ error: r.reason });
78740
- return reply.code(201).send(view7(r.row));
78741
- });
78742
- app3.delete("/projects/:id/changelog/:cid", async (req, reply) => {
78743
- const { id: id2, cid } = req.params;
78744
- const { count } = await prisma.changelog.deleteMany({ where: { id: cid, projectId: id2 } });
78745
- return count ? reply.code(204).send() : reply.code(404).send({ error: "not found" });
78746
- });
78747
- }
78748
-
78749
78436
  // src/routes/custom-agents.ts
78750
78437
  init_src2();
78751
78438
  init_src();
@@ -78789,7 +78476,7 @@ var selectionReasonOf = (r, availability) => {
78789
78476
  }
78790
78477
  return "selalu tersedia sepanjang sesi";
78791
78478
  };
78792
- var view8 = (r, projectId, stamps = {}, requestedRuntime) => {
78479
+ var view7 = (r, projectId, stamps = {}, requestedRuntime) => {
78793
78480
  const builtin = r.projectId === null && BUILTIN_AGENT_NAMES.includes(r.name);
78794
78481
  const availability = availabilityOf(r, requestedRuntime);
78795
78482
  return {
@@ -78861,7 +78548,7 @@ async function custom_agents_default(app3) {
78861
78548
  for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
78862
78549
  for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
78863
78550
  const stamps = await stampsOf();
78864
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view8(r, projectId, stamps, requestedRuntime));
78551
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view7(r, projectId, stamps, requestedRuntime));
78865
78552
  });
78866
78553
  app3.post("/custom-agents", async (req, reply) => {
78867
78554
  const parsed = zCreateCustomAgent.safeParse(req.body);
@@ -78920,7 +78607,7 @@ async function custom_agents_default(app3) {
78920
78607
  } });
78921
78608
  await loadCustomAgents();
78922
78609
  await notifySynced("customAgent", id2);
78923
- return reply.code(201).send(view8(row, void 0, await stampsOf()));
78610
+ return reply.code(201).send(view7(row, void 0, await stampsOf()));
78924
78611
  });
78925
78612
  app3.patch("/custom-agents/:id", async (req, reply) => {
78926
78613
  const { id: id2 } = req.params;
@@ -78991,7 +78678,7 @@ async function custom_agents_default(app3) {
78991
78678
  } });
78992
78679
  await loadCustomAgents();
78993
78680
  await notifySynced("customAgent", id2);
78994
- return view8(row, void 0, await stampsOf());
78681
+ return view7(row, void 0, await stampsOf());
78995
78682
  });
78996
78683
  app3.delete("/custom-agents/:id", async (req, reply) => {
78997
78684
  const { id: id2 } = req.params;
@@ -79147,13 +78834,13 @@ async function custom_agent_metrics_default(app3) {
79147
78834
  const body = zPatch.safeParse(req.body);
79148
78835
  if (!body.success) return reply.code(400).send({ error: body.error.issues[0]?.message });
79149
78836
  const id2 = String(req.params.id ?? "");
79150
- const view13 = await updateAgentInvocationDisposition(
78837
+ const view12 = await updateAgentInvocationDisposition(
79151
78838
  id2,
79152
78839
  body.data.disposition,
79153
78840
  body.data.note,
79154
78841
  body.data.reworkRequired
79155
78842
  );
79156
- return view13 ?? reply.code(404).send({ error: "invocation tidak ditemukan" });
78843
+ return view12 ?? reply.code(404).send({ error: "invocation tidak ditemukan" });
79157
78844
  });
79158
78845
  }
79159
78846
 
@@ -79161,7 +78848,7 @@ async function custom_agent_metrics_default(app3) {
79161
78848
  init_src();
79162
78849
  init_db();
79163
78850
  init_sync_notify();
79164
- var view9 = (m) => ({
78851
+ var view8 = (m) => ({
79165
78852
  id: m.id,
79166
78853
  name: m.name,
79167
78854
  email: m.email,
@@ -79179,7 +78866,7 @@ async function members_default(app3) {
79179
78866
  // punya nama, dan menyembunyikannya membuat assignee-nya terbaca sebagai id mentah.
79180
78867
  orderBy: [{ active: "desc" }, { name: "asc" }]
79181
78868
  });
79182
- return paginate(rows.map(view9), page, limit);
78869
+ return paginate(rows.map(view8), page, limit);
79183
78870
  });
79184
78871
  app3.post("/members", async (req, reply) => {
79185
78872
  const parsed = zCreateMember.safeParse(req.body);
@@ -79192,7 +78879,7 @@ async function members_default(app3) {
79192
78879
  data: { id: id2, name: p3.name, email: p3.email, role: p3.role ?? null }
79193
78880
  });
79194
78881
  await notifySynced("member", id2);
79195
- return reply.code(201).send(view9(row));
78882
+ return reply.code(201).send(view8(row));
79196
78883
  });
79197
78884
  app3.patch("/members/:id", async (req, reply) => {
79198
78885
  const { id: id2 } = req.params;
@@ -79205,7 +78892,7 @@ async function members_default(app3) {
79205
78892
  return reply.code(404).send({ error: "not found" });
79206
78893
  const row = await prisma.member.update({ where: { id: id2 }, data: parsed.data });
79207
78894
  await notifySynced("member", id2);
79208
- return view9(row);
78895
+ return view8(row);
79209
78896
  });
79210
78897
  app3.delete("/members/:id", async (req, reply) => {
79211
78898
  const { id: id2 } = req.params;
@@ -79394,8 +79081,8 @@ async function tasks_default(app3) {
79394
79081
  author: req.user?.email ?? (req.agent ? `agent:${req.agent.id}` : "system"),
79395
79082
  launchApprovedBy: launchPrincipal(req)
79396
79083
  });
79397
- const view13 = taskView(r.task, { id: r.spec.id, stage: r.spec.stage, priority: r.spec.priority });
79398
- return reply.code(r.created ? 201 : 200).send({ created: r.created, spec: r.spec, task: view13 });
79084
+ const view12 = taskView(r.task, { id: r.spec.id, stage: r.spec.stage, priority: r.spec.priority });
79085
+ return reply.code(r.created ? 201 : 200).send({ created: r.created, spec: r.spec, task: view12 });
79399
79086
  });
79400
79087
  app3.delete("/tasks/:id/escalate", async (req, reply) => {
79401
79088
  const { id: id2 } = req.params;
@@ -80795,6 +80482,103 @@ async function qaTransfer(app3) {
80795
80482
  // src/routes/qa-backlog.ts
80796
80483
  init_src();
80797
80484
  init_zod();
80485
+
80486
+ // src/services/qa-session.ts
80487
+ init_src2();
80488
+ init_local_binding();
80489
+ init_session_launch_gate();
80490
+ init_pty();
80491
+ init_settings3();
80492
+ init_codex_trust();
80493
+ init_qa_attachment_transfer();
80494
+ init_uploads();
80495
+ import { createHash as createHash18 } from "node:crypto";
80496
+ import { execFileSync as execFileSync8 } from "node:child_process";
80497
+ import { join as join43 } from "node:path";
80498
+ var QaSessionError = class extends Error {
80499
+ constructor(code, message, needsBind = false) {
80500
+ super(message);
80501
+ this.code = code;
80502
+ this.needsBind = needsBind;
80503
+ }
80504
+ };
80505
+ var qaSessionId = (findingId) => `qa-${createHash18("sha256").update(findingId).digest("hex").slice(0, 24)}`;
80506
+ async function startQaFindingSession(d, fid) {
80507
+ const f = d.findings.find((x) => x.id === fid);
80508
+ if (!f) throw new QaSessionError(404, "temuan tidak ditemukan");
80509
+ if (f.status === "wontfix") throw new QaSessionError(409, "Temuan ditandai tidak akan diperbaiki. Buka kembali sebelum dikerjakan.");
80510
+ if (f.spec) throw new QaSessionError(409, "Temuan sudah masuk backlog. Mulai sesi dari backlog tersebut.");
80511
+ const repoDir = await resolveRepoDir(d.projectId);
80512
+ if (!repoDir) throw new QaSessionError(400, "Project belum terikat ke repo lokal", true);
80513
+ const id2 = qaSessionId(fid);
80514
+ return withSessionAdmission({ id: id2 }, async () => {
80515
+ const linkedCase = d.cases.find((c) => c.id === f.caseId);
80516
+ const attachments = d.attachments.filter((a) => a.ownerType === "report" || a.ownerType === "finding" && a.ownerId === fid || a.ownerType === "case" && a.ownerId === linkedCase?.id);
80517
+ const files = [];
80518
+ for (const a of attachments) {
80519
+ const bytes = await readQaAttachmentBytes(a.id);
80520
+ if (!bytes) throw new QaSessionError(409, `Lampiran belum tersedia: ${a.filename}. Sinkronkan QA lalu coba lagi.`);
80521
+ const saved = await saveSessionUpload(id2, bytes, a.mimeType);
80522
+ files.push(`${JSON.stringify(a.filename)} (${a.mimeType}): ${saved.path}`);
80523
+ }
80524
+ const wt = join43(repoDir, ".worktrees", id2);
80525
+ const branch = `qa/${id2.slice(3)}`;
80526
+ const reused = realGit.worktreeAlive(wt);
80527
+ try {
80528
+ if (!reused) {
80529
+ const base3 = realGit.revParse(repoDir, branch) ? branch : "HEAD";
80530
+ realGit.addWorktree(repoDir, wt, base3);
80531
+ execFileSync8("git", ["checkout", "-B", branch], { cwd: wt, encoding: "utf8" });
80532
+ }
80533
+ } catch (e) {
80534
+ throw new QaSessionError(422, `Gagal membuat worktree QA: ${e.message}`);
80535
+ }
80536
+ const verifyScope = (await getSetting()).verifyScope;
80537
+ const baseSha = realGit.headSha(wt);
80538
+ const { agent, model, effort: effort2 } = await sessionAgentDefaults();
80539
+ if (agent === "codex") ensureCodexTrust(repoDir);
80540
+ const prompt = [
80541
+ `hanoman \xB7 perbaiki temuan QA ${f.code}: ${f.title}`,
80542
+ `Project ${d.projectId}; laporan ${d.code}: ${d.title}; reportId=${d.id}; findingId=${fid}.`,
80543
+ "Baca instruksi project dan docs Source of Truth. Audit penyebab, pilih perbaikan yang sesuai, lakukan dan verifikasi. Untuk temuan kecil langsung execute; bila perlu desain/spec/plan tulis dokumennya di worktree ini. Jangan membuat backlog terlebih dahulu.",
80544
+ `Build: ${d.buildVersion}
80545
+ Lingkungan: ${JSON.stringify(d.environment)}
80546
+ Cakupan: ${d.scope}
80547
+ Ringkasan laporan: ${d.summary}`,
80548
+ `Dampak: ${f.severity}; prioritas: ${f.priority}; area: ${f.area}
80549
+ Repro:
80550
+ ${f.steps.map((s3, i) => `${i + 1}. ${s3}`).join("\n")}
80551
+ Expected:
80552
+ ${f.expected}
80553
+ Actual:
80554
+ ${f.actual}`,
80555
+ linkedCase ? `Langkah uji terkait ${linkedCase.code}: ${linkedCase.title}
80556
+ ${linkedCase.steps}
80557
+ Expected: ${linkedCase.expected}
80558
+ Actual: ${linkedCase.actual}
80559
+ Status: ${linkedCase.status}` : "",
80560
+ files.length ? `Lampiran konteks (baca berkas berikut):
80561
+ ${files.join("\n")}` : "",
80562
+ `Kerjakan pada branch ${branch}. Laporkan perubahan, bukti verifikasi dan langkah retest; integrasi diputuskan operator melalui Terminal. Jangan menandai temuan selesai sebelum retest operator.`,
80563
+ verifyScopeClause(verifyScope),
80564
+ CODE_STYLE_CLAUSE,
80565
+ reused ? RESUMED_WORKTREE_NOTE : ""
80566
+ ].filter(Boolean).join("\n\n");
80567
+ const s2 = createSession(d.projectId, wt, {
80568
+ id: id2,
80569
+ branch,
80570
+ agent,
80571
+ model,
80572
+ effort: effort2,
80573
+ prompt,
80574
+ env: { HANOMAN_BASE_SHA: baseSha, HANOMAN_VERIFY_SCOPE: verifyScope },
80575
+ attachmentsDir: files.length ? sessionUploadDir(id2) : void 0
80576
+ });
80577
+ return { id: s2.id, reused: false };
80578
+ }, (pane) => ({ id: pane.id, reused: true }));
80579
+ }
80580
+
80581
+ // src/routes/qa-backlog.ts
80798
80582
  init_db();
80799
80583
 
80800
80584
  // src/services/qa-backlog.ts
@@ -80923,6 +80707,21 @@ async function qaBacklog(app3) {
80923
80707
  await prisma.qaReport.update({ where: { id: rid }, data: { updatedAt: /* @__PURE__ */ new Date() } });
80924
80708
  return reply.code(r.created ? 201 : 200).send({ ...r, report: await reportDetail(pid, rid) });
80925
80709
  });
80710
+ app3.post("/projects/:pid/qa/reports/:rid/findings/:fid/session", async (req, reply) => {
80711
+ if (!launchPrincipal(req)) return reply.code(403).send({ error: "Peluncuran sesi memerlukan sessions:write" });
80712
+ const { pid, rid, fid } = req.params;
80713
+ const parsed = external_exports.object({}).strict().safeParse(req.body ?? {});
80714
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
80715
+ const d = await reportDetail(pid, rid);
80716
+ if (!d || !d.findings.some((f) => f.id === fid)) return reply.code(404).send({ error: "not found" });
80717
+ try {
80718
+ const result = await startQaFindingSession(d, fid);
80719
+ return reply.code(result.reused ? 200 : 201).send(result);
80720
+ } catch (e) {
80721
+ if (e instanceof QaSessionError) return reply.code(e.code).send({ error: e.message, ...e.needsBind ? { needsBind: true } : {} });
80722
+ throw e;
80723
+ }
80724
+ });
80926
80725
  app3.post("/projects/:pid/qa/reports/:rid/backlog", async (req, reply) => {
80927
80726
  const { pid, rid } = req.params;
80928
80727
  const parsed = zBody.safeParse(req.body ?? {});
@@ -80999,7 +80798,7 @@ async function resolveGithubRepo(projectId) {
80999
80798
 
81000
80799
  // src/services/github-fetch.ts
81001
80800
  init_config2();
81002
- import { execFile as execFile18 } from "node:child_process";
80801
+ import { execFile as execFile17 } from "node:child_process";
81003
80802
  var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
81004
80803
  var norm4 = (s2) => s2.toLowerCase() === "closed" ? "closed" : "open";
81005
80804
  function issueFromGh(raw) {
@@ -81040,7 +80839,7 @@ function issuesFromRest(raw) {
81040
80839
  var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
81041
80840
  var API2 = "https://api.github.com";
81042
80841
  var defaultRunGh = (args, env) => new Promise((resolve24, reject2) => {
81043
- execFile18(
80842
+ execFile17(
81044
80843
  args[0],
81045
80844
  args.slice(1),
81046
80845
  { env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
@@ -81272,7 +81071,7 @@ var STATUS = {
81272
81071
  unauthorized: 401,
81273
81072
  other: 502
81274
81073
  };
81275
- var view10 = (i) => ({
81074
+ var view9 = (i) => ({
81276
81075
  id: i.id,
81277
81076
  projectId: i.projectId,
81278
81077
  repoSlug: i.repoSlug,
@@ -81307,7 +81106,7 @@ async function githubIssues(app3) {
81307
81106
  where: { projectId: id2, ...status2 ? { status: status2 } : {} },
81308
81107
  orderBy: [{ number: "desc" }]
81309
81108
  });
81310
- return reply.send(paginate(items.map(view10), page, limit));
81109
+ return reply.send(paginate(items.map(view9), page, limit));
81311
81110
  });
81312
81111
  app3.post("/github-issues/accept", async (req, reply) => {
81313
81112
  const parsed = zAcceptMany.safeParse(req.body ?? {});
@@ -82076,7 +81875,7 @@ init_settings3();
82076
81875
  // src/services/portal-chat/turn.ts
82077
81876
  init_src();
82078
81877
  init_db();
82079
- import { execFile as execFile19 } from "node:child_process";
81878
+ import { execFile as execFile18 } from "node:child_process";
82080
81879
 
82081
81880
  // src/services/portal-chat/guard-input.ts
82082
81881
  import { randomBytes as randomBytes10 } from "node:crypto";
@@ -82190,7 +81989,7 @@ init_src();
82190
81989
  init_db();
82191
81990
  import { mkdirSync as mkdirSync16, mkdtempSync as mkdtempSync4, rmSync as rmSync11, writeFileSync as writeFileSync14 } from "node:fs";
82192
81991
  import { tmpdir as tmpdir8 } from "node:os";
82193
- import { join as join43 } from "node:path";
81992
+ import { join as join44 } from "node:path";
82194
81993
  var STAGE_LABEL = {
82195
81994
  brainstorming: "Dirumuskan",
82196
81995
  objective: "Dirumuskan",
@@ -82230,22 +82029,10 @@ function renderTicketDoc(tickets2) {
82230
82029
  `);
82231
82030
  return `# Laporan yang pernah dikirim
82232
82031
 
82233
- ${baris.join("\n")}`;
82234
- }
82235
- function renderChangelogDoc(rows) {
82236
- if (!rows.length) return "# Catatan rilis\n\nBelum ada catatan rilis.\n";
82237
- const baris = rows.map((c) => `## ${c.title}
82238
-
82239
- ${tanggal(c.createdAt.toISOString())}
82240
-
82241
- ${c.body}
82242
- `);
82243
- return `# Catatan rilis
82244
-
82245
82032
  ${baris.join("\n")}`;
82246
82033
  }
82247
82034
  async function buildChatWorkspace(projectId) {
82248
- const dir2 = mkdtempSync4(join43(tmpdir8(), "hanoman-portal-chat-"));
82035
+ const dir2 = mkdtempSync4(join44(tmpdir8(), "hanoman-portal-chat-"));
82249
82036
  try {
82250
82037
  const project = await prisma.project.findUnique({
82251
82038
  where: { id: projectId },
@@ -82258,28 +82045,21 @@ async function buildChatWorkspace(projectId) {
82258
82045
  orderBy: { createdAt: "desc" },
82259
82046
  take: 100
82260
82047
  });
82261
- const changelogs = await prisma.changelog.findMany({
82262
- where: { projectId },
82263
- orderBy: { createdAt: "desc" },
82264
- take: 20,
82265
- select: { title: true, body: true, createdAt: true }
82266
- });
82267
82048
  const files = [];
82268
82049
  const tulis = (rel, isi) => {
82269
- writeFileSync14(join43(dir2, rel), isi, { mode: 384 });
82050
+ writeFileSync14(join44(dir2, rel), isi, { mode: 384 });
82270
82051
  files.push(rel);
82271
82052
  };
82272
82053
  tulis("project.md", renderProjectDoc(project));
82273
82054
  tulis("pekerjaan.md", renderBacklogDoc(specs));
82274
82055
  tulis("laporan.md", renderTicketDoc(tickets2.map((t) => toPortalTicket(t, null))));
82275
- tulis("catatan-rilis.md", renderChangelogDoc(changelogs));
82276
82056
  const prds = await listPrds(projectId);
82277
82057
  if (prds.length) {
82278
- mkdirSync16(join43(dir2, "dokumen"), { mode: 448 });
82058
+ mkdirSync16(join44(dir2, "dokumen"), { mode: 448 });
82279
82059
  for (const prd of prds) {
82280
82060
  const isi = await readPrd(projectId, prd.path);
82281
82061
  if (!isi) continue;
82282
- tulis(join43("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
82062
+ tulis(join44("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
82283
82063
  }
82284
82064
  }
82285
82065
  return { dir: dir2, files, cleanup: () => rmSync11(dir2, { recursive: true, force: true }) };
@@ -82363,7 +82143,7 @@ function chatFailureReason(err, stdout, stderr, timeoutMs) {
82363
82143
  }
82364
82144
  function runProcess(p3, timeoutMs) {
82365
82145
  return new Promise((resolve24, reject2) => {
82366
- const child = execFile19(p3.file, p3.args, {
82146
+ const child = execFile18(p3.file, p3.args, {
82367
82147
  cwd: p3.cwd,
82368
82148
  timeout: timeoutMs,
82369
82149
  maxBuffer: 16 * 1024 * 1024,
@@ -82701,7 +82481,7 @@ async function portal_chat_admin_default(app3) {
82701
82481
  // src/routes/client-accounts.ts
82702
82482
  init_src();
82703
82483
  init_db();
82704
- var view11 = (u) => ({
82484
+ var view10 = (u) => ({
82705
82485
  id: u.id,
82706
82486
  email: u.email,
82707
82487
  disabled: u.disabled,
@@ -82732,7 +82512,7 @@ async function client_accounts_default(app3) {
82732
82512
  orderBy: { createdAt: "asc" },
82733
82513
  include: { projectAccess: { select: { projectId: true } } }
82734
82514
  });
82735
- return { items: rows.map(view11) };
82515
+ return { items: rows.map(view10) };
82736
82516
  });
82737
82517
  app3.post("/client-accounts", async (req, reply) => {
82738
82518
  const p3 = zCreateClientAccount.safeParse(req.body);
@@ -82746,7 +82526,7 @@ async function client_accounts_default(app3) {
82746
82526
  await prisma.user.delete({ where: { id: user.id } });
82747
82527
  return reply.code(400).send({ error: "ada project yang tidak dikenal" });
82748
82528
  }
82749
- return reply.code(201).send(view11(await load(user.id)));
82529
+ return reply.code(201).send(view10(await load(user.id)));
82750
82530
  });
82751
82531
  app3.patch("/client-accounts/:id", async (req, reply) => {
82752
82532
  const { id: id2 } = req.params;
@@ -82760,7 +82540,7 @@ async function client_accounts_default(app3) {
82760
82540
  if (p3.data.password)
82761
82541
  await prisma.user.update({ where: { id: id2 }, data: { passwordHash: await hashPassword(p3.data.password) } });
82762
82542
  if (p3.data.disabled || p3.data.password) await deleteUserSessions(id2);
82763
- return view11(await load(id2));
82543
+ return view10(await load(id2));
82764
82544
  });
82765
82545
  app3.delete("/client-accounts/:id", async (req, reply) => {
82766
82546
  const { id: id2 } = req.params;
@@ -82792,15 +82572,15 @@ init_src();
82792
82572
  init_db();
82793
82573
 
82794
82574
  // src/services/bootstrap.ts
82795
- import { createHash as createHash18, randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
82575
+ import { createHash as createHash19, randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
82796
82576
  import { chmod as chmod2, lstat as lstat3, mkdir as mkdir11, readFile as readFile9, unlink as unlink5, writeFile as writeFile8 } from "node:fs/promises";
82797
- import { join as join44 } from "node:path";
82577
+ import { join as join45 } from "node:path";
82798
82578
  var TTL_MS5 = 15 * 6e4;
82799
82579
  var SETUP_TOKEN_FILE = "setup.token";
82800
82580
  var BootstrapError = class extends Error {
82801
82581
  code = "BOOTSTRAP_PROOF";
82802
82582
  };
82803
- var tokenPath = (home3) => join44(home3, SETUP_TOKEN_FILE);
82583
+ var tokenPath = (home3) => join45(home3, SETUP_TOKEN_FILE);
82804
82584
  async function readStored(home3) {
82805
82585
  try {
82806
82586
  const info = await lstat3(tokenPath(home3));
@@ -82845,8 +82625,8 @@ ${new Date(expiresAt).toISOString()}
82845
82625
  }
82846
82626
  async function verifySetupToken(candidate, home3, now = Date.now()) {
82847
82627
  const stored = await readStored(home3);
82848
- const got = createHash18("sha256").update(candidate).digest();
82849
- const want = createHash18("sha256").update(stored.token).digest();
82628
+ const got = createHash19("sha256").update(candidate).digest();
82629
+ const want = createHash19("sha256").update(stored.token).digest();
82850
82630
  if (stored.expiresAt <= now || !timingSafeEqual5(got, want)) throw new BootstrapError("invalid setup proof");
82851
82631
  }
82852
82632
  async function consumeSetupToken(home3) {
@@ -82857,7 +82637,7 @@ async function consumeSetupToken(home3) {
82857
82637
 
82858
82638
  // src/routes/auth.ts
82859
82639
  var BOOTSTRAP_USER_ID = "bootstrap-admin";
82860
- var view12 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
82640
+ var view11 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
82861
82641
  async function issue(req, reply, userId) {
82862
82642
  const token = await createSession2(userId);
82863
82643
  reply.setCookie(COOKIE_NAME, token, cookieOpts(req));
@@ -82897,7 +82677,7 @@ async function auth_default(app3, opts2) {
82897
82677
  if (opts2.bootstrapRequired) await consumeSetupToken(opts2.home);
82898
82678
  setupAttempts.clear(req.ip);
82899
82679
  await issue(req, reply, user.id);
82900
- return { user: view12(user) };
82680
+ return { user: view11(user) };
82901
82681
  });
82902
82682
  app3.post("/auth/login", async (req, reply) => {
82903
82683
  if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
@@ -82910,7 +82690,7 @@ async function auth_default(app3, opts2) {
82910
82690
  }
82911
82691
  clearLoginFails(req.ip);
82912
82692
  await issue(req, reply, user.id);
82913
- return { user: view12(user) };
82693
+ return { user: view11(user) };
82914
82694
  });
82915
82695
  app3.post("/auth/logout", async (req, reply) => {
82916
82696
  const token = req.cookies?.[COOKIE_NAME];
@@ -82918,7 +82698,7 @@ async function auth_default(app3, opts2) {
82918
82698
  reply.clearCookie(COOKIE_NAME, { path: "/" });
82919
82699
  return reply.code(204).send();
82920
82700
  });
82921
- app3.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view12));
82701
+ app3.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view11));
82922
82702
  app3.post("/auth/users", async (req, reply) => {
82923
82703
  const p3 = zSignup.safeParse(req.body);
82924
82704
  if (!p3.success) return reply.code(400).send({ error: p3.error.flatten() });
@@ -82929,7 +82709,7 @@ async function auth_default(app3, opts2) {
82929
82709
  // (/api/client-accounts) supaya "undang rekan" dan "beri akses klien" tak pernah tertukar.
82930
82710
  data: { email: p3.data.email, passwordHash: await hashPassword(p3.data.password), role: "admin" }
82931
82711
  });
82932
- return view12(user);
82712
+ return view11(user);
82933
82713
  });
82934
82714
  app3.delete("/auth/users/:id", async (req, reply) => {
82935
82715
  const target3 = await prisma.user.findUnique({ where: { id: req.params.id } });
@@ -82952,7 +82732,7 @@ async function auth_default(app3, opts2) {
82952
82732
  });
82953
82733
  await deleteUserSessions(user.id);
82954
82734
  await issue(req, reply, user.id);
82955
- return { user: view12(user) };
82735
+ return { user: view11(user) };
82956
82736
  });
82957
82737
  }
82958
82738
 
@@ -83041,15 +82821,15 @@ async function agent_tokens_default(app3) {
83041
82821
  app3.post("/agent-tokens", async (req, reply) => {
83042
82822
  const p3 = zAgentTokenCreate.safeParse(req.body);
83043
82823
  if (!p3.success) return reply.code(400).send({ error: p3.error.flatten() });
83044
- const { view: view13, token } = await issueAgentToken({ ...p3.data, createdBy: req.user?.id });
83045
- return reply.code(201).send({ ...view13, token });
82824
+ const { view: view12, token } = await issueAgentToken({ ...p3.data, createdBy: req.user?.id });
82825
+ return reply.code(201).send({ ...view12, token });
83046
82826
  });
83047
82827
  app3.patch("/agent-tokens/:id", async (req, reply) => {
83048
82828
  const p3 = zAgentTokenPatch.safeParse(req.body);
83049
82829
  if (!p3.success) return reply.code(400).send({ error: p3.error.flatten() });
83050
82830
  const { id: id2 } = req.params;
83051
- const view13 = await patchAgentToken(id2, p3.data);
83052
- return view13 ? reply.send(view13) : reply.code(404).send({ error: "not found" });
82831
+ const view12 = await patchAgentToken(id2, p3.data);
82832
+ return view12 ? reply.send(view12) : reply.code(404).send({ error: "not found" });
83053
82833
  });
83054
82834
  app3.delete("/agent-tokens/:id", async (req, reply) => {
83055
82835
  const { id: id2 } = req.params;
@@ -83336,7 +83116,7 @@ init_event_log();
83336
83116
  init_transcript_store();
83337
83117
  init_src2();
83338
83118
  import { readFile as readFile10 } from "node:fs/promises";
83339
- import { join as join45 } from "node:path";
83119
+ import { join as join46 } from "node:path";
83340
83120
  function decodeCursor(c) {
83341
83121
  if (!c) return null;
83342
83122
  try {
@@ -83374,7 +83154,7 @@ async function readRemoteTranscript(id2) {
83374
83154
  const row = await prisma.logEntry.findUnique({ where: { id: id2 } });
83375
83155
  if (!row || row.lane !== "transcript" || !row.transcriptKey) return null;
83376
83156
  if (row.deviceId === "local") return readTranscript(row.transcriptKey);
83377
- const path = join45(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
83157
+ const path = join46(resolveDataDirs().home, "remote-transcripts", row.transcriptKey);
83378
83158
  try {
83379
83159
  return await readFile10(path, "utf8");
83380
83160
  } catch {
@@ -83566,7 +83346,7 @@ var SETUP_PUBLIC = /* @__PURE__ */ new Set([
83566
83346
  "POST /api/setup"
83567
83347
  ]);
83568
83348
  function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {}) {
83569
- const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname17(fileURLToPath5(import.meta.url)), env, existsSync23);
83349
+ const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname17(fileURLToPath5(import.meta.url)), env, existsSync22);
83570
83350
  const ingress = loadIngressPolicy(env);
83571
83351
  const app3 = Fastify({ logger: false, trustProxy: trustProxyFromEnv(env) });
83572
83352
  app3.setErrorHandler((error, _req, reply) => {
@@ -83694,7 +83474,6 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
83694
83474
  await api.register(githubIssues);
83695
83475
  await api.register(telegramRoutes);
83696
83476
  await api.register(webhooks_default);
83697
- await api.register(changelog_default);
83698
83477
  await api.register(portal_default);
83699
83478
  await api.register(portal_chat_default);
83700
83479
  await api.register(portal_chat_admin_default);
@@ -83707,7 +83486,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
83707
83486
  await api.register(qaBacklog);
83708
83487
  }, { prefix: "/api" });
83709
83488
  if (shouldServeWeb(env)) {
83710
- const dist = pickWebDir(dirname17(fileURLToPath5(import.meta.url)), env, existsSync23);
83489
+ const dist = pickWebDir(dirname17(fileURLToPath5(import.meta.url)), env, existsSync22);
83711
83490
  if (dist) {
83712
83491
  app3.register(fastifyStatic, { root: dist });
83713
83492
  app3.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : req.url.startsWith("/assets/") ? reply.code(404).type("text/plain").send("not found") : reply.sendFile("index.html"));
@@ -84411,18 +84190,18 @@ function startWebhookEngine() {
84411
84190
  // src/services/auto-merge.ts
84412
84191
  init_src();
84413
84192
  init_db();
84414
- import { execFile as execFile20 } from "node:child_process";
84415
- import { promisify as promisify17 } from "node:util";
84193
+ import { execFile as execFile19 } from "node:child_process";
84194
+ import { promisify as promisify16 } from "node:util";
84416
84195
  init_local_binding();
84417
84196
  init_notifications2();
84418
84197
  var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
84419
84198
  var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
84420
84199
  var TICK_MS4 = 6e4;
84421
- var exec12 = promisify17(execFile20);
84422
- var GIT9 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
84200
+ var exec11 = promisify16(execFile19);
84201
+ var GIT8 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
84423
84202
  var gitOut = async (cwd, args) => {
84424
84203
  try {
84425
- return (await exec12("git", args, { cwd, ...GIT9 })).stdout.trim();
84204
+ return (await exec11("git", args, { cwd, ...GIT8 })).stdout.trim();
84426
84205
  } catch {
84427
84206
  return null;
84428
84207
  }
@@ -84631,9 +84410,9 @@ async function pruneLogs(now, retention, opts2 = {}) {
84631
84410
  }
84632
84411
  async function reconcileRemoteTranscripts() {
84633
84412
  const { readdir: readdir10, unlink: unlink6, stat: stat8 } = await import("node:fs/promises");
84634
- const { join: join46 } = await import("node:path");
84413
+ const { join: join47 } = await import("node:path");
84635
84414
  const { resolveDataDirs: resolveDataDirs2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
84636
- const root = join46(resolveDataDirs2().home, "remote-transcripts");
84415
+ const root = join47(resolveDataDirs2().home, "remote-transcripts");
84637
84416
  let deviceDirs;
84638
84417
  try {
84639
84418
  deviceDirs = await readdir10(root);
@@ -84647,16 +84426,16 @@ async function reconcileRemoteTranscripts() {
84647
84426
  for (const dev of deviceDirs) {
84648
84427
  let files;
84649
84428
  try {
84650
- files = await readdir10(join46(root, dev));
84429
+ files = await readdir10(join47(root, dev));
84651
84430
  } catch {
84652
84431
  continue;
84653
84432
  }
84654
84433
  for (const f of files) {
84655
84434
  const key = `${dev}/${f}`;
84656
84435
  if (referenced.has(key)) continue;
84657
- const st = await stat8(join46(root, dev, f)).catch(() => null);
84436
+ const st = await stat8(join47(root, dev, f)).catch(() => null);
84658
84437
  if (!st || Date.now() - st.mtimeMs < gracePeriodMs) continue;
84659
- await unlink6(join46(root, dev, f)).catch(() => {
84438
+ await unlink6(join47(root, dev, f)).catch(() => {
84660
84439
  });
84661
84440
  orphans++;
84662
84441
  }