Orkestra için minimum arayüz: görev kartları, denetim durumu, ajan x süreç maliyeti ve ortak defterin son kayıtları bir panelde; prompt üstünde tek satırlık…

Claude ekosistemini kullanan kişisel bir ajan orkestrasyon sistemi. Hepsi Claude Code içinde çalışır: plugin, subagent, skill, hook, workflow, mod ve otomatik hafıza.
denetci ajanı kabul kriterlerine göre denetler. Kimse kendi işini onaylayamaz. Kural üç katmanda zorlanır: ajan araç kısıtı, hook kimlik enjeksiyonu ve CLI kontrolü.sayac modülü yazar: motorun her tur sonunda bildirdiği kullanımı kaydeder.cost-state) gösterilir.≈ ile işaretlenir..claude/orkestra/ozet.json anlık görüntüsünü yazar; arayüzler yalnız bu dosyayı okur./orkestra, Pane orkestra, plugins/orkestra-ui/hooks/register.tsx paneCiz): işlerin yapılanlarını ve yapılacaklarını gösterir. Üstte yapılan / toplam iş sayısı, yüzde ve çubuk. Yapılanlar: en az bir maddesi biten işler (biten/toplam ad). Yapılacaklar: açık maddesi olan işler (ad · N açık), her birinin ilk üç açık maddesi ○ ile. Kaynak ozet.json içindeki isler alanı; üretimi plugins/orkestra/scripts/ozet.py _is_todolari (açık işlerin todo.md maddeleri, en çok beş). Panel veri yaşını değil üretim saatini (HH:MM) yazar; veri 10 dakikadan eskiyse veri eski (HH:MM) sarıyla gösterilir (register.tsx 238–239 ve 260–261). Kart listesi, maliyet ve son olaylar bu panelde yok; onlar /orkestra-web sayfalarındadır.AbovePrompt, register.tsx): Yalnız kullanıcıdan eylem beklenen durumda çıkar: ret (reddedildi), teslime hazır kart (onaylandi) ya da hazır perspektif raporu. Metin Orkestra · N ret · N teslime hazır · perspektif raporu hazır; Panel düğmesi Claude içi paneli açar. Yalnız maliyet bant çıkarmaz. Gizle o anki kart kimliklerini ve rapor durumunu imza olarak saklar; imza değişince (yeni ret, teslim ya da rapor) bant geri gelir./orkestra-web, ya da Bash'te orkestra panel, ya da araclarpaneli_ac.cmd): http://127.0.0.1:8765/ adresinde sekiz sayfa açar: Şimdi, Maliyet, Verim, Onaylar, Kurallar, İşler, Etki, Yapılacaklar. Sayfalar üç saniyede bir yenilenir. Yalnız koyu temadadır ve yalnız bu bilgisayardan erişilebilir./): ne oluyor, senden ne bekleniyor. Sohbetler (kimi bekliyor), görev kartları, son olaylar. "Kimi bekliyor"u hook'lar yazar (UserPromptSubmit → Claude çalışıyor, Stop → seni bekliyor, SessionEnd → kapandı). "Ne/neden"i ana ajan yeni bir iş başlarken orkestra niyet --ne "…" --neden "…" ile yazar, bitince --sonuc "…" ekler. Kayıt bütün projeler için tek dosyada: ~/.claude/orkestra-panel/niyet.jsonl./maliyet): ne kadar harcanıyor, nereye. Kapsam seçici, iş türü × model, ajan tablosu, zaman içinde harcama, kota, tüm projeler./verim): sistem iyi çalışıyor mu. Basamak verimliliği, birim maliyet, kart akışı ve paralel zaman çizelgesi, skill getirisi, sağlık (hafıza sınırı, son perspektif, etiketsiz ajan, hook hataları)./yapilacaklar): açık işlerin maddeleri ve kullanıcının kendi yapılacakları (/api/todo, /api/kullanici-todo)./analitik ve /analitik.html adresleri /verim'e yönlendirilir (301, sorgu dizesi korunur).projeye_kur.py, proje ayarında statusLine yoksa scripts/kota.py'yi bağlar; betik değerleri ~/.claude/orkestra-panel/kota.jsonl'a yazar..claude-plugin/marketplace.json yerel klasör marketplace'i (claude-orchestration)
plugins/orkestra/ çekirdek plugin
agents/ triyaj, isci-t1..t4, denetci, arastirmaci, hafiza-bakimcisi, skill-avcisi, arsivci
skills/ is, baslat, maliyet, defter, perspektif, skill-avi → /orkestra:<ad>
workflows/ calistir.js (DAG yürütme, işçi → ayrı denetçi, 2 ret → basamak yükseltme)
hooks/ hooks.json → scripts/hook.py (9 olay); sayac.ts (turn.complete → kesin token kaydı)
scripts/ ork_lib.py, orkestra.py (CLI), hook.py, perspektif.py, ozet.py (anlık görüntü), panel.py (HTML sunucusu: sekiz sayfa, /analitik → /verim; sunulan dosyalar STATIK),
analitik.py (analitik hesapları), kota.py (durum satırı: kota kaydı), niyet.py (Şimdi sayfasındaki Sohbetler bölümü: ne/neden/kimi bekliyor)
assets/ sayfa başına <sayfa>.html ile <sayfa>.js (büyük sayfalarda ayrıca <sayfa>.css); ortak.css ve ortak.js (window.O) ortak kabuk; logo.png
bin/ orkestra, orkestra.cmd (CLI'yı PATH'e koyar), fiyat.json
tests/ test_cekirdek.py (python -m unittest)
evals/ claude plugin eval vakaları
plugins/orkestra-ui/ arayüz modu (Pane + AbovePrompt bandı + /orkestra)
araclar/sifre_temizle.py oturum kayıtlarından düz metin şifre temizleme (elle çalıştırılır)
araclar/yayin_kur.py yayın klonunu açar, git hook'larını yazar; araclar/git_hooklar/ hook şablonları
python <depo>/araclar/projeye_kur.py <proje klasörü>
Komut şunları yapar:
.gitignore dosyasına satır ekler.CLAUDE.md dosyasına kısa bir bölüm ekler.Genel ayarlara dokunmaz ve tekrar çalıştırmak güvenlidir.
--kuru, arayüz olmadan kurmak için --ui-yok, projeden çıkarmak için --kaldir kullan.Depo GitHub'da özel (private) tutulur. İki yol vardır; ikisi de marketplace kaynağı olarak GitHub deposunu kullanır (kök .claude/settings.json içindeki extraKnownMarketplaces → source: github).
claude komutu ya da masaüstü uygulaması).orkestra CLI'ı Python betikleridir. Alt sürüm için resmi bir bildirim yoktur; kod taramasında en yüksek gereksinim 3.8'dir (atama ifadesi :=, plugins/orkestra/scripts/ozet.py:160). Her sürümde koşturulmadı, bu yüzden 3.8 alt sınırı doğrulanmadı; güncel bir 3.x kullan. python komutu PATH'te olmalıdır.is-bitti) pytest ister; kurulu değilse python plugins/orkestra/scripts/kur.py açık bir uyarı basar (kurulum yapmaz). Kur: python -m pip install pytest.py -3.13 bulunursa o, yoksa çalışan Python (sys.executable) kullanılır. test_komutu içindeki çağrılara dokunulmaz.git ile, soru sormadan (etkileşimsiz) klonlar; Claude Code'un kendi git belirteci yoktur. Kimlik doğrulama (kaynak: Grant access to a private marketplace):ssh-agent) ve github.com known_hosts içinde olmalı. owner/repo biçiminde Claude Code önce ssh -T git@github.com dener, olmazsa HTTPS'e düşer.gh auth login ve ardından gh auth setup-git bunu kaydeder.CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 ayarlanır (oturum içi /plugin kurulumları ve güncellemeler HTTPS'e kendiliğinden düşmez).GITHUB_TOKEN ortam değişkeni arka plan denetimini tek başına doğrulamaz; belirteç ancak gh gibi bir kimlik yardımcısı üzerinden işe yarar.git clone https://github.com/mertdurukan/agenchestra.git..claude/settings.json (extraKnownMarketplaces + enabledPlugins: orkestra@claude-orchestration, orkestra-ui@claude-orchestration) devreye girer; plugin'ler marketplace'ten yüklenir./plugin ile plugin'lerin listelendiğini doğrula. Sistem yalnız açılmış projelerde iz tutar: bu klasörde izleme istiyorsan /orkestra:baslat çalıştır.python <depo>/araclar/projeye_kur.py <proje klasörü> --kaynak github
--repo SAHIP/DEPO ile GitHub deposu verilir; verilmezse klonun origin adresinden türetilir, bulunamazsa komut hata verir.--kaynak yerel (varsayılan) yayın klonunu kullanır: --yayin <yol>, yoksa ORKESTRA_YAYIN, yoksa <depo>/../claude-orchestration-yayin. --kaynak github bu yola ihtiyaç duymaz.--kuru ile ne yapılacağını görebilirsin. Seçenekler için python <depo>/araclar/projeye_kur.py --help.Depo mertdurukan/agenchestra (özel), bu klasörün origin'i. Kök .claude/settings.json bu depoyu marketplace kaynağı olarak gösterir. Yol B'de --repo verilmezse origin'den türetilir. Geliştirme akışı: commitle → git push. Yayın klonu ve autoUpdate ile işleyen yerel akış değişmez.
.claude/orkestra/ (kartlar, defter, maliyet, özet; .gitignore'da). Her makinede orkestra baslat ile yeniden açılır.~/.claude/settings.json, ~/.claude/orkestra-panel/ (niyet, kota kayıtları) ve plugin önbelleği ~/.claude/plugins/. Depodaki tek ayar dosyası kök .claude/settings.json'dır..claude/settings.json, .gitignore ve CLAUDE.md dosyalarını değiştirir; bunlar o projenin deposuna aittir.| İddia | Sonuç | Kaynak |
|---|---|---|
(a) directory kaynağı göreli yol kabul eder mi, neye göre çözülür | Kısmen doğrulandı: proje .claude/settings.json içinde directory ya da file kaynağında göreli yol, deponun ana checkout'una göre çözülür (git worktree'den çalışsan da ana checkout). directory için path "marketplace kökü"dür (.claude-plugin/marketplace.json'u içeren klasör). Başka bir çözüm tabanı (oturum klasörü, ayar dosyası konumu) açıkça belirtilmemiştir: doğrulanmadı. Bu depo directory değil github kaynağı kullanır; göreli directory yolu bu kurulum için gerekli değildir. | Require plugins per repository, Marketplace sources |
(b) Proje .claude/settings.json içindeki extraKnownMarketplaces + enabledPlugins güven onayından sonra kurulumu kendiliğinden yapar mı | Kısmen doğrulandı: extraKnownMarketplaces yalnız klasöre güvenildikten sonra uygulanır; güvenilmeyen klasörde mesajsız yok sayılır. enabledPlugins oturum başında uygulanır. Marketplace'in göreli yolla (./plugins/...) listelediği plugin'ler marketplace kopyasından yüklenir; harici kaynaklı (örn. kendi GitHub deposu) plugin'ler için claude plugin install <ad>@<marketplace> --scope project gerekir. Bu deponun marketplace.json girdileri göreli yoldur (./plugins/orkestra, ./plugins/orkestra-ui). Güven onayı sonrası ayrı bir "kurulsun mu" sorusu çıkıp çıkmadığı dokümanda yazmaz: doğrulanmadı; çıkarsa kabul et, çıkmazsa /plugin ile kontrol et. | Require plugins per repository, When each surface applies the plugin keys, Settings: key waits for trust |
| (c) Özel GitHub kaynağı için kimlik doğrulama | Doğrulandı: makinedeki git ve mevcut kimlik bilgileri (SSH anahtarı ya da kimlik yardımcısı); Claude Code'un kendi belirteci yok, etkileşimli soru yok. GitHub'da gh auth login + gh auth setup-git. | Grant access to a private marketplace |
Ayarlar bilgisayara değil çalışma alanına bağlıdır. Plugin'ler proje kapsamında kurulur ve yalnız o projede etkindir. Genel ayar dosyası (~/.claude/settings.json) boş kalır.
cd <proje klasörü>
claude plugin marketplace add <yayın klonu klasörü>
claude plugin install orkestra@claude-orchestration --scope project
claude plugin install orkestra-ui@claude-orchestration --scope project
Komutlar <proje>\.claude\settings.json dosyasına enabledPlugins ve extraKnownMarketplaces alanlarını yazar.
Model, effort, hafıza ve perspektif ayarları da aynı dosyadadır. Proje ayarı, genel ayardan önce gelir.
Masaüstü uygulamasında claude PATH'te yoktur. Gömülü ikili %APPDATA%\Claude\claude-code\<sürüm>\…\claude.exe altında bulunur. Hook'ların içinde aynı yol CLAUDE_CODE_EXECPATH değişkeninde durur.
Claude Code plugin'leri bu klasörden değil, kurulumda alınan kopyadan okur: ~/.claude/plugins/cache/claude-orchestration/<plugin>/<sürüm>/. plugin.json ve marketplace.json içinde version alanı bilerek yoktur; git deposundaki göreli yol girdilerinde sürüm commit SHA'sı olur. Bu yüzden bir değişiklik commitlenmeden kopyaya geçmez (claude plugin validate --strict yalnız bu eksik sürüm uyarısı yüzünden hata verir; --strictsiz geçer). Marketplace bu klasörden değil yayın klonundan okur (aşağıda). Akış: geliştir → commitle (push gerekmez) → yayın klonu otomatik ilerler → autoUpdate ya da orkestra guncelle.
extraKnownMarketplaces.claude-orchestration.autoUpdate değeri true (projeye_kur.py yazar). Claude Code ilk mesajdan sonra en çok 10 dakika içinde günceller, yeni kopya sonraki oturumda yüklenir.orkestra guncelle (--kuru yalnız komutları yazar). Marketplace'i ve projede kurulu her plugin'i günceller; ardından yeni oturum aç ya da /reload-plugins.claude plugin update commit'i değil marketplace klasörünün o anki çalışma ağacını kopyalar. Paralel oturumların commitlenmemiş işi çalışan plugin'e sızmasın diye marketplace bu klasörden değil, bu deponun temiz git klonundan (yayın klonu) okur. Varsayılan konum <depo>/../claude-orchestration-yayin, --yayin ya da ORKESTRA_YAYIN ile değişir (origin = bu klasör).
python araclar\yayin_kur.py (--yayin <yol> ya da ORKESTRA_YAYIN ile yol değişir). Klonu açar ve .git/hooks/ altına post-commit, post-rewrite (amend/rebase) ve orkestra-yayin-esitle.sh yazar; hook şablonları araclar/git_hooklar/ altında izlenir.plugin_yayin elle düzenlenmez: hook klondaki yerel değişikliği checkout -f ile ezer; yalnız main'deki commit'ler yayına çıkar (başka dalda atılan commit klona dokunmaz).orkestra guncelle önce klonu fetch + hızlı ileri sarar; klon kirliyse nedenini yazıp reddeder.projeye_kur.py marketplace kaynağını varsayılan olarak bu klondan alır.HTML panel bu kuralın dışındadır: python <bu klasör>\plugins\orkestra\scripts\orkestra.py panel doğrudan bu klasörden çalışır.
Bir projede orkestrayı açmak için /orkestra:baslat kullan; sistem yalnız açılmış projelerde iz tutar.
| Komut | Ne yapar | |
|---|---|---|
/orkestra:is <istek> | Planla → kart → paralel işçi + ayrı denetçi → topla ve teslim et | |
/orkestra | Claude içi özeti açar: dikkat isteyen kartlar, çalışanlar, tek satır maliyet, son olaylar, veri yaşı | |
/orkestra-web | HTML panelini başlatır (Şimdi, Maliyet, Verim ve sağlık) ve adresini verir (Bash: orkestra panel) | |
/orkestra:maliyet [gün] | Ajan × süreç × model token ve maliyet raporu | |
orkestra niyet --ne … --neden … | Sohbetin şu an ne yaptığını ve neye yarayacağını "Şimdi" sayfasındaki Sohbetler bölümüne yazar (ana ajan; --sonuc, --goster) | |
/orkestra:defter [görev] | Ortak defterin son kayıtları | |
/orkestra:perspektif [şimdi] | Perspektif raporunu gösterir, onaylananları uygular | |
/orkestra:skill-avi | Tekrar eden işlerden skill taslağı çıkarır | |
/orkestra:onay | Panelde onaylanan kararları (perspektif önerileri, skill taslakları, ajan soruları) uygular | |
orkestra onay liste/goster/iste/karar/al/uygulandi/iptal/esitle | Onay kuyruğu: panelin Onaylar sayfasıyla aynı kayıtlar; iste ajanın panelden karar istemesi, `karar --onay\ | --ret` sohbette verilen kararı kaydeder |
orkestra kural liste/oner/ekle/duzenle/kaldir | Kalıcı kurallar: oner panelde Kurallar sayfasında onaya düşer, etkin kurallar .claude/rules/orkestra_kurallari.md olarak yazılır | |
orkestra … (Bash) | Kart, defter, maliyet, perspektif CLI'ı (orkestra --help) |
Paralel oturumlar ve işçiler aynı commitlenmemiş çalışma ağacını paylaşır. Bir işçi dosyayı baştan yazarsa başka bir oturumun arada yaptığı değişiklik sessizce silinebilir. İki deterministik kapı bunu önler:
isleniyora geçerken (orkestra kart durum <id> isleniyor) yazma alanı, aynı fiziksel ağaçta açık olan bütün kartların (hangi oturumdan olursa olsun) alanlarıyla karşılaştırılır.çakışma: G-0xx (<desen>) — bekle ya da ayrı çalışma ağacı aç.ini kapsayan alan paylaşılan ağaçta reddedilir. Bu kural bir izin listesidir. Desenin sabit öneki, ilk joker bileşenden önceki yoldur; göreli desende proje köküne göre hesaplanır. Önek bir git deposunun kesin alt yolu olmalıdır: RNV/, src/**/*.py, docs/a.md.**, */, /*.py, *.md. fnmatch'te * / dahil her karaktere uyar; kökteki *.md de bütün alt klasörlere uzanır.Üst/**, Depo*/, Z:/Ortak/. Depo dışında yalnız tek dosya literal yolu geçer.RNV/**, altındaki bir depoda ya da o deponun worktree'sinde açık olan kartla çakışır.kart yeni ve kart toplu aynı denetimi yalnız uyarı olarak yazar. Yazma alanı boş kart denetlenmez.orkestra kart agac <id> kartın deposunda <depo>/.claude/worktrees/kart-<id> klasörüne kart/<id> dalıyla bir git worktree açar (ORKESTRA_AGAC_KOKU doluysa <kök>/<depo-adı>/kart-<id>). Klasör .git/info/excludea bir kez eklenir; .gitignore değişmez. Kayıtlı calisma_agaci.yol korunur, eski <depo>-kart-<id> düzeni çalışmaya devam eder.git worktree remove ile kaldırılır, birleşmiş dal branch -d ile silinir (force yok). Kirli ağaç ve kayıt kalır, uyarı yazılır. orkestra kart agac-temizle proje deposunu ve kartlardaki tüm depoları (calisma_agaci, _birlesik, _kaldirildi) tarayıp biten/kartsız ağaçları listeler (kuru koşu), --uygula kaldırır; açık kartın ağacına ve kalıp dışı ağaçlara dokunmaz.calisma_agaci alanı (depo, yol, dal, taban) karta yazılır. Çalışma ağacı: <yol> satırı kartın girdisinin başına eklenir; işçi istemi bu satırı görür.orkestra kart birlestir <id> kart dalını git merge --no-ff ile birleştirir, sonra worktree'yi kaldırır ve dalı siler.git merge --abort yapar, kartı beklemedeye alır ve çatışan dosyaları karta not eder. Çatışmayı asla otomatik çözmez.orkestra kart teslim, çalışma ağacı olan kartta önce birleştirir; birleşmezse teslimi reddeder.Çatışma yüzünden beklemede kalan kart, çalışma ağacı ve dalı korunmuş hâlde bekler. Yeniden birleştirmek için çatışmayı kart dalında çöz:
git -C <kart ağacı> merge <ana dal>.birlestirme_catismasi alanı) elle düzelt ve commit et.orkestra kart birlestir <id> komutunu yeniden çalıştır; bu kez çatışmasız birleşir ve worktree temizlenir.Çözüm işin kendisini değiştirdiği için kart yeniden işçi ve denetçiden geçer: isleniyor → is-bitti → onay → orkestra kart teslim <id>.
| İşçi basamağı | İşçi | Denetçi |
|---|---|---|
| T0 betik / T1 | Haiku 5.5 | Haiku 5.5 (T0 kartları T1 yoluyla; denetçiden önce T0 ön kontrol betiği koşar) |
| T2 | Sonnet 5.5 | Sonnet 5.5 medium |
| T3 | Opus 5.5 medium | Opus 5.5 high, iki mercek |
| T4 | Opus 5.5 xhigh | Opus 5.5 xhigh (ayrı örnek), iki mercek |
Bir iş aynı basamakta iki kez reddedilirse (AYNI_BASAMAKTA_EN_COK_RET = 2) bir üst basamakta yeniden denenir; yetenek-yetersiz nedeninde ilk ret de yükseltir. Kartta toplam ret DENETCI_OPUS_RET_ESIGI (= 2) değerine ulaşınca denetimler opus'a geçer (T4'te xhigh).
python -m unittest discover -s plugins/orkestra/tests -v
claude plugin validate plugins/orkestra --strict
claude plugin test plugins/orkestra-ui
claude plugin eval plugins/orkestra --trust-plugin --runs 1
hooks/register.tsx 267 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { HamOzet, KartOzeti, UiOzet } from '../types'
5
6// Veri kaynağı: çekirdek plugin'in hook'ları ve CLI'ı her durum değişikliğinde `.claude/orkestra/ozet.json` yazar.
7// Mod yalnız bu dosyayı okur (süreç başlatmaz); okuyamazsa nedenini panelde gösterir.
8const OZET = '.claude/orkestra/ozet.json'
9const PANE = 'orkestra'
10const veri = atom({ plugin: 'orkestra-ui', key: 'veri' } as const, null)
11// Gizle, gizlendiği andaki durum imzasını saklar; imza değişince (yeni ret/teslim/rapor) bant geri gelir.
12const bantGizli = atom({ plugin: 'orkestra-ui', key: 'bantGizli' } as const, '')
13
14const ACIK = ['isleniyor', 'denetimde', 'reddedildi', 'onaylandi', 'beklemede', 'planlandi']
15const ESKI_DK = 10
16
17// tr-TR para biçimi: |v| < 1000 iki ondalık ($5,73); ≥ 1000 sıfır ondalık ($1.668); negatifte −$.
18export function usd(n: number | undefined): string {
19 const v = n ?? 0
20 const binlik = (t: string) => t.replace(/\B(?=(\d{3})+(?!\d))/g, '.')
21 const govde = Math.abs(v) < 1000
22 ? (([t, o]) => `${binlik(t)},${o}`)(Math.abs(v).toFixed(2).split('.'))
23 : binlik(String(Math.round(Math.abs(v))))
24 return `${v < 0 ? '−' : ''}$${govde}`
25}
26
27function kes(s: string, n: number): string {
28 return s.length <= n ? s : s.slice(0, Math.max(0, n - 1)) + '…'
29}
30
31// Markdown işaretlerini soyar (**, __, `, [a](b)) ve boşlukları sadeleştirir.
32export function temizMetin(s: string): string {
33 return (s ?? '')
34 .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
35 .replace(/\*{1,3}|_{2}|`/g, '')
36 .replace(/\s+/g, ' ')
37 .trim()
38}
39
40// Eski ozet.json'da `olay` yoksa sez: en çok 4 kelime ve kart kimliği / teslim / denetim içeren metin yalnız durum işaretidir.
41function olaymi(r: { metin: string; olay?: boolean }): boolean {
42 if (r.olay !== undefined) return r.olay
43 const m = temizMetin(r.metin)
44 return m.split(' ').length <= 4 && /\bg-\d+\b|teslim|denetim/.test(m.replace(/İ/g, 'i').toLowerCase())
45}
46
47// Ham anlık görüntüyü bu oturumun gözünden panel verisine çevirir.
48export function donustur(h: HamOzet, oturum: string): UiOzet {
49 const ot = h.maliyet?.oturumlar?.[oturum]
50 const hepsi = h.kartlar ?? []
51 const kartlar: KartOzeti[] = hepsi
52 .filter(k => ACIK.includes(k.durum))
53 .slice(0, 25)
54 .map(k => ({ id: k.id, baslik: k.baslik, durum: k.durum, basamak: k.basamak, isci: k.isci, denetim: k.denetim, usd: k.usd }))
55 const teslimler = hepsi.filter(k => k.durum === 'teslim')
56 const son = teslimler.reduce<(typeof teslimler)[number] | undefined>(
57 (a, k) => (a === undefined || (k.guncelleme ?? '') > (a.guncelleme ?? '') ? k : a), undefined)
58 return {
59 acik: true,
60 kartlar,
61 sayac: h.sayac ?? {},
62 maliyet: { bugun: h.maliyet?.bugun ?? 0, oturum: ot?.usd ?? 0 },
63 eylem: hepsi.filter(k => k.durum === 'reddedildi' || k.durum === 'onaylandi').map(k => `${k.durum}:${k.id}`).sort(),
64 teslim: { sayi: teslimler.length, sonId: son?.id, sonSaat: (son?.guncelleme ?? '').slice(11, 16) || undefined },
65 defter: (h.defter ?? []).filter(r => !olaymi(r)).slice(-3).map(r => ({
66 saat: (r.zaman ?? '').slice(11, 16), gorev: r.gorev, metin: kes(temizMetin(r.metin), 160),
67 })),
68 perspektif: h.perspektif ?? h.ruya, // ruya: eski ozet.json anahtarı
69 uretim: h.uretim,
70 isler: h.isler ?? [],
71 }
72}
73
74// Bantta eylem gerektiren durumun imzası; boşsa (ret yok, teslim bekleyen yok, rapor yok) bant çizilmez.
75// Kart kimliklerinden kurulur: sayı aynı kalsa da reddedilen kart değişirse imza değişir ve gizlenmiş bant geri gelir.
76export function eylemImzasi(o: UiOzet): string {
77 const kartlar = o.eylem ?? []
78 const rapor = o.perspektif?.rapor === true
79 return kartlar.length === 0 && !rapor ? '' : `${kartlar.join(',')}|${rapor ? 'rapor' : ''}`
80}
81
82// Modül düzeyi durum: yeniden yüklemede sıfırlanır (gösterilen veri $.state'te kalır).
83let sonImza = ''
84let sonRet = 0
85let calisiyor = false
86
87async function yenile($: EngineInterface, zorla: boolean): Promise<void> {
88 if (calisiyor) return
89 calisiyor = true
90 try {
91 const st = await $.fs.stat(OZET).catch(() => undefined)
92 if (st === undefined) {
93 const acik = await $.fs.exists('.claude/orkestra').catch(() => false)
94 await update($, veri, () => (acik
95 ? { acik: true, hata: 'ozet.json henüz yok: bir tur bitince oluşur (ya da Bash: orkestra ui-ozet --yaz).' }
96 : { acik: false }))
97 return
98 }
99 const imza = `${st.mtimeMs}:${st.size}`
100 if (!zorla && imza === sonImza) return
101 sonImza = imza
102 const metin = await $.fs.read(OZET)
103 const ham = JSON.parse(metin) as HamOzet
104 const o = donustur(ham, await $.session.id())
105 const ret = o.sayac?.reddedildi ?? 0
106 if (ret > sonRet) $.ui.toast(`Orkestra: denetçi ${ret - sonRet} işi reddetti — düzeltme ya da yükseltme gerekiyor.`)
107 sonRet = ret
108 await update($, veri, () => o)
109 } catch (hata) {
110 const mesaj = hata instanceof Error ? hata.message : String(hata)
111 await update($, veri, eski => ({ ...(eski ?? { acik: true }), hata: `Veri okunamadı: ${kes(mesaj, 200)}` }))
112 } finally {
113 calisiyor = false
114 }
115}
116
117async function webPaneli($: EngineInterface): Promise<string> {
118 const betik = `${$.plugin.root}/../orkestra/scripts/orkestra.py`
119 const r = await $.process.run(['python', betik, 'panel'], { timeoutMs: 20000 })
120 const cikti = (r.stdout || r.stderr).trim()
121 return r.exitCode === 0 ? `HTML canlı panel: ${cikti} — tarayıcıda aç (Claude tarayıcı paneli ya da Chrome).`
122 : `HTML panel başlatılamadı (${r.exitCode}): ${kes(cikti, 300)}. Terminalde dene: orkestra panel`
123}
124
125export const register: Register = on => {
126 on('session.start', async ($, e, next) => {
127 await $.command.register({ name: 'orkestra', description: 'Orkestra panelini aç: dikkat isteyen kartlar, çalışanlar, maliyet özeti, son olaylar' })
128 await $.command.register({ name: 'orkestra-web', description: 'Orkestra HTML canlı panelini başlat ve adresini ver (zengin görünüm, tarayıcıda)' })
129 await yenile($, true)
130 $.clock.every(3000, () => void yenile($, false))
131 return next(e)
132 })
133
134 on('command.run', { command: 'orkestra' }, async $ => {
135 await yenile($, true)
136 await update($, bantGizli, () => '')
137 await $.ui.open({ id: PANE, title: 'Orkestra' })
138 return { text: 'Orkestra paneli açıldı. Daha zengin canlı görünüm için /orkestra-web.' }
139 })
140
141 on('command.run', { command: 'orkestra-web' }, async $ => {
142 try {
143 return { text: await webPaneli($) }
144 } catch (hata) {
145 return { text: `HTML panel başlatılamadı: ${hata instanceof Error ? hata.message : String(hata)}. Terminalde dene: orkestra panel` }
146 }
147 })
148
149 on('turn.complete', async ($, e, next) => {
150 const sonuc = await next(e)
151 await yenile($, true)
152 return sonuc
153 })
154
155 // Pane: kimliği tam eşleşmeyebilir (düğmeden/komuttan açılış); "orkestra" içeren her örneği çiz, gerisini motora bırak.
156 // Çizim hata verirse boş pencere yerine hatanın kendisi gösterilir (.catch: zaman aşımı / istisna).
157 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
158 if (!String(e.requestId ?? '').includes(PANE)) return next(e)
159 const { Box, Text } = $.ui.resolve(e)
160 try {
161 return await paneCiz($, e)
162 } catch (hata) {
163 return (
164 <Box flexDirection="column">
165 <Text color="error">Panel çizilemedi: {kes(hata instanceof Error ? hata.message : String(hata), 300)}</Text>
166 <Text dimColor>Canlı HTML panel çalışıyor: /orkestra-web</Text>
167 </Box>
168 )
169 }
170 }).catch(($, e, next) => {
171 if (!String(e.requestId ?? '').includes(PANE)) return next(e)
172 const { Box, Text } = $.ui.resolve(e)
173 return (
174 <Box flexDirection="column">
175 <Text color="error">Panel zamanında çizilemedi ({next.error?.kind ?? 'hata'}).</Text>
176 <Text dimColor>Canlı HTML panel çalışıyor: /orkestra-web</Text>
177 </Box>
178 )
179 })
180
181 // Bant yalnız kullanıcıdan eylem beklenirken: ret > 0, teslim bekleyen (onaylandi) > 0 ya da perspektif raporu hazır.
182 // Yalnız oturum maliyeti için çizilmez. "Gizle" o anki durum imzası için gizler.
183 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
184 const o = await read($, veri)
185 if (e.props.hasSurvey || o === null || !o.acik || o.kartlar === undefined) return next(e)
186 const imza = eylemImzasi(o)
187 if (imza === '' || (await read($, bantGizli)) === imza) return next(e)
188 const ret = (o.eylem ?? []).filter(x => x.startsWith('reddedildi:')).length
189 const onay = (o.eylem ?? []).filter(x => x.startsWith('onaylandi:')).length
190 const { Box, Button, Text } = $.ui.resolve(e)
191 const parca: string[] = []
192 if (ret > 0) parca.push(`${ret} ret`)
193 if (onay > 0) parca.push(`${onay} teslime hazır`)
194 if (o.perspektif?.rapor) parca.push('perspektif raporu hazır')
195 return (
196 <Box>
197 <Text dimColor>{`Orkestra · ${parca.join(' · ')} `}</Text>
198 <Button key="panel" label="Panel" onPress={() => $.ui.open({ id: PANE, title: 'Orkestra' })} />
199 <Button key="gizle" label="Gizle" onPress={() => update($, bantGizli, () => imza)} />
200 </Box>
201 )
202 })
203}
204
205// Panel ağacı: bir bakışta özet (satırlar iç içe Text yerine Box içinde yan yana Text: her yüzeyde güvenli).
206async function paneCiz($: EngineInterface, e: Parameters<EngineInterface['ui']['resolve']>[0] & { props: { bodyColumns?: number } }) {
207 {
208 const { Box, Text } = $.ui.resolve(e)
209 const o = await read($, veri)
210 const w = Math.max(40, e.props.bodyColumns ?? 80)
211 if (o === null) {
212 return <Box><Text dimColor>Orkestra verisi yükleniyor…</Text></Box>
213 }
214 if (!o.acik) {
215 return (
216 <Box flexDirection="column">
217 <Text>Bu projede orkestra kapalı.</Text>
218 <Text dimColor>/orkestra:baslat ile aç.</Text>
219 </Box>
220 )
221 }
222 if (o.kartlar === undefined && o.isler === undefined) {
223 return (
224 <Box flexDirection="column">
225 <Text color="error">{o.hata ?? 'Veri yok.'}</Text>
226 </Box>
227 )
228 }
229 const isler = (o.isler ?? []).filter(x => x.toplam > 0)
230 const biten = isler.reduce((a, x) => a + x.biten, 0)
231 const toplam = isler.reduce((a, x) => a + x.toplam, 0)
232 const yuzde = toplam === 0 ? 0 : Math.round((biten / toplam) * 100)
233 const dolu = Math.round((yuzde / 100) * Math.min(30, w - 20))
234 const cubuk = '█'.repeat(dolu) + '░'.repeat(Math.min(30, w - 20) - dolu)
235 const yapilanlar = isler.filter(x => x.biten > 0)
236 const yapilacaklar = isler.filter(x => x.acik.length > 0)
237 const uretimSaat = (o.uretim ?? '').slice(11, 16)
238 const yas = o.uretim !== undefined ? Date.now() - Date.parse(o.uretim) : 0
239 const eski = Number.isFinite(yas) && yas > ESKI_DK * 60000
240 return (
241 <Box flexDirection="column">
242 {o.hata !== undefined && <Text color="error" wrap="truncate">{o.hata}</Text>}
243 <Text bold>{`Yapılan ${biten} / toplam ${toplam} iş · %${yuzde}`}</Text>
244 <Text color="success">{cubuk}</Text>
245 <Text> </Text>
246 <Text bold color="success">{`Yapılanlar (${biten})`}</Text>
247 {yapilanlar.length === 0 && <Text dimColor>Henüz biten madde yok</Text>}
248 {yapilanlar.map(x => <Text wrap="truncate">{kes(`${x.biten}/${x.toplam} ${x.ad}`, w)}</Text>)}
249 <Text> </Text>
250 <Text bold color="warning">{`Yapılacaklar (${toplam - biten})`}</Text>
251 {yapilacaklar.length === 0 && <Text dimColor>Açık madde yok</Text>}
252 {yapilacaklar.map(x => (
253 <Box flexDirection="column">
254 <Text bold wrap="truncate">{kes(`${x.ad} · ${x.toplam - x.biten} açık`, w)}</Text>
255 {x.acik.slice(0, 3).map(m => <Text dimColor wrap="truncate">{kes(` ○ ${m}`, w)}</Text>)}
256 </Box>
257 ))}
258 <Text> </Text>
259 <Box>
260 <Text dimColor>{uretimSaat === '' ? 'Ayrıntı: /orkestra-web' : 'Ayrıntı: /orkestra-web · '}</Text>
261 {uretimSaat === '' ? null : eski ? <Text color="warning">{`veri eski (${uretimSaat})`}</Text> : <Text dimColor>{`veri ${uretimSaat}`}</Text>}
262 </Box>
263 </Box>
264 )
265 }
266}
267types/index.d.ts 59 lines1export type KartOzeti = {
2 id: string
3 baslik: string
4 durum: string
5 basamak: string
6 isci: number
7 denetim: string
8 usd: number
9}
10
11export type IsTodosu = { id: string; ad: string; biten: number; toplam: number; acik: string[] }
12
13export type DefterSatiri = { saat: string; gorev: string; metin: string }
14
15// Panelin gösterdiği, bu oturumun gözünden veri.
16export type UiOzet = {
17 acik: boolean
18 hata?: string
19 uretim?: string
20 kartlar?: KartOzeti[]
21 sayac?: Record<string, number>
22 maliyet?: { bugun: number; oturum: number }
23 // Eylem bekleyen kartlar (reddedildi/onaylandi) "durum:id" olarak sıralı; bant durum imzası bundan kurulur.
24 eylem?: string[]
25 // Teslim edilmiş kartlar yalnız sayı olarak; son teslim "açık iş yok" satırı için.
26 teslim?: { sayi: number; sonId?: string; sonSaat?: string }
27 defter?: DefterSatiri[] // olay olmayan, temiz son 3 satır
28 isler?: IsTodosu[] // açık işlerin todo durumu: panelin tek gösterimi
29 perspektif?: { zamani_geldi: boolean; rapor: boolean; calisiyor: boolean }
30}
31
32// Çekirdeğin yazdığı ham anlık görüntü (.claude/orkestra/ozet.json, surum 2).
33export type HamOzet = {
34 surum: number
35 acik: boolean
36 uretim: string
37 proje: string
38 kartlar?: (KartOzeti & { oturum?: string; bagimli?: string[]; guncelleme?: string })[]
39 sayac?: Record<string, number>
40 maliyet?: {
41 bugun: number
42 toplam: number
43 surec: Record<string, number>
44 model: Record<string, number>
45 oturumlar?: Record<string, { usd: number; surec: Record<string, number>; model: Record<string, number> }>
46 }
47 sohbetler?: { oturum: string; baslik: string; usd: number; kaynak: 'motor' | 'defter'; son: string }[]
48 defter?: { zaman?: string; gorev: string; surec: string; ajan: string; metin: string; oturum?: string | null; olay?: boolean }[]
49 isler?: IsTodosu[]
50 perspektif?: { zamani_geldi: boolean; rapor: boolean; calisiyor: boolean }
51 ruya?: { zamani_geldi: boolean; rapor: boolean; calisiyor: boolean } // eski anahtar (geriye uyum)
52}
53
54declare module 'claude-code' {
55 interface PluginState {
56 'orkestra-ui': { veri: UiOzet | null; bantGizli: string }
57 }
58}
59