hanoman 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -4913,6 +4913,12 @@ var init_agent = __esm({
4913
4913
  // capability domain produk masing-masing; domain ini tidak memberi akses shell atau sesi.
4914
4914
  "telegram:read",
4915
4915
  "telegram:write",
4916
+ // ADR-0157 · papan Tim (`/api/tasks`, `/api/members`). Domain TERSENDIRI, bukan menumpang
4917
+ // `backlog`: kartu tim adalah pekerjaan MANUSIA yang `status`-nya bebas dipindah, sementara
4918
+ // `Spec.stage` diturunkan dari fase sesi (ADR-0008/0024). Menumpangkannya berarti satu centang
4919
+ // "Backlog — tulis" diam-diam membuka papan orang, dan sebaliknya.
4920
+ "team:read",
4921
+ "team:write",
4916
4922
  // ADR-0155 · akses KETIGA: `danger`. Dipecah dari `:write` karena keempat operasi ini bukan
4917
4923
  // "menulis lebih banyak", melainkan menjalankan sesuatu di luar proses hanoman — sesi agen di
4918
4924
  // worktree, perintah di VPS, git yang mengubah sejarah, penghapusan artefak dokumen.
@@ -4957,6 +4963,8 @@ var init_agent = __esm({
4957
4963
  { id: "agents:write", domain: "agents", access: "write", label: "Custom agent \u2014 tulis", desc: "Buat/ubah/hapus custom agent; definisinya dipakai setiap sesi baru.", risk: "exec" },
4958
4964
  { id: "telegram:read", domain: "telegram", access: "read", label: "Telegram \u2014 baca", desc: "Baca status gateway, binding, memory, dan audit Telegram." },
4959
4965
  { id: "telegram:write", domain: "telegram", access: "write", label: "Telegram \u2014 tulis", desc: "Perbarui context/memory dan terbitkan reply sesi operator Telegram." },
4966
+ { id: "team:read", domain: "team", access: "read", label: "Tim \u2014 baca", desc: "Lihat kartu papan Tim & direktori anggota." },
4967
+ { id: "team:write", domain: "team", access: "write", label: "Tim \u2014 tulis", desc: "Buat/ubah/hapus kartu papan Tim & anggota; eskalasi kartu jadi backlog item." },
4960
4968
  // ADR-0155 · akses `danger`. Tak satu pun diimplikasikan `:write` di domainnya; manusia harus
4961
4969
  // mencentangnya sendiri di Settings → Akses AI Agent.
4962
4970
  { id: "sessions:spawn", domain: "sessions", access: "danger", label: "Sesi \u2014 buka sesi baru", desc: "Membuka sesi agen BARU di worktree (menjalankan claude/codex dengan izin penuh). Dipisah dari Sesi \u2014 tulis: mengendalikan sesi yang sudah ada tak lagi cukup untuk membuka yang baru.", risk: "rce" },
@@ -6520,7 +6528,15 @@ var init_api = __esm({
6520
6528
  breakdown: (id, prd) => `${API}/projects/${id}/breakdown?prd=${encodeURIComponent(prd)}`,
6521
6529
  specsBatch: `${API}/specs/batch`,
6522
6530
  // SPEC-182 · IDE Visual
6523
- ideTree: (id, ref = "") => `${API}/projects/${id}/tree${ref ? `?ref=${encodeURIComponent(ref)}` : ""}`,
6531
+ // `hidden` menyertakan entri .gitignore; `under` membuka satu tingkat isi sebuah direktori.
6532
+ ideTree: (id, ref = "", opts) => {
6533
+ const q = new URLSearchParams();
6534
+ if (ref) q.set("ref", ref);
6535
+ if (opts?.hidden) q.set("hidden", "1");
6536
+ if (opts?.under) q.set("under", opts.under);
6537
+ const s2 = q.toString();
6538
+ return `${API}/projects/${id}/tree${s2 ? `?${s2}` : ""}`;
6539
+ },
6524
6540
  ideFile: (id, path, ref = "") => `${API}/projects/${id}/file${path ? `?path=${encodeURIComponent(path)}${ref ? `&ref=${encodeURIComponent(ref)}` : ""}` : ""}`,
6525
6541
  ideGraph: (id, limit = 200, opts) => {
6526
6542
  const p3 = new URLSearchParams({ limit: String(limit) });
@@ -7640,6 +7656,51 @@ var init_webhook = __esm({
7640
7656
  }
7641
7657
  });
7642
7658
 
7659
+ // ../shared/src/team.ts
7660
+ var TASK_STATUSES, zTaskStatus, memberId, zEmail, zName, zIso, zCreateMember, zPatchMember, zCreateTask, zPatchTask, ESCALATE_SOURCES, zEscalateTask;
7661
+ var init_team = __esm({
7662
+ "../shared/src/team.ts"() {
7663
+ "use strict";
7664
+ init_zod();
7665
+ init_enums();
7666
+ TASK_STATUSES = ["backlog", "doing", "review", "done"];
7667
+ zTaskStatus = external_exports.enum(TASK_STATUSES);
7668
+ memberId = (email) => email.trim().toLowerCase();
7669
+ zEmail = external_exports.string().trim().min(3).max(200).email();
7670
+ zName = external_exports.string().trim().min(1).max(120);
7671
+ zIso = external_exports.string().datetime({ offset: true });
7672
+ zCreateMember = external_exports.object({
7673
+ name: zName,
7674
+ email: zEmail,
7675
+ role: external_exports.string().trim().max(60).nullable().optional()
7676
+ });
7677
+ zPatchMember = zCreateMember.omit({ email: true }).partial().extend({
7678
+ active: external_exports.boolean().optional()
7679
+ });
7680
+ zCreateTask = external_exports.object({
7681
+ title: external_exports.string().trim().min(1).max(300),
7682
+ detail: external_exports.string().max(2e4).nullable().optional(),
7683
+ projectId: external_exports.string().max(120).nullable().optional(),
7684
+ status: zTaskStatus.default("backlog"),
7685
+ priority: zPriority.default("sedang"),
7686
+ memberId: external_exports.string().max(200).nullable().optional(),
7687
+ startDate: zIso.nullable().optional(),
7688
+ dueDate: zIso.nullable().optional(),
7689
+ order: external_exports.number().finite().optional()
7690
+ });
7691
+ zPatchTask = zCreateTask.partial();
7692
+ ESCALATE_SOURCES = ["brief", "qa", "audit"];
7693
+ zEscalateTask = external_exports.object({
7694
+ source: external_exports.enum(ESCALATE_SOURCES).default("brief"),
7695
+ priority: zPriority.default("sedang"),
7696
+ // Dipakai HANYA saat kartunya belum punya project (`nextSpecId` butuh repo, dan repo milik
7697
+ // project). Route menolak 400 bila ia menyebut project LAIN — kartu tak boleh berpindah project
7698
+ // sebagai efek samping yang tak diminta.
7699
+ projectId: external_exports.string().max(120).optional()
7700
+ });
7701
+ }
7702
+ });
7703
+
7643
7704
  // ../shared/src/mcp-schema.ts
7644
7705
  function obj(o) {
7645
7706
  return {
@@ -7651,11 +7712,12 @@ function obj(o) {
7651
7712
  ...o.allOf ? { allOf: o.allOf } : {}
7652
7713
  };
7653
7714
  }
7654
- var str, enumStr, bool, int, strArray, PRIORITY_ENUM, SOURCE_ENUM, STAGE_ENUM, SEVERITY_ENUM, PRIORITY, BRIEF_PAYLOAD, QA_PAYLOAD, GOAL_PAYLOAD, SPEC_PAYLOAD_ONEOF, SOURCE_PAYLOAD_ALLOF, PAGE_PARAMS, DATE_PARAMS;
7715
+ var str, enumStr, bool, int, strArray, PRIORITY_ENUM, SOURCE_ENUM, STAGE_ENUM, SEVERITY_ENUM, TASK_STATUS_ENUM, ESCALATE_SOURCE_ENUM, PRIORITY, BRIEF_PAYLOAD, QA_PAYLOAD, GOAL_PAYLOAD, SPEC_PAYLOAD_ONEOF, SOURCE_PAYLOAD_ALLOF, PAGE_PARAMS, DATE_PARAMS;
7655
7716
  var init_mcp_schema = __esm({
7656
7717
  "../shared/src/mcp-schema.ts"() {
7657
7718
  "use strict";
7658
7719
  init_enums();
7720
+ init_team();
7659
7721
  str = (description, extra = {}) => ({ type: "string", description, ...extra });
7660
7722
  enumStr = (values, description) => ({ type: "string", enum: values, description });
7661
7723
  bool = (description) => ({ type: "boolean", description });
@@ -7665,6 +7727,8 @@ var init_mcp_schema = __esm({
7665
7727
  SOURCE_ENUM = zSpecSource.options;
7666
7728
  STAGE_ENUM = zStage.options;
7667
7729
  SEVERITY_ENUM = ["critical", "major", "minor"];
7730
+ TASK_STATUS_ENUM = TASK_STATUSES;
7731
+ ESCALATE_SOURCE_ENUM = ESCALATE_SOURCES;
7668
7732
  PRIORITY = enumStr(
7669
7733
  PRIORITY_ENUM,
7670
7734
  "Prioritas backlog. Nilainya bahasa Indonesia \u2014 `tinggi`, `sedang`, `rendah`. Bukan high/medium/low."
@@ -7744,7 +7808,7 @@ function paginateLocal(items, page, limit) {
7744
7808
  const p3 = Math.max(1, Math.floor(page ?? 1));
7745
7809
  return { items: items.slice((p3 - 1) * size, p3 * size), total: items.length, page: p3, pageSize: size };
7746
7810
  }
7747
- var DEFAULT_MAX_BYTES, DEFAULT_LIMIT, MAX_LIMIT, OBJECTIVE_CLIP, MARK, pick, shapeProject, shapeProjectDetail, SPEC_BASE, shapeSpecDetail, shapeSession, shapeNotification, shapeTicket, shapeGithubIssue, shapeLeadDecision;
7811
+ var DEFAULT_MAX_BYTES, DEFAULT_LIMIT, MAX_LIMIT, OBJECTIVE_CLIP, MARK, pick, shapeProject, shapeProjectDetail, SPEC_BASE, shapeSpecDetail, shapeSession, shapeNotification, shapeTicket, shapeGithubIssue, shapeTask, shapeMember, shapeLeadDecision;
7748
7812
  var init_mcp_shape = __esm({
7749
7813
  "../shared/src/mcp-shape.ts"() {
7750
7814
  "use strict";
@@ -7806,6 +7870,25 @@ var init_mcp_shape = __esm({
7806
7870
  ...pick(row, ["id", "projectId", "repoSlug", "number", "title", "authorLogin", "labels", "url", "issueState", "status", "specId", "issueUpdatedAt"]),
7807
7871
  body: clip(row.body, 500)
7808
7872
  });
7873
+ shapeTask = (row) => ({
7874
+ ...pick(row, [
7875
+ "id",
7876
+ "projectId",
7877
+ "title",
7878
+ "status",
7879
+ "priority",
7880
+ "memberId",
7881
+ "startDate",
7882
+ "dueDate",
7883
+ "order",
7884
+ "specId",
7885
+ "spec",
7886
+ "createdAt",
7887
+ "updatedAt"
7888
+ ]),
7889
+ detail: clip(row.detail, 500)
7890
+ });
7891
+ shapeMember = (row) => pick(row, ["id", "name", "email", "role", "active"]);
7809
7892
  shapeLeadDecision = (row) => ({
7810
7893
  ...pick(row, ["id", "projectId", "specId", "sessionId", "gate", "kind", "status", "confidence", "action", "choice", "createdAt"]),
7811
7894
  question: clip(row.question, 300),
@@ -8815,16 +8898,25 @@ var init_ide = __esm({
8815
8898
  {
8816
8899
  name: "hanoman_ide_tree",
8817
8900
  title: "Pohon berkas repo project",
8818
- description: "Daftar berkas repo project pada sebuah ref. Tanpa `ref` ia membaca working tree apa adanya, termasuk perubahan yang belum di-commit.",
8901
+ description: "Daftar berkas repo project pada sebuah ref. Tanpa `ref` ia membaca working tree apa adanya, termasuk perubahan yang belum di-commit. `hidden` ikut menyertakan yang .gitignore sembunyikan \u2014 direktori yang seluruhnya diabaikan (mis. `node_modules`) diruntuhkan jadi satu entri di `dirs`; buka isinya dengan `under`, satu tingkat per panggilan.",
8819
8902
  inputSchema: obj({
8820
- properties: { project: PROJECT2, ref: str("Ref git (branch/tag/sha). Kosongkan untuk working tree.") },
8903
+ properties: {
8904
+ project: PROJECT2,
8905
+ ref: str("Ref git (branch/tag/sha). Kosongkan untuk working tree."),
8906
+ hidden: bool("Sertakan berkas & direktori yang diabaikan .gitignore. Tak berlaku bersama `ref`."),
8907
+ under: str("Jalur direktori; balasannya berisi SATU tingkat isinya. Dipakai untuk membuka direktori terabaikan yang diruntuhkan.")
8908
+ },
8821
8909
  required: ["project"]
8822
8910
  }),
8823
8911
  mode: "read",
8824
8912
  capability: "ide:read",
8825
8913
  samplePath: "/projects/hanoman/tree",
8826
8914
  sampleMethod: "GET",
8827
- build: (a) => ({ method: "GET", path: `${p(a.project)}/tree`, query: query({ ref: s(a.ref) }) }),
8915
+ build: (a) => ({
8916
+ method: "GET",
8917
+ path: `${p(a.project)}/tree`,
8918
+ query: query({ ref: s(a.ref), hidden: a.hidden ? "1" : "", under: s(a.under) })
8919
+ }),
8828
8920
  shape: (raw) => raw
8829
8921
  },
8830
8922
  {
@@ -10907,6 +10999,311 @@ var init_lead2 = __esm({
10907
10999
  }
10908
11000
  });
10909
11001
 
11002
+ // ../shared/src/mcp-catalog/team.ts
11003
+ var TASK_ID, MEMBER_ID, nullable, CLEAR, TASK_FIELDS, taskBody, TASKS, MEMBERS, DESTRUCTIVE, TEAM_TOOLS;
11004
+ var init_team2 = __esm({
11005
+ "../shared/src/mcp-catalog/team.ts"() {
11006
+ "use strict";
11007
+ init_mcp_schema();
11008
+ init_mcp_shape();
11009
+ init_helpers();
11010
+ TASK_ID = str("Id kartu papan Tim, seperti muncul di hanoman_tasks_list.");
11011
+ MEMBER_ID = str("Id anggota \u2014 email ternormalisasi (huruf kecil), seperti muncul di hanoman_members_list.");
11012
+ nullable = (v) => {
11013
+ if (typeof v !== "string") return void 0;
11014
+ return v === "" ? null : v;
11015
+ };
11016
+ CLEAR = " Kirim string KOSONG untuk mengosongkannya; tak menyebutnya sama sekali berarti biarkan apa adanya.";
11017
+ TASK_FIELDS = {
11018
+ title: str("Judul kartu, satu baris."),
11019
+ detail: str("Isi kartu. Markdown bebas." + CLEAR),
11020
+ project: str("Id project pemilik kartu. Kartu BOLEH tanpa project \u2014 papan Tim tak mensyaratkannya." + CLEAR),
11021
+ status: enumStr(TASK_STATUS_ENUM, "Kolom papan. Milik MANUSIA dan bebas dipindah \u2014 ini BUKAN `stage` backlog, yang diturunkan dari fase sesi."),
11022
+ priority: PRIORITY,
11023
+ member: str("Id anggota yang ditugaskan (email huruf kecil), dari hanoman_members_list." + CLEAR),
11024
+ startDate: str("Tanggal mulai, ISO 8601 berzona mis. `2026-09-01T00:00:00.000Z`. `besok` ditolak." + CLEAR),
11025
+ dueDate: str("Tanggal jatuh tempo, ISO 8601 berzona. Ini tanggal KARTU, bukan estimasi backlog." + CLEAR)
11026
+ };
11027
+ taskBody = (a) => {
11028
+ const body = {};
11029
+ if (s(a.title)) body.title = s(a.title);
11030
+ if (s(a.status)) body.status = s(a.status);
11031
+ if (s(a.priority)) body.priority = s(a.priority);
11032
+ if (n(a.order) !== void 0) body.order = n(a.order);
11033
+ for (const [key, arg] of [
11034
+ ["detail", a.detail],
11035
+ ["projectId", a.project],
11036
+ ["memberId", a.member],
11037
+ ["startDate", a.startDate],
11038
+ ["dueDate", a.dueDate]
11039
+ ]) {
11040
+ const v = nullable(arg);
11041
+ if (v !== void 0) body[key] = v;
11042
+ }
11043
+ return body;
11044
+ };
11045
+ TASKS = [
11046
+ {
11047
+ name: "hanoman_tasks_list",
11048
+ title: "Kartu papan Tim",
11049
+ description: "Kartu kerja MANUSIA di papan Tim, terurut sesuai posisinya di kolom. Empat kolom: `backlog`, `doing`, `review`, `done` \u2014 milik manusia, tak diturunkan dari sesi agen. `specId`/`spec` terisi hanya untuk kartu yang sudah dieskalasi jadi backlog item; `specId` terisi sementara `spec` null berarti tautannya PUTUS (backlognya sudah dihapus), bukan belum dieskalasi.",
11050
+ inputSchema: obj({
11051
+ properties: {
11052
+ project: str("Id project. Tanpa ini, seluruh project \u2014 termasuk kartu tanpa project."),
11053
+ status: enumStr(TASK_STATUS_ENUM, "Kolom papan."),
11054
+ member: MEMBER_ID,
11055
+ q: str("Cari substring pada judul + detail, tak peka huruf besar-kecil. Disaring SEBELUM paginasi, jadi hasilnya tak buta terhadap kartu di luar halaman pertama."),
11056
+ ...PAGE_PARAMS
11057
+ }
11058
+ }),
11059
+ mode: "read",
11060
+ capability: "team:read",
11061
+ samplePath: "/tasks",
11062
+ sampleMethod: "GET",
11063
+ build: (a) => ({
11064
+ method: "GET",
11065
+ path: "/tasks",
11066
+ query: query({
11067
+ projectId: s(a.project),
11068
+ status: s(a.status),
11069
+ memberId: s(a.member),
11070
+ q: s(a.q),
11071
+ page: n(a.page) === void 0 ? void 0 : String(n(a.page)),
11072
+ limit: n(a.limit) === void 0 ? void 0 : String(n(a.limit))
11073
+ })
11074
+ }),
11075
+ shape: (raw) => reshapePage(raw, shapeTask)
11076
+ },
11077
+ {
11078
+ name: "hanoman_task_create",
11079
+ title: "Buat kartu papan Tim",
11080
+ description: "Membuat kartu kerja manusia di papan Tim. Kartu BARU tak pernah tertaut backlog \u2014 tautan itu lahir dari hanoman_task_escalate, bukan dari field. Untuk pekerjaan yang akan dikerjakan AGEN, pakai hanoman_backlog_create: papan ini bukan antrean sesi.",
11081
+ inputSchema: obj({ properties: { ...TASK_FIELDS }, required: ["title"] }),
11082
+ mode: "write",
11083
+ capability: "team:write",
11084
+ samplePath: "/tasks",
11085
+ sampleMethod: "POST",
11086
+ build: (a) => ({ method: "POST", path: "/tasks", body: taskBody(a) }),
11087
+ shape: (raw) => shapeTask(raw)
11088
+ },
11089
+ {
11090
+ name: "hanoman_task_update",
11091
+ title: "Ubah kartu papan Tim",
11092
+ description: "Mengubah kartu papan Tim. Hanya field yang disebut yang ditulis. Kartu yang SUDAH tertaut backlog menolak pindah project dengan 400 \u2014 lepas tautannya dulu lewat hanoman_task_unlink. `specId` sengaja tak bisa diisi di sini.",
11093
+ inputSchema: obj({ properties: { task: TASK_ID, ...TASK_FIELDS }, required: ["task"] }),
11094
+ mode: "write",
11095
+ capability: "team:write",
11096
+ samplePath: "/tasks/t1",
11097
+ sampleMethod: "PATCH",
11098
+ build: (a) => ({ method: "PATCH", path: `/tasks/${enc(String(a.task))}`, body: taskBody(a) }),
11099
+ shape: (raw) => shapeTask(raw)
11100
+ },
11101
+ {
11102
+ name: "hanoman_task_escalate",
11103
+ title: "Eskalasi kartu jadi backlog item",
11104
+ description: "Melahirkan backlog item dari sebuah kartu papan Tim, lalu menautkan keduanya. IDEMPOTEN: kartu yang sudah tertaut menjawab 200 dengan `created: false` dan spec yang sama, bukan membuat backlog kedua. Kartu TANPA project ditolak 400 \u2014 id backlog diturunkan dari repo milik project, jadi sebutkan `project` lebih dulu. Ini TIDAK membuka sesi agen; peluncurannya tetap tindakan terpisah, dan Spec-nya lahir tanpa persetujuan peluncuran bila token pemanggil tak memegang `sessions:write`.",
11105
+ inputSchema: obj({
11106
+ properties: {
11107
+ task: TASK_ID,
11108
+ source: enumStr(ESCALATE_SOURCE_ENUM, "Bentuk backlog yang dilahirkan. Default `brief`. `goal`/`no_effort`/`help` sengaja tak tersedia: dua yang pertama menuntut kalimat yang hanya operator bisa tulis, yang terakhir milik tiket Help Center."),
11109
+ priority: PRIORITY,
11110
+ project: str("Id project tujuan. HANYA dipakai bila kartunya belum punya project \u2014 menyebut project LAIN dari milik kartu ditolak 400.")
11111
+ },
11112
+ required: ["task"]
11113
+ }),
11114
+ mode: "write",
11115
+ capability: "team:write",
11116
+ samplePath: "/tasks/t1/escalate",
11117
+ sampleMethod: "POST",
11118
+ build: (a) => ({
11119
+ method: "POST",
11120
+ path: `/tasks/${enc(String(a.task))}/escalate`,
11121
+ body: query({ source: s(a.source), priority: s(a.priority), projectId: s(a.project) })
11122
+ }),
11123
+ shape: (raw) => raw
11124
+ },
11125
+ {
11126
+ name: "hanoman_task_unlink",
11127
+ title: "Lepas tautan kartu dari backlog",
11128
+ description: "Melepas tautan kartu papan Tim dari backlog item-nya. NON-DESTRUKTIF: backlog item-nya dibiarkan hidup \u2014 hapus manual lewat hanoman_backlog_delete bila memang salah eskalasi. Idempoten: kartu yang memang tak tertaut menjawab 200 apa adanya.",
11129
+ inputSchema: obj({ properties: { task: TASK_ID }, required: ["task"] }),
11130
+ mode: "write",
11131
+ capability: "team:write",
11132
+ samplePath: "/tasks/t1/escalate",
11133
+ sampleMethod: "DELETE",
11134
+ build: (a) => ({ method: "DELETE", path: `/tasks/${enc(String(a.task))}/escalate` }),
11135
+ shape: (raw) => shapeTask(raw)
11136
+ }
11137
+ ];
11138
+ MEMBERS = [
11139
+ {
11140
+ name: "hanoman_members_list",
11141
+ title: "Anggota tim",
11142
+ description: "Direktori orang untuk papan Tim. GLOBAL, bukan per project \u2014 kartu boleh tanpa project. Anggota nonaktif TETAP terdaftar (di bawah): kartu lama yang ditugaskan padanya harus tetap punya nama. `id` = email ternormalisasi, dan itu yang dipakai sebagai `member` di tool kartu.",
11143
+ inputSchema: obj({ properties: { activeOnly: bool("Hanya anggota aktif. Default: semua, nonaktif di urutan bawah."), ...PAGE_PARAMS } }),
11144
+ mode: "read",
11145
+ capability: "team:read",
11146
+ samplePath: "/members",
11147
+ sampleMethod: "GET",
11148
+ build: (a) => ({
11149
+ method: "GET",
11150
+ path: "/members",
11151
+ query: query({
11152
+ active: a.activeOnly === true ? "true" : void 0,
11153
+ page: n(a.page) === void 0 ? void 0 : String(n(a.page)),
11154
+ limit: n(a.limit) === void 0 ? void 0 : String(n(a.limit))
11155
+ })
11156
+ }),
11157
+ shape: (raw) => reshapePage(raw, shapeMember)
11158
+ },
11159
+ {
11160
+ name: "hanoman_member_create",
11161
+ title: "Tambah anggota tim",
11162
+ description: "Menambah orang ke direktori papan Tim. `id` DITURUNKAN dari email (dipangkas + huruf kecil), bukan diacak \u2014 email yang sudah terdaftar menjawab 409 berikut id-nya, bukan membuat baris kedua.",
11163
+ inputSchema: obj({
11164
+ properties: {
11165
+ name: str("Nama yang ditampilkan di kartu."),
11166
+ email: str("Email. Menjadi id anggota sesudah dipangkas & dihurufkecilkan \u2014 dan id itu TAK BISA diubah kemudian."),
11167
+ role: str("Peran, mis. `desainer`. Bebas teks.")
11168
+ },
11169
+ required: ["name", "email"]
11170
+ }),
11171
+ mode: "write",
11172
+ capability: "team:write",
11173
+ samplePath: "/members",
11174
+ sampleMethod: "POST",
11175
+ build: (a) => ({
11176
+ method: "POST",
11177
+ path: "/members",
11178
+ body: { name: String(a.name), email: String(a.email), ...s(a.role) ? { role: s(a.role) } : {} }
11179
+ }),
11180
+ shape: (raw) => shapeMember(raw)
11181
+ },
11182
+ {
11183
+ name: "hanoman_member_update",
11184
+ title: "Ubah anggota tim",
11185
+ description: "Mengubah nama, peran, atau status aktif seorang anggota. `email` TIDAK bisa diubah dan ditolak eksplisit 400: id diturunkan darinya dan sync tak punya operasi rename, jadi id yang berubah meninggalkan baris yatim di setiap mesin lain. Ganti email = hapus lalu buat baru. Menonaktifkan TIDAK melepas kartu-kartunya.",
11186
+ inputSchema: obj({
11187
+ properties: {
11188
+ member: MEMBER_ID,
11189
+ name: str("Nama baru."),
11190
+ role: str("Peran baru." + CLEAR),
11191
+ active: bool("false = nonaktif: tetap terdaftar dan tetap memegang kartunya, hanya turun di daftar.")
11192
+ },
11193
+ required: ["member"]
11194
+ }),
11195
+ mode: "write",
11196
+ capability: "team:write",
11197
+ samplePath: "/members/a@b.id",
11198
+ sampleMethod: "PATCH",
11199
+ build: (a) => {
11200
+ const body = {};
11201
+ if (s(a.name)) body.name = s(a.name);
11202
+ if (typeof a.active === "boolean") body.active = a.active;
11203
+ const role = nullable(a.role);
11204
+ if (role !== void 0) body.role = role;
11205
+ return { method: "PATCH", path: `/members/${enc(String(a.member))}`, body };
11206
+ },
11207
+ shape: (raw) => shapeMember(raw)
11208
+ }
11209
+ ];
11210
+ DESTRUCTIVE = [
11211
+ {
11212
+ name: "hanoman_task_delete",
11213
+ title: "Hapus kartu papan Tim (BERBAHAYA)",
11214
+ description: "BERBAHAYA \u2014 menghapus kartu papan Tim secara permanen di SELURUH mesin yang tersync. Backlog item yang tertaut TIDAK ikut terhapus. Untuk sekadar mengarsipkan, pindahkan kartunya ke kolom `done` lewat hanoman_task_update. Hanya muncul saat tingkat `--danger` menyala.",
11215
+ inputSchema: obj({ properties: { task: TASK_ID }, required: ["task"] }),
11216
+ mode: "danger",
11217
+ capability: "team:write",
11218
+ samplePath: "/tasks/t1",
11219
+ sampleMethod: "DELETE",
11220
+ build: (a) => ({ method: "DELETE", path: `/tasks/${enc(String(a.task))}` }),
11221
+ shape: (raw) => raw
11222
+ },
11223
+ {
11224
+ name: "hanoman_member_delete",
11225
+ title: "Hapus anggota tim (BERBAHAYA)",
11226
+ description: "BERBAHAYA \u2014 menghapus anggota secara permanen di SELURUH mesin yang tersync. Kartu-kartunya TIDAK ikut terhapus: penugasannya dikosongkan (`memberId` jadi null), dan tak ada jejak siapa pemegangnya sebelum itu. Untuk orang yang sekadar berhenti aktif, pakai hanoman_member_update dengan `active: false` \u2014 ia tetap terdaftar dan kartunya tetap bernama. Hanya muncul saat tingkat `--danger` menyala.",
11227
+ inputSchema: obj({ properties: { member: MEMBER_ID }, required: ["member"] }),
11228
+ mode: "danger",
11229
+ capability: "team:write",
11230
+ samplePath: "/members/a@b.id",
11231
+ sampleMethod: "DELETE",
11232
+ build: (a) => ({ method: "DELETE", path: `/members/${enc(String(a.member))}` }),
11233
+ shape: (raw) => raw
11234
+ }
11235
+ ];
11236
+ TEAM_TOOLS = [...TASKS, ...MEMBERS, ...DESTRUCTIVE];
11237
+ }
11238
+ });
11239
+
11240
+ // ../shared/src/mcp-catalog/system.ts
11241
+ var SYSTEM_TOOLS;
11242
+ var init_system = __esm({
11243
+ "../shared/src/mcp-catalog/system.ts"() {
11244
+ "use strict";
11245
+ init_mcp_schema();
11246
+ SYSTEM_TOOLS = [
11247
+ {
11248
+ name: "hanoman_limits",
11249
+ title: "Sisa kuota langganan Claude",
11250
+ description: "Sisa kuota langganan Claude milik mesin ini berikut kapan jendelanya reset. Dibaca realtime dan di-cache 30 detik di server. Dipakai untuk memutuskan APAKAH pekerjaan berat layak dimulai sekarang \u2014 bukan gerbang: hanoman tak pernah menolak membuka sesi karena angka ini. Tak menuntut capability apa pun.",
11251
+ inputSchema: obj({ properties: {} }),
11252
+ mode: "read",
11253
+ capability: null,
11254
+ samplePath: "/limits",
11255
+ sampleMethod: "GET",
11256
+ build: () => ({ method: "GET", path: "/limits" }),
11257
+ shape: (raw) => raw
11258
+ },
11259
+ {
11260
+ name: "hanoman_limits_codex",
11261
+ title: "Sisa kuota Codex",
11262
+ description: "Sisa kuota codex, dibaca dari snapshot `rate_limits` di rollout sesi codex terakhir \u2014 TANPA panggilan jaringan, jadi kesegarannya seumur sesi codex terakhir di mesin ini, bukan realtime. Endpoint terpisah dari hanoman_limits karena sumber dan semantik kesegarannya memang beda. Tak menuntut capability apa pun.",
11263
+ inputSchema: obj({ properties: {} }),
11264
+ mode: "read",
11265
+ capability: null,
11266
+ samplePath: "/limits/codex",
11267
+ sampleMethod: "GET",
11268
+ build: () => ({ method: "GET", path: "/limits/codex" }),
11269
+ shape: (raw) => raw
11270
+ },
11271
+ {
11272
+ name: "hanoman_update_status",
11273
+ title: "Status versi & pembaruan",
11274
+ description: "Versi hanoman yang berjalan, versi terbaru di registry npm, dan apakah instance ini PUNYA supervisor yang bisa memasangnya (`canApply`). Baca-saja: memasang pembaruan me-restart instance dan sengaja tetap tindakan manusia di dashboard \u2014 tak ada tool untuk itu, apa pun tingkat modenya. Tak menuntut capability apa pun.",
11275
+ inputSchema: obj({ properties: {} }),
11276
+ mode: "read",
11277
+ capability: null,
11278
+ samplePath: "/update",
11279
+ sampleMethod: "GET",
11280
+ build: () => ({ method: "GET", path: "/update" }),
11281
+ shape: (raw) => raw
11282
+ },
11283
+ {
11284
+ name: "hanoman_fs_browse",
11285
+ title: "Telusuri folder mesin ini",
11286
+ description: "Sub-folder nyata di MESIN tempat hanoman berjalan, berikut path absolutnya \u2014 dipakai untuk mengisi `repoDir` saat mengikat project ke codebase yang sudah ada (hanoman_project_binding_set), yang tanpa ini hanya bisa ditebak. Hanya direktori, berkas tak ikut; yang berawalan titik disembunyikan. Tanpa `path`, ia mulai dari home. Tak menuntut capability apa pun.",
11287
+ inputSchema: obj({
11288
+ properties: {
11289
+ path: str("Path absolut folder yang dibuka. Kosong = folder home pengguna yang menjalankan hanoman. Folder yang tak terbaca menjawab 400, bukan daftar kosong.")
11290
+ }
11291
+ }),
11292
+ mode: "read",
11293
+ capability: null,
11294
+ samplePath: "/fs/browse",
11295
+ sampleMethod: "GET",
11296
+ build: (a) => ({
11297
+ method: "GET",
11298
+ path: "/fs/browse",
11299
+ ...typeof a.path === "string" && a.path !== "" ? { query: { path: a.path } } : {}
11300
+ }),
11301
+ shape: (raw) => raw
11302
+ }
11303
+ ];
11304
+ }
11305
+ });
11306
+
10910
11307
  // ../shared/src/mcp-catalog/index.ts
10911
11308
  var MCP_TOOLS;
10912
11309
  var init_mcp_catalog = __esm({
@@ -10927,6 +11324,8 @@ var init_mcp_catalog = __esm({
10927
11324
  init_notifications();
10928
11325
  init_support();
10929
11326
  init_lead2();
11327
+ init_team2();
11328
+ init_system();
10930
11329
  MCP_TOOLS = [
10931
11330
  ...ABOUT_TOOLS,
10932
11331
  ...PROJECTS_TOOLS,
@@ -10940,7 +11339,12 @@ var init_mcp_catalog = __esm({
10940
11339
  ...SESSIONS_TOOLS,
10941
11340
  ...NOTIFICATIONS_TOOLS,
10942
11341
  ...SUPPORT_TOOLS,
10943
- ...LEAD_TOOLS
11342
+ ...LEAD_TOOLS,
11343
+ // ADR-0157 · papan Tim (kerja manusia) lalu status instance. Keduanya di ekor daftar dengan
11344
+ // sengaja: urutan di sini = urutan yang dibaca model, dan pekerjaan sehari-hari agen tetap
11345
+ // backlog/sesi/IDE — bukan kartu orang lain.
11346
+ ...TEAM_TOOLS,
11347
+ ...SYSTEM_TOOLS
10944
11348
  ];
10945
11349
  }
10946
11350
  });
@@ -11247,51 +11651,6 @@ var init_presence = __esm({
11247
11651
  }
11248
11652
  });
11249
11653
 
11250
- // ../shared/src/team.ts
11251
- var TASK_STATUSES, zTaskStatus, memberId, zEmail, zName, zIso, zCreateMember, zPatchMember, zCreateTask, zPatchTask, ESCALATE_SOURCES, zEscalateTask;
11252
- var init_team = __esm({
11253
- "../shared/src/team.ts"() {
11254
- "use strict";
11255
- init_zod();
11256
- init_enums();
11257
- TASK_STATUSES = ["backlog", "doing", "review", "done"];
11258
- zTaskStatus = external_exports.enum(TASK_STATUSES);
11259
- memberId = (email) => email.trim().toLowerCase();
11260
- zEmail = external_exports.string().trim().min(3).max(200).email();
11261
- zName = external_exports.string().trim().min(1).max(120);
11262
- zIso = external_exports.string().datetime({ offset: true });
11263
- zCreateMember = external_exports.object({
11264
- name: zName,
11265
- email: zEmail,
11266
- role: external_exports.string().trim().max(60).nullable().optional()
11267
- });
11268
- zPatchMember = zCreateMember.omit({ email: true }).partial().extend({
11269
- active: external_exports.boolean().optional()
11270
- });
11271
- zCreateTask = external_exports.object({
11272
- title: external_exports.string().trim().min(1).max(300),
11273
- detail: external_exports.string().max(2e4).nullable().optional(),
11274
- projectId: external_exports.string().max(120).nullable().optional(),
11275
- status: zTaskStatus.default("backlog"),
11276
- priority: zPriority.default("sedang"),
11277
- memberId: external_exports.string().max(200).nullable().optional(),
11278
- startDate: zIso.nullable().optional(),
11279
- dueDate: zIso.nullable().optional(),
11280
- order: external_exports.number().finite().optional()
11281
- });
11282
- zPatchTask = zCreateTask.partial();
11283
- ESCALATE_SOURCES = ["brief", "qa", "audit"];
11284
- zEscalateTask = external_exports.object({
11285
- source: external_exports.enum(ESCALATE_SOURCES).default("brief"),
11286
- priority: zPriority.default("sedang"),
11287
- // Dipakai HANYA saat kartunya belum punya project (`nextSpecId` butuh repo, dan repo milik
11288
- // project). Route menolak 400 bila ia menyebut project LAIN — kartu tak boleh berpindah project
11289
- // sebagai efek samping yang tak diminta.
11290
- projectId: external_exports.string().max(120).optional()
11291
- });
11292
- }
11293
- });
11294
-
11295
11654
  // ../shared/src/index.ts
11296
11655
  var init_src = __esm({
11297
11656
  "../shared/src/index.ts"() {
@@ -26508,6 +26867,7 @@ function ownsWorktree(repoDir, cwd) {
26508
26867
  import { execFile as execFile7 } from "node:child_process";
26509
26868
  import { promisify as promisify6 } from "node:util";
26510
26869
  import { existsSync as existsSync7 } from "node:fs";
26870
+ import { readdir as readdir2 } from "node:fs/promises";
26511
26871
  import { resolve as resolve14 } from "node:path";
26512
26872
  var exec6 = promisify6(execFile7);
26513
26873
  var GIT4 = { maxBuffer: 1 << 24 };
@@ -26527,6 +26887,43 @@ async function listRepoTree(repoDir, ref = "") {
26527
26887
  return [];
26528
26888
  }
26529
26889
  }
26890
+ async function listIgnoredEntries(repoDir) {
26891
+ const empty = { files: [], dirs: [] };
26892
+ if (!repoDir || !existsSync7(repoDir)) return empty;
26893
+ try {
26894
+ const { stdout } = await exec6(
26895
+ "git",
26896
+ ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "--no-empty-directory", "-z"],
26897
+ { cwd: repoDir, ...GIT4 }
26898
+ );
26899
+ const files = [], dirs = [];
26900
+ for (const e of new Set(splitZ2(stdout))) {
26901
+ if (e.endsWith("/")) dirs.push(e.slice(0, -1));
26902
+ else files.push(e);
26903
+ }
26904
+ return { files: files.sort(), dirs: dirs.sort() };
26905
+ } catch {
26906
+ return empty;
26907
+ }
26908
+ }
26909
+ var DIR_ENTRY_MAX = 5e3;
26910
+ async function listDirEntries(repoDir, rel) {
26911
+ const empty = { files: [], dirs: [], truncated: false };
26912
+ if (!repoDir) return empty;
26913
+ const abs = repoAbsPath(repoDir, rel);
26914
+ let raw;
26915
+ try {
26916
+ raw = await readdir2(abs, { withFileTypes: true });
26917
+ } catch {
26918
+ return empty;
26919
+ }
26920
+ const files = [], dirs = [];
26921
+ for (const d of raw.slice(0, DIR_ENTRY_MAX)) {
26922
+ if (d.name === ".git") continue;
26923
+ (d.isDirectory() ? dirs : files).push(`${rel}/${d.name}`);
26924
+ }
26925
+ return { files: files.sort(), dirs: dirs.sort(), truncated: raw.length > DIR_ENTRY_MAX };
26926
+ }
26530
26927
  async function readRepoFile2(repoDir, rel, ref = "") {
26531
26928
  if (!repoDir) return null;
26532
26929
  repoAbsPath(repoDir, rel, !!ref);
@@ -31222,7 +31619,7 @@ async function dropSpecAttachments(specId) {
31222
31619
 
31223
31620
  // src/services/spec-attachment-dir.ts
31224
31621
  init_db();
31225
- import { mkdir as mkdir4, readdir as readdir2, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
31622
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
31226
31623
  import { join as join13 } from "node:path";
31227
31624
  init_session_id();
31228
31625
  init_uploads();
@@ -31287,7 +31684,7 @@ async function syncSpecAttachmentsDir(specId, projectId) {
31287
31684
  }
31288
31685
  await writeFile3(join13(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
31289
31686
  const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
31290
- for (const name2 of await readdir2(dir2)) {
31687
+ for (const name2 of await readdir3(dir2)) {
31291
31688
  if (!keep.has(name2)) await rm4(join13(dir2, name2), { recursive: true, force: true }).catch(() => {
31292
31689
  });
31293
31690
  }
@@ -35153,8 +35550,25 @@ async function ide_default(app2) {
35153
35550
  app2.get("/projects/:id/tree", async (req, reply) => {
35154
35551
  const repoDir = await repoOf(req.params.id);
35155
35552
  if (repoDir === void 0) return reply.code(404).send({ error: "not found" });
35156
- const ref = req.query.ref ?? "";
35157
- return { ref, files: await listRepoTree(repoDir, ref) };
35553
+ const q = req.query;
35554
+ const ref = q.ref ?? "";
35555
+ const hidden = q.hidden === "1" || q.hidden === "true";
35556
+ if (q.under) {
35557
+ try {
35558
+ return { ref: "", ...await listDirEntries(repoDir, q.under), ignored: [] };
35559
+ } catch (e) {
35560
+ return entryError(reply, e);
35561
+ }
35562
+ }
35563
+ const files = await listRepoTree(repoDir, ref);
35564
+ if (!hidden || ref) return { ref, files, dirs: [], ignored: [] };
35565
+ const ign = await listIgnoredEntries(repoDir);
35566
+ return {
35567
+ ref,
35568
+ files: [...files, ...ign.files].sort(),
35569
+ dirs: ign.dirs,
35570
+ ignored: [...ign.files, ...ign.dirs]
35571
+ };
35158
35572
  });
35159
35573
  app2.get("/projects/:id/file", async (req, reply) => {
35160
35574
  const id = req.params.id;
@@ -35597,7 +36011,7 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
35597
36011
  }
35598
36012
 
35599
36013
  // src/routes/fs.ts
35600
- import { readdir as readdir3 } from "node:fs/promises";
36014
+ import { readdir as readdir4 } from "node:fs/promises";
35601
36015
  import { homedir as homedir4 } from "node:os";
35602
36016
  import { resolve as resolve17, dirname as dirname9, join as join16 } from "node:path";
35603
36017
  async function fs_default(app2) {
@@ -35605,7 +36019,7 @@ async function fs_default(app2) {
35605
36019
  const q = req.query.path;
35606
36020
  const dir2 = q && q.trim() ? resolve17(q.trim()) : homedir4();
35607
36021
  try {
35608
- const ents = await readdir3(dir2, { withFileTypes: true });
36022
+ const ents = await readdir4(dir2, { withFileTypes: true });
35609
36023
  const entries3 = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join16(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
35610
36024
  const parent = dirname9(dir2);
35611
36025
  return { path: dir2, parent: parent === dir2 ? null : parent, entries: entries3 };
@@ -39011,7 +39425,7 @@ async function getLimits() {
39011
39425
  }
39012
39426
 
39013
39427
  // src/services/codex-limits.ts
39014
- import { open as open3, readdir as readdir4, stat as stat2 } from "node:fs/promises";
39428
+ import { open as open3, readdir as readdir5, stat as stat2 } from "node:fs/promises";
39015
39429
  import { homedir as homedir8 } from "node:os";
39016
39430
  import { join as join24 } from "node:path";
39017
39431
  var TTL_MS3 = 3e4;
@@ -39048,7 +39462,7 @@ function toWindow(slot, raw, reached) {
39048
39462
  async function recentRollouts(dir2) {
39049
39463
  let entries3;
39050
39464
  try {
39051
- entries3 = (await readdir4(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
39465
+ entries3 = (await readdir5(dir2, { recursive: true })).filter((f) => f.endsWith(".jsonl"));
39052
39466
  } catch {
39053
39467
  return [];
39054
39468
  }
@@ -40764,7 +41178,7 @@ import { randomUUID as randomUUID9 } from "node:crypto";
40764
41178
  init_config2();
40765
41179
  init_src2();
40766
41180
  import { randomUUID as randomUUID8 } from "node:crypto";
40767
- import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir5, stat as stat3 } from "node:fs/promises";
41181
+ import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir6, stat as stat3 } from "node:fs/promises";
40768
41182
  import { join as join25, resolve as resolve19, basename as basename6 } from "node:path";
40769
41183
  var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
40770
41184
  function transcriptDir() {
@@ -40809,7 +41223,7 @@ async function listTranscripts() {
40809
41223
  const dir2 = transcriptDir();
40810
41224
  let names;
40811
41225
  try {
40812
- names = await readdir5(dir2);
41226
+ names = await readdir6(dir2);
40813
41227
  } catch (e) {
40814
41228
  if (e.code === "ENOENT") return [];
40815
41229
  throw e;
@@ -42878,7 +43292,10 @@ async function tasks_default(app2) {
42878
43292
  projectId,
42879
43293
  source,
42880
43294
  priority,
42881
- author: req.user?.email ?? "system",
43295
+ // ADR-0157 · agen punya NAMA di sini. Tanpa cabang kedua setiap eskalasi lewat MCP menulis
43296
+ // `Tim · system`, dan asal-usul backlog jadi tak bisa ditelusuri persis pada jalur yang baru
43297
+ // saja dibuka — `launchPrincipal` di bawah sudah membedakan keduanya, penulisnya harus juga.
43298
+ author: req.user?.email ?? (req.agent ? `agent:${req.agent.id}` : "system"),
42882
43299
  launchApprovedBy: launchPrincipal(req)
42883
43300
  });
42884
43301
  const view13 = taskView(r.task, { id: r.spec.id, stage: r.spec.stage, priority: r.spec.priority });
@@ -45092,6 +45509,7 @@ function capabilityForRoute(method, path) {
45092
45509
  return rw("vps");
45093
45510
  }
45094
45511
  if (top === "prds") return rw("docs");
45512
+ if (top === "tasks" || top === "members") return rw("team");
45095
45513
  if (top === "terminal") {
45096
45514
  if (seg[1] === "workspace") return "COOKIE_ONLY";
45097
45515
  if (seg[seg.length - 1] === "ws") return "sessions:write";