hanoman 0.9.28 → 0.9.30

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 (35) hide show
  1. package/README.md +5 -2
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +360 -90
  4. package/dist/server.js +2904 -1418
  5. package/docs/agent-integration.md +100 -6
  6. package/package.json +1 -1
  7. package/prisma/migrations/20261014130000_cron_report/migration.sql +19 -0
  8. package/prisma/schema.prisma +25 -0
  9. package/scripts/vps/provision.sh +26 -3
  10. package/web/assets/{DalangHanomanScreen-C3UMPHFg.js → DalangHanomanScreen-Fd7_DcSP.js} +1 -1
  11. package/web/assets/{DocPreviewModal-Bp5stb4M.js → DocPreviewModal-SMRN2K98.js} +1 -1
  12. package/web/assets/{DocsWorkspace-C9yFxK4D.js → DocsWorkspace-pxj0N6QW.js} +1 -1
  13. package/web/assets/{IdeScreen-CRFRAP6E.js → IdeScreen-Cr9Vqdrc.js} +1 -1
  14. package/web/assets/LeadScreen-CQXpJy0y.js +1 -0
  15. package/web/assets/{ReviewScreen-CMHOlmgX.js → ReviewScreen-B7NNEjxk.js} +1 -1
  16. package/web/assets/SchedulerScreen-D3rPdS-F.js +1 -0
  17. package/web/assets/SettingsScreen-Bx13PWKQ.js +62 -0
  18. package/web/assets/{TeamScreen-D4tA34Mj.js → TeamScreen-BBAsYLpT.js} +1 -1
  19. package/web/assets/TerminalScreen-DpVp4h1n.js +2 -0
  20. package/web/assets/{TriageScreen-B8ZCexaX.js → TriageScreen-wAPnttRe.js} +1 -1
  21. package/web/assets/{VpsScreen-BCrLfZh7.js → VpsScreen-DeQVx9Jy.js} +2 -2
  22. package/web/assets/fields-qnBoIMR_.js +1 -0
  23. package/web/assets/index-BQw6xQI4.js +1012 -0
  24. package/web/assets/{index-BqAP-VDq.css → index-C3KwOJxI.css} +2 -2
  25. package/web/assets/{live-DuBdI0MZ.js → live-CONO0GTu.js} +1 -1
  26. package/web/assets/{sched-lead-shared-QqbtjoVv.js → sched-lead-shared-CDR0pOqs.js} +1 -1
  27. package/web/assets/{scheduler-CQt-VP9-.css → scheduler-D64RlGvZ.css} +1 -1
  28. package/web/assets/settings-rebase-CpaFw0Tm.js +1 -0
  29. package/web/index.html +2 -2
  30. package/web/assets/LeadScreen-DfVUawoK.js +0 -1
  31. package/web/assets/SchedulerScreen-CK5QvX5c.js +0 -1
  32. package/web/assets/SettingsScreen-D7P5JgjU.js +0 -62
  33. package/web/assets/TerminalScreen-CKySL63X.js +0 -2
  34. package/web/assets/index-BRR3JL_o.js +0 -1012
  35. package/web/assets/settings-rebase-Cbh-JGJ-.js +0 -1
@@ -18,7 +18,7 @@ Naskah ini punya **satu sumber** dan tiga cara membacanya — isinya byte yang s
18
18
  ## 0. Apa itu hanoman, dan bagaimana ia bekerja
19
19
 
20
20
  hanoman adalah **orchestrator + dashboard** untuk pengembangan yang digerakkan dokumentasi. Ia tidak
21
- menulis kode sendiri; ia **menjalankan agen** (Claude Code atau Codex CLI) sebagai sesi interaktif,
21
+ menulis kode sendiri; ia **menjalankan agen** (Claude Code, Codex CLI atau Antigravity CLI) sebagai sesi interaktif,
22
22
  lalu memantau semuanya dalam satu tempat.
23
23
 
24
24
  Model kerjanya tiga tingkat, dan penting kamu pegang sebelum memanggil endpoint apa pun:
@@ -114,7 +114,7 @@ membuat token; endpoint katalognya (`GET /api/agent-tokens/capabilities`) kini t
114
114
  | `docs` | `/api/prds*`, `/api/projects/:id/{docs,prds}*` | dokumen SoT project & PRD |
115
115
  | `ide` | `/api/projects/:id/{tree,file,file-diff,working-status,graph,commit,git,status,stashes,remotes,compare,archive,pr-url}*` | tree/file working tree, operasi git |
116
116
  | `vps` | `/api/vps*` | kelola VPS, audit, harden, konsol — **high-risk (remote exec)** |
117
- | `settings` | `/api/settings*`, `/api/config*`, `/api/scheduler*` kecuali `/api/scheduler/crons*` | setelan instance & config runtime |
117
+ | `settings` | `/api/settings*`, `/api/config*`, `/api/scheduler*` kecuali `/api/scheduler/crons*` dan `/api/scheduler/reports*` | setelan instance & config runtime |
118
118
  | `support` | `/api/tickets*`, `/api/github-issues*`, `/api/projects/:id/github*` | tiket Help Center & issue GitHub (triase) |
119
119
  | `notifications` | `/api/notifications*` | notifikasi |
120
120
  | `lead` | `/api/lead*` | minta putusan ke hanoman-lead & baca jejaknya — **`lead:write` bisa menggerakkan sesi** (ADR-0091) |
@@ -131,7 +131,7 @@ membuat token; endpoint katalognya (`GET /api/agent-tokens/capabilities`) kini t
131
131
  | `team` | `/api/tasks*`, `/api/members*` | papan **Tim**: kartu kerja MANUSIA & direktori anggota (ADR-0157). `status` kartu milik manusia — ia **bukan** `stage` backlog. `POST /api/tasks/:id/escalate` melahirkan backlog item dan tetap `team:write` (cermin `POST /api/tickets/:id/accept`) |
132
132
  | `qa` | `/api/projects/:id/qa/**`, `/api/qa/**` | laporan QA, test case, temuan, lampiran, ekspor/impor. Method `DELETE` menuntut `qa:delete` (akses `danger`, ADR-0185), bukan `qa:write` |
133
133
  | `decisions` | `/api/terminal/sessions/:id/dialog*` (capability `sessions:decide`, akses `danger`) | **gerbang manusia-yang-memutuskan** (ADR-0190, ADR-0192): baca, jawab, dan rebut dialog `AskUserQuestion` sebuah sesi. **Tak diimplikasikan `sessions:write`.** Beri HANYA ke token yang tidak dipakai agen di dalam sesi hanoman — lihat §13 |
134
- | `scheduler` | `/api/scheduler/crons*` | cronjob per project — **cron = sesi agen berizin penuh yang ditunda** (ADR-0191). Membuat, mengubah prompt/agen/model/effort/jadwal, mengaktifkan, dan menjalankan cron menuntut **`scheduler:write` DAN `sessions:spawn`**; server menjawab 403 `need: sessions:spawn` bila yang kedua tak dicentang |
134
+ | `scheduler` | `/api/scheduler/crons*`, `/api/scheduler/reports*` (baca-saja, ADR-0222) | cronjob per project — **cron = sesi agen berizin penuh yang ditunda** (ADR-0191). Membuat, mengubah prompt/agen/model/effort/jadwal, mengaktifkan, dan menjalankan cron menuntut **`scheduler:write` DAN `sessions:spawn`**; server menjawab 403 `need: sessions:spawn` bila yang kedua tak dicentang |
135
135
  | `tokens` | `/api/agent-tokens*` | cetak/ubah/cabut **agent token**. Dulu cookie-only (ADR-0065); ADR-0196 membukanya dengan **pagar tak-eskalasi-diri** (§5b). `tokens:write` **bukan** izin menaikkan hak sendiri |
136
136
  | `devices` | `/api/device-tokens*` | **device token** (identitas mesin untuk sync & relay) atas nama akun admin yang menerbitkan agent token ini (ADR-0197). Plaintext sekali-tampil |
137
137
  | `access` | `/api/auth/users*`, `POST /api/auth/change-password` | daftar akun (`access:read`); **`access:admin`** (danger) membuat akun admin, menghapus akun, dan menyetel ulang password akun lain — **setara akses penuh instance** |
@@ -143,6 +143,17 @@ membuat token; endpoint katalognya (`GET /api/agent-tokens/capabilities`) kini t
143
143
  | `sync` | `GET /api/sync/{conflicts,pending}`, `POST /api/sync/now`, `POST /api/sync/conflicts/:entity/:recordId/resolve` | **hanya tiga permukaan UI sync** (ADR-0199): antrean konflik & hapus-tertunda (`sync:read`), pemicu sync manual (`sync:write`), memutuskan konflik (`sync:resolve`, akses `danger` — **membuang data satu sisi tanpa jalan pulang**, tak diimplikasikan `sync:write`). Kanal mesin-ke-mesin (`/api/sync/{pull,bootstrap,push,logs,attachments,qa-attachments,ws,relay}`) tetap bukan milik agen — lihat §5 |
144
144
  | `instance` | `POST /api/update/apply`, `POST /api/restart` | satu capability `instance:admin` (akses `danger`, ADR-0200): memasang versi hanoman terbaru dari registry npm dan me-**restart** proses server. Hanya sah bila instance punya supervisor (`canApply` di `GET /api/update`); wajib `{ "confirm": true }` dan balasan 202 dikirim **sebelum** proses keluar |
145
145
 
146
+ SPEC-1442 / ADR-0221: tool Custom Agent existing diperluas additive, tanpa endpoint/tool baru.
147
+ `hanoman_agent_create` dan `hanoman_agent_update` menerima runtime `antigravity`; model child
148
+ hanya tier `inherit|flash|pro`, bukan slug parent dan bukan effort independen. Mode `write` dan
149
+ capability `agents:write` tetap. `hanoman_agents_list` menerima filter `runtime` opsional untuk
150
+ menampilkan availability terhadap parent itu, dengan mode `read`/`agents:read` tetap. Profil
151
+ warisan yang tidak kompatibel tetap terbaca dengan `availabilityReason`; auditor read-only,
152
+ isolated-worktree dan allowlist tool eksplisit unavailable pada Antigravity. `maxTurns`/timeout
153
+ bersifat instruksional (`executionLimits:"instructional"`), bukan jaminan native. Runtime tetap
154
+ penyaring persona, bukan pemilih proses anak. Keputusan MCP ini memenuhi ADR-0202 untuk perluasan
155
+ kontrak existing; tak ada pengecualian auth baru.
156
+
146
157
  ## 4. Aturan gate & kode status
147
158
 
148
159
  Gate `onRequest` yang sama menegakkan semuanya:
@@ -306,6 +317,14 @@ Tool MCP-nya (semua tulis bermode `danger`, hanya tampil dengan `--danger`):
306
317
  | `GET /api/scheduler/queue` | `settings:read` | antrean scheduler (`?status=queued\|launched\|done\|failed`). `GET /api/scheduler/state` **tak lagi** memuat `queue` — ia memuat `queueCounts`. |
307
318
  | `POST /api/lead/decisions` | `lead:write` | minta putusan — baca **§8** dan **§11** dulu. |
308
319
 
320
+ Settings memakai tool existing `hanoman_settings_get`/`hanoman_settings_set` dan capability
321
+ `settings:read|write` untuk runtime Antigravity (SPEC-1442/ADR-0221). Panggil `_get` tepat sebelum
322
+ `_set`, kirim objek terbaru dengan perubahan yang diperlukan. Default parent berada di
323
+ `antigravity.model/effort`; sel `orchestration.<flow>.antigravity` menerima tier native
324
+ `inherit|flash|pro` atau model `null`, dengan effort `null`. Slug parent/effort independen pada
325
+ sel baru/diubah ditolak 400. Sel legacy yang tidak berubah tetap terbaca untuk diagnosis;
326
+ blok runtime ketiga yang tidak dikirim client lama dipertahankan beserta ownership-nya.
327
+
309
328
  ## 7. `POST /api/specs` — bentuk payload per `source`
310
329
 
311
330
  `source` dan bentuk `payload` **saling mengikat**. Salah pasang → **400**
@@ -497,9 +516,10 @@ capability yang sama** — bukan jalur otorisasi baru — jadi seluruh aturan §
497
516
 
498
517
  Prasyarat: `npm i -g hanoman` di mesin tempat klien AI-nya jalan.
499
518
 
500
- **Claude Code / Claude Desktop / Cursor / Copilot** (`~/.claude.json`,
519
+ **Claude Code / Claude Desktop / Cursor / Copilot / Antigravity** (`~/.claude.json`,
501
520
  `claude_desktop_config.json`, `~/.cursor/mcp.json`, `.vscode/mcp.json` — Cursor & Copilot memakai
502
- kunci `"servers"` alih-alih `"mcpServers"`):
521
+ kunci `"servers"` alih-alih `"mcpServers"`; Antigravity memakai global
522
+ `~/.gemini/config/mcp_config.json`):
503
523
 
504
524
  ```json
505
525
  {
@@ -525,7 +545,25 @@ args = ["mcp"]
525
545
  env = { HANOMAN_HOST = "https://hanoman.example", HANOMAN_AGENT_TOKEN = "hnm_agt_…" }
526
546
  ```
527
547
 
528
- Panduan siap salin untuk keempat klien, berikut tabel tool → capability, ada di dashboard:
548
+ **Antigravity — pemasangan global oleh operator (SPEC-1442/ADR-0221):** gunakan blok JSON
549
+ `mcpServers.hanoman` di atas. Pilih token instance ini lewat alur **Akses AI Agent** existing,
550
+ lalu ganti placeholder secara lokal. Untuk konfigurasi yang sudah ada, gabungkan **hanya** entri
551
+ `mcpServers.hanoman`; pertahankan semua server lain dan field root yang tidak dikenal. Jangan
552
+ mengganti seluruh berkas dengan contoh. Jika JSON rusak, bentuk root/`mcpServers` tidak berupa
553
+ objek, atau entri `hanoman` operator berbeda sudah ada, hentikan pemasangan dan lakukan
554
+ rekonsiliasi manual. Simpan berkas global dengan izin `0600`. Konfigurasi workspace
555
+ `.agents/mcp_config.json` tetap utuh. Dashboard hanya menyediakan panduan/snippet; tidak ada
556
+ writer konfigurasi lokal, endpoint baru, penerbitan token otomatis, atau penambahan capability.
557
+
558
+ Pilihan tingkat tetap sama: `args: ["mcp"]` untuk default, `["mcp", "--read-only"]` untuk
559
+ baca-saja, atau `["mcp", "--danger"]` untuk menampilkan tool berbahaya; jangan gabungkan dua
560
+ flag itu. Tingkat tampilan bukan otorisasi tambahan dan token tetap tunduk pada capability
561
+ existing. Buka sesi TUI Antigravity baru, periksa `/mcp` atau daftar tools native, lalu panggil
562
+ `hanoman_about`. Receipt cukup mencatat nama tool/status hasil; jangan menyalin token, host,
563
+ atau config credential mentah ke log/dokumen. Keberhasilan snippet/fixture sendiri belum
564
+ membuktikan koneksi native.
565
+
566
+ Panduan siap salin untuk klien di atas, berikut tabel tool → capability, ada di dashboard:
529
567
  **Settings → Akses AI Agent → MCP server**.
530
568
 
531
569
  ### Tool
@@ -545,6 +583,7 @@ Panduan siap salin untuk keempat klien, berikut tabel tool → capability, ada d
545
583
  | `hanoman_scheduler_queue_cancel`, `hanoman_scheduler_queue_requeue` | tulis | `settings:write`; requeue baris `failed` juga menuntut `sessions:spawn` (ADR-0204, 403 bila tak ada) |
546
584
  | `hanoman_lead_ask` | tulis | `lead:write` |
547
585
  | `hanoman_health`, `hanoman_models`, `hanoman_codex_version`, `hanoman_methods_status` | baca | — (`GLOBAL_READ`, ADR-0187) |
586
+ | `hanoman_antigravity_version` | baca | — (`GLOBAL_READ`, ADR-0221) |
548
587
  | `hanoman_overview_get` | baca | — (`GLOBAL_READ`, ADR-0220) |
549
588
  | `hanoman_activity_list` | baca | `activity:read` |
550
589
  | `hanoman_activity_purge` | **berbahaya** | `activity:write` |
@@ -587,6 +626,11 @@ praktik: **jangan pasang token ber-`sessions:decide` di konfigurasi yang dibaca
587
626
  (mis. `~/.claude.json` tingkat-user diwarisi setiap sesi). Pakai token terpisah, di klien MCP yang
588
627
  dijalankan manusia di luar sesi.
589
628
 
629
+ **Laporan run cron** (ADR-0222): pesan akhir agen tiap run cron disimpan sebagai dokumen markdown, satu
630
+ per run, tersync antar mesin (retensi 90/cron). Tool baca `hanoman_scheduler_reports` (daftar, tanpa
631
+ isi; saring `cron`/`project`) dan `hanoman_scheduler_report_get` (`report` = id atau `latest`; isi di
632
+ `body`), keduanya `scheduler:read`. Laporan terakhir sebuah cron: `hanoman_scheduler_report_get {cron:"<id>"}`.
633
+
590
634
  **Cronjob** (`/api/scheduler/crons*`, ADR-0191 membalik ADR-0112) kini punya tool
591
635
  `hanoman_scheduler_crons_list|cron_runs` (baca, `scheduler:read`) dan
592
636
  `hanoman_scheduler_cron_create|update|delete|run` (`danger`, `scheduler:write`). Karena sebuah cron
@@ -659,3 +703,53 @@ Memori project dan changelog dihapus (ADR-0182). Tool `hanoman_memory_*` dan
659
703
  `/api/projects/:id/changelog*` tidak terdaftar. Token tidak lagi memiliki
660
704
  capability `memory:*` atau allowlist project khusus memori. Fungsi context/memory
661
705
  kanal Telegram tetap tersedia melalui domain `telegram`.
706
+
707
+
708
+ SPEC-1442 memperluas response `hanoman_methods_status` additive: untuk Antigravity,
709
+ `available:false`, `availabilityReason`, `ready:false`, `install:[]` sampai installer upstream
710
+ terverifikasi. Tool tetap baca `GLOBAL_READ`. Terminal install Antigravity ditolak 400
711
+ `{error:string}` sebelum spawn lewat route existing; tidak menambah installer endpoint/tool.
712
+
713
+ SPEC-1442 memperluas skill library existing: `loadedBy` dapat memuat `antigravity`;
714
+ source user `.gemini/antigravity-cli` menunjuk skill native global. Source tersebut hanya
715
+ diterima untuk `layer:user`, bukan project; source asing/traversal tetap 400. Keputusan MCP
716
+ ADR-0202: `hanoman_skills_list`/`hanoman_skill_read` tetap `skills:read`, sedangkan
717
+ `hanoman_skill_create`/`hanoman_skill_fork` memperluas enum source melalui tool existing,
718
+ mode `write` dan capability `skills:write` tetap sama. Menulis berkas native lewat key
719
+ existing tetap memakai `hanoman_skill_write`/baseHash. Tidak ada endpoint atau capability baru.
720
+
721
+
722
+ SPEC-1442 Task 7 memperluas kontrak phase override transient menjadi
723
+ `{model?:string|null,effort?:string|null}`. Runtime efektif memakai `body.agent` atau Settings
724
+ bila tidak dikirim. Antigravity menerima tier `inherit|flash|pro`/model null dan effort null;
725
+ slug parent atau effort independen ditolak 400 sebelum perubahan worktree/backlog. Sel Settings
726
+ mengikuti kontrak tier yang sama. Keputusan MCP ADR-0202: tools Settings existing tetap memakai
727
+ blok penuh; deskripsi `hanoman_settings_set` dan `hanoman_session_phases` menjelaskan kontrak
728
+ native. Tidak ada endpoint baru di task ini. Terminal/events WebSocket tetap streaming exception;
729
+ relay native `invoke_subagent`/`send_message` adalah tool runtime, bukan route REST tambahan.
730
+
731
+
732
+ SPEC-1442 Task 8: `hanoman_session_create` menerima runtime `antigravity` dan phaseOverrides
733
+ nullable/berbatas untuk backlog; tier child inherit|flash|pro, effort null. Launch tetap mode danger
734
+ dengan capability `sessions:spawn`; cron existing juga menerima runtime additive. `hanoman_models`
735
+ filter Antigravity mengembalikan model dan provider runtime itu. Sandbox/hardening menolak launch
736
+ 409 `antigravity-sandbox-unsupported`, goal native 409 `antigravity-goal-unsupported`, trust/config
737
+ gagal 500 `antigravity-config-failed` teredaksi. Tidak ada fallback host/runtime. Override invalid
738
+ 400 `{error:string}` mengikuti runtime efektif sebelum approval. Frame fase Antigravity memberi
739
+ `evidence:unknown` dan attempts/effort/durasi/token null; ini bukan bukti nol penggunaan atau gagal
740
+ delegasi. Completion tetap berdasarkan marker lengkap dan checked plan. Sesi kerja tetap
741
+ interaktif; hanya advisor internal lead memakai JSON one-shot.
742
+
743
+ ### Versi Antigravity (SPEC-1442 / ADR-0221)
744
+
745
+ `hanoman_antigravity_version` membungkus `GET /api/antigravity/version`: cookie admin atau
746
+ agent token sah tanpa capability tambahan memperoleh 200 `{version,error}`. Version semver
747
+ atau null dengan alasan aman untuk biner hilang, timeout, output cacat/berlebih, dan sandbox
748
+ yang belum didukung; tidak mengungkap stdout/stderr mentah, path home, atau token. Properti
749
+ mesin ini read-only: method tulis dan path lain di bawah `/antigravity` tetap cookie-only.
750
+
751
+ Cache lima menit keyed biner efektif DB→env→`agy` dan boundary eksekusi; perubahan biner/
752
+ boundary langsung membuang cache dan hasil in-flight lama. Probe hanya `--version`, maksimum
753
+ 10 detik dan 16 KiB per stream; sandbox aktif/hardening tidak mem-probe host. Kegagalan
754
+ observasi tidak memblokir sesi host. Tool tidak memasang CLI atau mengubah konfigurasi dan
755
+ endpoint/tool versi Codex tidak berubah.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanoman",
3
- "version": "0.9.28",
3
+ "version": "0.9.30",
4
4
  "description": "Orchestrator + dashboard workflow docs-driven untuk sesi Claude Code / Codex",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,19 @@
1
+ -- ADR-0222 · laporan run cron (tersync). Ditulis tangan, ADITIF murni: satu tabel baru.
2
+ CREATE TABLE "CronReport" (
3
+ "id" TEXT NOT NULL PRIMARY KEY,
4
+ "projectId" TEXT NOT NULL,
5
+ "cronId" TEXT NOT NULL,
6
+ "cronName" TEXT NOT NULL,
7
+ "runDate" TEXT NOT NULL,
8
+ "dueAt" DATETIME NOT NULL,
9
+ "sessionId" TEXT,
10
+ "body" TEXT NOT NULL,
11
+ "bytes" INTEGER NOT NULL DEFAULT 0,
12
+ "version" INTEGER NOT NULL DEFAULT 0,
13
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
+ "updatedAt" DATETIME NOT NULL,
15
+ CONSTRAINT "CronReport_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
16
+ );
17
+
18
+ CREATE INDEX "CronReport_cronId_dueAt_idx" ON "CronReport"("cronId", "dueAt");
19
+ CREATE INDEX "CronReport_projectId_dueAt_idx" ON "CronReport"("projectId", "dueAt");
@@ -35,6 +35,7 @@ model Project {
35
35
  clientAccess ClientProjectAccess[] // SPEC-617 · ADR-0110 · akun klien yang boleh melihat project ini
36
36
  portalChats PortalChatSession[] // SPEC-854 · ADR-0129 · percakapan portal klien project ini
37
37
  tasks Task[] // SPEC-945 · ADR-0150 · kartu kerja manusia di project ini
38
+ cronReports CronReport[] // ADR-0222 · laporan run cron project ini
38
39
  qaReports QaReport[] // Workspace QA · laporan QA project ini
39
40
  }
40
41
 
@@ -1041,6 +1042,30 @@ model LogCursor {
1041
1042
  @@id([deviceId, lane])
1042
1043
  }
1043
1044
 
1045
+ // ADR-0222 · laporan hasil SATU run cron: pesan akhir agen (hook `Stop`) sebagai markdown. TERSYNC
1046
+ // (entitas `cronReport`). `id` = `SchedulerCronRun.id` supaya tulis ulang idempoten dan dua mesin tak
1047
+ // bisa menerbitkan laporan ganda untuk run yang sama. `cronId` soft-link TANPA FK: SchedulerCron
1048
+ // LOCAL-only, jadi cron milik mesin lain tak ada di DB ini sementara laporannya tetap harus terbaca.
1049
+ // Nama berkas `.md` diturunkan (`cronReportFilename`), tak disimpan.
1050
+ model CronReport {
1051
+ id String @id
1052
+ projectId String
1053
+ cronId String
1054
+ cronName String
1055
+ runDate String // YYYY-MM-DD, zona lokal mesin penjalan, dari dueAt
1056
+ dueAt DateTime
1057
+ sessionId String?
1058
+ body String
1059
+ bytes Int @default(0)
1060
+ version Int @default(0)
1061
+ createdAt DateTime @default(now())
1062
+ updatedAt DateTime @updatedAt
1063
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1064
+
1065
+ @@index([cronId, dueAt])
1066
+ @@index([projectId, dueAt])
1067
+ }
1068
+
1044
1069
  // Workspace QA · bagian 1 · laporan QA per project (design 2026-10-01).
1045
1070
  //
1046
1071
  // LOCAL-only di bagian ini, tetapi `version` sudah ada pada tiga entitas utama supaya bagian 3
@@ -11,6 +11,7 @@
11
11
  # NOL asumsi tentang SSH: skrip ini dijalankan lewat `ssh … bash -s` DAN secara lokal oleh
12
12
  # `hanoman provision`. Jangan pernah membaca $SSH_*, /dev/tty, atau berasumsi ada tty.
13
13
  # NOL rahasia: tak pernah membaca, menulis, atau meminta kredensial agen.
14
+ unset GEMINI_API_KEY GOOGLE_GEMINI_BASE_URL
14
15
  set -uo pipefail # sengaja TANPA -e: satu komponen gagal tak boleh membunuh laporan sisanya
15
16
 
16
17
  MODE="${MODE:-probe}"
@@ -59,9 +60,21 @@ probe_agent_cli() {
59
60
  else comp "$id" absent ""; fi
60
61
  }
61
62
 
63
+ probe_antigravity() {
64
+ if [ "$PROFILE" = production ]; then comp antigravity absent sandbox-unsupported; return; fi
65
+ local bin="${HANOMAN_ANTIGRAVITY_BIN:-agy}" v
66
+ if ! have "$bin"; then comp antigravity absent ""; return; fi
67
+ if ! v="$(timeout -s KILL 10 "$bin" --version 2>/dev/null | head -c 16385)"; then
68
+ comp antigravity partial "version-unavailable auth-manual"; return
69
+ fi
70
+ if [ "${#v}" -le 16384 ] && [[ "$v" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
71
+ comp antigravity partial "auth-manual $v"
72
+ else comp antigravity partial "version-unavailable auth-manual"; fi
73
+ }
74
+
62
75
  do_probe() {
63
76
  probe_base; probe_node; probe_hanoman; probe_caddy; probe_podman; probe_agent_image
64
- probe_agent_cli claude; probe_agent_cli codex; probe_gh
77
+ probe_agent_cli claude; probe_agent_cli codex; probe_antigravity; probe_gh
65
78
  }
66
79
 
67
80
  # ---------- apply ----------
@@ -80,7 +93,7 @@ deps_of() {
80
93
  podman) echo "base" ;;
81
94
  agent-image) echo "podman" ;;
82
95
  claude|codex) if [ "$PROFILE" = production ]; then echo "agent-image"; else echo "node"; fi ;;
83
- gh) echo "base" ;;
96
+ antigravity|gh) echo "base" ;;
84
97
  *) echo "" ;;
85
98
  esac
86
99
  }
@@ -105,6 +118,11 @@ apply_gh() { pkg_install gh; }
105
118
  apply_claude() { npm i -g @anthropic-ai/claude-code >/dev/null 2>&1; }
106
119
  apply_codex() { npm i -g @openai/codex >/dev/null 2>&1; }
107
120
 
121
+ # SPEC-1442: official host installer; never run it inside or as a replacement for Podman.
122
+ apply_antigravity() {
123
+ curl -fsSL https://antigravity.google/cli/install.sh 2>/dev/null | bash >/dev/null 2>&1
124
+ }
125
+
108
126
  apply_podman() {
109
127
  pkg_install podman || return 1
110
128
  id -u "$HANOMAN_USER" >/dev/null 2>&1 || useradd --system --create-home --home-dir "$HANOMAN_DATA" "$HANOMAN_USER"
@@ -215,6 +233,7 @@ probe_one() {
215
233
  base) probe_base ;; node) probe_node ;; hanoman) probe_hanoman ;; caddy) probe_caddy ;;
216
234
  podman) probe_podman ;; agent-image) probe_agent_image ;; gh) probe_gh ;;
217
235
  claude|codex) probe_agent_cli "$1" ;;
236
+ antigravity) probe_antigravity ;;
218
237
  esac | awk '{print $3}'
219
238
  }
220
239
 
@@ -224,13 +243,16 @@ already_present() {
224
243
  local id="$1" st="$2"
225
244
  [ "$st" = ok ] && return 0
226
245
  if [ "$st" = partial ]; then
227
- case "$id" in claude|codex|gh) return 0 ;; esac
246
+ case "$id" in claude|codex|antigravity|gh) return 0 ;; esac
228
247
  fi
229
248
  return 1
230
249
  }
231
250
 
232
251
  apply_one() {
233
252
  local id="$1"
253
+ if [ "$id" = antigravity ] && [ "$PROFILE" != lab ]; then
254
+ step "$id" fail "sandbox-unsupported; Antigravity hanya host/lab"; mark_failed "$id"; return
255
+ fi
234
256
 
235
257
  for d in $(deps_of "$id"); do
236
258
  if has_failed "$d"; then step "$id" skip "blocked-by $d"; mark_failed "$id"; return; fi
@@ -251,6 +273,7 @@ apply_one() {
251
273
  base) apply_base || rc=$? ;; node) apply_node || rc=$? ;; hanoman) apply_hanoman || rc=$? ;;
252
274
  caddy) apply_caddy || rc=$? ;; podman) apply_podman || rc=$? ;;
253
275
  agent-image) apply_agent_image || rc=$? ;; claude) apply_claude || rc=$? ;;
276
+ antigravity) apply_antigravity || rc=$? ;;
254
277
  codex) apply_codex || rc=$? ;; gh) apply_gh || rc=$? ;;
255
278
  *) step "$id" fail "komponen tak dikenal"; mark_failed "$id"; return ;;
256
279
  esac
@@ -1,4 +1,4 @@
1
- import{j as e,u as be,I as C,l as I,S as se,F as te}from"./index-BRR3JL_o.js";import{R as d}from"./react-DfGhg7ZW.js";import"./markdown-CWEY34jk.js";import"./xterm-3VOAfa_q.js";function ye({height:s,color:a,rod:l}){return e.jsxs("svg",{viewBox:"0 0 64 128",width:s*64/128,height:s,"aria-hidden":"true",focusable:"false",children:[e.jsx("path",{d:"M31.2 74 L30.2 127 L33.8 127 L32.8 74 Z",fill:l}),e.jsx("path",{d:"M51 79 L50.2 122 L53 122 L52.4 79 Z",fill:l}),e.jsx("path",{d:`M32 73
1
+ import{j as e,u as be,I as C,l as I,S as se,F as te}from"./index-BQw6xQI4.js";import{R as d}from"./react-DfGhg7ZW.js";import"./markdown-CWEY34jk.js";import"./xterm-3VOAfa_q.js";function ye({height:s,color:a,rod:l}){return e.jsxs("svg",{viewBox:"0 0 64 128",width:s*64/128,height:s,"aria-hidden":"true",focusable:"false",children:[e.jsx("path",{d:"M31.2 74 L30.2 127 L33.8 127 L32.8 74 Z",fill:l}),e.jsx("path",{d:"M51 79 L50.2 122 L53 122 L52.4 79 Z",fill:l}),e.jsx("path",{d:`M32 73
2
2
  C 24 76, 19 88, 17.5 103 C 17 106, 20 107, 24 106.4
3
3
  C 28 106, 30 104, 32 104 C 34 104, 36 106, 40 106.4 C 44 107, 47 106, 46.5 103
4
4
  C 45 88, 40 76, 32 73 Z`,fill:a}),e.jsx("path",{d:"M32 74 C 29.5 66, 29.5 60, 31 53 L 37 53 C 38.5 60, 37.5 67, 34.5 74 Z",fill:a}),e.jsx("path",{d:`M31 53
@@ -1 +1 @@
1
- import{j as e,M as r,aO as d,aQ as a,m as x}from"./index-BRR3JL_o.js";import"./react-DfGhg7ZW.js";function f({path:i,text:o,eyebrow:n,download:t,onClose:s}){const l=i.slice(i.lastIndexOf("/")+1)||i;return e.jsx(r,{open:!0,title:l,eyebrow:n??i,icon:"book-open",onClose:s,width:980,fillHeight:!0,children:e.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[t&&e.jsx("div",{style:{display:"flex",justifyContent:"flex-end",paddingBottom:6,borderBottom:"1px solid var(--border-hair)",marginBottom:8},children:e.jsx(d,{href:t})}),e.jsx("div",{"data-testid":"doc-preview-scroll",style:{flex:"1 1 0",minHeight:0,overflow:"auto",padding:"0 4px 8px"},children:o?e.jsx(a,{text:o,name:i}):e.jsx(x,{kind:"empty",icon:"file-text",title:"Berkas kosong",hint:i})})]})})}export{f as D};
1
+ import{j as e,M as r,aP as d,aR as a,m as x}from"./index-BQw6xQI4.js";import"./react-DfGhg7ZW.js";function f({path:i,text:o,eyebrow:n,download:t,onClose:s}){const l=i.slice(i.lastIndexOf("/")+1)||i;return e.jsx(r,{open:!0,title:l,eyebrow:n??i,icon:"book-open",onClose:s,width:980,fillHeight:!0,children:e.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[t&&e.jsx("div",{style:{display:"flex",justifyContent:"flex-end",paddingBottom:6,borderBottom:"1px solid var(--border-hair)",marginBottom:8},children:e.jsx(d,{href:t})}),e.jsx("div",{"data-testid":"doc-preview-scroll",style:{flex:"1 1 0",minHeight:0,overflow:"auto",padding:"0 4px 8px"},children:o?e.jsx(a,{text:o,name:i}):e.jsx(x,{kind:"empty",icon:"file-text",title:"Berkas kosong",hint:i})})]})})}export{f as D};
@@ -1,3 +1,3 @@
1
- import{q as Ne,d as De,h as S,j as e,m as w,I as N,U as Se,Y as Ce,B as O,f as Ke,n as q,O as Le,aO as Me,aQ as te,D as $e,aT as ze,i as ie}from"./index-BRR3JL_o.js";import{R as o}from"./react-DfGhg7ZW.js";import"./markdown-CWEY34jk.js";import"./xterm-3VOAfa_q.js";function de(n){let t={...n,kids:n.kids.map(de)};for(;!t.cat&&t.kids.length===1;){const i=t.kids[0];t={...i,label:t.label+"/"+i.label}}return t}function re(n){const t={path:"",kids:[]};for(const i of n){if(i.cat==="."){t.kids.push({path:".",label:".",cat:i,kids:[]});continue}let l=t;for(const u of i.cat.split("/")){const k=l.path?l.path+"/"+u:u;let m=l.kids.find(s=>s.path===k);m||(m={path:k,label:u,kids:[]},l.kids.push(m)),l=m}l.cat=i}return t.kids.map(de)}function le(n){const t=n.find(i=>i.scored&&i.linked)??n.find(i=>i.scored)??n[0];return t&&t.files[0]?`${t.cat}/${t.files[0]}`:""}function oe(n){return/(^|\/)adr(s)?(\/|$)/i.test(n)?"ADR":/(^|\/)prd(s)?(\/|$)/i.test(n)?"PRD":/(^|\/)spec(s|ification)?(\/|$)/i.test(n)?"Spec":"Lainnya"}const Fe=["PRD","Spec","ADR","Lainnya"],ue=n=>`dir:${n}`,Pe=(n,t)=>!!t&&t.startsWith(n.path+"/");function he({node:n,depth:t,ctx:i}){var h,p,y;const l=Pe(n,i.selected),[u,k]=o.useState(l);o.useEffect(()=>{l&&k(!0)},[l]);const m=((h=n.cat)==null?void 0:h.scored)??!0,s=((p=n.cat)==null?void 0:p.linked)??!0,v=ue(n.path),b=()=>k(d=>!d),c=d=>{d.key==="Enter"||d.key===" "?(d.preventDefault(),b()):d.key==="ArrowRight"&&!u?(d.preventDefault(),k(!0)):d.key==="ArrowLeft"&&u&&(d.preventDefault(),k(!1))};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{role:"treeitem","aria-level":t+1,"aria-expanded":u,tabIndex:i.tabbable===v?0:-1,className:"hn-docs-node hn-docs-node-dir",style:{paddingLeft:6+t*12},onClick:b,onKeyDown:c,onFocus:()=>i.setFocusKey(v),children:[e.jsx(N,{name:u?"chevron-down":"chevron-right",size:14,color:"var(--text-subtle)"}),e.jsx(N,{name:"folder",size:15,color:m?s?"var(--brass-500)":"var(--clay-500)":"var(--text-subtle)"}),e.jsxs("span",{className:"hn-docs-dir-label",children:[n.label,"/"]}),n.cat&&m&&(n.cat.linked?e.jsx(N,{name:"link",size:13,color:"var(--leaf-600)"}):e.jsx(N,{name:"unlink",size:13,color:"var(--clay-500)"}))]}),u&&e.jsxs("div",{role:"group",className:"hn-docs-group",children:[n.kids.map(d=>e.jsx(he,{node:d,depth:t+1,ctx:i},d.path)),(y=n.cat)==null?void 0:y.files.map(d=>{const f=n.path+"/"+d,j=f===i.selected;return e.jsxs("div",{role:"treeitem","aria-level":t+2,"aria-selected":j,"aria-current":j?"true":void 0,tabIndex:i.tabbable===f?0:-1,className:`hn-docs-node hn-docs-node-file${j?" is-selected":""}`,style:{paddingLeft:18+t*12},onClick:()=>i.onSelect(f),onFocus:()=>i.setFocusKey(f),onKeyDown:g=>{(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),i.onSelect(f))},children:[e.jsx(N,{name:"file-text",size:13,color:j?"var(--brass-700)":"var(--text-subtle)"}),e.jsx("span",{className:`hn-docs-file-label${!m||s?"":" is-unlinked"}`,children:d})]},d)})]})]})}function ce({label:n,nodes:t,selected:i,onSelect:l}){const[u,k]=o.useState(null),m=t[0]?ue(t[0].path):"",v={selected:i,onSelect:l,tabbable:u??(i||m),setFocusKey:c=>k(c)},b=c=>{if(c.key!=="ArrowDown"&&c.key!=="ArrowUp")return;const h=Array.from(c.currentTarget.querySelectorAll('[role="treeitem"]')),p=h.indexOf(document.activeElement);if(p<0)return;c.preventDefault();const y=h[Math.min(h.length-1,Math.max(0,p+(c.key==="ArrowDown"?1:-1)))];y==null||y.focus()};return e.jsx("div",{role:"tree","aria-label":n,className:"hn-docs-tree",onKeyDown:b,children:t.map(c=>e.jsx(he,{node:c,depth:0,ctx:v},c.path))})}function Ie({projectId:n,projectName:t,docStatus:i}){const[l,u]=o.useState([]),[k,m]=o.useState(0),[s,v]=Ne(ze("docs",n),"selected","",$e),[b,c]=o.useState({}),[h,p]=o.useState("preview"),[y,d]=o.useState("tree"),[f,j]=o.useState(""),[g,U]=o.useState(!1),[L,W]=o.useState(!1),[G,C]=o.useState(""),[$,Q]=o.useState(""),[D,z]=o.useState("all"),{confirm:J,dialog:me}=De(),[F,K]=o.useState("loading"),[pe,ke]=o.useState(0);o.useEffect(()=>{let a=!0;return K("loading"),S.getDocs(n).then(r=>{if(!a)return;const x=r.tree;u(x),m(r.coverage),c({}),p("preview"),K("ready"),C(""),Q(""),z("all"),v(le(x))}).catch(()=>{a&&(u([]),v(""),K("error"))}),()=>{a=!1}},[n,pe]),o.useEffect(()=>{if(!s||s in b)return;let a=!0;return S.getDoc(n,s).then(r=>{a&&c(x=>({...x,[s]:r.content}))}).catch(()=>{a&&c(r=>({...r,[s]:null}))}),()=>{a=!1}},[s,n,b]);const P=!!s&&!(s in b),A=s?b[s]===null:!1,fe=()=>c(a=>{const r={...a};return delete r[s],r}),V=a=>D==="all"||oe(a.cat)===D,xe=re(l.filter(a=>a.scored&&V(a))),Y=re(l.filter(a=>!a.scored&&V(a))),B=b[s]??"",be=s.includes("/")?s.slice(0,s.lastIndexOf("/")):".",T=l.find(a=>a.cat===be),_=s,E=h==="edit"&&f!==B,I=o.useMemo(()=>l.flatMap(a=>a.files.map(r=>({key:`${a.cat}/${r}`,scored:a.scored,linked:a.linked,kind:oe(a.cat)}))),[l]),R=$.trim().toLowerCase(),X=R?I.filter(a=>(D==="all"||a.kind===D)&&a.key.toLowerCase().includes(R)):[],ve=o.useMemo(()=>{const a={PRD:0,Spec:0,ADR:0,Lainnya:0};for(const r of I)a[r.kind]+=1;return a},[I]),ye=i==="broken"?"broken":i==="drift"?"drift":"ok";function Z(a){if(!E){p("preview"),a();return}J({title:"Buang perubahan?",eyebrow:s,message:"Isi dokumen yang kamu ubah belum disimpan.",impact:["Perubahan di editor hilang dan tak bisa dikembalikan dari dashboard."],confirmLabel:"Buang perubahan",cancelLabel:"Lanjut mengedit",tone:"danger",icon:"trash-2"}).then(r=>{r&&(p("preview"),a())})}function ge(){C(""),j(B),p("edit")}const ee=()=>Z(()=>{});async function ae(){if(!(L||!s)){W(!0),C("");try{await S.putDoc(n,s,f),c(a=>({...a,[s]:f})),p("preview")}catch(a){C(ie("menyimpan dokumen",a))}finally{W(!1)}}}function H(a){Z(()=>{v(a),p("preview"),d("viewer")})}async function se(){const a=await S.getDocs(n),r=a.tree;u(r),m(a.coverage),r.some(x=>x.files.some(M=>`${x.cat}/${M}`===s))||v(le(r))}async function ne(){if(!g){U(!0);try{await se()}catch{K("error")}finally{U(!1)}}}async function je(){if(!s)return;const a=s;let r=!1;try{r=await J({title:`Hapus ${a}?`,eyebrow:"Source of Truth",message:"Dokumen ini adalah Source of Truth project. Berkas aslinya di disk ikut dihapus.",impact:["Berkas di disk dihapus dan tak bisa dikembalikan dari dashboard.","Kategori indeks dihitung ulang setelah penghapusan."],confirmLabel:"Hapus dokumen",tone:"danger",icon:"trash-2",run:()=>S.deleteDoc(n,a)})}catch(x){C(ie("menghapus dokumen",x));return}if(r){c(x=>{const M={...x};return delete M[a],M});try{await se()}catch{K("error")}}}const we=F==="loading"?e.jsx(w,{kind:"loading",compact:!0,title:"Memuat indeks…"}):F==="error"?e.jsx(w,{kind:"error",compact:!0,title:"Gagal memuat indeks dokumen",hint:t,action:()=>ke(a=>a+1)}):l.length===0?e.jsx(w,{kind:"empty",compact:!0,icon:"folder-open",title:"Belum ada dokumen",hint:"Belum ada Markdown di repo ini.",action:ne,actionLabel:"Muat ulang",actionIcon:"radar"}):R?X.length===0?e.jsx(w,{kind:"empty",compact:!0,icon:"search",title:"Tak ada berkas cocok",hint:`Tak ada yang cocok dengan “${$.trim()}”.`}):e.jsx("div",{role:"list","aria-label":"Hasil pencarian berkas",className:"hn-docs-results",children:X.map(a=>e.jsxs("button",{type:"button","aria-current":a.key===s?"true":void 0,onClick:()=>H(a.key),className:`hn-docs-result${a.key===s?" is-selected":""}`,children:[e.jsx(N,{name:"file-text",size:13,color:"var(--text-subtle)"}),e.jsx("span",{className:"hn-prj-mono",children:a.key}),e.jsx("span",{className:"hn-docs-result-kind",children:a.kind})]},a.key))}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{label:"Berkas dokumen",nodes:xe,selected:s,onSelect:H}),Y.length>0&&e.jsxs("div",{className:"hn-docs-unscored",children:[e.jsx("div",{className:"hn-eyebrow hn-docs-unscored-label",children:"Lainnya (tidak dinilai)"}),e.jsx(ce,{label:"Dokumen tidak dinilai",nodes:Y,selected:s,onSelect:H})]})]});return e.jsxs(e.Fragment,{children:[e.jsx(Se,{ariaLabel:"Panel Dokumen",active:y,onActiveChange:a=>d(a),masterWidth:288,className:"hn-workspace-panels",panels:[{id:"tree",label:"Indeks",className:"hn-panel-flex",content:e.jsxs("aside",{"aria-label":"Indeks dokumen",className:"hn-docs-index",children:[e.jsxs("div",{className:"hn-docs-index-head",children:[e.jsxs("span",{className:"hn-eyebrow",children:["dokumen · ",t]}),e.jsxs("div",{className:"hn-docs-index-actions",children:[e.jsx(Ce,{status:ye,size:"sm"}),e.jsx(O,{size:"sm",variant:"ghost",leftIcon:g?"loader":"radar",onClick:ne,disabled:g,children:g?"Memuat…":"Muat ulang"})]})]}),F==="ready"&&l.length>0&&e.jsxs("div",{className:"hn-docs-index-tools",children:[e.jsx(Ke,{size:"sm","aria-label":"Cari berkas dokumen",leftIcon:"search",value:$,placeholder:"mis. product/prd",onChange:a=>Q(a.target.value),className:"hn-prj-full"}),e.jsxs("div",{role:"group","aria-label":"Kategori dokumen",className:"hn-prj-chips",children:[e.jsx("button",{type:"button",className:"hn-prj-chip hn-touch-target","aria-pressed":D==="all",onClick:()=>z("all"),children:"Semua"}),Fe.map(a=>e.jsxs("button",{type:"button",className:"hn-prj-chip hn-touch-target","aria-pressed":D===a,onClick:()=>z(a),children:[a,e.jsx("span",{className:"hn-prj-chip-count",children:ve[a]})]},a))]}),e.jsxs("span",{className:"hn-docs-cov",title:"Persentase kategori yang terindeks",children:["Cakupan ",Math.round(k),"%"]})]}),e.jsx("div",{className:"hn-docs-tree-scroll",children:we})]})},{id:"viewer",label:"Dokumen",className:"hn-panel-flex",content:e.jsxs("section",{"aria-label":"Isi dokumen",className:"hn-docs-viewer",style:{display:"flex",flexDirection:"column",minHeight:0},children:[e.jsxs("div",{className:"hn-docs-viewer-head",children:[e.jsx(N,{name:"file-text",size:15,color:"var(--text-muted)"}),e.jsx("span",{className:"hn-prj-mono hn-docs-path",children:_}),T&&T.scored&&(T.linked?e.jsx(q,{tone:"ok",size:"sm",icon:"link",children:"terindeks"}):e.jsx(q,{tone:"err",size:"sm",icon:"unlink",children:"tak tertaut"})),e.jsx("span",{className:"hn-grow"}),e.jsxs("div",{role:"group","aria-label":"Mode dokumen",className:"hn-prj-seg",children:[e.jsx("button",{type:"button","aria-pressed":h==="preview",className:"hn-touch-target",onClick:()=>{h==="edit"&&ee()},children:"Baca"}),e.jsx("button",{type:"button","aria-pressed":h==="edit",className:"hn-touch-target",onClick:()=>{h!=="edit"&&ge()},disabled:!s||P||A,children:"Edit"})]}),h==="preview"&&e.jsx(Le,{label:"Aksi untuk dokumen",icon:"ellipsis",items:[{key:"delete",label:"Hapus dokumen…",icon:"trash-2",tone:"danger",disabled:!s,onSelect:()=>{je()}}],children:e.jsx(Me,{href:a=>S.docDownloadUrl(n,s,a),disabled:!s||P||A})})]}),h==="edit"&&e.jsxs("div",{role:"region","aria-label":"Simpan perubahan",className:"hn-docs-savebar",children:[E?e.jsx(q,{tone:"warn",size:"sm",children:"belum disimpan"}):e.jsx("span",{className:"hn-prj-hint",children:"Tak ada perubahan"}),e.jsx("span",{className:"hn-grow"}),e.jsx(O,{size:"sm",variant:"ghost",onClick:ee,disabled:L,children:"Batal"}),e.jsx(O,{size:"sm",leftIcon:"check",onClick:()=>void ae(),loading:L,disabled:L||!E,children:"Simpan"})]}),G&&e.jsx("div",{role:"alert",className:"hn-docs-error",children:G}),h==="preview"?e.jsx("div",{"data-testid":"doc-preview-scroll",className:"hn-docs-body",style:{flex:"1 1 auto",minHeight:0,overflow:"auto"},children:s?P?e.jsx(w,{kind:"loading",title:"Memuat dokumen…",hint:s}):A?e.jsx(w,{kind:"error",title:"Gagal memuat dokumen",hint:s,action:fe}):e.jsx("div",{className:"hn-docs-read",children:e.jsx(te,{text:B,name:s})}):e.jsx(w,{kind:"empty",icon:"file-text",title:"Tidak ada dokumen dipilih",hint:"Pilih file dari indeks di kiri."})}):e.jsxs("div",{className:"hn-docs-edit",children:[e.jsxs("div",{className:"hn-docs-edit-pane",children:[e.jsx("div",{className:"hn-docs-edit-label",children:e.jsx("span",{className:"hn-eyebrow",children:"Markdown"})}),e.jsx("textarea",{"aria-label":`Isi ${_}`,value:f,spellCheck:!1,onChange:a=>j(a.target.value),onKeyDown:a=>{(a.metaKey||a.ctrlKey)&&a.key.toLowerCase()==="s"&&(a.preventDefault(),ae())},placeholder:`# Judul dokumen
1
+ import{q as Ne,d as De,h as S,j as e,m as w,I as N,U as Se,Y as Ce,B as O,f as Ke,n as U,O as Le,aP as Me,aR as te,D as Pe,aU as $e,i as ie}from"./index-BQw6xQI4.js";import{R as o}from"./react-DfGhg7ZW.js";import"./markdown-CWEY34jk.js";import"./xterm-3VOAfa_q.js";function de(n){let t={...n,kids:n.kids.map(de)};for(;!t.cat&&t.kids.length===1;){const i=t.kids[0];t={...i,label:t.label+"/"+i.label}}return t}function re(n){const t={path:"",kids:[]};for(const i of n){if(i.cat==="."){t.kids.push({path:".",label:".",cat:i,kids:[]});continue}let l=t;for(const u of i.cat.split("/")){const k=l.path?l.path+"/"+u:u;let m=l.kids.find(s=>s.path===k);m||(m={path:k,label:u,kids:[]},l.kids.push(m)),l=m}l.cat=i}return t.kids.map(de)}function le(n){const t=n.find(i=>i.scored&&i.linked)??n.find(i=>i.scored)??n[0];return t&&t.files[0]?`${t.cat}/${t.files[0]}`:""}function oe(n){return/(^|\/)adr(s)?(\/|$)/i.test(n)?"ADR":/(^|\/)prd(s)?(\/|$)/i.test(n)?"PRD":/(^|\/)spec(s|ification)?(\/|$)/i.test(n)?"Spec":"Lainnya"}const ze=["PRD","Spec","ADR","Lainnya"],ue=n=>`dir:${n}`,Fe=(n,t)=>!!t&&t.startsWith(n.path+"/");function he({node:n,depth:t,ctx:i}){var h,p,y;const l=Fe(n,i.selected),[u,k]=o.useState(l);o.useEffect(()=>{l&&k(!0)},[l]);const m=((h=n.cat)==null?void 0:h.scored)??!0,s=((p=n.cat)==null?void 0:p.linked)??!0,v=ue(n.path),b=()=>k(d=>!d),c=d=>{d.key==="Enter"||d.key===" "?(d.preventDefault(),b()):d.key==="ArrowRight"&&!u?(d.preventDefault(),k(!0)):d.key==="ArrowLeft"&&u&&(d.preventDefault(),k(!1))};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{role:"treeitem","aria-level":t+1,"aria-expanded":u,tabIndex:i.tabbable===v?0:-1,className:"hn-docs-node hn-docs-node-dir",style:{paddingLeft:6+t*12},onClick:b,onKeyDown:c,onFocus:()=>i.setFocusKey(v),children:[e.jsx(N,{name:u?"chevron-down":"chevron-right",size:14,color:"var(--text-subtle)"}),e.jsx(N,{name:"folder",size:15,color:m?s?"var(--brass-500)":"var(--clay-500)":"var(--text-subtle)"}),e.jsxs("span",{className:"hn-docs-dir-label",children:[n.label,"/"]}),n.cat&&m&&(n.cat.linked?e.jsx(N,{name:"link",size:13,color:"var(--leaf-600)"}):e.jsx(N,{name:"unlink",size:13,color:"var(--clay-500)"}))]}),u&&e.jsxs("div",{role:"group",className:"hn-docs-group",children:[n.kids.map(d=>e.jsx(he,{node:d,depth:t+1,ctx:i},d.path)),(y=n.cat)==null?void 0:y.files.map(d=>{const f=n.path+"/"+d,j=f===i.selected;return e.jsxs("div",{role:"treeitem","aria-level":t+2,"aria-selected":j,"aria-current":j?"true":void 0,tabIndex:i.tabbable===f?0:-1,className:`hn-docs-node hn-docs-node-file${j?" is-selected":""}`,style:{paddingLeft:18+t*12},onClick:()=>i.onSelect(f),onFocus:()=>i.setFocusKey(f),onKeyDown:g=>{(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),i.onSelect(f))},children:[e.jsx(N,{name:"file-text",size:13,color:j?"var(--brass-700)":"var(--text-subtle)"}),e.jsx("span",{className:`hn-docs-file-label${!m||s?"":" is-unlinked"}`,children:d})]},d)})]})]})}function ce({label:n,nodes:t,selected:i,onSelect:l}){const[u,k]=o.useState(null),m=t[0]?ue(t[0].path):"",v={selected:i,onSelect:l,tabbable:u??(i||m),setFocusKey:c=>k(c)},b=c=>{if(c.key!=="ArrowDown"&&c.key!=="ArrowUp")return;const h=Array.from(c.currentTarget.querySelectorAll('[role="treeitem"]')),p=h.indexOf(document.activeElement);if(p<0)return;c.preventDefault();const y=h[Math.min(h.length-1,Math.max(0,p+(c.key==="ArrowDown"?1:-1)))];y==null||y.focus()};return e.jsx("div",{role:"tree","aria-label":n,className:"hn-docs-tree",onKeyDown:b,children:t.map(c=>e.jsx(he,{node:c,depth:0,ctx:v},c.path))})}function Ie({projectId:n,projectName:t,docStatus:i}){const[l,u]=o.useState([]),[k,m]=o.useState(0),[s,v]=Ne($e("docs",n),"selected","",Pe),[b,c]=o.useState({}),[h,p]=o.useState("preview"),[y,d]=o.useState("tree"),[f,j]=o.useState(""),[g,q]=o.useState(!1),[L,W]=o.useState(!1),[G,C]=o.useState(""),[P,J]=o.useState(""),[D,$]=o.useState("all"),{confirm:Q,dialog:me}=De(),[z,K]=o.useState("loading"),[pe,ke]=o.useState(0);o.useEffect(()=>{let a=!0;return K("loading"),S.getDocs(n).then(r=>{if(!a)return;const x=r.tree;u(x),m(r.coverage),c({}),p("preview"),K("ready"),C(""),J(""),$("all"),v(le(x))}).catch(()=>{a&&(u([]),v(""),K("error"))}),()=>{a=!1}},[n,pe]),o.useEffect(()=>{if(!s||s in b)return;let a=!0;return S.getDoc(n,s).then(r=>{a&&c(x=>({...x,[s]:r.content}))}).catch(()=>{a&&c(r=>({...r,[s]:null}))}),()=>{a=!1}},[s,n,b]);const F=!!s&&!(s in b),A=s?b[s]===null:!1,fe=()=>c(a=>{const r={...a};return delete r[s],r}),V=a=>D==="all"||oe(a.cat)===D,xe=re(l.filter(a=>a.scored&&V(a))),Y=re(l.filter(a=>!a.scored&&V(a))),B=b[s]??"",be=s.includes("/")?s.slice(0,s.lastIndexOf("/")):".",T=l.find(a=>a.cat===be),_=s,E=h==="edit"&&f!==B,I=o.useMemo(()=>l.flatMap(a=>a.files.map(r=>({key:`${a.cat}/${r}`,scored:a.scored,linked:a.linked,kind:oe(a.cat)}))),[l]),R=P.trim().toLowerCase(),X=R?I.filter(a=>(D==="all"||a.kind===D)&&a.key.toLowerCase().includes(R)):[],ve=o.useMemo(()=>{const a={PRD:0,Spec:0,ADR:0,Lainnya:0};for(const r of I)a[r.kind]+=1;return a},[I]),ye=i==="broken"?"broken":i==="drift"?"drift":"ok";function Z(a){if(!E){p("preview"),a();return}Q({title:"Buang perubahan?",eyebrow:s,message:"Isi dokumen yang kamu ubah belum disimpan.",impact:["Perubahan di editor hilang dan tak bisa dikembalikan dari dashboard."],confirmLabel:"Buang perubahan",cancelLabel:"Lanjut mengedit",tone:"danger",icon:"trash-2"}).then(r=>{r&&(p("preview"),a())})}function ge(){C(""),j(B),p("edit")}const ee=()=>Z(()=>{});async function ae(){if(!(L||!s)){W(!0),C("");try{await S.putDoc(n,s,f),c(a=>({...a,[s]:f})),p("preview")}catch(a){C(ie("menyimpan dokumen",a))}finally{W(!1)}}}function H(a){Z(()=>{v(a),p("preview"),d("viewer")})}async function se(){const a=await S.getDocs(n),r=a.tree;u(r),m(a.coverage),r.some(x=>x.files.some(M=>`${x.cat}/${M}`===s))||v(le(r))}async function ne(){if(!g){q(!0);try{await se()}catch{K("error")}finally{q(!1)}}}async function je(){if(!s)return;const a=s;let r=!1;try{r=await Q({title:`Hapus ${a}?`,eyebrow:"Source of Truth",message:"Dokumen ini adalah Source of Truth project. Berkas aslinya di disk ikut dihapus.",impact:["Berkas di disk dihapus dan tak bisa dikembalikan dari dashboard.","Kategori indeks dihitung ulang setelah penghapusan."],confirmLabel:"Hapus dokumen",tone:"danger",icon:"trash-2",run:()=>S.deleteDoc(n,a)})}catch(x){C(ie("menghapus dokumen",x));return}if(r){c(x=>{const M={...x};return delete M[a],M});try{await se()}catch{K("error")}}}const we=z==="loading"?e.jsx(w,{kind:"loading",compact:!0,title:"Memuat indeks…"}):z==="error"?e.jsx(w,{kind:"error",compact:!0,title:"Gagal memuat indeks dokumen",hint:t,action:()=>ke(a=>a+1)}):l.length===0?e.jsx(w,{kind:"empty",compact:!0,icon:"folder-open",title:"Belum ada dokumen",hint:"Belum ada Markdown di repo ini.",action:ne,actionLabel:"Muat ulang",actionIcon:"radar"}):R?X.length===0?e.jsx(w,{kind:"empty",compact:!0,icon:"search",title:"Tak ada berkas cocok",hint:`Tak ada yang cocok dengan “${P.trim()}”.`}):e.jsx("div",{role:"list","aria-label":"Hasil pencarian berkas",className:"hn-docs-results",children:X.map(a=>e.jsxs("button",{type:"button","aria-current":a.key===s?"true":void 0,onClick:()=>H(a.key),className:`hn-docs-result${a.key===s?" is-selected":""}`,children:[e.jsx(N,{name:"file-text",size:13,color:"var(--text-subtle)"}),e.jsx("span",{className:"hn-prj-mono",children:a.key}),e.jsx("span",{className:"hn-docs-result-kind",children:a.kind})]},a.key))}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{label:"Berkas dokumen",nodes:xe,selected:s,onSelect:H}),Y.length>0&&e.jsxs("div",{className:"hn-docs-unscored",children:[e.jsx("div",{className:"hn-eyebrow hn-docs-unscored-label",children:"Lainnya (tidak dinilai)"}),e.jsx(ce,{label:"Dokumen tidak dinilai",nodes:Y,selected:s,onSelect:H})]})]});return e.jsxs(e.Fragment,{children:[e.jsx(Se,{ariaLabel:"Panel Dokumen",active:y,onActiveChange:a=>d(a),masterWidth:288,className:"hn-workspace-panels",panels:[{id:"tree",label:"Indeks",className:"hn-panel-flex",content:e.jsxs("aside",{"aria-label":"Indeks dokumen",className:"hn-docs-index",children:[e.jsxs("div",{className:"hn-docs-index-head",children:[e.jsxs("span",{className:"hn-eyebrow",children:["dokumen · ",t]}),e.jsxs("div",{className:"hn-docs-index-actions",children:[e.jsx(Ce,{status:ye,size:"sm"}),e.jsx(O,{size:"sm",variant:"ghost",leftIcon:g?"loader":"radar",onClick:ne,disabled:g,children:g?"Memuat…":"Muat ulang"})]})]}),z==="ready"&&l.length>0&&e.jsxs("div",{className:"hn-docs-index-tools",children:[e.jsx(Ke,{size:"sm","aria-label":"Cari berkas dokumen",leftIcon:"search",value:P,placeholder:"mis. product/prd",onChange:a=>J(a.target.value),className:"hn-prj-full"}),e.jsxs("div",{role:"group","aria-label":"Kategori dokumen",className:"hn-prj-chips",children:[e.jsx("button",{type:"button",className:"hn-prj-chip hn-touch-target","aria-pressed":D==="all",onClick:()=>$("all"),children:"Semua"}),ze.map(a=>e.jsxs("button",{type:"button",className:"hn-prj-chip hn-touch-target","aria-pressed":D===a,onClick:()=>$(a),children:[a,e.jsx("span",{className:"hn-prj-chip-count",children:ve[a]})]},a))]}),e.jsxs("span",{className:"hn-docs-cov",title:"Persentase kategori yang terindeks",children:["Cakupan ",Math.round(k),"%"]})]}),e.jsx("div",{className:"hn-docs-tree-scroll",children:we})]})},{id:"viewer",label:"Dokumen",className:"hn-panel-flex",content:e.jsxs("section",{"aria-label":"Isi dokumen",className:"hn-docs-viewer",style:{display:"flex",flexDirection:"column",minHeight:0},children:[e.jsxs("div",{className:"hn-docs-viewer-head",children:[e.jsx(N,{name:"file-text",size:15,color:"var(--text-muted)"}),e.jsx("span",{className:"hn-prj-mono hn-docs-path",children:_}),T&&T.scored&&(T.linked?e.jsx(U,{tone:"ok",size:"sm",icon:"link",children:"terindeks"}):e.jsx(U,{tone:"err",size:"sm",icon:"unlink",children:"tak tertaut"})),e.jsx("span",{className:"hn-grow"}),e.jsxs("div",{role:"group","aria-label":"Mode dokumen",className:"hn-prj-seg",children:[e.jsx("button",{type:"button","aria-pressed":h==="preview",className:"hn-touch-target",onClick:()=>{h==="edit"&&ee()},children:"Baca"}),e.jsx("button",{type:"button","aria-pressed":h==="edit",className:"hn-touch-target",onClick:()=>{h!=="edit"&&ge()},disabled:!s||F||A,children:"Edit"})]}),h==="preview"&&e.jsx(Le,{label:"Aksi untuk dokumen",icon:"ellipsis",items:[{key:"delete",label:"Hapus dokumen…",icon:"trash-2",tone:"danger",disabled:!s,onSelect:()=>{je()}}],children:e.jsx(Me,{href:a=>S.docDownloadUrl(n,s,a),disabled:!s||F||A})})]}),h==="edit"&&e.jsxs("div",{role:"region","aria-label":"Simpan perubahan",className:"hn-docs-savebar",children:[E?e.jsx(U,{tone:"warn",size:"sm",children:"belum disimpan"}):e.jsx("span",{className:"hn-prj-hint",children:"Tak ada perubahan"}),e.jsx("span",{className:"hn-grow"}),e.jsx(O,{size:"sm",variant:"ghost",onClick:ee,disabled:L,children:"Batal"}),e.jsx(O,{size:"sm",leftIcon:"check",onClick:()=>void ae(),loading:L,disabled:L||!E,children:"Simpan"})]}),G&&e.jsx("div",{role:"alert",className:"hn-docs-error",children:G}),h==="preview"?e.jsx("div",{"data-testid":"doc-preview-scroll",className:"hn-docs-body",style:{flex:"1 1 auto",minHeight:0,overflow:"auto"},children:s?F?e.jsx(w,{kind:"loading",title:"Memuat dokumen…",hint:s}):A?e.jsx(w,{kind:"error",title:"Gagal memuat dokumen",hint:s,action:fe}):e.jsx("div",{className:"hn-docs-read",children:e.jsx(te,{text:B,name:s})}):e.jsx(w,{kind:"empty",icon:"file-text",title:"Tidak ada dokumen dipilih",hint:"Pilih file dari indeks di kiri."})}):e.jsxs("div",{className:"hn-docs-edit",children:[e.jsxs("div",{className:"hn-docs-edit-pane",children:[e.jsx("div",{className:"hn-docs-edit-label",children:e.jsx("span",{className:"hn-eyebrow",children:"Markdown"})}),e.jsx("textarea",{"aria-label":`Isi ${_}`,value:f,spellCheck:!1,onChange:a=>j(a.target.value),onKeyDown:a=>{(a.metaKey||a.ctrlKey)&&a.key.toLowerCase()==="s"&&(a.preventDefault(),ae())},placeholder:`# Judul dokumen
2
2
 
3
3
  Paragraf pembuka…`,className:"hn-docs-textarea"})]}),e.jsxs("div",{className:"hn-docs-edit-pane",children:[e.jsx("div",{className:"hn-docs-edit-label",children:e.jsx("span",{className:"hn-eyebrow",children:"Pratinjau langsung"})}),e.jsx("div",{className:"hn-docs-preview",children:e.jsx(te,{text:f,name:s})})]})]})]})}]}),me]})}export{Ie as DocsWorkspace,re as buildTree,oe as docKind,le as firstDoc};