hanoman 0.9.12 → 0.9.14

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 (24) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +337 -35
  3. package/dist/server.js +20798 -17844
  4. package/package.json +1 -1
  5. package/prisma/migrations/20261001081455_qa_workspace/migration.sql +85 -0
  6. package/prisma/migrations/20261001093314_qa_attachment_sync/migration.sql +31 -0
  7. package/prisma/schema.prisma +97 -0
  8. package/web/assets/{ChangelogScreen-dSx0GN6n.js → ChangelogScreen-CS_GSORI.js} +1 -1
  9. package/web/assets/{DalangHanomanScreen-CxluNxA4.js → DalangHanomanScreen-maIEXuvi.js} +1 -1
  10. package/web/assets/{DocPreviewModal-Dh5W00Jj.js → DocPreviewModal-DFqCURCs.js} +1 -1
  11. package/web/assets/{DocsWorkspace-BmNqLMpK.js → DocsWorkspace-hxJ037xu.js} +1 -1
  12. package/web/assets/{IdeScreen-HMV7PR16.js → IdeScreen-CHXLJ0Az.js} +1 -1
  13. package/web/assets/{LeadScreen-KY5Xm_nj.js → LeadScreen-DI8TW6QX.js} +1 -1
  14. package/web/assets/{ReviewScreen-eeSPJSzp.js → ReviewScreen-CAi2WDfW.js} +1 -1
  15. package/web/assets/{SchedulerScreen-B3Co7Nqy.js → SchedulerScreen-D1vCNuRr.js} +1 -1
  16. package/web/assets/{SettingsScreen-CGED6Nhv.js → SettingsScreen-1GM6-O69.js} +4 -4
  17. package/web/assets/{TeamScreen-C-5AYuby.js → TeamScreen-eYlsFpzN.js} +1 -1
  18. package/web/assets/{TerminalScreen-O9aIsNxm.js → TerminalScreen-u6e-6CB7.js} +1 -1
  19. package/web/assets/{TriageScreen-B7cDwzbY.js → TriageScreen-B6czWJOz.js} +1 -1
  20. package/web/assets/{VpsScreen-BYjIBuH2.js → VpsScreen-CaYPux2B.js} +1 -1
  21. package/web/assets/index--wY0Cvay.js +1043 -0
  22. package/web/assets/{live-Ck3G14iM.js → live-WWHlTNwv.js} +1 -1
  23. package/web/index.html +1 -1
  24. package/web/assets/index-CvvGNA1i.js +0 -1017
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.12",
3
- "sha": "d6b7560c",
4
- "builtAt": "2026-09-28T08:05:21.786Z"
2
+ "version": "0.9.14",
3
+ "sha": "ca8210ed",
4
+ "builtAt": "2026-10-01T23:35:24.906Z"
5
5
  }
package/dist/cli.js CHANGED
@@ -3626,23 +3626,23 @@ var init_types2 = __esm({
3626
3626
  }
3627
3627
  if (effect.type === "transform") {
3628
3628
  if (ctx.common.async === false) {
3629
- const base = this._def.schema._parseSync({
3629
+ const base2 = this._def.schema._parseSync({
3630
3630
  data: ctx.data,
3631
3631
  path: ctx.path,
3632
3632
  parent: ctx
3633
3633
  });
3634
- if (!isValid(base))
3634
+ if (!isValid(base2))
3635
3635
  return INVALID;
3636
- const result = effect.transform(base.value, checkCtx);
3636
+ const result = effect.transform(base2.value, checkCtx);
3637
3637
  if (result instanceof Promise) {
3638
3638
  throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);
3639
3639
  }
3640
3640
  return { status: status.value, value: result };
3641
3641
  } else {
3642
- return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => {
3643
- if (!isValid(base))
3642
+ return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base2) => {
3643
+ if (!isValid(base2))
3644
3644
  return INVALID;
3645
- return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({
3645
+ return Promise.resolve(effect.transform(base2.value, checkCtx)).then((result) => ({
3646
3646
  status: status.value,
3647
3647
  value: result
3648
3648
  }));
@@ -5202,6 +5202,8 @@ var init_agent = __esm({
5202
5202
  // "Backlog — tulis" diam-diam membuka papan orang, dan sebaliknya.
5203
5203
  "team:read",
5204
5204
  "team:write",
5205
+ "qa:read",
5206
+ "qa:write",
5205
5207
  // ADR-0155 · akses KETIGA: `danger`. Dipecah dari `:write` karena keempat operasi ini bukan
5206
5208
  // "menulis lebih banyak", melainkan menjalankan sesuatu di luar proses hanoman — sesi agen di
5207
5209
  // worktree, perintah di VPS, git yang mengubah sejarah, penghapusan artefak dokumen.
@@ -7289,9 +7291,9 @@ var init_api = __esm({
7289
7291
  branches: (id) => `${API}/projects/${id}/branches`,
7290
7292
  // SPEC-360 · ADR-0077 · daftar branch (nilai turunan git) + hapus batch local/origin.
7291
7293
  // SPEC-859 · `include=all` memuat branch yang belum ter-merge; tanpa itu hanya yang ter-merge.
7292
- branchesUnused: (id, base, include) => {
7294
+ branchesUnused: (id, base2, include) => {
7293
7295
  const q = new URLSearchParams();
7294
- if (base) q.set("base", base);
7296
+ if (base2) q.set("base", base2);
7295
7297
  if (include) q.set("include", include);
7296
7298
  const s2 = q.toString();
7297
7299
  return `${API}/projects/${id}/branches/unused${s2 ? `?${s2}` : ""}`;
@@ -7367,7 +7369,7 @@ var init_api = __esm({
7367
7369
  // SPEC-233 · remote mgmt + pr-url + archive
7368
7370
  ideRemotes: (id) => `${API}/projects/${id}/remotes`,
7369
7371
  ideRemote: (id, name) => `${API}/projects/${id}/remotes/${encodeURIComponent(name)}`,
7370
- idePrUrl: (id, branch, base) => `${API}/projects/${id}/pr-url?branch=${encodeURIComponent(branch)}${base ? `&base=${encodeURIComponent(base)}` : ""}`,
7372
+ idePrUrl: (id, branch, base2) => `${API}/projects/${id}/pr-url?branch=${encodeURIComponent(branch)}${base2 ? `&base=${encodeURIComponent(base2)}` : ""}`,
7371
7373
  ideArchive: (id, ref, format = "zip") => `${API}/projects/${id}/archive?ref=${encodeURIComponent(ref)}&format=${format}`,
7372
7374
  ideCommit: (id, sha) => `${API}/projects/${id}/commit/${sha}`,
7373
7375
  // SPEC-233 · diff satu file di commit (vs parent)
@@ -7515,6 +7517,20 @@ var init_api = __esm({
7515
7517
  skillFile: (key) => `${API}/skills/${encodeURIComponent(key)}/file`,
7516
7518
  skillEntry: (key) => `${API}/skills/${encodeURIComponent(key)}/entry`,
7517
7519
  skillFork: (key) => `${API}/skills/${encodeURIComponent(key)}/fork`,
7520
+ // Workspace QA · laporan QA per project. Semua di bawah /projects/:id/qa (capability `qa`).
7521
+ qaReports: (pid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports`,
7522
+ qaReport: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}`,
7523
+ qaCases: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/cases`,
7524
+ qaCase: (pid, rid, cid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/cases/${encodeURIComponent(cid)}`,
7525
+ qaFindings: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/findings`,
7526
+ qaFinding: (pid, rid, fid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/findings/${encodeURIComponent(fid)}`,
7527
+ qaAttachments: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/attachments`,
7528
+ qaAttachment: (pid, rid, aid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/attachments/${encodeURIComponent(aid)}`,
7529
+ qaFindingBacklog: (pid, rid, fid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/findings/${encodeURIComponent(fid)}/backlog`,
7530
+ qaReportBacklog: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/backlog`,
7531
+ qaCasesImport: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/cases/import`,
7532
+ qaImport: (pid) => `${API}/projects/${encodeURIComponent(pid)}/qa/import`,
7533
+ qaTemplate: `${API}/qa/template.xlsx`,
7518
7534
  // SPEC-945 · ADR-0150 · papan tim. `members` GLOBAL (bukan per project) — task boleh tanpa
7519
7535
  // project, jadi direktori orang tak bisa digantung pada project.
7520
7536
  members: `${API}/members`,
@@ -7572,7 +7588,7 @@ var init_api = __esm({
7572
7588
  githubIssueUnlink: (id) => `${API}/github-issues/${encodeURIComponent(id)}/unlink`,
7573
7589
  // SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
7574
7590
  // (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
7575
- download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
7591
+ download: (base2, fmt) => `${base2}${base2.includes("?") ? "&" : "?"}download=${fmt}`
7576
7592
  };
7577
7593
  }
7578
7594
  });
@@ -7931,6 +7947,69 @@ var init_semver = __esm({
7931
7947
  }
7932
7948
  });
7933
7949
 
7950
+ // ../shared/src/qa.ts
7951
+ var QA_REPORT_STATUSES, QA_VERDICTS, QA_CASE_STATUSES, QA_SEVERITIES, QA_PRIORITIES, zQaEnvironment, zSteps, zCreateQaReport, zPatchQaReport, zCreateQaCase, zPatchQaCase, zCreateQaFinding, zPatchQaFinding;
7952
+ var init_qa = __esm({
7953
+ "../shared/src/qa.ts"() {
7954
+ "use strict";
7955
+ init_zod();
7956
+ QA_REPORT_STATUSES = ["draft", "submitted", "closed"];
7957
+ QA_VERDICTS = ["go", "no-go", "conditional"];
7958
+ QA_CASE_STATUSES = ["todo", "pass", "fail", "blocked", "skipped"];
7959
+ QA_SEVERITIES = ["blocker", "critical", "major", "minor", "trivial"];
7960
+ QA_PRIORITIES = ["P0", "P1", "P2", "P3"];
7961
+ zQaEnvironment = external_exports.record(external_exports.string().trim().min(1).max(60), external_exports.string().max(500)).refine((e) => Object.keys(e).length <= 20, "maksimal 20 entri environment");
7962
+ zSteps = external_exports.array(external_exports.string().trim().min(1).max(2e3)).max(50);
7963
+ zCreateQaReport = external_exports.object({
7964
+ title: external_exports.string().trim().min(1).max(300),
7965
+ buildVersion: external_exports.string().trim().max(120).default(""),
7966
+ environment: zQaEnvironment.default({}),
7967
+ scope: external_exports.string().max(5e3).default(""),
7968
+ tester: external_exports.string().trim().max(200).default(""),
7969
+ summary: external_exports.string().max(2e4).default(""),
7970
+ verdict: external_exports.enum(QA_VERDICTS).nullable().default(null)
7971
+ });
7972
+ zPatchQaReport = zCreateQaReport.partial().extend({ status: external_exports.enum(QA_REPORT_STATUSES).optional() });
7973
+ zCreateQaCase = external_exports.object({
7974
+ title: external_exports.string().trim().min(1).max(300),
7975
+ steps: external_exports.string().max(1e4).default(""),
7976
+ expected: external_exports.string().max(1e4).default(""),
7977
+ actual: external_exports.string().max(1e4).default(""),
7978
+ status: external_exports.enum(QA_CASE_STATUSES).default("todo"),
7979
+ order: external_exports.number().finite().optional()
7980
+ });
7981
+ zPatchQaCase = zCreateQaCase.partial();
7982
+ zCreateQaFinding = external_exports.object({
7983
+ title: external_exports.string().trim().min(1).max(300),
7984
+ caseId: external_exports.string().max(120).nullable().default(null),
7985
+ severity: external_exports.enum(QA_SEVERITIES).default("major"),
7986
+ priority: external_exports.enum(QA_PRIORITIES).default("P2"),
7987
+ area: external_exports.string().trim().max(120).default(""),
7988
+ steps: zSteps.default([]),
7989
+ expected: external_exports.string().max(1e4).default(""),
7990
+ actual: external_exports.string().max(1e4).default(""),
7991
+ status: external_exports.enum(["open", "wontfix"]).default("open")
7992
+ });
7993
+ zPatchQaFinding = zCreateQaFinding.partial();
7994
+ }
7995
+ });
7996
+
7997
+ // ../shared/src/qa-markdown.ts
7998
+ var init_qa_markdown = __esm({
7999
+ "../shared/src/qa-markdown.ts"() {
8000
+ "use strict";
8001
+ init_qa();
8002
+ }
8003
+ });
8004
+
8005
+ // ../shared/src/qa-tabular.ts
8006
+ var init_qa_tabular = __esm({
8007
+ "../shared/src/qa-tabular.ts"() {
8008
+ "use strict";
8009
+ init_qa();
8010
+ }
8011
+ });
8012
+
7934
8013
  // ../shared/src/webhook.ts
7935
8014
  var WEBHOOK_BACKOFF_SEC, WEBHOOK_MAX_ATTEMPTS, WEBHOOK_MAX_BYTES, WEBHOOK_PING_TYPE, WEBHOOK_ENTITIES, BY_MODEL, WEBHOOK_EVENTS, zCreateWebhookEndpoint, zUpdateWebhookEndpoint;
7936
8015
  var init_webhook = __esm({
@@ -9183,8 +9262,8 @@ var init_backlog = __esm({
9183
9262
  sampleMethod: "GET",
9184
9263
  build: (a) => {
9185
9264
  const path = s(a.path);
9186
- const base = `/specs/${enc(String(a.spec))}/review`;
9187
- return { method: "GET", path: path ? `${base}/${String(path).split("/").map(enc).join("/")}` : base };
9265
+ const base2 = `/specs/${enc(String(a.spec))}/review`;
9266
+ return { method: "GET", path: path ? `${base2}/${String(path).split("/").map(enc).join("/")}` : base2 };
9188
9267
  },
9189
9268
  shape: (raw) => raw
9190
9269
  },
@@ -9861,8 +9940,8 @@ var init_ide = __esm({
9861
9940
  sampleMethod: "GET",
9862
9941
  build: (a) => {
9863
9942
  const path = s(a.path);
9864
- const base = `${p(a.project)}/commit/${enc(String(a.sha))}`;
9865
- return path ? { method: "GET", path: `${base}/file`, query: { path } } : { method: "GET", path: base };
9943
+ const base2 = `${p(a.project)}/commit/${enc(String(a.sha))}`;
9944
+ return path ? { method: "GET", path: `${base2}/file`, query: { path } } : { method: "GET", path: base2 };
9866
9945
  },
9867
9946
  shape: (raw) => raw
9868
9947
  },
@@ -11099,8 +11178,8 @@ var init_sessions = __esm({
11099
11178
  sampleMethod: "GET",
11100
11179
  build: (a) => {
11101
11180
  const path = s(a.path);
11102
- const base = `/terminal/sessions/${enc(String(a.session))}/review`;
11103
- return { method: "GET", path: path ? `${base}/${String(path).split("/").map(enc).join("/")}` : base };
11181
+ const base2 = `/terminal/sessions/${enc(String(a.session))}/review`;
11182
+ return { method: "GET", path: path ? `${base2}/${String(path).split("/").map(enc).join("/")}` : base2 };
11104
11183
  },
11105
11184
  shape: (raw) => raw
11106
11185
  },
@@ -12054,6 +12133,216 @@ var init_team2 = __esm({
12054
12133
  }
12055
12134
  });
12056
12135
 
12136
+ // ../shared/src/mcp-catalog/qa.ts
12137
+ var PROJECT3, REPORT, CASE, FINDING, base, one, parseEnv, parseSteps, nullable2, put, REPORT_FIELDS, reportBody, caseBody, findingBody, CASE_FIELDS, FINDING_FIELDS, QA_TOOLS;
12138
+ var init_qa2 = __esm({
12139
+ "../shared/src/mcp-catalog/qa.ts"() {
12140
+ "use strict";
12141
+ init_qa();
12142
+ init_mcp_schema();
12143
+ init_helpers();
12144
+ PROJECT3 = str("Id project, seperti muncul di hanoman_projects_list.");
12145
+ REPORT = str("Id laporan QA (cuid), seperti muncul di hanoman_qa_reports_list \u2014 BUKAN nomor tampil `QA-007`, yang dihitung saat render.");
12146
+ CASE = str("Id test case (cuid), dari hanoman_qa_report_get.");
12147
+ FINDING = str("Id temuan (cuid), dari hanoman_qa_report_get.");
12148
+ base = (a) => `/projects/${enc(String(a.project))}/qa/reports`;
12149
+ one = (a) => `${base(a)}/${enc(String(a.report))}`;
12150
+ parseEnv = (v) => {
12151
+ if (typeof v !== "string" || !v.trim()) return void 0;
12152
+ const out = {};
12153
+ for (const part of v.split(/[\n;]/)) {
12154
+ const t = part.trim();
12155
+ if (!t) continue;
12156
+ const i = t.indexOf("=");
12157
+ const k = (i < 0 ? t : t.slice(0, i)).trim();
12158
+ if (k) out[k] = i < 0 ? "" : t.slice(i + 1).trim();
12159
+ }
12160
+ return Object.keys(out).length ? out : void 0;
12161
+ };
12162
+ parseSteps = (v) => typeof v === "string" && v.trim() ? v.split("\n").map((l) => l.trim().replace(/^(?:\d+[.)]|[-*])\s+/, "")).filter(Boolean) : void 0;
12163
+ nullable2 = (v) => typeof v !== "string" ? void 0 : v === "" ? null : v;
12164
+ put = (body, key, v) => {
12165
+ if (v !== void 0) body[key] = v;
12166
+ };
12167
+ REPORT_FIELDS = {
12168
+ title: str("Judul laporan, satu baris."),
12169
+ buildVersion: str("Versi/build yang diuji, mis. `0.9.12`."),
12170
+ environment: str("Lingkungan uji: `kunci=nilai` dipisah baris atau `;`, mis. `os=macOS 15; browser=Chrome 130; url=https://staging.x.id`. Maks 20 entri."),
12171
+ scope: str("Cakupan: apa yang diuji dan apa yang sengaja tidak."),
12172
+ tester: str("Nama penguji."),
12173
+ summary: str("Ringkasan hasil, 2\u20133 kalimat, memuat alasan keputusan."),
12174
+ verdict: enumStr(QA_VERDICTS, "Keputusan rilis. Wajib terisi sebelum `status` boleh `submitted`/`closed`.")
12175
+ };
12176
+ reportBody = (a) => {
12177
+ const b = {};
12178
+ for (const k of ["title", "buildVersion", "scope", "tester", "summary", "verdict", "status"]) put(b, k, s(a[k]));
12179
+ put(b, "environment", parseEnv(a.environment));
12180
+ return b;
12181
+ };
12182
+ caseBody = (a) => {
12183
+ const b = {};
12184
+ for (const k of ["title", "steps", "expected", "actual", "status"]) put(b, k, s(a[k]));
12185
+ return b;
12186
+ };
12187
+ findingBody = (a) => {
12188
+ const b = {};
12189
+ for (const k of ["title", "severity", "priority", "area", "expected", "actual", "status"]) put(b, k, s(a[k]));
12190
+ put(b, "steps", parseSteps(a.steps));
12191
+ put(b, "caseId", nullable2(a.testCase));
12192
+ return b;
12193
+ };
12194
+ CASE_FIELDS = {
12195
+ title: str("Judul test case."),
12196
+ steps: str("Langkah uji (teks bebas)."),
12197
+ expected: str("Hasil yang diharapkan."),
12198
+ actual: str("Hasil aktual."),
12199
+ status: enumStr(QA_CASE_STATUSES, "Status eksekusi.")
12200
+ };
12201
+ FINDING_FIELDS = {
12202
+ title: str("Judul temuan \u2014 satu masalah, satu baris."),
12203
+ severity: enumStr(QA_SEVERITIES, "Dampak TEKNIS. Bukan prioritas."),
12204
+ priority: enumStr(QA_PRIORITIES, "Urutan perbaikan P0 (paling mendesak)\u2013P3. Terpisah dari severity: bug minor di halaman checkout bisa P0."),
12205
+ area: str("Area/fitur, mis. `checkout`."),
12206
+ steps: str("Langkah reproduksi, SATU PER BARIS (penomoran `1.` di awal dibuang; server menomori)."),
12207
+ expected: str("Yang seharusnya terjadi."),
12208
+ actual: str("Yang benar-benar terjadi."),
12209
+ status: enumStr(["open", "wontfix"], "`open` atau `wontfix`. `sent` (sudah jadi backlog) hanya ditulis server."),
12210
+ testCase: str("Id test case terkait, dari hanoman_qa_report_get. String KOSONG melepas tautan; tak menyebutnya membiarkan apa adanya.")
12211
+ };
12212
+ QA_TOOLS = [
12213
+ {
12214
+ name: "hanoman_qa_reports_list",
12215
+ title: "Daftar laporan QA",
12216
+ description: "Laporan QA sebuah project, terbaru-diubah dulu, dengan nomor tampil (`QA-001`), status (`draft|submitted|closed`), keputusan (`go|no-go|conditional`), dan statistik (pass-rate dari yang dieksekusi, temuan per severity, jumlah open). Nomor tampil dihitung dari urutan pembuatan \u2014 pakai `id`, bukan nomor, saat memanggil tool lain.",
12217
+ inputSchema: obj({ properties: { project: PROJECT3 }, required: ["project"] }),
12218
+ mode: "read",
12219
+ capability: "qa:read",
12220
+ samplePath: "/projects/p1/qa/reports",
12221
+ sampleMethod: "GET",
12222
+ build: (a) => ({ method: "GET", path: base(a) }),
12223
+ shape: (raw) => raw
12224
+ },
12225
+ {
12226
+ name: "hanoman_qa_report_get",
12227
+ title: "Detail laporan QA",
12228
+ description: "Satu laporan QA lengkap: header, test case (`TC-01\u2026`), temuan (`F-01\u2026`, dengan repro bernomor, severity, prioritas, `caseCode`), dan metadata lampiran (nama, tipe, ukuran \u2014 bukan byte).",
12229
+ inputSchema: obj({ properties: { project: PROJECT3, report: REPORT }, required: ["project", "report"] }),
12230
+ mode: "read",
12231
+ capability: "qa:read",
12232
+ samplePath: "/projects/p1/qa/reports/r1",
12233
+ sampleMethod: "GET",
12234
+ build: (a) => ({ method: "GET", path: one(a) }),
12235
+ shape: (raw) => raw
12236
+ },
12237
+ {
12238
+ name: "hanoman_qa_report_create",
12239
+ title: "Buat laporan QA",
12240
+ description: "Membuat laporan QA berstatus `draft`. Hanya `project` dan `title` yang wajib. Test case dan temuan ditambahkan lewat tool terpisah.",
12241
+ inputSchema: obj({ properties: { project: PROJECT3, ...REPORT_FIELDS }, required: ["project", "title"] }),
12242
+ mode: "write",
12243
+ capability: "qa:write",
12244
+ samplePath: "/projects/p1/qa/reports",
12245
+ sampleMethod: "POST",
12246
+ build: (a) => ({ method: "POST", path: base(a), body: reportBody(a) }),
12247
+ shape: (raw) => raw
12248
+ },
12249
+ {
12250
+ name: "hanoman_qa_report_update",
12251
+ title: "Ubah laporan QA",
12252
+ description: "Mengubah header laporan; hanya field yang disebut yang ditulis. `status`: `draft \u2192 submitted \u2192 closed`; `submitted`/`closed` DITOLAK 400 bila `verdict` belum terisi. Laporan `closed` read-only (409) untuk semua tool QA \u2014 satu-satunya ubahan yang lolos adalah `status: draft` untuk membukanya kembali.",
12253
+ inputSchema: obj({
12254
+ properties: { project: PROJECT3, report: REPORT, ...REPORT_FIELDS, status: enumStr(QA_REPORT_STATUSES, "Status laporan.") },
12255
+ required: ["project", "report"]
12256
+ }),
12257
+ mode: "write",
12258
+ capability: "qa:write",
12259
+ samplePath: "/projects/p1/qa/reports/r1",
12260
+ sampleMethod: "PATCH",
12261
+ build: (a) => ({ method: "PATCH", path: one(a), body: reportBody(a) }),
12262
+ shape: (raw) => raw
12263
+ },
12264
+ {
12265
+ name: "hanoman_qa_case_create",
12266
+ title: "Tambah test case",
12267
+ description: "Menambah test case ke akhir daftar laporan. Hanya `title` yang wajib; status awal `todo`. Jawabannya adalah laporan terbaru (test case baru = entri terakhir `cases`).",
12268
+ inputSchema: obj({ properties: { project: PROJECT3, report: REPORT, ...CASE_FIELDS }, required: ["project", "report", "title"] }),
12269
+ mode: "write",
12270
+ capability: "qa:write",
12271
+ samplePath: "/projects/p1/qa/reports/r1/cases",
12272
+ sampleMethod: "POST",
12273
+ build: (a) => ({ method: "POST", path: `${one(a)}/cases`, body: caseBody(a) }),
12274
+ shape: (raw) => raw
12275
+ },
12276
+ {
12277
+ name: "hanoman_qa_case_update",
12278
+ title: "Ubah test case",
12279
+ description: "Mengubah test case; hanya field yang disebut yang ditulis. Biasa dipakai untuk mencatat hasil: `status` (`pass|fail|blocked|skipped`) dan `actual`.",
12280
+ inputSchema: obj({ properties: { project: PROJECT3, report: REPORT, case: CASE, ...CASE_FIELDS }, required: ["project", "report", "case"] }),
12281
+ mode: "write",
12282
+ capability: "qa:write",
12283
+ samplePath: "/projects/p1/qa/reports/r1/cases/c1",
12284
+ sampleMethod: "PATCH",
12285
+ build: (a) => ({ method: "PATCH", path: `${one(a)}/cases/${enc(String(a.case))}`, body: caseBody(a) }),
12286
+ shape: (raw) => raw
12287
+ },
12288
+ {
12289
+ name: "hanoman_qa_finding_create",
12290
+ title: "Catat temuan QA",
12291
+ description: "Mencatat satu temuan (bug) di laporan. Isi `severity` (dampak teknis) DAN `priority` (urutan perbaikan) secara terpisah \u2014 keduanya tak selalu sama. Tulis `steps` sebagai langkah reproduksi yang bisa diulang orang lain, satu per baris. Temuan baru berstatus `open`; jawabannya laporan terbaru (temuan baru = entri terakhir `findings`).",
12292
+ inputSchema: obj({ properties: { project: PROJECT3, report: REPORT, ...FINDING_FIELDS }, required: ["project", "report", "title"] }),
12293
+ mode: "write",
12294
+ capability: "qa:write",
12295
+ samplePath: "/projects/p1/qa/reports/r1/findings",
12296
+ sampleMethod: "POST",
12297
+ build: (a) => ({ method: "POST", path: `${one(a)}/findings`, body: findingBody(a) }),
12298
+ shape: (raw) => raw
12299
+ },
12300
+ {
12301
+ name: "hanoman_qa_finding_update",
12302
+ title: "Ubah temuan QA",
12303
+ description: "Mengubah temuan; hanya field yang disebut yang ditulis. `status`: `open` atau `wontfix`.",
12304
+ inputSchema: obj({ properties: { project: PROJECT3, report: REPORT, finding: FINDING, ...FINDING_FIELDS }, required: ["project", "report", "finding"] }),
12305
+ mode: "write",
12306
+ capability: "qa:write",
12307
+ samplePath: "/projects/p1/qa/reports/r1/findings/f1",
12308
+ sampleMethod: "PATCH",
12309
+ build: (a) => ({ method: "PATCH", path: `${one(a)}/findings/${enc(String(a.finding))}`, body: findingBody(a) }),
12310
+ shape: (raw) => raw
12311
+ },
12312
+ {
12313
+ name: "hanoman_qa_finding_to_backlog",
12314
+ title: "Kirim temuan QA ke backlog",
12315
+ description: "Membuat backlog item (`source: qa`) dari satu temuan: judul, repro, expected/actual, lingkungan, dan lampiran temuan ikut sebagai konteks agen. Temuan jadi `sent` dan menyimpan `backlogId`; hasil `findings[].spec` menampilkan stage backlog-nya. IDEMPOTEN: temuan yang sudah dikirim menjawab 200 `created: false` dengan spec yang sama. Pemetaan LOSSY: severity blocker/critical\u2192critical, minor/trivial\u2192minor; P0/P1\u2192tinggi, P2\u2192sedang, P3\u2192rendah (severity & prioritas QA asli tetap tertulis di teks backlog). Ini TIDAK membuka sesi agen; Spec-nya lahir tanpa persetujuan peluncuran bila token tak memegang `sessions:write`. Laporan `closed` tetap boleh (hanya tautan yang berubah).",
12316
+ inputSchema: obj({
12317
+ properties: { project: PROJECT3, report: REPORT, finding: FINDING, priority: enumStr(["tinggi", "sedang", "rendah"], "Timpa prioritas backlog. Tanpa ini: diturunkan dari prioritas P0\u2013P3 temuan.") },
12318
+ required: ["project", "report", "finding"]
12319
+ }),
12320
+ mode: "write",
12321
+ capability: "qa:write",
12322
+ samplePath: "/projects/p1/qa/reports/r1/findings/f1/backlog",
12323
+ sampleMethod: "POST",
12324
+ build: (a) => ({ method: "POST", path: `${one(a)}/findings/${enc(String(a.finding))}/backlog`, body: s(a.priority) ? { priority: s(a.priority) } : {} }),
12325
+ shape: (raw) => raw
12326
+ },
12327
+ {
12328
+ name: "hanoman_qa_report_to_backlog",
12329
+ title: "Kirim semua temuan open ke backlog",
12330
+ description: "Mengirim SEMUA temuan berstatus `open` sebuah laporan ke backlog sekaligus (`wontfix` dan yang sudah `sent` dilewati). Jawabannya `results` per temuan (satu yang gagal tak menggagalkan yang lain) dan `sent` = jumlah backlog baru. Semantik per temuan sama dengan hanoman_qa_finding_to_backlog.",
12331
+ inputSchema: obj({
12332
+ properties: { project: PROJECT3, report: REPORT, priority: enumStr(["tinggi", "sedang", "rendah"], "Timpa prioritas SEMUA backlog yang dibuat. Tanpa ini: diturunkan per temuan.") },
12333
+ required: ["project", "report"]
12334
+ }),
12335
+ mode: "write",
12336
+ capability: "qa:write",
12337
+ samplePath: "/projects/p1/qa/reports/r1/backlog",
12338
+ sampleMethod: "POST",
12339
+ build: (a) => ({ method: "POST", path: `${one(a)}/backlog`, body: s(a.priority) ? { priority: s(a.priority) } : {} }),
12340
+ shape: (raw) => raw
12341
+ }
12342
+ ];
12343
+ }
12344
+ });
12345
+
12057
12346
  // ../shared/src/mcp-catalog/system.ts
12058
12347
  var SYSTEM_TOOLS;
12059
12348
  var init_system = __esm({
@@ -12143,6 +12432,7 @@ var init_mcp_catalog = __esm({
12143
12432
  init_support();
12144
12433
  init_lead2();
12145
12434
  init_team2();
12435
+ init_qa2();
12146
12436
  init_system();
12147
12437
  MCP_TOOLS = [
12148
12438
  ...ABOUT_TOOLS,
@@ -12163,6 +12453,8 @@ var init_mcp_catalog = __esm({
12163
12453
  // sengaja: urutan di sini = urutan yang dibaca model, dan pekerjaan sehari-hari agen tetap
12164
12454
  // backlog/sesi/IDE — bukan kartu orang lain.
12165
12455
  ...TEAM_TOOLS,
12456
+ ...QA_TOOLS,
12457
+ // Workspace QA · laporan, test case, temuan
12166
12458
  ...SYSTEM_TOOLS
12167
12459
  ];
12168
12460
  }
@@ -12488,6 +12780,9 @@ var init_src = __esm({
12488
12780
  init_config_registry();
12489
12781
  init_semver();
12490
12782
  init_telegram();
12783
+ init_qa();
12784
+ init_qa_markdown();
12785
+ init_qa_tabular();
12491
12786
  init_webhook();
12492
12787
  init_mcp();
12493
12788
  init_auto_merge();
@@ -18956,7 +19251,7 @@ function exactOptional(innerType) {
18956
19251
  innerType
18957
19252
  });
18958
19253
  }
18959
- function nullable2(innerType) {
19254
+ function nullable3(innerType) {
18960
19255
  return new ZodNullable2({
18961
19256
  type: "nullable",
18962
19257
  innerType
@@ -19102,10 +19397,10 @@ var init_schemas2 = __esm({
19102
19397
  return exactOptional(this);
19103
19398
  },
19104
19399
  nullable() {
19105
- return nullable2(this);
19400
+ return nullable3(this);
19106
19401
  },
19107
19402
  nullish() {
19108
- return optional(nullable2(this));
19403
+ return optional(nullable3(this));
19109
19404
  },
19110
19405
  nonoptional(params) {
19111
19406
  return nonoptional(this, params);
@@ -23376,8 +23671,8 @@ function setNegotiatedProtocolVersion(instance, version3) {
23376
23671
  function isPlainObject$1(value) {
23377
23672
  return value !== null && typeof value === "object" && !Array.isArray(value);
23378
23673
  }
23379
- function mergeCapabilities(base, additional) {
23380
- const result = { ...base };
23674
+ function mergeCapabilities(base2, additional) {
23675
+ const result = { ...base2 };
23381
23676
  for (const key in additional) {
23382
23677
  const k = key;
23383
23678
  const addValue = additional[k];
@@ -28463,10 +28758,10 @@ var init_ajvProvider_CEoC_sr = __esm({
28463
28758
  schemelessOptions.skipEscape = true;
28464
28759
  return serialize(resolved, schemelessOptions);
28465
28760
  }
28466
- function resolveComponent(base, relative2, options, skipNormalization) {
28761
+ function resolveComponent(base2, relative2, options, skipNormalization) {
28467
28762
  const target = {};
28468
28763
  if (!skipNormalization) {
28469
- base = parse4(serialize(base, options), options);
28764
+ base2 = parse4(serialize(base2, options), options);
28470
28765
  relative2 = parse4(serialize(relative2, options), options);
28471
28766
  }
28472
28767
  options = options || {};
@@ -28486,24 +28781,24 @@ var init_ajvProvider_CEoC_sr = __esm({
28486
28781
  target.query = relative2.query;
28487
28782
  } else {
28488
28783
  if (!relative2.path) {
28489
- target.path = base.path;
28784
+ target.path = base2.path;
28490
28785
  if (relative2.query !== void 0) target.query = relative2.query;
28491
- else target.query = base.query;
28786
+ else target.query = base2.query;
28492
28787
  } else {
28493
28788
  if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
28494
28789
  else {
28495
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
28496
- else if (!base.path) target.path = relative2.path;
28497
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
28790
+ if ((base2.userinfo !== void 0 || base2.host !== void 0 || base2.port !== void 0) && !base2.path) target.path = "/" + relative2.path;
28791
+ else if (!base2.path) target.path = relative2.path;
28792
+ else target.path = base2.path.slice(0, base2.path.lastIndexOf("/") + 1) + relative2.path;
28498
28793
  target.path = removeDotSegments(target.path);
28499
28794
  }
28500
28795
  target.query = relative2.query;
28501
28796
  }
28502
- target.userinfo = base.userinfo;
28503
- target.host = base.host;
28504
- target.port = base.port;
28797
+ target.userinfo = base2.userinfo;
28798
+ target.host = base2.host;
28799
+ target.port = base2.port;
28505
28800
  }
28506
- target.scheme = base.scheme;
28801
+ target.scheme = base2.scheme;
28507
28802
  }
28508
28803
  target.fragment = relative2.fragment;
28509
28804
  return target;
@@ -35057,7 +35352,14 @@ var init_migrate_pg = __esm({
35057
35352
  "Task",
35058
35353
  // SPEC-481 · ADR-0100 · WebhookDelivery WAJIB sesudah WebhookEndpoint (FK endpointId).
35059
35354
  "WebhookEndpoint",
35060
- "WebhookDelivery"
35355
+ "WebhookDelivery",
35356
+ // Workspace QA · ADR-0174 · QaReport sesudah Project (FK projectId, cascade); QaCase/QaFinding/
35357
+ // QaAttachment sesudah QaReport (FK reportId, cascade). Keempatnya LOCAL-only dan TIDAK ada di
35358
+ // sumber Postgres lama — jalur 42P01 memperlakukannya sebagai nol baris (cermin Changelog).
35359
+ "QaReport",
35360
+ "QaCase",
35361
+ "QaFinding",
35362
+ "QaAttachment"
35061
35363
  ];
35062
35364
  CHUNK = 200;
35063
35365
  INT8_OID = 20;
@@ -35204,8 +35506,8 @@ import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "
35204
35506
  function addLink(indexPath, relPath, category) {
35205
35507
  let md = readFileSync7(indexPath, "utf8");
35206
35508
  if (md.includes(`(${relPath})`)) return;
35207
- const base = relPath.split("/").pop().replace(/\.md$/, "");
35208
- const line = `- [${base}](${relPath})`;
35509
+ const base2 = relPath.split("/").pop().replace(/\.md$/, "");
35510
+ const line = `- [${base2}](${relPath})`;
35209
35511
  const head = `## ${category}`;
35210
35512
  if (md.includes(head)) {
35211
35513
  const i = md.indexOf(head) + head.length;