@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 +2 -0
- package/orchestrator/README.md +35 -33
- package/orchestrator/config.default.json +7 -1
- package/orchestrator/roles/executor.md +26 -18
- package/orchestrator/roles/operator.md +4 -2
- package/orchestrator/roles/planner.md +5 -0
- package/orchestrator/roles/reviewer.md +9 -0
- package/orchestrator/src/cli-registry.js +4 -1
- package/orchestrator/src/engine.js +55 -2
- package/orchestrator/src/server.js +34 -1
- package/orchestrator/src/store.js +21 -1
- package/orchestrator/web/android-chrome-192x192.png +0 -0
- package/orchestrator/web/android-chrome-512x512.png +0 -0
- package/orchestrator/web/apple-touch-icon.png +0 -0
- package/orchestrator/web/board.html +5 -0
- package/orchestrator/web/code.html +5 -0
- package/orchestrator/web/favicon-16x16.png +0 -0
- package/orchestrator/web/favicon-32x32.png +0 -0
- package/orchestrator/web/favicon.ico +0 -0
- package/orchestrator/web/flow.html +5 -0
- package/orchestrator/web/index.html +37 -18
- package/orchestrator/web/site.webmanifest +12 -0
- package/package.json +1 -1
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ü.**
|
package/orchestrator/README.md
CHANGED
|
@@ -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
|
-
[](https://github.com/omergocmen/CrewCtl/stargazers)
|
|
8
|
-
[](https://github.com/omergocmen/CrewCtl/network/members)
|
|
9
|
-
[](https://github.com/omergocmen/CrewCtl/issues)
|
|
10
|
-
[](https://github.com/omergocmen/CrewCtl/commits)
|
|
9
|
+
[](https://github.com/omergocmen/CrewCtl/stargazers)
|
|
10
|
+
[](https://github.com/omergocmen/CrewCtl/network/members)
|
|
11
|
+
[](https://github.com/omergocmen/CrewCtl/issues)
|
|
12
|
+
[](https://github.com/omergocmen/CrewCtl/commits)
|
|
11
13
|

|
|
12
14
|

|
|
13
15
|
[](LICENSE)
|
|
14
16
|

|
|
15
|
-
[](https://github.com/omergocmen/CrewCtl/pulls)
|
|
17
|
+
[](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.
|
|
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
|
|
6
|
-
en
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
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 iş 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
|
-
|
|
23
|
-
-
|
|
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.
|
|
34
|
-
|
|
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
|
-
|
|
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 &&
|
|
421
|
+
: (!hasExplicitSkills && autoMatchOn
|
|
421
422
|
? skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind)
|
|
422
423
|
: []);
|
|
423
|
-
|
|
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
|
-
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|
|
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>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
|
|
Binary file
|
|
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] .
|
|
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 (
|
|
89
|
-
.feed{max-height:
|
|
90
|
-
.
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
.
|
|
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'
|
|
429
|
-
function
|
|
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
|
+
}
|