@omerrgocmen/crewctl 1.0.0 → 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 +21 -10
- package/orchestrator/README.md +27 -17
- 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 +42 -5
- package/orchestrator/src/engine.js +59 -2
- package/orchestrator/src/server.js +66 -6
- package/orchestrator/src/store.js +36 -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 +4 -4
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
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ü.**
|
|
6
8
|
|
|
7
9
|
_A local, self-hosted, zero-dependency multi-agent AI orchestrator with an operator-led team and a live web command center._
|
|
8
10
|
|
|
9
|
-
[](https://github.com/omergocmen/CrewCtl/stargazers)
|
|
12
|
+
[](https://github.com/omergocmen/CrewCtl/network/members)
|
|
13
|
+
[](https://github.com/omergocmen/CrewCtl/issues)
|
|
14
|
+
[](https://github.com/omergocmen/CrewCtl/commits)
|
|
13
15
|

|
|
14
16
|

|
|
15
17
|
[](orchestrator/LICENSE)
|
|
@@ -118,6 +120,10 @@ En hızlı yol — **tek komut, kurulum gerektirmez:**
|
|
|
118
120
|
npx @omerrgocmen/crewctl
|
|
119
121
|
~~~
|
|
120
122
|
|
|
123
|
+
> **Kaynak depo notu:** Bu komutu CrewCtl kaynak deposunun kendi kökünde değil, yönetmek
|
|
124
|
+
> istediğiniz proje klasöründe çalıştırın. Klonlanmış CrewCtl deposunda npm aynı isimli
|
|
125
|
+
> yerel manifesti önceliklendirebilir; geliştirme kopyasını başlatmak için `npm start` kullanın.
|
|
126
|
+
|
|
121
127
|
Ya da global kurun:
|
|
122
128
|
|
|
123
129
|
~~~bash
|
|
@@ -129,18 +135,23 @@ Arayüz varsayılan olarak [http://localhost:4317](http://localhost:4317) adresi
|
|
|
129
135
|
|
|
130
136
|
> **Veri konumu:** config, kuyruk ve görev geçmişi kullanıcı klasörünüzdeki <code>~/.crewctl</code> altında tutulur (ortam değişkeni <code>CREWCTL_HOME</code> ile değiştirilebilir). Çalışma klasörü varsayılan olarak komutu çalıştırdığınız dizindir; panelden değiştirilebilir. Makineye özel <code>config.json</code> ilk çalıştırmada otomatik üretilir.
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
### Developer modu / uzun yol (Git clone)
|
|
139
|
+
|
|
140
|
+
Kaynak kodu geliştirmek, testleri çalıştırmak veya npm paketi yerine doğrudan Git kopyasını
|
|
141
|
+
kullanmak için:
|
|
134
142
|
|
|
135
143
|
~~~bash
|
|
136
|
-
git clone https://github.com/omergocmen/
|
|
137
|
-
cd
|
|
144
|
+
git clone https://github.com/omergocmen/CrewCtl.git
|
|
145
|
+
cd CrewCtl
|
|
146
|
+
npm install
|
|
138
147
|
npm run doctor
|
|
148
|
+
npm test
|
|
139
149
|
npm start
|
|
140
150
|
~~~
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
`npm install` runtime bağımlılığı indirmez; lock/yerel npm ortamını hazırlar. Kaynaktan
|
|
153
|
+
çalıştırıldığında geliştirme verileri <code>orchestrator/</code> klasöründe tutulur. Sunucu
|
|
154
|
+
çalışırken panel [http://localhost:4317](http://localhost:4317) adresindedir.
|
|
144
155
|
|
|
145
156
|
### CLI kullanımı
|
|
146
157
|
|
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)
|
|
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/
|
|
17
|
+
[](https://github.com/omergocmen/CrewCtl/pulls)
|
|
16
18
|
|
|
17
|
-
🔗 **Repo:** [github.com/omergocmen/
|
|
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
|
|
@@ -83,13 +85,17 @@ ayrı ayrı kullanmak yerine **tek bir yapay zeka geliştirici takımı** gibi k
|
|
|
83
85
|
**Tek komut — kurulum gerektirmez:**
|
|
84
86
|
|
|
85
87
|
```bash
|
|
86
|
-
npx @omerrgocmen/crewctl # paneli anında başlatır (komutsuz = start)
|
|
88
|
+
npx @omerrgocmen/crewctl # paneli anında başlatır (komutsuz = start)
|
|
87
89
|
```
|
|
88
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
|
+
|
|
89
95
|
Ya da global kurun:
|
|
90
96
|
|
|
91
97
|
```bash
|
|
92
|
-
npm install -g @omerrgocmen/crewctl
|
|
98
|
+
npm install -g @omerrgocmen/crewctl
|
|
93
99
|
crewctl # panel
|
|
94
100
|
crewctl status
|
|
95
101
|
crewctl task "Testleri düzelt" --dir . --mode balanced
|
|
@@ -100,19 +106,23 @@ crewctl doctor # salt-okunur ortam kontrolü
|
|
|
100
106
|
> (`CREWCTL_HOME` ile değiştirilebilir). Çalışma klasörü varsayılan olarak komutu çalıştırdığınız
|
|
101
107
|
> dizindir; panelden değiştirilebilir.
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
|
|
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:
|
|
105
113
|
|
|
106
114
|
```bash
|
|
107
|
-
git clone https://github.com/omergocmen/
|
|
108
|
-
cd
|
|
109
|
-
npm
|
|
110
|
-
npm
|
|
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
|
|
111
121
|
```
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
`
|
|
115
|
-
|
|
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.
|
|
116
126
|
|
|
117
127
|
`crewctl doctor` ayarları değiştirmez. Yalnızca keşif sonucunu `config.json` dosyasına uygulamak
|
|
118
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}"],
|
|
@@ -59,6 +62,34 @@ const DEFINITIONS = {
|
|
|
59
62
|
const RESOLVED = new Map();
|
|
60
63
|
let OPEN_CODE_MODELS = [];
|
|
61
64
|
|
|
65
|
+
// En fazla bir CLI'yi (or. codex) ayni anda hem canli saglik/model probe'u hem de motorun
|
|
66
|
+
// operatör kosumu calistirirsa, codex gibi bazi CLI'lar ayni is dizininde ikinci exec oturumunu
|
|
67
|
+
// SIGTERM ile keser. Probe'lar "en iyi caba"dir; motor onceliklidir. Aktif probe cocuklarini
|
|
68
|
+
// izleyip motor bir goreve baslarken hepsini sonlandiriyoruz.
|
|
69
|
+
const ACTIVE_PROBES = new Set();
|
|
70
|
+
function registerProbe(child) {
|
|
71
|
+
if (!child) return () => {};
|
|
72
|
+
ACTIVE_PROBES.add(child);
|
|
73
|
+
return () => ACTIVE_PROBES.delete(child);
|
|
74
|
+
}
|
|
75
|
+
// Uctan uca proses agacini oldur (codex kabuk altinda alt proses baslatabilir).
|
|
76
|
+
function terminateProbeTree(child) {
|
|
77
|
+
if (!child) return;
|
|
78
|
+
try {
|
|
79
|
+
if (isWin && child.pid) spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
|
|
80
|
+
else child.kill("SIGKILL");
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
// Motor bir goreve baslarken cagirir: uctaki tum canli probe'lari sonlandirir ki operatörun
|
|
84
|
+
// CLI'si (codex vb.) temiz bir alanda calissin. Probe'lar sonuc olarak "timeout/failed" doner;
|
|
85
|
+
// bu zararsizdir, motor bosaldiginda yeniden denenir.
|
|
86
|
+
function abortActiveProbes() {
|
|
87
|
+
const count = ACTIVE_PROBES.size;
|
|
88
|
+
for (const child of ACTIVE_PROBES) terminateProbeTree(child);
|
|
89
|
+
ACTIVE_PROBES.clear();
|
|
90
|
+
return count;
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
function selectOpenCodeModel(models) {
|
|
63
94
|
const list = Array.isArray(models) ? models.map(String).filter(Boolean) : [];
|
|
64
95
|
const priorities = [
|
|
@@ -366,10 +397,12 @@ function testInstalledCli(id, options = {}) {
|
|
|
366
397
|
prepared.cleanup();
|
|
367
398
|
return resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(error.message) });
|
|
368
399
|
}
|
|
400
|
+
const unregister = registerProbe(child);
|
|
369
401
|
const timer = setTimeout(() => {
|
|
370
402
|
if (settled) return;
|
|
371
403
|
settled = true;
|
|
372
|
-
|
|
404
|
+
unregister();
|
|
405
|
+
terminateProbeTree(child);
|
|
373
406
|
prepared.cleanup();
|
|
374
407
|
resolve({ id, installed: true, version: found.version, health: { status: "timeout", label: "Yanıt zaman aşımına uğradı", detail: `Sağlık testi ${Math.round(timeoutMs / 1000)} saniyede tamamlanmadı.` } });
|
|
375
408
|
}, timeoutMs);
|
|
@@ -381,6 +414,7 @@ function testInstalledCli(id, options = {}) {
|
|
|
381
414
|
if (settled) return;
|
|
382
415
|
settled = true;
|
|
383
416
|
clearTimeout(timer);
|
|
417
|
+
unregister();
|
|
384
418
|
prepared.cleanup();
|
|
385
419
|
const failure = classifyHealthFailure(error.message);
|
|
386
420
|
resolve({ id, installed: true, version: found.version, health: failure });
|
|
@@ -389,6 +423,7 @@ function testInstalledCli(id, options = {}) {
|
|
|
389
423
|
if (settled) return;
|
|
390
424
|
settled = true;
|
|
391
425
|
clearTimeout(timer);
|
|
426
|
+
unregister();
|
|
392
427
|
prepared.cleanup();
|
|
393
428
|
const raw = `${stdout}\n${stderr}`.trim();
|
|
394
429
|
if (code === 0 && /HEALTH_OK/i.test(raw)) resolve({ id, installed: true, version: found.version, health: { status: "ready", label: "Hazır", detail: "Gerçek sağlık testi başarılı." } });
|
|
@@ -409,17 +444,19 @@ function listCodexModels(options = {}) {
|
|
|
409
444
|
const timeoutMs = Math.max(5000, Number(options.timeoutMs || 20000));
|
|
410
445
|
return new Promise((resolve, reject) => {
|
|
411
446
|
let buffer = "", settled = false, modelRequestSent = false;
|
|
412
|
-
let child;
|
|
447
|
+
let child, unregister = () => {};
|
|
413
448
|
const finish = (error, value) => {
|
|
414
449
|
if (settled) return;
|
|
415
450
|
settled = true;
|
|
416
451
|
clearTimeout(timer);
|
|
417
|
-
|
|
452
|
+
unregister();
|
|
453
|
+
terminateProbeTree(child);
|
|
418
454
|
if (error) reject(error); else resolve(value);
|
|
419
455
|
};
|
|
420
456
|
const timer = setTimeout(() => finish(new Error(`Codex model listesi ${Math.round(timeoutMs / 1000)} saniyede alınamadı.`)), timeoutMs);
|
|
421
457
|
try { child = spawn(command.file, command.args, { shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim }); }
|
|
422
458
|
catch (error) { finish(error); return; }
|
|
459
|
+
unregister = registerProbe(child);
|
|
423
460
|
child.stdout?.setEncoding("utf8");
|
|
424
461
|
child.stderr?.setEncoding("utf8");
|
|
425
462
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -576,4 +613,4 @@ function ensureValidOperator(cfg, discovered) {
|
|
|
576
613
|
return changed;
|
|
577
614
|
}
|
|
578
615
|
|
|
579
|
-
module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, healthCheckAll, listCodexModels, selectOpenCodeModel, parseOpenCodeModels, addMissingAgents, ensureValidOperator };
|
|
616
|
+
module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, healthCheckAll, listCodexModels, abortActiveProbes, selectOpenCodeModel, parseOpenCodeModels, addMissingAgents, ensureValidOperator };
|
|
@@ -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 {
|
|
@@ -757,6 +768,10 @@ class Engine extends EventEmitter {
|
|
|
757
768
|
this.emit("status", this.status());
|
|
758
769
|
this.publish("activity", { ...base, kind: "process.started", cmd: agent.cmd, args: rawArgs, cwd });
|
|
759
770
|
|
|
771
|
+
// Motor onceliklidir: canli CLI saglik/model probe'lari (or. codex app-server / codex exec)
|
|
772
|
+
// ayni CLI'yi ayni anda calistirirsa codex ikinci exec oturumunu SIGTERM ile keser. Bu yuzden
|
|
773
|
+
// operatör/agent kosumuna girmeden uctaki tum probe'lari sonlandiriyoruz.
|
|
774
|
+
cliRegistry.abortActiveProbes();
|
|
760
775
|
let child;
|
|
761
776
|
try { child = spawn(file, args, { cwd, env: cliRegistry.agentEnvironment(agent), windowsHide: true, shell: command.shell, windowsVerbatimArguments: !!command.verbatim }); }
|
|
762
777
|
catch (error) { return reject(error); }
|
|
@@ -846,6 +861,15 @@ class Engine extends EventEmitter {
|
|
|
846
861
|
});
|
|
847
862
|
}
|
|
848
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
|
+
|
|
849
873
|
agentCatalog(cfg, operatorName) {
|
|
850
874
|
return Object.entries(cfg.agents)
|
|
851
875
|
.filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.unhealthyAgents.has(name))
|
|
@@ -1156,6 +1180,22 @@ class Engine extends EventEmitter {
|
|
|
1156
1180
|
recoveryRounds++;
|
|
1157
1181
|
this.publish("log", { level: "warn", msg: `CLI altyapi hatasi tur butcesinden dusulmedi; ${maxRecoveryRounds - recoveryRounds} ek kurtarma turu kaldi.` }, task.id);
|
|
1158
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
|
+
}
|
|
1159
1199
|
if (task.executionMode === "fast" && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")) {
|
|
1160
1200
|
const reports = assignments.map((assignment) => state.results[assignment.id].result).join("\n\n");
|
|
1161
1201
|
this.publish("log", { level: "info", msg: "FAST mod: uzman teslimati basarili; ikinci operator degerlendirme cagrisi atlandi." }, task.id);
|
|
@@ -1351,9 +1391,26 @@ class Engine extends EventEmitter {
|
|
|
1351
1391
|
store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
|
|
1352
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);
|
|
1353
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 });
|
|
1354
1395
|
this.emit("queue");
|
|
1355
1396
|
this._textBefore = null;
|
|
1356
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
|
+
}
|
|
1357
1414
|
}
|
|
1358
1415
|
|
|
1359
1416
|
module.exports = new Engine();
|
|
@@ -73,17 +73,55 @@ 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 || []);
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
// Pano/kuyruk listesi yalnizca ozet alanlari gosterir; teamState (tum agent ciktilari/mesajlari),
|
|
110
|
+
// conversation ve changes gibi agir ic alanlar liste yukunu gereksiz sisirir ve her SSE queue
|
|
111
|
+
// yayininda serilestirilir. Detay gorunumleri tam gorevi ayri /api/tasks/:id ile ceker; burada
|
|
112
|
+
// bu agir alanlari cikartip yayin boyutunu kuculturuz. (planPreview/delivery/summary korunur.)
|
|
113
|
+
function liteTask(task) {
|
|
114
|
+
const { teamState, conversation, changes, ...rest } = task;
|
|
115
|
+
return rest;
|
|
116
|
+
}
|
|
81
117
|
function snapshot() {
|
|
82
118
|
return {
|
|
83
|
-
pending: store.listTasks("pending"),
|
|
84
|
-
approval: store.listTasks("approval"),
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
pending: store.listTasks("pending").map(liteTask),
|
|
120
|
+
approval: store.listTasks("approval").map(liteTask),
|
|
121
|
+
// Operator-chat gorevleri de done'a yazildigi icin filtreden sonra 30 gercek gorev kalsin
|
|
122
|
+
// diye biraz genis bir pencere (80) okunur; yine de sabit ve gecmisten bagimsizdir.
|
|
123
|
+
done: store.listRecentTasks("done", 80).filter((task) => task.kind !== "operator-chat").slice(-30).reverse().map(liteTask),
|
|
124
|
+
failed: store.listRecentTasks("failed", 30).reverse().map(liteTask),
|
|
87
125
|
};
|
|
88
126
|
}
|
|
89
127
|
|
|
@@ -122,8 +160,15 @@ function applyHealthToConfig(results) {
|
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
let healthRunning = false;
|
|
163
|
+
// Motor bir gorev calistirirken (or. operatör codex) ayni CLI'ye karsi canli probe acmak
|
|
164
|
+
// codex'in ikinci exec/app-server oturumunu SIGTERM ile kesmesine yol acar. Probe'lar en iyi
|
|
165
|
+
// cabadir; motor mesgulken canli probe atlanip son bilinen sonuc dondurulur.
|
|
166
|
+
function engineBusy() {
|
|
167
|
+
try { return Boolean(engine.status().current); } catch { return false; }
|
|
168
|
+
}
|
|
125
169
|
async function getCodexModels(force = false) {
|
|
126
170
|
if (!force && codexModelCache.models.length && Date.now() - codexModelCache.checkedAt < CODEX_MODEL_CACHE_TTL_MS) return codexModelCache.models;
|
|
171
|
+
if (engineBusy()) return codexModelCache.models;
|
|
127
172
|
const models = await cliRegistry.listCodexModels({ timeoutMs: 20000 });
|
|
128
173
|
codexModelCache = { checkedAt: Date.now(), models };
|
|
129
174
|
const cfg = store.loadConfig();
|
|
@@ -139,6 +184,10 @@ async function refreshCliHealth(force = false) {
|
|
|
139
184
|
broadcast("cli-health", cliStatus);
|
|
140
185
|
return cliStatus;
|
|
141
186
|
}
|
|
187
|
+
if (engineBusy()) {
|
|
188
|
+
broadcast("cli-health", cliStatus);
|
|
189
|
+
return cliStatus;
|
|
190
|
+
}
|
|
142
191
|
healthRunning = true;
|
|
143
192
|
cliStatus = cliStatus.map((cli) => ({ ...cli, health: { status: "testing", label: "Test ediliyor", detail: "Gerçek CLI sağlık testi çalışıyor." } }));
|
|
144
193
|
broadcast("cli-health", cliStatus);
|
|
@@ -170,7 +219,7 @@ function readBody(req) {
|
|
|
170
219
|
});
|
|
171
220
|
});
|
|
172
221
|
}
|
|
173
|
-
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" };
|
|
174
223
|
function serveStatic(res, file) {
|
|
175
224
|
const p = path.resolve(WEB, file);
|
|
176
225
|
if ((p !== WEB && !p.startsWith(WEB + path.sep)) || !fs.existsSync(p)) {
|
|
@@ -563,6 +612,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
563
612
|
if (!name.trim() || !agent.cmd || typeof agent.cmd !== "string") return send(res, 400, { error: `gecersiz agent: ${name}` });
|
|
564
613
|
if (!Array.isArray(agent.args)) return send(res, 400, { error: `${name}.args dizi olmali` });
|
|
565
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
|
+
}
|
|
566
620
|
store.saveConfig(cfg);
|
|
567
621
|
broadcast("status", engine.status());
|
|
568
622
|
return send(res, 200, { ok: true });
|
|
@@ -642,7 +696,13 @@ function startupBanner() {
|
|
|
642
696
|
console.log(` ${DIM}Ayrıntı için: npm run doctor${X}`);
|
|
643
697
|
}
|
|
644
698
|
console.log(`\n ${B}▶ Panel: ${C}${url}${X}`);
|
|
645
|
-
console.log(` ${DIM}Panelde 'Başlat'a basıp bir görev gönderin. (tarayıcı otomatik açılmazsa yukarıdaki adresi açın)${X}
|
|
699
|
+
console.log(` ${DIM}Panelde 'Başlat'a basıp bir görev gönderin. (tarayıcı otomatik açılmazsa yukarıdaki adresi açın)${X}`);
|
|
700
|
+
// npx gecici onbellekten calisiyorsa kalici 'crewctl' komutu kurulmamistir; kullaniciya nasil
|
|
701
|
+
// kalici hale getirecegini hatirlat (aksi halde kapatinca 'crewctl' bulunamaz).
|
|
702
|
+
if (/[\\/]_npx[\\/]/.test(__dirname)) {
|
|
703
|
+
console.log(` ${DIM}Kalıcı 'crewctl' komutu için: ${Y}npm i -g @omerrgocmen/crewctl${X}`);
|
|
704
|
+
}
|
|
705
|
+
console.log("");
|
|
646
706
|
openBrowser(url);
|
|
647
707
|
}
|
|
648
708
|
|
|
@@ -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
|
}
|
|
@@ -209,6 +229,20 @@ function listTasks(state) {
|
|
|
209
229
|
.filter(Boolean)
|
|
210
230
|
.sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
211
231
|
}
|
|
232
|
+
// Yalnizca en yeni `limit` gorevi okur. Gorev id'si zaman damgasiyla basladigi icin dosya adi
|
|
233
|
+
// siralamasi = kronolojik siralama; boylece done/failed gecmisi buyudukce (yuzlerce/binlerce
|
|
234
|
+
// dosya) her pano yenilemesinde TUM dosyalari okuyup ayristirma maliyeti sabit N'e iner.
|
|
235
|
+
function listRecentTasks(state, limit) {
|
|
236
|
+
const dir = path.join(Q, state);
|
|
237
|
+
if (!fs.existsSync(dir)) return [];
|
|
238
|
+
let names = fs.readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
|
|
239
|
+
if (limit && names.length > limit) names = names.slice(-limit);
|
|
240
|
+
return names
|
|
241
|
+
.map((f) => {
|
|
242
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); } catch { return null; }
|
|
243
|
+
})
|
|
244
|
+
.filter(Boolean);
|
|
245
|
+
}
|
|
212
246
|
function nextPending() {
|
|
213
247
|
return listTasks("pending")[0] || null;
|
|
214
248
|
}
|
|
@@ -344,6 +378,7 @@ module.exports = {
|
|
|
344
378
|
writeRole,
|
|
345
379
|
deleteRole,
|
|
346
380
|
listTasks,
|
|
381
|
+
listRecentTasks,
|
|
347
382
|
nextPending,
|
|
348
383
|
addTask,
|
|
349
384
|
addChatTask,
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omerrgocmen/crewctl",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"type": "commonjs",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/omergocmen/
|
|
20
|
+
"url": "git+https://github.com/omergocmen/CrewCtl.git"
|
|
21
21
|
},
|
|
22
|
-
"homepage": "https://github.com/omergocmen/
|
|
22
|
+
"homepage": "https://github.com/omergocmen/CrewCtl#readme",
|
|
23
23
|
"bugs": {
|
|
24
|
-
"url": "https://github.com/omergocmen/
|
|
24
|
+
"url": "https://github.com/omergocmen/CrewCtl/issues"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"orchestrator/src/",
|