@omerrgocmen/crewctl 1.0.1 → 1.0.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <div align="center">
2
2
 
3
+ <img src="icon.png" alt="CrewCtl" width="140">
4
+
3
5
  # CrewCtl 🛰️
4
6
 
5
7
  **Kurulu CLI kodlama agent'larınızı — Codex, Claude Code, Gemini ve OpenCode — tek bir operatör liderliğindeki ekip olarak çalıştıran yerel, açık kaynak ve sıfır runtime bağımlılıklı Node.js orkestratörü.**
@@ -1,20 +1,22 @@
1
+ <p align="center"><img src="https://raw.githubusercontent.com/omergocmen/CrewCtl/main/icon.png" alt="CrewCtl" width="140"></p>
2
+
1
3
  # CrewCtl 🛰️
2
4
 
3
5
  > **Kurulu CLI kodlama agent'larınızı — Codex, Claude Code, Gemini ve OpenCode — tek bir operatör‑liderliğindeki takım halinde çalıştıran, sıfır bağımlılıklı, yerel ve açık kaynak Node.js çok‑agent (multi‑agent) AI orkestratörü. Canlı web komuta merkezi dahil.**
4
6
  >
5
7
  > _A zero‑dependency, local, self‑hosted **multi‑agent AI orchestrator** that runs your installed CLI coding agents (OpenAI Codex, Claude Code, Google Gemini, OpenCode) as one **operator‑led team**, with a live web dashboard._
6
8
 
7
- [![GitHub stars](https://img.shields.io/github/stars/omergocmen/CrewCtl?style=flat&logo=github)](https://github.com/omergocmen/CrewCtl/stargazers)
8
- [![GitHub forks](https://img.shields.io/github/forks/omergocmen/CrewCtl?style=flat&logo=github)](https://github.com/omergocmen/CrewCtl/network/members)
9
- [![GitHub issues](https://img.shields.io/github/issues/omergocmen/CrewCtl)](https://github.com/omergocmen/CrewCtl/issues)
10
- [![Last commit](https://img.shields.io/github/last-commit/omergocmen/CrewCtl)](https://github.com/omergocmen/CrewCtl/commits)
9
+ [![GitHub stars](https://img.shields.io/github/stars/omergocmen/CrewCtl?style=flat&logo=github)](https://github.com/omergocmen/CrewCtl/stargazers)
10
+ [![GitHub forks](https://img.shields.io/github/forks/omergocmen/CrewCtl?style=flat&logo=github)](https://github.com/omergocmen/CrewCtl/network/members)
11
+ [![GitHub issues](https://img.shields.io/github/issues/omergocmen/CrewCtl)](https://github.com/omergocmen/CrewCtl/issues)
12
+ [![Last commit](https://img.shields.io/github/last-commit/omergocmen/CrewCtl)](https://github.com/omergocmen/CrewCtl/commits)
11
13
  ![Node.js](https://img.shields.io/badge/Node.js-%E2%89%A518-339933?logo=node.js&logoColor=white)
12
14
  ![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
13
15
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
16
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
15
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-ff69b4)](https://github.com/omergocmen/CrewCtl/pulls)
17
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-ff69b4)](https://github.com/omergocmen/CrewCtl/pulls)
16
18
 
17
- 🔗 **Repo:** [github.com/omergocmen/CrewCtl](https://github.com/omergocmen/CrewCtl)
19
+ 🔗 **Repo:** [github.com/omergocmen/CrewCtl](https://github.com/omergocmen/CrewCtl)
18
20
 
19
21
  Elinizde zaten **Codex CLI**, **Claude Code**, **Gemini CLI** veya **OpenCode** varsa; bu araç onları
20
22
  ayrı ayrı kullanmak yerine **tek bir yapay zeka geliştirici takımı** gibi koordine eder. Bir CLI
@@ -82,18 +84,18 @@ ayrı ayrı kullanmak yerine **tek bir yapay zeka geliştirici takımı** gibi k
82
84
 
83
85
  **Tek komut — kurulum gerektirmez:**
84
86
 
85
- ```bash
86
- npx @omerrgocmen/crewctl # paneli anında başlatır (komutsuz = start)
87
- ```
88
-
89
- > **Kaynak depo notu:** `npx` komutunu CrewCtl kaynak deposunun kendi kökünde değil, yönetmek
90
- > istediğiniz proje klasöründe çalıştırın. Klonlanmış geliştirme kopyasında `npm start`
91
- > kullanılır; aksi halde npm aynı isimli yerel manifesti seçip bin shim'ini bulamayabilir.
92
-
93
- Ya da global kurun:
87
+ ```bash
88
+ npx @omerrgocmen/crewctl # paneli anında başlatır (komutsuz = start)
89
+ ```
90
+
91
+ > **Kaynak depo notu:** `npx` komutunu CrewCtl kaynak deposunun kendi kökünde değil, yönetmek
92
+ > istediğiniz proje klasöründe çalıştırın. Klonlanmış geliştirme kopyasında `npm start`
93
+ > kullanılır; aksi halde npm aynı isimli yerel manifesti seçip bin shim'ini bulamayabilir.
94
+
95
+ Ya da global kurun:
94
96
 
95
97
  ```bash
96
- npm install -g @omerrgocmen/crewctl
98
+ npm install -g @omerrgocmen/crewctl
97
99
  crewctl # panel
98
100
  crewctl status
99
101
  crewctl task "Testleri düzelt" --dir . --mode balanced
@@ -104,23 +106,23 @@ crewctl doctor # salt-okunur ortam kontrolü
104
106
  > (`CREWCTL_HOME` ile değiştirilebilir). Çalışma klasörü varsayılan olarak komutu çalıştırdığınız
105
107
  > dizindir; panelden değiştirilebilir.
106
108
 
107
- ### Developer modu / uzun yol (Git clone)
108
-
109
- Kaynak kodu geliştirmek, testleri çalıştırmak veya npm paketi yerine doğrudan Git kopyasını
110
- kullanmak için:
111
-
112
- ```bash
113
- git clone https://github.com/omergocmen/CrewCtl.git
114
- cd CrewCtl
115
- npm install
116
- npm run cli -- doctor # salt-okunur ortam kontrolü
117
- npm test # tam regresyon zinciri
118
- npm start # sunucuyu başlatır ve tarayıcıyı açar
119
- ```
120
-
121
- `npm install` runtime bağımlılığı indirmez; lock/yerel npm ortamını hazırlar. Kaynaktan
122
- çalıştırıldığında (repo `test/` klasörü mevcutken) geliştirme verileri `orchestrator/`
123
- klasöründe tutulur.
109
+ ### Developer modu / uzun yol (Git clone)
110
+
111
+ Kaynak kodu geliştirmek, testleri çalıştırmak veya npm paketi yerine doğrudan Git kopyasını
112
+ kullanmak için:
113
+
114
+ ```bash
115
+ git clone https://github.com/omergocmen/CrewCtl.git
116
+ cd CrewCtl
117
+ npm install
118
+ npm run cli -- doctor # salt-okunur ortam kontrolü
119
+ npm test # tam regresyon zinciri
120
+ npm start # sunucuyu başlatır ve tarayıcıyı açar
121
+ ```
122
+
123
+ `npm install` runtime bağımlılığı indirmez; lock/yerel npm ortamını hazırlar. Kaynaktan
124
+ çalıştırıldığında (repo `test/` klasörü mevcutken) geliştirme verileri `orchestrator/`
125
+ klasöründe tutulur.
124
126
 
125
127
  `crewctl doctor` ayarları değiştirmez. Yalnızca keşif sonucunu `config.json` dosyasına uygulamak
126
128
  istediğinizde açıkça `crewctl doctor --fix` kullanın.
@@ -14,6 +14,12 @@
14
14
  "liveDiffIntervalMs": 2500,
15
15
  "versioning": true,
16
16
  "versioningRetention": 20,
17
+ "notify": {
18
+ "_comment": "Görev bitince/başarısız olunca webhookUrl'e {text,...} POST edilir (Slack incoming webhook ile uyumlu). Boş webhookUrl = bildirim kapalı.",
19
+ "webhookUrl": "",
20
+ "onComplete": true,
21
+ "onFailed": true
22
+ },
17
23
  "operator": {
18
24
  "roleFile": "roles/operator.md",
19
25
  "maxRounds": 6,
@@ -27,7 +33,7 @@
27
33
  "opencode": { "model": "" }
28
34
  },
29
35
  "skills": {
30
- "_comment": "Yalnizca enabled listesindeki beceriler taranir. Operator tum katalog yerine goreve gore kisa liste gorur; uzman tam rehberi ancak gerekirse dosyadan okur. Bos enabled = beceri kullanilmaz.",
36
+ "_comment": "Yalnizca enabled listesindeki beceriler taranir. Operator tum katalog yerine goreve gore kisa liste gorur; uzman tam rehberi ancak gerekirse dosyadan okur. ILK KURULUMDA bu liste paketle gelen tum becerilerle otomatik doldurulur (Ayarlar > Beceriler'den kapatilabilir); scorer her goreve yalnizca ilgili olanlari surer.",
31
37
  "enabled": [],
32
38
  "autoMatch": true,
33
39
  "catalogLimit": 12,
@@ -2,31 +2,39 @@
2
2
 
3
3
  ## Amaç
4
4
 
5
- Sana devredilen işi çalışma klasöründe gerçekten uygula ve sonucu kanıtla. Ana hedefe bağlı kal;
6
- en küçük, bakımı kolay ve mevcut proje yaklaşımıyla uyumlu değişikliği tercih et.
7
-
8
- ## Çalışma biçimi
9
-
10
- 1. İlgili dosyaları, yerel talimatları, mevcut değişiklikleri ve test düzenini incele.
11
- 2. Delegasyonun teslimatını ve kabul kriterlerini netleştir; makul ayrıntılarda soru sormadan ilerle.
12
- 3. Değişikliği mevcut mimari, stil ve adlandırma kurallarına uygun uygula.
13
- 4. Etkilenen davranışı hedefli testlerle doğrula; riskle orantılı olarak daha geniş kontroller çalıştır.
14
- 5. Yalnızca gerçekten yaptığın ve doğruladığın şeyleri raporla.
15
-
16
- ## Kalite ve güvenlik kuralları
5
+ Sana devredilen işi çalışma klasöründe gerçekten uygula ve sonucu **kanıtla**. Ana hedefe bağlı kal;
6
+ mevcut projeye en iyi oturan, en küçük ve bakımı kolay değişikliği yap. Amacın "çalışan bir şey"
7
+ değil, bu kod tabanına ait, doğrulanmış ve tam bir teslimattır.
8
+
9
+ ## Çalışma ilkeleri (bunlar seni standart bir asistandan ayırır)
10
+
11
+ 1. **Önce anla, sonra değiştir.** Kör dokunma. İlgili dosyaları, çağrı yerlerini, mevcut desenleri,
12
+ testleri ve yerel talimatları OKU. Varsayım yerine kod tabanındaki gerçeği esas al.
13
+ 2. **En küçük doğru değişiklik.** Sorunu çözen en dar diff'i yaz. İstenmeyen refactor, yeniden
14
+ adlandırma, bağımlılık veya yeni soyutlama ekleme. Var olanı yeniden yazmak yerine düzenle.
15
+ 3. **Çevreye uy.** Komşu kodun stilini, adlandırmasını, hata yönetimini ve mimari desenini birebir
16
+ taklit et. Kendi tercihini projeye dayatma; kod sanki hep oradaymış gibi görünsün.
17
+ 4. **Tamamla, kısma.** `TODO`, yer tutucu, boş gövde, "buraya X gelecek" bırakma. Uçları bağla:
18
+ importlar, tipler, hata yolları, kenar durumlar. Yarım iş, başarısız iştir.
19
+ 5. **Kanıtla.** Değişikliği en dar ölçekte gerçekten çalıştır (hedefli test/derleme/lint). Testi sen
20
+ çalıştırmadıysan "geçti" deme. Test yoksa ve riskliyse davranışı doğrulayan minimal bir kontrol ekle.
21
+ 6. **Dürüst ol.** Çalıştıramadığın bir kontrolü başarı gibi sunma; sahte çıktı, gizlenmiş hata veya
22
+ gevşetilmiş test üretme. Bilmediğini bildiğin gibi raporla.
23
+
24
+ ## Güvenlik ve kapsam
17
25
 
18
26
  - Kullanıcının mevcut veya ilgisiz değişikliklerini koru; geri alma, silme ya da üzerine yazma.
19
- - Yetki verilen kapsamın dışına çıkma. Gereksiz refactor, bağımlılık veya yeni soyutlama ekleme.
20
- - Hataları gizlemek için testleri gevşetme, kontrolleri kapatma veya sahte başarı üretme.
27
+ - Yetki verilen kapsamın dışına çıkma. Planda olmayan riskli gerekiyorsa yapma, BLOCKED bildir.
21
28
  - Gizli bilgi yazdırma; yıkıcı işlem, dış sisteme yazma, deploy, push veya kullanıcı adına iletişim
22
- gerekiyorsa açık yetki olmadan yapma.
23
- - Doğrulama çalıştırılamadıysa bunu başarı gibi sunma; nedenini ve kalan riski belirt.
29
+ için açık yetki olmadan hareket etme.
30
+ - Sana beceri rehberi verildiyse önce özetini uygula; yetmezse rehber dosyasını OKU ve prosedürüne uy.
24
31
  - Küçük ve güvenli bir sapma hedefi daha doğru karşılıyorsa gerekçesini raporla. Kapsamı veya riski
25
- anlamlı biçimde değiştiren sapmada dur.
32
+ anlamlı biçimde değiştiren sapmada dur ve BLOCKED bildir.
26
33
 
27
34
  ## Çıktı sözleşmesi
28
35
 
29
- Operatöre kısa, taranabilir ve kanıta dayalı bir teslimat raporu ver:
36
+ Operatöre kısa, taranabilir ve kanıta dayalı bir teslimat raporu ver. İç düşünce dökümü veya ham log
37
+ yığma; operatörün karar vereceği somut bilgiyi ver:
30
38
 
31
39
  ```text
32
40
  DURUM: COMPLETED
@@ -30,8 +30,10 @@ Motor her çağrıda çalışma evresini, agent kataloğunu, çalışma modunu v
30
30
  ## Beceriler
31
31
 
32
32
  - Motorun görev için tarayıp verdiği kısa listeden gerçekten ilgili en fazla birkaç beceriyi delegasyonun
33
- `skills` alanına ekle. Yalnızca listedeki adları kullan; uygun beceri yoksa alanı boş bırak. Uzman ayrıntılı
34
- rehberi ihtiyaç halinde dosyadan okuyacaktır.
33
+ `skills` alanına ekle. Kısa listede işe uyan bir beceri VARSA, ilgili `implement`, `plan` ve `review`
34
+ delegasyonlarında bunu iliştirmek beklenendir — beceriler kullanıcının koyduğu proje standardıdır ve
35
+ teslimat kalitesini yükseltir; ilgili beceriyi boşuna atlama. Yalnızca listedeki adları kullan; gerçekten
36
+ uygun beceri yoksa alanı boş bırak. Uzman ayrıntılı rehberi ihtiyaç halinde dosyadan okuyacaktır.
35
37
  - "Beceriler (OTORİTER kaynak)" bölümü sistemin beceri envanteridir ve tek doğru kaynaktır. Beceri sayısı, adı
36
38
  veya varlığı sorulduğunda daima bu bölümü esas al; çalıştığın CLI'nin kendi dahili becerilerini bu sistemin
37
39
  becerileri gibi sayma veya karıştırma. Bu bölüm hiç yoksa sistemde etkin beceri yok demektir.
@@ -7,6 +7,11 @@ doğrulanabilir bir plana dönüştür. Bu rolde çözümü uygulamaz, dosya de
7
7
  işlem çalıştırmazsın. Projeyi anlamak için yalnızca salt okunur inceleme ve keşif araçlarını
8
8
  kullanabilirsin.
9
9
 
10
+ İyi bir plan, uygulayıcının "ne yapacağım?" diye durup düşünmesini ortadan kaldırır. Bunu
11
+ başarmak için tahminle değil **gerçek kod tabanıyla** çalış: dosya adı, fonksiyon, komut veya API
12
+ yazmadan önce projede gerçekten var olduklarını inceleyerek doğrula. Uydurma bir yol, planın
13
+ tamamını çürütür.
14
+
10
15
  ## Sorumluluklar
11
16
 
12
17
  - Ana hedefi, kapsamı, kapsam dışını ve ölçülebilir kabul kriterlerini ayır.
@@ -6,6 +6,15 @@ Teslimatı kullanıcı hedefi, kabul kriterleri ve mevcut proje davranışına k
6
6
  doğrula. Uygulayıcının raporunu kanıt kabul etme; dosyaları, farkları ve test sonuçlarını kendin
7
7
  incele. Bu rolde çözümü değiştirmezsin.
8
8
 
9
+ ## İnceleme ilkeleri
10
+
11
+ - **Rapora değil koda güven.** "Yaptım/geçti" ifadesini doğrulama sayma; değişen dosyayı ve etkilediği
12
+ çağrı yerlerini kendin oku.
13
+ - **Çalıştırabildiğini çalıştır.** Hedefli testleri veya salt okunur kontrolleri gerçekten yürüt ve
14
+ gözlemlediğin sonucu kanıt yaz. Çalıştıramadığını `NOT RUN` işaretle, uydurma.
15
+ - **Kanıtsız reddetme, göz boyamayla onaylama.** Her bulguya dosya/konum ve somut kanıt bağla; bulgu
16
+ yoksa yapay sorun icat etme, gerçek riski de örtme.
17
+
9
18
  ## İnceleme sırası
10
19
 
11
20
  1. Ana hedefi ve delegasyon kapsamını çıkar.
@@ -41,7 +41,10 @@ const DEFINITIONS = {
41
41
  // OpenCode saglayici/model kombinasyonuna gore uzun sure sessiz kalabilir. Varsayilan
42
42
  // agent timeout'u, basarili bir calismayi SIGTERM ile yarida kesmeyecek kadar genis tut.
43
43
  timeoutSeconds: 1800,
44
- silenceTimeoutSeconds: 180,
44
+ // OpenCode adim baslattiktan sonra model/provider yanitini beklerken uzun sure hic cikti
45
+ // akitmayabilir (ozellikle yavas/rate-limitli modeller). 180sn cok agresifti ve calisan bir
46
+ // kosmayi yariyordu; sessizlik payini genis tut. Gercek takilma yine timeoutSeconds ile yakalanir.
47
+ silenceTimeoutSeconds: 300,
45
48
  // Bazi eski OpenCode surumleri --auto bayragini tanimaz. Otonom izinler
46
49
  // engine tarafinda OPENCODE_CONFIG_CONTENT ile surumden bagimsiz aktarilir.
47
50
  defaultArgs: ["run", "--format", "json", "Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}"],
@@ -415,12 +415,22 @@ function normalizeAssignments(value, cfg, operatorName, usedIds, taskText = "")
415
415
  // Beceriler kullanici-kapilidir: operator yalnizca cfg.skills.enabled icindekileri iliştirebilir.
416
416
  // Tanimsiz/etkin olmayan beceri sessizce dusurulur; asla gorevi olduren bir hata degildir.
417
417
  const hasExplicitSkills = Object.prototype.hasOwnProperty.call(raw, "skills");
418
+ const autoMatchOn = cfg.skills?.autoMatch !== false;
418
419
  const requestedSkills = Array.isArray(raw.skills)
419
420
  ? raw.skills
420
- : (!hasExplicitSkills && cfg.skills?.autoMatch !== false
421
+ : (!hasExplicitSkills && autoMatchOn
421
422
  ? skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind)
422
423
  : []);
423
- const skills = skillRegistry.resolveForAssignment(requestedSkills, cfg).map((skill) => skill.name);
424
+ let skills = skillRegistry.resolveForAssignment(requestedSkills, cfg).map((skill) => skill.name);
425
+ // Operator kodlama/planlama/review isinde beceri iliştirmeyi atlarsa (or. skills:[] dönerse),
426
+ // kullanicinin etkin becerilerinden goreve GERCEKTEN uyanlari otomatik ekle. suggest() yalnizca
427
+ // lexical eslesme oldugunda skill döner; alakasiz beceri asla iliştirilmez. Boylece "operator
428
+ // becerileri kullanmiyor" durumu ortadan kalkar.
429
+ if (!skills.length && autoMatchOn && ["implement", "plan", "review"].includes(kind)) {
430
+ skills = skillRegistry.resolveForAssignment(
431
+ skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind), cfg
432
+ ).map((skill) => skill.name);
433
+ }
424
434
  let agent = requestedAgent;
425
435
  const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
426
436
  if (unavailable.has(agent) || !supportsKind(cfg.agents[agent], kind)) {
@@ -688,6 +698,7 @@ class Engine extends EventEmitter {
688
698
  const failure = classifyCliError(error);
689
699
  this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: task.parentTaskId, status: "failed", error: failure.summary }, task.id);
690
700
  }
701
+ this.notifyOutcome(task, "failed", { error: error.message });
691
702
  this.emit("queue");
692
703
  }
693
704
  } finally {
@@ -850,6 +861,15 @@ class Engine extends EventEmitter {
850
861
  });
851
862
  }
852
863
 
864
+ // Katalogda dosya yazabilen (implement) kullanilabilir bir uzman var mi? Operatorun kendisi ve
865
+ // bu oturumda karantinaya alinan agentler haric. Tek executor (or. opencode) sessizlik nedeniyle
866
+ // dustugunde takim fiilen dosya uretemez hale gelir; bu kontrol o durumu erken yakalar.
867
+ hasUsableImplementAgent(cfg, operatorName) {
868
+ return Object.entries(cfg.agents || {}).some(([name, agent]) =>
869
+ name !== operatorName && agent.enabled !== false && agentUsable(agent)
870
+ && !this.unhealthyAgents.has(name) && supportsKind(agent, "implement"));
871
+ }
872
+
853
873
  agentCatalog(cfg, operatorName) {
854
874
  return Object.entries(cfg.agents)
855
875
  .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.unhealthyAgents.has(name))
@@ -1160,6 +1180,22 @@ class Engine extends EventEmitter {
1160
1180
  recoveryRounds++;
1161
1181
  this.publish("log", { level: "warn", msg: `CLI altyapi hatasi tur butcesinden dusulmedi; ${maxRecoveryRounds - recoveryRounds} ek kurtarma turu kaldi.` }, task.id);
1162
1182
  }
1183
+ // Uygulama agenti kalmadi guvenligi: Gorev acikca dosya olusturma/degistirme gerektiriyor
1184
+ // fakat katalogda 'implement' yapabilen kullanilabilir hicbir agent kalmadiysa (or. tek
1185
+ // executor opencode sessizlik nedeniyle karantinaya alindi) ve simdiye kadar tamamlanmis bir
1186
+ // uygulama isi da yoksa, HICBIR ek tur dosya uretemez. Operatoru planlayici/gap-analizi
1187
+ // dongusunde bosuna dondurup tur ve cagri butcesini yakmak yerine net, eyleme donuk bir
1188
+ // hatayla dur (kullanicinin gordugu "circle seklinde uzayip gitme" sorununu bu keser).
1189
+ if (taskRequiresDelegation(task.prompt)
1190
+ && !this.hasUsableImplementAgent(cfg, operatorCli)
1191
+ && !Object.values(state.results).some((result) => result.kind === "implement" && result.status === "completed")) {
1192
+ const dead = [...this.unhealthyAgents.keys()];
1193
+ throw new Error(
1194
+ "Bu gorev dosya olusturma/degistirme gerektiriyor fakat kullanilabilir bir uygulama (implement) agenti yok" +
1195
+ (dead.length ? ` (${dead.join(", ")} bu oturumda kullanim disi kaldi)` : "") +
1196
+ ". Ayarlar > Agent'lar'dan 'implementation' yetenekli bir agent (Codex/Claude/Gemini/OpenCode) ekleyip etkinlestirin veya opencode'un oturum/model durumunu kontrol edin, sonra gorevi tekrar calistirin."
1197
+ );
1198
+ }
1163
1199
  if (task.executionMode === "fast" && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")) {
1164
1200
  const reports = assignments.map((assignment) => state.results[assignment.id].result).join("\n\n");
1165
1201
  this.publish("log", { level: "info", msg: "FAST mod: uzman teslimati basarili; ikinci operator degerlendirme cagrisi atlandi." }, task.id);
@@ -1355,9 +1391,26 @@ class Engine extends EventEmitter {
1355
1391
  store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
1356
1392
  this.publish("result", { id: task.id, prompt: task.prompt, status, dir: this._cwd, changes, summary: task.summary, delivery: task.delivery, operator: task.operatorCli, rounds: state.round }, task.id);
1357
1393
  this.publish("log", { level: "ok", msg: `GOREV tamamlandi: ${task.id}` }, task.id);
1394
+ this.notifyOutcome(task, status, { summary: task.summary, rounds: state.round, fileCount: files.length, verification: task.delivery.verification });
1358
1395
  this.emit("queue");
1359
1396
  this._textBefore = null;
1360
1397
  }
1398
+
1399
+ // Dis bildirim icin tek cikis noktasi: server.js bu event'i dinleyip (varsa) webhook'a POST atar.
1400
+ // Motoru HTTP'den bagimsiz tutar; bildirim basarisiz olsa bile gorev akisini etkilemez.
1401
+ notifyOutcome(task, status, extra = {}) {
1402
+ try {
1403
+ this.emit("notify", {
1404
+ status,
1405
+ id: task.id,
1406
+ prompt: task.prompt,
1407
+ operator: task.operatorCli || null,
1408
+ dir: this._cwd || task.targetDir || null,
1409
+ at: new Date().toISOString(),
1410
+ ...extra,
1411
+ });
1412
+ } catch {}
1413
+ }
1361
1414
  }
1362
1415
 
1363
1416
  module.exports = new Engine();
@@ -73,6 +73,34 @@ engine.on("activity", (d) => broadcast("activity", d));
73
73
  engine.on("message", (d) => broadcast("message", d));
74
74
  engine.on("filechange", (d) => broadcast("filechange", d));
75
75
  engine.on("queue", () => broadcast("queue", snapshot()));
76
+ engine.on("notify", (d) => { sendNotification(d); broadcast("notify", d); });
77
+
78
+ // Görev bitince/başarısız olunca kullanıcının yapılandırdığı webhook'a POST atar (Slack incoming
79
+ // webhook formatıyla uyumlu: {text}). Tamamen opsiyonel ve ateşle-unut; Node'un yerleşik fetch'ini
80
+ // kullanır (yeni bağımlılık yok). Hata görevi asla etkilemez. Yapılandırılmamışsa sessizce atlanır.
81
+ function sendNotification(outcome) {
82
+ try {
83
+ const cfg = store.loadConfig();
84
+ const n = cfg.notify || {};
85
+ const url = String(n.webhookUrl || "").trim();
86
+ if (!url || !/^https?:\/\//i.test(url)) return;
87
+ if (outcome.status === "failed" ? n.onFailed === false : n.onComplete === false) return;
88
+ const icon = outcome.status === "failed" ? "❌" : "✅";
89
+ const head = outcome.status === "failed" ? "Görev başarısız" : "Görev tamamlandı";
90
+ const detail = outcome.status === "failed"
91
+ ? (outcome.error ? `\nHata: ${String(outcome.error).slice(0, 300)}` : "")
92
+ : `${outcome.operator ? ` · ${outcome.operator}` : ""}${outcome.rounds ? ` · ${outcome.rounds} tur` : ""}${Number.isFinite(outcome.fileCount) ? ` · ${outcome.fileCount} dosya` : ""}${outcome.verification ? ` · ✓ ${String(outcome.verification).slice(0, 120)}` : ""}`;
93
+ const text = `${icon} CrewCtl — ${head}: ${String(outcome.prompt || outcome.id).slice(0, 200)}${detail}`;
94
+ const controller = new AbortController();
95
+ const timer = setTimeout(() => controller.abort(), 8000);
96
+ fetch(url, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({ text, crewctl: outcome }),
100
+ signal: controller.signal,
101
+ }).catch(() => {}).finally(() => clearTimeout(timer));
102
+ } catch {}
103
+ }
76
104
 
77
105
  function broadcastSchedules() {
78
106
  broadcast("schedules", store.loadConfig().schedules || []);
@@ -191,7 +219,7 @@ function readBody(req) {
191
219
  });
192
220
  });
193
221
  }
194
- const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css" };
222
+ const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".png": "image/png", ".ico": "image/x-icon", ".svg": "image/svg+xml", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".webmanifest": "application/manifest+json", ".json": "application/json" };
195
223
  function serveStatic(res, file) {
196
224
  const p = path.resolve(WEB, file);
197
225
  if ((p !== WEB && !p.startsWith(WEB + path.sep)) || !fs.existsSync(p)) {
@@ -584,6 +612,11 @@ const server = http.createServer(async (req, res) => {
584
612
  if (!name.trim() || !agent.cmd || typeof agent.cmd !== "string") return send(res, 400, { error: `gecersiz agent: ${name}` });
585
613
  if (!Array.isArray(agent.args)) return send(res, 400, { error: `${name}.args dizi olmali` });
586
614
  }
615
+ if (cfg.notify !== undefined) {
616
+ if (typeof cfg.notify !== "object" || cfg.notify === null) return send(res, 400, { error: "notify nesne olmali" });
617
+ const url = String(cfg.notify.webhookUrl || "").trim();
618
+ if (url && !/^https?:\/\//i.test(url)) return send(res, 400, { error: "notify.webhookUrl http(s) ile baslamali veya bos olmali" });
619
+ }
587
620
  store.saveConfig(cfg);
588
621
  broadcast("status", engine.status());
589
622
  return send(res, 200, { ok: true });
@@ -125,6 +125,7 @@ const FALLBACK_CONFIG = {
125
125
  discoveryIgnoredAdapters: [],
126
126
  liveDiff: true, liveDiffIntervalMs: 2500,
127
127
  versioning: true, versioningRetention: 20,
128
+ notify: { webhookUrl: "", onComplete: true, onFailed: true },
128
129
  operator: { roleFile: "roles/operator.md", maxRounds: 6, maxDelegationsPerRound: 8, maxInfrastructureRecoveryRounds: 2, protocolRetries: 1 },
129
130
  cliSettings: {
130
131
  codex: { model: "", reasoningEffort: "medium", serviceTier: "fast" },
@@ -159,12 +160,31 @@ function normalizeConfig(cfg) {
159
160
  }
160
161
  // config.json kisiye ozeldir (gitignore). Yoksa config.default.json sablonundan uretilir;
161
162
  // boylece kullanici klonlayip `npm start` dedigi anda calisir ve kurulu CLI'lar keşifle eklenir.
163
+ // Paketle gelen beceri dosyalarinin slug'lari (dosya adi = frontmatter name, dogrulanir).
164
+ // Ilk config uretiminde beceriler VARSAYILAN OLARAK ETKIN gelsin diye kullanilir; boylece
165
+ // operator, kullanicinin becerilerini kodlama/planlama/review delegasyonlarinda kullanabilir.
166
+ function shippedSkillSlugs() {
167
+ try {
168
+ return fs.readdirSync(path.join(ASSETS, "skills"))
169
+ .filter((f) => f.toLowerCase().endsWith(".md"))
170
+ .map((f) => f.replace(/\.md$/i, ""));
171
+ } catch { return []; }
172
+ }
162
173
  function loadConfig() {
163
174
  const file = path.join(ROOT, "config.json");
164
175
  if (!fs.existsSync(file)) {
165
176
  const template = path.join(ASSETS, "config.default.json");
166
177
  const seed = fs.existsSync(template) ? fs.readFileSync(template, "utf8") : JSON.stringify(FALLBACK_CONFIG, null, 2);
167
- atomicWrite(file, seed);
178
+ let seedObj;
179
+ try { seedObj = JSON.parse(seed); } catch { seedObj = { ...FALLBACK_CONFIG }; }
180
+ // Ilk kurulumda tum paket becerilerini etkinlestir (kullanici Ayarlar'dan kapatabilir).
181
+ // Sonraki yuklemelerde dosya var oldugundan bu blok CALISMAZ; kullanicinin secimi korunur.
182
+ seedObj.skills = seedObj.skills || {};
183
+ if (!Array.isArray(seedObj.skills.enabled) || !seedObj.skills.enabled.length) {
184
+ const slugs = shippedSkillSlugs();
185
+ if (slugs.length) seedObj.skills.enabled = slugs;
186
+ }
187
+ atomicWrite(file, JSON.stringify(seedObj, null, 2));
168
188
  }
169
189
  return normalizeConfig(JSON.parse(fs.readFileSync(file, "utf8")));
170
190
  }
@@ -4,6 +4,11 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>Pano · CrewCtl</title>
7
+ <link rel="icon" href="/favicon.ico" sizes="any">
8
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
9
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
11
+ <link rel="manifest" href="/site.webmanifest">
7
12
  <style>
8
13
  *{box-sizing:border-box}
9
14
  :root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
@@ -4,6 +4,11 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>Canlı Kodlama · CrewCtl</title>
7
+ <link rel="icon" href="/favicon.ico" sizes="any">
8
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
9
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
11
+ <link rel="manifest" href="/site.webmanifest">
7
12
  <style>
8
13
  *{box-sizing:border-box}
9
14
  :root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
Binary file
@@ -4,6 +4,11 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>Ekip Akışı · CrewCtl</title>
7
+ <link rel="icon" href="/favicon.ico" sizes="any">
8
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
9
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
11
+ <link rel="manifest" href="/site.webmanifest">
7
12
  <style>
8
13
  *{box-sizing:border-box}
9
14
  :root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
@@ -4,6 +4,11 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>CrewCtl</title>
7
+ <link rel="icon" href="/favicon.ico" sizes="any">
8
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
9
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
11
+ <link rel="manifest" href="/site.webmanifest">
7
12
  <style>
8
13
  *{box-sizing:border-box}
9
14
  :root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
@@ -13,7 +18,7 @@
13
18
  [data-theme=light] .team-node.operator .node-info{border-color:color-mix(in srgb,var(--nacc) 38%,var(--line));background:linear-gradient(180deg,color-mix(in srgb,var(--nacc) 6%,var(--panel2)),var(--nodebg))}
14
19
  [data-theme=light] .friendly-error{background:#fdecee;color:#8f1f28;border-color:#d6404b55}[data-theme=light] .friendly-error b{color:#c02e39}[data-theme=light] .friendly-error small{color:#9a5a60}
15
20
  [data-theme=light] .friendly-error.success{background:#e9f8f0;color:#0d5c3c;border-color:#0f9d6355}[data-theme=light] .friendly-error.success b{color:var(--ok)}[data-theme=light] .friendly-error.success small{color:#4f7d68}
16
- [data-theme=light] .msg.failure,[data-theme=light] .msg.blocked{background:#fdecef}
21
+ [data-theme=light] .fi-failure,[data-theme=light] .fi-blocked{background:#fdecef}
17
22
  body{margin:0;background:var(--bg);color:var(--tx);font:14px/1.5 Inter,Segoe UI,system-ui,sans-serif;transition:background .3s,color .3s}
18
23
  button,input,textarea,select{font:inherit}
19
24
  button{cursor:pointer;color:var(--tx);background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:7px 11px;transition:border-color .18s,background .18s,transform .1s,box-shadow .18s}
@@ -85,16 +90,25 @@ details summary{cursor:pointer;color:var(--dim);font-size:12px} pre{white-space:
85
90
  .friendly-error b{display:block;color:#ff8f97;margin-bottom:3px}.friendly-error small{color:#d5aeb2}
86
91
  .friendly-error.success{border-color:#42d39255;background:#10261e;color:#d7ffec}.friendly-error.success b{color:var(--ok)}.friendly-error.success small{color:#a7cfbd}
87
92
  .plan-pills{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}.plan-pills span{font-size:10px;border:1px solid #42d39255;border-radius:999px;padding:2px 7px}
88
- /* Birlesik akis (mesaj + olay) */
89
- .feed{max-height:340px;overflow:auto;display:flex;flex-direction:column;gap:6px}
90
- .msg{border-left:3px solid var(--purple);background:var(--panel2);border-radius:0 8px 8px 0;padding:7px 9px}
91
- .msg.result{border-color:var(--ok)}.msg.failure,.msg.blocked{border-color:var(--err);background:#27161d}
92
- .msg .route{font-size:11px;color:var(--purple);font-weight:700}
93
- .msg.result .route{color:var(--ok)}.msg.failure .route,.msg.blocked .route{color:var(--err)}
94
- .msg .body{font-size:12px;white-space:pre-wrap;max-height:90px;overflow:auto}
95
- .log{font:11.5px ui-monospace;color:var(--dim);padding:2px 2px}
96
- .log.error{color:var(--err)}.log.warn{color:var(--warn)}.log.ok{color:var(--ok)}.log.task{color:var(--acc)}
97
- .log time,.msg time{color:var(--dim);font:10px ui-monospace;margin-right:6px}
93
+ /* Birlesik akis — zaman cizelgesi (olay tipine gore renkli, uzun icerik katlanir) */
94
+ .feed{max-height:360px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding-left:2px}
95
+ .feed-item{position:relative;display:flex;gap:9px;padding:6px 8px 6px 4px;border-radius:9px;transition:background .14s}
96
+ .feed-item:hover{background:color-mix(in srgb,var(--panel2) 55%,transparent)}
97
+ .feed-item:not(:last-child)::before{content:'';position:absolute;left:13px;top:25px;bottom:-2px;width:1px;background:color-mix(in srgb,var(--line) 65%,transparent)}
98
+ .fi-dot{position:relative;z-index:1;flex:none;margin-top:2px;width:19px;height:19px;border-radius:50%;display:grid;place-items:center;font:700 10px/1 ui-monospace,Consolas,monospace;color:var(--evc,var(--dim));border:1px solid color-mix(in srgb,var(--evc,var(--line)) 50%,var(--line));background:color-mix(in srgb,var(--evc,var(--dim)) 13%,var(--panel))}
99
+ .fi-main{min-width:0;flex:1}
100
+ .fi-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:11.5px;line-height:1.5}
101
+ .fi-from{font-weight:700}.fi-arrow{color:var(--dim);opacity:.55}.fi-to{font-weight:600;color:var(--tx)}
102
+ .fi-tag{font:10px ui-monospace,Consolas,monospace;color:var(--dim);border:1px solid var(--line);border-radius:5px;padding:0 5px}
103
+ .fi-head time{margin-left:auto;color:var(--dim);font:10px ui-monospace,Consolas,monospace;flex:none}
104
+ .fi-msg{color:var(--tx)}
105
+ .fi-body{margin-top:3px;font-size:12px;line-height:1.55;color:var(--dim);white-space:pre-wrap;word-break:break-word}
106
+ .fi-body.clamp{max-height:74px;overflow:hidden;-webkit-mask-image:linear-gradient(180deg,#000 58%,transparent);mask-image:linear-gradient(180deg,#000 58%,transparent)}
107
+ .fi-more{margin-top:3px;font-size:11px;color:var(--acc);background:none;border:0;padding:1px 0}.fi-more:hover{text-decoration:underline}
108
+ .fi-delegation{--evc:var(--purple)}.fi-result{--evc:var(--ok)}.fi-failure,.fi-blocked{--evc:var(--err)}
109
+ .fi-failure,.fi-blocked{background:color-mix(in srgb,var(--err) 7%,transparent)}
110
+ .fi-log{--evc:var(--dim)}.fi-log.task{--evc:var(--acc)}.fi-log.ok{--evc:var(--ok)}.fi-log.error{--evc:var(--err)}.fi-log.warn{--evc:var(--warn)}.fi-log.stage{--evc:var(--purple)}.fi-log.info{--evc:var(--acc)}
111
+ .fi-log.error .fi-msg,.fi-log.warn .fi-msg{color:var(--evc)}.fi-log.ok .fi-msg,.fi-log.task .fi-msg{font-weight:600}
98
112
  /* Teslimat */
99
113
  .delivery{margin-top:8px;border-top:1px solid var(--line);padding-top:7px}
100
114
  .file-change{display:flex;gap:6px;align-items:center;font:11px ui-monospace;margin:3px 0}
@@ -380,7 +394,7 @@ function setMissionStatus(state,text){const el=$('#missionStatus'),tx=$('#missio
380
394
  function missionCompleted(info){setMissionStatus('completed','Tamamlandı');const i=$('#mbIco'),t=$('#mbTitle'),s=$('#mbSub');if(i)i.textContent='✓';if(t)t.textContent='Görev başarıyla tamamlandı';if(s)s.textContent=info||''}
381
395
  function missionFailed(info){setMissionStatus('failed','Başarısız');const i=$('#mbIco'),t=$('#mbTitle'),s=$('#mbSub');if(i)i.textContent='✕';if(t)t.textContent='Görev başarısız oldu';if(s)s.textContent=info||''}
382
396
  function showToast(kind,title,sub){let box=$('#toasts');if(!box){box=document.createElement('div');box.id='toasts';document.body.appendChild(box)}const t=document.createElement('div');t.className='toast '+(kind||'');t.innerHTML=`<div class="t-ico">${kind==='err'?'✕':'✓'}</div><div><div class="t-title">${esc(title)}</div>${sub?`<div class="t-sub">${esc(sub)}</div>`:''}</div>`;box.appendChild(t);setTimeout(()=>{t.classList.add('hide');setTimeout(()=>t.remove(),300)},5200)}
383
- async function addTask(){const prompt=$('#prompt').value.trim(),operatorCli=$('#taskOperator').value,targetDir=$('#targetDir').value.trim(),executionMode=$('#executionMode').value;if(!prompt)return;try{await api('/api/tasks','POST',{prompt,operatorCli,executionMode,targetDir:targetDir||undefined});$('#prompt').value='';showToast('ok','Görev kuyruğa eklendi','Operatör sıraya alındığında çalışmaya başlayacak.')}catch(e){showToast('err','Görev eklenemedi',e.message)}}
397
+ async function addTask(){const prompt=$('#prompt').value.trim(),operatorCli=$('#taskOperator').value,targetDir=$('#targetDir').value.trim(),executionMode=$('#executionMode').value;if(!prompt)return;ensureNotifyPermission();try{await api('/api/tasks','POST',{prompt,operatorCli,executionMode,targetDir:targetDir||undefined});$('#prompt').value='';showToast('ok','Görev kuyruğa eklendi','Operatör sıraya alındığında çalışmaya başlayacak.')}catch(e){showToast('err','Görev eklenemedi',e.message)}}
384
398
 
385
399
  /* ---- Kuyruk ---- */
386
400
  function deliveryHtml(t){const d=t.delivery;if(!d)return '';const labels={created:'eklendi',modified:'değişti',deleted:'silindi'};return `<div class="delivery"><div class="muted">📁 ${esc(d.location||'')}</div><div class="muted">⚡ ${(d.mode||'').toUpperCase()} · ${d.rounds||0} tur · ${(d.agents||[]).join(', ')}</div>${(d.files||[]).map(f=>`<div class="file-change"><span class="${esc(f.action)}">${esc(labels[f.action]||f.action)}</span><code>${esc(f.path)}</code></div>`).join('')||'<div class="muted">Dosya değişikliği yok.</div>'}${d.verification?`<div class="muted" style="margin-top:5px">✓ ${esc(d.verification)}</div>`:''}${(d.warnings||[]).map(w=>`<div style="margin-top:5px;color:var(--warn)">⚠ ${esc(w)}</div>`).join('')}</div>`}
@@ -425,8 +439,9 @@ function activity(e){const c=ensureCall(e);const role=String(e.stage||'').starts
425
439
 
426
440
  /* ---- Birlesik akis: mesajlar + sistem olaylari ---- */
427
441
  function feedReady(){const feed=$('#feed');if(feed.querySelector('.empty'))feed.innerHTML='';return feed}
428
- function message(e){missionMessages++;updateMissionStats();const feed=feedReady();ensureTeamNode(e.from,e.from===STATE.status.defaultOperator?'operator':'agent');ensureTeamNode(e.to,e.to===STATE.status.defaultOperator?'operator':'agent');document.querySelectorAll('.team-link').forEach(x=>{x.classList.add('active');setTimeout(()=>x.classList.remove('active'),1500)});const mt=e.messageType||'',plain=mt!=='failure'&&mt!=='blocked',col=plain?agentColor(e.from):'';const div=document.createElement('div');div.className='msg '+mt;if(plain)div.style.borderLeftColor=col;div.innerHTML=`<div class="route"${plain?` style="color:${col}"`:''}><time>${time(e.at)}</time>${esc(e.from)} → ${esc(e.to)} · ${esc(e.assignmentId||'')}</div><div class="body">${esc(e.body)}</div>`;feed.prepend(div)}
429
- function logEvent(e){if(e.level==='task')startMission(e);if(!replaying&&e.level==='error'&&/^HATA:/.test(String(e.msg||''))){const reason=String(e.msg).replace(/^HATA:\s*/,'').slice(0,180);missionFailed(reason);showToast('err','Görev başarısız',reason.slice(0,120))}const feed=feedReady();const d=document.createElement('div');d.className='log '+(e.level||'');d.innerHTML=`<time>${time(e.at)}</time>${esc(e.msg)}`;feed.prepend(d)}
442
+ function message(e){missionMessages++;updateMissionStats();const feed=feedReady();ensureTeamNode(e.from,e.from===STATE.status.defaultOperator?'operator':'agent');ensureTeamNode(e.to,e.to===STATE.status.defaultOperator?'operator':'agent');document.querySelectorAll('.team-link').forEach(x=>{x.classList.add('active');setTimeout(()=>x.classList.remove('active'),1500)});const mt=e.messageType||'',plain=mt!=='failure'&&mt!=='blocked';const typeClass=mt==='failure'?'fi-failure':mt==='blocked'?'fi-blocked':mt==='result'?'fi-result':'fi-delegation';const ico=mt==='failure'?'✕':mt==='blocked'?'⊘':mt==='result'?'✓':'⇢';const col=plain?agentColor(e.from):'';const body=String(e.body||'');const long=body.length>300||(body.match(/\n/g)||[]).length>4;const div=document.createElement('div');div.className='feed-item '+typeClass;div.innerHTML=`<span class="fi-dot">${ico}</span><div class="fi-main"><div class="fi-head"><span class="fi-from"${plain&&col?` style="color:${col}"`:''}>${esc(e.from)}</span><span class="fi-arrow">→</span><span class="fi-to">${esc(e.to)}</span>${e.assignmentId?`<span class="fi-tag">${esc(e.assignmentId)}</span>`:''}<time>${time(e.at)}</time></div><div class="fi-body${long?' clamp':''}">${esc(body)}</div>${long?`<button class="fi-more" onclick="toggleFeedBody(this)">Devamını göster</button>`:''}</div>`;feed.prepend(div)}
443
+ function toggleFeedBody(btn){const body=btn.previousElementSibling;if(!body)return;const clamped=body.classList.toggle('clamp');btn.textContent=clamped?'Devamını göster':'Gizle'}
444
+ function logEvent(e){if(e.level==='task')startMission(e);if(!replaying&&e.level==='error'&&/^HATA:/.test(String(e.msg||''))){const reason=String(e.msg).replace(/^HATA:\s*/,'').slice(0,180);missionFailed(reason);showToast('err','Görev başarısız',reason.slice(0,120))}const feed=feedReady();const lvl=e.level||'info';const ico={task:'▶',ok:'✓',error:'✕',warn:'!',stage:'→',info:'·'}[lvl]||'·';const d=document.createElement('div');d.className='feed-item fi-log '+lvl;d.innerHTML=`<span class="fi-dot">${ico}</span><div class="fi-main"><div class="fi-head"><span class="fi-msg">${esc(e.msg)}</span><time>${time(e.at)}</time></div></div>`;feed.prepend(d)}
430
445
  function applyReplayOutcome(id){const done=(STATE.queue.done||[]).find(t=>t.id===id),failed=(STATE.queue.failed||[]).find(t=>t.id===id);if(done){const d=done.delivery||{},files=(d.files||[]).length;missionCompleted(`${done.operatorCli||'Operatör'} · ${d.rounds||0} tur · ${files} dosya değişikliği${d.verification?` · ✓ ${d.verification}`:''}${(d.warnings||[]).length?` · ${d.warnings.length} uyarı`:''}`)}else if(failed)missionFailed(String(failed.error||'Görev tamamlanamadı').slice(0,180))}
431
446
  async function replayRun(id,{scroll=false}={}){const previousReplay=replaying;replaying=true;try{clearLive();const events=await api(`/api/tasks/${id}/events?limit=1500`);for(const e of events){if(e.type==='activity')activity(e);else if(e.type==='message')message(e);else if(e.type==='log')logEvent(e)}applyReplayOutcome(id);if(scroll){const mc=$('#missionCard');if(mc)mc.scrollIntoView({behavior:'smooth',block:'start'})}return events}finally{replaying=previousReplay}}
432
447
  function latestDashboardRun(){if(STATE.status?.current?.id)return STATE.status.current;return [...(STATE.queue.done||[]),...(STATE.queue.failed||[])].sort((a,b)=>new Date(b.finishedAt||b.createdAt||0)-new Date(a.finishedAt||a.createdAt||0))[0]||null}
@@ -448,9 +463,13 @@ async function sendChat(){const question=$('#chatQuestion').value.trim();if(!que
448
463
 
449
464
  /* ---- SSE ---- */
450
465
  async function resultEvent(r){if(r.kind==='operator-chat'){if(chatParent&&chatParent.id===r.parentTaskId){if(r.status==='failed'){const pending=$('#chatHistory [data-pending]');if(pending){pending.removeAttribute('data-pending');pending.textContent='Operatör yanıt veremedi: '+(r.error||'Bilinmeyen hata')}$('#chatState').textContent='Sohbet başarısız.'}else{const found=await api(`/api/tasks/${r.parentTaskId}`);chatParent=found.task;renderChat();$('#chatState').textContent='Yanıt hazır.'}}return}const fileCount=(r.changes?.created||[]).length+(r.changes?.modified||[]).length+(r.changes?.deleted||[]).length,warnings=(r.delivery?.warnings||[]).length;$('#missionTitle').textContent='Görev tamamlandı';$('#missionSub').textContent=`${r.operator||'Operatör'} · ${r.rounds||0} takım turu · ${fileCount} dosya değişikliği`;teamNodes.forEach(n=>{n.node.classList.remove('working');n.node.classList.add('done');n.state.textContent='tamamlandı'});missionCompleted(`${r.operator||'Operatör'} · ${r.rounds||0} tur · ${fileCount} dosya değişikliği${r.delivery?.verification?` · ✓ ${r.delivery.verification}`:''}${warnings?` · ${warnings} uyarı`:''}`);showToast('ok','Görev tamamlandı',r.summary||r.prompt||'Teslimat hazır.');logEvent({...r,level:'ok',msg:'Operatör görevi tamamladı'})}
451
- function dispatchDashboardEvent(type,data){if(type==='status')applyStatus(data);else if(type==='queue')renderQueue(data);else if(type==='activity')activity(data);else if(type==='message')message(data);else if(type==='log')logEvent(data);else if(type==='result')resultEvent(data);else if(type==='schedules'){STATE.schedules=data;renderScheduleList()}}
466
+ function dispatchDashboardEvent(type,data){if(type==='status')applyStatus(data);else if(type==='queue')renderQueue(data);else if(type==='activity')activity(data);else if(type==='message')message(data);else if(type==='log')logEvent(data);else if(type==='result')resultEvent(data);else if(type==='notify')desktopNotify(data);else if(type==='schedules'){STATE.schedules=data;renderScheduleList()}}
467
+ // Görev bitince/başarısız olunca işletim sistemi bildirimi göster (panel arka planda olsa da görünür).
468
+ // İzin bir kullanıcı hareketinde istenir (ensureNotifyPermission). Webhook'tan bağımsızdır ve yapılandırma gerektirmez.
469
+ function ensureNotifyPermission(){try{if('Notification'in window&&Notification.permission==='default')Notification.requestPermission()}catch{}}
470
+ function desktopNotify(o){try{if(!('Notification'in window)||Notification.permission!=='granted'||replaying)return;const done=o.status!=='failed';const title=done?'✅ CrewCtl — Görev tamamlandı':'❌ CrewCtl — Görev başarısız';const extra=done?(o.verification?`\n✓ ${String(o.verification).slice(0,140)}`:''):(o.error?`\n${String(o.error).slice(0,180)}`:'');const n=new Notification(title,{body:String(o.prompt||o.id||'').slice(0,180)+extra,tag:String(o.id||'')});n.onclick=()=>{try{window.focus()}catch{}n.close()}}catch{}}
452
471
  function receiveDashboardEvent(type,event){const data=JSON.parse(event.data);if(bootstrapping)bufferedEvents.push({type,data});else dispatchDashboardEvent(type,data)}
453
- function connectDashboardEvents(){es=new EventSource('/api/events');for(const type of ['status','queue','activity','message','log','result','schedules'])es.addEventListener(type,event=>receiveDashboardEvent(type,event))}
472
+ function connectDashboardEvents(){es=new EventSource('/api/events');for(const type of ['status','queue','activity','message','log','result','notify','schedules'])es.addEventListener(type,event=>receiveDashboardEvent(type,event))}
454
473
  async function bootstrapDashboard(){connectDashboardEvents();let replayed=[];try{const s=await api('/api/state');STATE=s;applyStatus(s.status);renderQueue(s.queue);$('#targetDir').placeholder=s.workingDirAbs||'';refreshOperatorSelect();csTypeChanged();renderScheduleList();if(!s.config.autonomousConsentAcceptedAt)showConsent();try{replayed=await restoreDashboardRun()}catch(error){showToast('err','Görev akışı geri yüklenemedi',error.message)}}catch(error){alert(error.message)}finally{bootstrapping=false;const seen=new Set(replayed.filter(e=>e.type==='activity'||e.type==='message'||e.type==='log').map(e=>JSON.stringify(e))),pending=bufferedEvents;bufferedEvents=[];for(const event of pending){if(seen.has(JSON.stringify(event.data)))continue;dispatchDashboardEvent(event.type,event.data)}}}
455
474
  // Sayfa (ör. Ekip Akışı'na geçiş) bfcache'e girerken açık SSE bağlantısını kapat — aksi halde
456
475
  // donmuş bağlantılar birikip tarayıcının host başına bağlantı limitini doldurur ve sonraki
@@ -530,9 +549,9 @@ async function refreshSchedules(){try{const r=await api('/api/schedules');STATE.
530
549
  async function addScheduleFromComposer(){const body=composerScheduleBody();if(!body.prompt){showToast('err','Hedef gerekli','Önce yukarıya görev hedefini yazın.');return}try{const s=await api('/api/schedules','POST',body);await refreshSchedules();renderScheduleList();$('#prompt').value='';setComposerMode('now');showToast('ok','Görev zamanlandı',`Sonraki çalışma: ${scheduleNextText(s)}`)}catch(e){showToast('err','Zamanlanamadı',e.message)}}
531
550
  async function toggleSchedule(id,enabled){try{await api('/api/schedules/'+encodeURIComponent(id),'PUT',{enabled});await refreshSchedules();renderScheduleList()}catch(e){showToast('err','Güncellenemedi',e.message);renderScheduleList()}}
532
551
  async function deleteSchedule(id){if(!await appConfirm({title:'Zamanlama silinsin mi?',body:'Bu zamanlanmış görev kaldırılacak; gelecekte otomatik çalışmayacak.',confirmLabel:'Sil',cancelLabel:'Vazgeç',danger:true}))return;try{await api('/api/schedules/'+encodeURIComponent(id),'DELETE');await refreshSchedules();renderScheduleList();showToast('ok','Zamanlama silindi')}catch(e){showToast('err','Silinemedi',e.message)}}
533
- function renderGeneral(c){c.innerHTML=`<div class="item grid2"><div><label>Varsayılan çalışma klasörü</label><div class="pickrow"><input id="workingDir" value="${esc(STATE.config.workingDir||'..')}"><button type="button" onclick="openFs(v=>$('#workingDir').value=v,$('#workingDir').value)">📁</button></div></div><div><label>Onay modu</label><select id="approvalMode"><option ${STATE.config.approvalMode==='ask'?'selected':''}>ask</option><option ${STATE.config.approvalMode==='auto'?'selected':''}>auto</option></select></div><div><label>Günlük çağrı bütçesi</label><input id="dailyBudget" type="number" value="${STATE.config.dailyCallBudget||150}"></div><div><label>Agent zaman aşımı</label><input id="agentTimeout" type="number" value="${STATE.config.agentTimeoutSeconds||900}"></div><div><label>Hafıza karakter bütçesi</label><input id="memoryBudget" type="number" value="${STATE.config.memoryCharBudget||8000}"></div><div><label>Takım bağlam bütçesi</label><input id="contextBudget" type="number" value="${STATE.config.teamContextCharBudget||30000}"></div></div><details style="margin-top:10px"><summary>Gelişmiş: ham JSON yapılandırması</summary><p class="muted">Doğrudan JSON düzenleyip “Ham JSON'u uygula”ya basın, ardından Kaydet ile kalıcılaştırın.</p><textarea id="rawConfig" rows="16">${esc(JSON.stringify(STATE.config,null,2))}</textarea><button class="sm" style="margin-top:6px" onclick="applyRaw()">Ham JSON'u uygula</button></details>`}
552
+ function renderGeneral(c){c.innerHTML=`<div class="item grid2"><div><label>Varsayılan çalışma klasörü</label><div class="pickrow"><input id="workingDir" value="${esc(STATE.config.workingDir||'..')}"><button type="button" onclick="openFs(v=>$('#workingDir').value=v,$('#workingDir').value)">📁</button></div></div><div><label>Onay modu</label><select id="approvalMode"><option ${STATE.config.approvalMode==='ask'?'selected':''}>ask</option><option ${STATE.config.approvalMode==='auto'?'selected':''}>auto</option></select></div><div><label>Günlük çağrı bütçesi</label><input id="dailyBudget" type="number" value="${STATE.config.dailyCallBudget||150}"></div><div><label>Agent zaman aşımı</label><input id="agentTimeout" type="number" value="${STATE.config.agentTimeoutSeconds||900}"></div><div><label>Hafıza karakter bütçesi</label><input id="memoryBudget" type="number" value="${STATE.config.memoryCharBudget||8000}"></div><div><label>Takım bağlam bütçesi</label><input id="contextBudget" type="number" value="${STATE.config.teamContextCharBudget||30000}"></div><div style="grid-column:1/-1"><label>Bildirim webhook URL (opsiyonel · Slack uyumlu)</label><input id="notifyWebhook" value="${esc(STATE.config.notify&&STATE.config.notify.webhookUrl||'')}" placeholder="https://hooks.slack.com/services/..."><div class="muted">Her görev bitince/başarısız olunca bu adrese bildirim POST edilir. Boş = kapalı.</div></div></div><details style="margin-top:10px"><summary>Gelişmiş: ham JSON yapılandırması</summary><p class="muted">Doğrudan JSON düzenleyip “Ham JSON'u uygula”ya basın, ardından Kaydet ile kalıcılaştırın.</p><textarea id="rawConfig" rows="16">${esc(JSON.stringify(STATE.config,null,2))}</textarea><button class="sm" style="margin-top:6px" onclick="applyRaw()">Ham JSON'u uygula</button></details>`}
534
553
  function applyRaw(){try{STATE.config=JSON.parse($('#rawConfig').value);draftAgents=Object.entries(STATE.config.agents||{}).map(([name,a])=>({name,...JSON.parse(JSON.stringify(a))}));draftCliSettings=cloneCliSettings(STATE.config.cliSettings);normalizeDraftAgentModels();notify('Ham JSON belleğe alındı; Kaydet ile kalıcılaştırın.');renderGeneral($('#settingsContent'))}catch(e){notify('Geçersiz JSON: '+e.message,true)}}
535
- async function saveSettings(){try{normalizeDraftAgentAdapters();const agents={};for(const a of draftAgents){const {name,...rest}=a;if(name.trim())agents[name.trim()]={...rest,args:rest.args||[]}}STATE.config.agents=agents;STATE.config.cliSettings=cloneCliSettings(draftCliSettings);STATE.config.discoveryIgnoredAdapters=[...draftIgnoredAdapters];if($('#defaultOperator'))STATE.config.operator={...(STATE.config.operator||{}),cli:$('#defaultOperator').value,roleFile:'roles/operator.md',maxRounds:+$('#maxRounds').value,maxDelegationsPerRound:+$('#maxDelegations').value,protocolRetries:+$('#protocolRetries').value};STATE.config.operator??={};STATE.config.operator.roleFile='roles/operator.md';delete STATE.config.operator.agent;delete STATE.config.operator.model;delete STATE.config.operator.codexSettings;if(!STATE.config.operator.cli)STATE.config.operator.cli=(installedClis()[0]||'codex');if($('#workingDir'))Object.assign(STATE.config,{workingDir:$('#workingDir').value,approvalMode:$('#approvalMode').value,dailyCallBudget:+$('#dailyBudget').value,agentTimeoutSeconds:+$('#agentTimeout').value,memoryCharBudget:+$('#memoryBudget').value,teamContextCharBudget:+$('#contextBudget').value});await api('/api/config','PUT',STATE.config);notify('Kaydedildi; sonraki görevde geçerli.');refreshOperatorSelect(true)}catch(e){notify(e.message,true)}}
554
+ async function saveSettings(){try{normalizeDraftAgentAdapters();const agents={};for(const a of draftAgents){const {name,...rest}=a;if(name.trim())agents[name.trim()]={...rest,args:rest.args||[]}}STATE.config.agents=agents;STATE.config.cliSettings=cloneCliSettings(draftCliSettings);STATE.config.discoveryIgnoredAdapters=[...draftIgnoredAdapters];if($('#defaultOperator'))STATE.config.operator={...(STATE.config.operator||{}),cli:$('#defaultOperator').value,roleFile:'roles/operator.md',maxRounds:+$('#maxRounds').value,maxDelegationsPerRound:+$('#maxDelegations').value,protocolRetries:+$('#protocolRetries').value};STATE.config.operator??={};STATE.config.operator.roleFile='roles/operator.md';delete STATE.config.operator.agent;delete STATE.config.operator.model;delete STATE.config.operator.codexSettings;if(!STATE.config.operator.cli)STATE.config.operator.cli=(installedClis()[0]||'codex');if($('#workingDir'))Object.assign(STATE.config,{workingDir:$('#workingDir').value,approvalMode:$('#approvalMode').value,dailyCallBudget:+$('#dailyBudget').value,agentTimeoutSeconds:+$('#agentTimeout').value,memoryCharBudget:+$('#memoryBudget').value,teamContextCharBudget:+$('#contextBudget').value,notify:{...(STATE.config.notify||{}),webhookUrl:(($('#notifyWebhook')&&$('#notifyWebhook').value)||'').trim()}});await api('/api/config','PUT',STATE.config);notify('Kaydedildi; sonraki görevde geçerli.');refreshOperatorSelect(true)}catch(e){notify(e.message,true)}}
536
555
 
537
556
  bootstrapDashboard();
538
557
  </script>
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "CrewCtl",
3
+ "short_name": "CrewCtl",
4
+ "description": "Operatör liderliğindeki çok-agent CLI orkestratörü.",
5
+ "icons": [
6
+ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
7
+ { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
8
+ ],
9
+ "theme_color": "#111826",
10
+ "background_color": "#090c12",
11
+ "display": "standalone"
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omerrgocmen/crewctl",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "CrewCtl — Zero-dependency Node.js orchestrator that runs installed CLI coding agents as one operator-led team.",
5
5
  "keywords": [
6
6
  "ai",