hanoman 0.9.11 → 0.9.13

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 +358 -37
  3. package/dist/server.js +20667 -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-CYHlMkuo.js → ChangelogScreen-Ceqp0J6L.js} +1 -1
  9. package/web/assets/{DalangHanomanScreen-BIREPQkt.js → DalangHanomanScreen-CPJtZxV3.js} +1 -1
  10. package/web/assets/{DocPreviewModal-Dy5PZX3L.js → DocPreviewModal-C_vQOOHV.js} +1 -1
  11. package/web/assets/{DocsWorkspace-sstV_mwF.js → DocsWorkspace-D4P_5t9b.js} +2 -2
  12. package/web/assets/{IdeScreen-BoZ21X0r.js → IdeScreen-bD4qiBDt.js} +1 -1
  13. package/web/assets/{LeadScreen-B8j5AbCq.js → LeadScreen-Db9PHjdT.js} +1 -1
  14. package/web/assets/{ReviewScreen-9jkwgxsp.js → ReviewScreen-DLlDOQod.js} +1 -1
  15. package/web/assets/{SchedulerScreen-BmeeP6iC.js → SchedulerScreen-D6ffDlTM.js} +1 -1
  16. package/web/assets/{SettingsScreen-BpjqRvvn.js → SettingsScreen-72EvmiY4.js} +4 -4
  17. package/web/assets/{TeamScreen-ftIlQ3wy.js → TeamScreen-CY-5tnPa.js} +1 -1
  18. package/web/assets/TerminalScreen-CKac8Ti6.js +2 -0
  19. package/web/assets/{TriageScreen-CMJajN4R.js → TriageScreen-BEb2vxPb.js} +1 -1
  20. package/web/assets/{VpsScreen-B1tHTxXY.js → VpsScreen-DTJd5Gks.js} +1 -1
  21. package/web/assets/index-BsjfXxQM.css +32 -0
  22. package/web/assets/index-DYNYB1Nj.js +1039 -0
  23. package/web/assets/{live-DZWDT8yU.js → live-nMnpVkPn.js} +1 -1
  24. package/web/index.html +2 -2
  25. package/web/assets/TerminalScreen-CQuR2PLL.js +0 -2
  26. package/web/assets/index-C0hF6fPX.js +0 -1017
  27. package/web/assets/index-ezlZRsLF.css +0 -32
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.11",
3
- "sha": "5fede1a9",
4
- "builtAt": "2026-09-26T09:38:49.339Z"
2
+ "version": "0.9.13",
3
+ "sha": "e52e0b3d",
4
+ "builtAt": "2026-10-01T10:10:56.736Z"
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.md`,
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
  }
@@ -12232,19 +12524,24 @@ var init_changelog = __esm({
12232
12524
  });
12233
12525
 
12234
12526
  // ../shared/src/terminal-workspace.ts
12235
- var zSessionId, zTerminalGroup, zTerminalWorkspaceV1, zTerminalWorkspaceWrite, zTerminalWorkspaceSnapshot;
12527
+ var zSessionId, zTrackSizes, zTerminalGroup, zTerminalWorkspaceV1, zTerminalWorkspaceWrite, zTerminalWorkspaceSnapshot;
12236
12528
  var init_terminal_workspace = __esm({
12237
12529
  "../shared/src/terminal-workspace.ts"() {
12238
12530
  "use strict";
12239
12531
  init_zod();
12240
12532
  zSessionId = external_exports.string().trim().min(1).max(256);
12533
+ zTrackSizes = external_exports.array(external_exports.number().positive()).max(12);
12241
12534
  zTerminalGroup = external_exports.object({
12242
12535
  id: external_exports.string().trim().min(1).max(128),
12243
12536
  name: external_exports.string().trim().min(1).max(80),
12244
12537
  layout: external_exports.object({
12245
12538
  rows: external_exports.number().int().min(1).max(12),
12246
12539
  cols: external_exports.number().int().min(1).max(12),
12247
- cells: external_exports.array(zSessionId.nullable()).max(144)
12540
+ cells: external_exports.array(zSessionId.nullable()).max(144),
12541
+ // Bobot fr per track dari drag-resize; undefined = semua track sama besar. Opsional supaya
12542
+ // workspace lama tanpa keduanya tetap valid.
12543
+ colSizes: zTrackSizes.optional(),
12544
+ rowSizes: zTrackSizes.optional()
12248
12545
  }).strict().superRefine((layout, ctx) => {
12249
12546
  if (layout.cells.length !== layout.rows * layout.cols) {
12250
12547
  ctx.addIssue({
@@ -12253,6 +12550,20 @@ var init_terminal_workspace = __esm({
12253
12550
  message: "cells must match rows \xD7 cols"
12254
12551
  });
12255
12552
  }
12553
+ if (layout.colSizes && layout.colSizes.length !== layout.cols) {
12554
+ ctx.addIssue({
12555
+ code: external_exports.ZodIssueCode.custom,
12556
+ path: ["colSizes"],
12557
+ message: "colSizes must match cols"
12558
+ });
12559
+ }
12560
+ if (layout.rowSizes && layout.rowSizes.length !== layout.rows) {
12561
+ ctx.addIssue({
12562
+ code: external_exports.ZodIssueCode.custom,
12563
+ path: ["rowSizes"],
12564
+ message: "rowSizes must match rows"
12565
+ });
12566
+ }
12256
12567
  })
12257
12568
  }).strict();
12258
12569
  zTerminalWorkspaceV1 = external_exports.object({
@@ -12469,6 +12780,9 @@ var init_src = __esm({
12469
12780
  init_config_registry();
12470
12781
  init_semver();
12471
12782
  init_telegram();
12783
+ init_qa();
12784
+ init_qa_markdown();
12785
+ init_qa_tabular();
12472
12786
  init_webhook();
12473
12787
  init_mcp();
12474
12788
  init_auto_merge();
@@ -18937,7 +19251,7 @@ function exactOptional(innerType) {
18937
19251
  innerType
18938
19252
  });
18939
19253
  }
18940
- function nullable2(innerType) {
19254
+ function nullable3(innerType) {
18941
19255
  return new ZodNullable2({
18942
19256
  type: "nullable",
18943
19257
  innerType
@@ -19083,10 +19397,10 @@ var init_schemas2 = __esm({
19083
19397
  return exactOptional(this);
19084
19398
  },
19085
19399
  nullable() {
19086
- return nullable2(this);
19400
+ return nullable3(this);
19087
19401
  },
19088
19402
  nullish() {
19089
- return optional(nullable2(this));
19403
+ return optional(nullable3(this));
19090
19404
  },
19091
19405
  nonoptional(params) {
19092
19406
  return nonoptional(this, params);
@@ -23357,8 +23671,8 @@ function setNegotiatedProtocolVersion(instance, version3) {
23357
23671
  function isPlainObject$1(value) {
23358
23672
  return value !== null && typeof value === "object" && !Array.isArray(value);
23359
23673
  }
23360
- function mergeCapabilities(base, additional) {
23361
- const result = { ...base };
23674
+ function mergeCapabilities(base2, additional) {
23675
+ const result = { ...base2 };
23362
23676
  for (const key in additional) {
23363
23677
  const k = key;
23364
23678
  const addValue = additional[k];
@@ -28444,10 +28758,10 @@ var init_ajvProvider_CEoC_sr = __esm({
28444
28758
  schemelessOptions.skipEscape = true;
28445
28759
  return serialize(resolved, schemelessOptions);
28446
28760
  }
28447
- function resolveComponent(base, relative2, options, skipNormalization) {
28761
+ function resolveComponent(base2, relative2, options, skipNormalization) {
28448
28762
  const target = {};
28449
28763
  if (!skipNormalization) {
28450
- base = parse4(serialize(base, options), options);
28764
+ base2 = parse4(serialize(base2, options), options);
28451
28765
  relative2 = parse4(serialize(relative2, options), options);
28452
28766
  }
28453
28767
  options = options || {};
@@ -28467,24 +28781,24 @@ var init_ajvProvider_CEoC_sr = __esm({
28467
28781
  target.query = relative2.query;
28468
28782
  } else {
28469
28783
  if (!relative2.path) {
28470
- target.path = base.path;
28784
+ target.path = base2.path;
28471
28785
  if (relative2.query !== void 0) target.query = relative2.query;
28472
- else target.query = base.query;
28786
+ else target.query = base2.query;
28473
28787
  } else {
28474
28788
  if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
28475
28789
  else {
28476
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
28477
- else if (!base.path) target.path = relative2.path;
28478
- 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;
28479
28793
  target.path = removeDotSegments(target.path);
28480
28794
  }
28481
28795
  target.query = relative2.query;
28482
28796
  }
28483
- target.userinfo = base.userinfo;
28484
- target.host = base.host;
28485
- target.port = base.port;
28797
+ target.userinfo = base2.userinfo;
28798
+ target.host = base2.host;
28799
+ target.port = base2.port;
28486
28800
  }
28487
- target.scheme = base.scheme;
28801
+ target.scheme = base2.scheme;
28488
28802
  }
28489
28803
  target.fragment = relative2.fragment;
28490
28804
  return target;
@@ -35038,7 +35352,14 @@ var init_migrate_pg = __esm({
35038
35352
  "Task",
35039
35353
  // SPEC-481 · ADR-0100 · WebhookDelivery WAJIB sesudah WebhookEndpoint (FK endpointId).
35040
35354
  "WebhookEndpoint",
35041
- "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"
35042
35363
  ];
35043
35364
  CHUNK = 200;
35044
35365
  INT8_OID = 20;
@@ -35185,8 +35506,8 @@ import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "
35185
35506
  function addLink(indexPath, relPath, category) {
35186
35507
  let md = readFileSync7(indexPath, "utf8");
35187
35508
  if (md.includes(`(${relPath})`)) return;
35188
- const base = relPath.split("/").pop().replace(/\.md$/, "");
35189
- const line = `- [${base}](${relPath})`;
35509
+ const base2 = relPath.split("/").pop().replace(/\.md$/, "");
35510
+ const line = `- [${base2}](${relPath})`;
35190
35511
  const head = `## ${category}`;
35191
35512
  if (md.includes(head)) {
35192
35513
  const i = md.indexOf(head) + head.length;