@omerrgocmen/crewctl 1.1.1 → 1.3.1

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.
@@ -9,11 +9,49 @@ const checkpoints = require("./checkpoints");
9
9
 
10
10
  const isWin = process.platform === "win32";
11
11
 
12
+ // Ajan hapsi (path-aware, Claude icin). cfg.sandbox.mode === "workspace" iken orkestratorun
13
+ // kendi dizinlerini (veri koku ROOT = config/memory/state, paket ASSETS, kurulum WORK_BASE)
14
+ // Claude'un gozunden gizler: Read/Edit/Write bu yollara permissions.deny ile kapatilir.
15
+ // Docker/Git GEREKTIRMEZ. Codex ayni korumayi effectiveAgent'ta OS-native sandbox ile alir;
16
+ // gemini/opencode icin path-scoped native kural olmadigindan yalnizca prompt talimati gecerli.
17
+ // Calisma klasoru bu korunan dizinlerden birinin ICINDEYSE (or. gelistirme modunda repo'nun
18
+ // kendisinde calismak) o dizin KORUNMAZ — mevcut is bozulmasin diye.
19
+ function claudeSandboxSettings(agent, cwd, cfg) {
20
+ if (cfg?.sandbox?.mode !== "workspace") return null;
21
+ const adapter = agent?.adapter || cliRegistry.adapterId(agent?.cmd);
22
+ if (adapter !== "claude") return null;
23
+ const norm = (p) => path.resolve(p).replace(/\\/g, "/");
24
+ // Windows dosya sistemi buyuk/kucuk harfe duyarsizdir: C:\Test ile c:\test ayni dizindir.
25
+ // Karsilastirmayi case-fold'la yap; aksi halde farkli case yazilmis bir cwd, kurulum
26
+ // dizinini yanlislikla "disarida" sanip DENY eder (ajan kendi calisma agacinin ustune
27
+ // erisemez, hatta cwd==kurulum dizini iken kendi klasorune yazamaz).
28
+ const fold = (p) => (isWin ? p.toLowerCase() : p);
29
+ const workDir = norm(cwd);
30
+ const workCmp = fold(workDir);
31
+ const overlaps = (dir) => {
32
+ const d = fold(dir);
33
+ return d === workCmp || d.startsWith(workCmp + "/") || workCmp.startsWith(d + "/");
34
+ };
35
+ const protectedDirs = [store.ROOT, store.ASSETS, store.WORK_BASE]
36
+ .map(norm)
37
+ .filter((dir, i, all) => all.findIndex((other) => fold(other) === fold(dir)) === i)
38
+ .filter((dir) => !overlaps(dir));
39
+ if (!protectedDirs.length) return null;
40
+ const deny = [];
41
+ for (const dir of protectedDirs) {
42
+ for (const tool of ["Read", "Edit", "Write"]) deny.push(`${tool}(${dir}/**)`);
43
+ }
44
+ return { permissions: { deny } };
45
+ }
46
+
12
47
  function snapshotDir(dir) {
13
48
  const map = new Map();
14
49
  const ignored = (rel) => {
15
50
  const r = rel.replace(/\\/g, "/");
51
+ // .crewctl/ orkestratorun kendi proje-context'ini (CONTEXT.md) tutar; gorev diff'lerinde,
52
+ // live-diff'te ve teslimat dosya listesinde gozukmesin (kullanicinin isi degil).
16
53
  return r.includes("node_modules") || r.includes(".git/") || r.startsWith(".git") ||
54
+ r === ".crewctl" || r.startsWith(".crewctl/") ||
17
55
  r.startsWith("orchestrator/queue") || r.startsWith("orchestrator/state") ||
18
56
  r.startsWith("orchestrator/memory") || r.startsWith("orchestrator/node_modules");
19
57
  };
@@ -223,6 +261,14 @@ function clip(value, limit = 12000) {
223
261
  return text.length <= limit ? text : `${text.slice(0, limit)}\n...[kesildi]`;
224
262
  }
225
263
 
264
+ // Sohbet odakli CLI'lar profili bazen ``` ile sarar; profil dosyasina ham metni yazariz.
265
+ // Yalnizca tum icerigi saran TEK bir dis fence'i soyar; ic kod bloklarina dokunmaz.
266
+ function stripCodeFence(text) {
267
+ const trimmed = String(text || "").trim();
268
+ const match = trimmed.match(/^```[a-zA-Z0-9]*\s*\n([\s\S]*?)\n?```$/);
269
+ return match ? match[1] : trimmed;
270
+ }
271
+
226
272
  // Bas ve sonu koruyarak kirpar. Denetci raporlarinda VERDICT satiri metnin SONUNDadir;
227
273
  // yalnizca bastan kirpmak operatorun karari gormemesine ve gereksiz yeniden inceleme
228
274
  // turlarina yol acar.
@@ -257,20 +303,30 @@ function meaningfulErrorLine(raw) {
257
303
  // daraltildi ve kimlik kontrollerinin arkasina alindi.
258
304
  function classifyCliError(error) {
259
305
  const raw = String(error?.message || error || "Bilinmeyen CLI hatasi");
260
- const of = (code, summary, action) => ({ code, summary, action, raw: clip(raw, 5000) });
306
+ // Hatanin geldigi CLI biliniyorsa tavsiye ona gore uretilir (runCli her hatayi etiketler).
307
+ const adapter = error?.adapter || "";
308
+ const agentName = error?.agentName || "";
309
+ const of = (code, summary, action) => ({ code, summary, action, agent: agentName, adapter, raw: clip(raw, 5000) });
261
310
  if (/requires a newer version|upgrade to the latest|model metadata.*not found|unsupported.*model|model.*unsupported/i.test(raw)) {
262
311
  return of("VERSION_INCOMPATIBLE", "CLI sürümü seçilen modeli desteklemiyor.", "CLI aracını güncelleyin veya desteklenen bir model seçin.");
263
312
  }
264
313
  if (/sessiz kaldi|cikti uretmedi|CLI_STALLED/i.test(raw)) {
265
314
  return of("CLI_STALLED", "CLI uzun süre yeni çıktı üretmeyince otomatik durduruldu.", "Önceki ilerleme kayıtları korundu; operatör bu oturumda farklı bir agent kullanacak.");
266
315
  }
316
+ // "No provider available" 401 ile gelir ama OTURUM sorunu degildir: secili model hicbir
317
+ // saglayiciya cozulemiyordur. Genel 401/403 kuralindan ONCE gelmeli, yoksa zaten girisli
318
+ // kullaniciya bosuna "oturum acin" denir.
319
+ if (/no provider available/i.test(raw)) {
320
+ return of("PROVIDER_UNAVAILABLE", "Seçili model hiçbir yapılandırılmış sağlayıcıya çözümlenemedi (oturum sorunu değil).",
321
+ "Ayarlar → Agent'lar bölümünde bu agent için açık bir model seçin. Model boşken OpenCode kendi son kullandığı modele düşer; o model erişilemezse görev bu hatayla durur.");
322
+ }
267
323
  if (/API key not valid|API_KEY_INVALID|invalid[_ ]api[_ ]key|incorrect api key/i.test(raw)) {
268
- return of("AUTH_INVALID", "API anahtarı geçersiz veya reddedildi.", "Agent ayarlarındaki API anahtarını yenileyin (Gemini için GEMINI_API_KEY / GOOGLE_API_KEY).");
324
+ return of("AUTH_INVALID", "API anahtarı geçersiz veya reddedildi.", `Bu agent'ın API anahtarını yenileyin. ${cliRegistry.authHint(adapter)}`);
269
325
  }
270
326
  // Gemini CLI oturum acilmadiginda "Please set an Auth method" / "GEMINI_API_KEY environment
271
327
  // variable not found" yazar; her ikisi de kurulum degil OTURUM sorunudur.
272
328
  if (/set an auth method|GEMINI_API_KEY|GOOGLE_API_KEY|unauthorized|unauthenticated|authentication|login required|not logged in|please (run )?login|credentials? (not found|missing|expired)|PERMISSION_DENIED|\b40[13]\b/i.test(raw)) {
273
- return of("AUTH_REQUIRED", "CLI oturum açmamış; kimlik doğrulaması gerekiyor.", "Terminalde ilgili CLI ile oturum açın (Gemini: `gemini` çalıştırıp 'Login with Google' veya GEMINI_API_KEY tanımlayın), sonra görevi tekrar deneyin.");
329
+ return of("AUTH_REQUIRED", "CLI oturum açmamış; kimlik doğrulaması gerekiyor.", `${cliRegistry.authHint(adapter)} Sonra görevi tekrar deneyin.`);
274
330
  }
275
331
  // Kota (gunluk/aylik hak bitti) ile hiz siniri (kisa sureli) AYRI teshislerdir: ilki
276
332
  // beklemekle gecmez, ikincisi gecer. Kullanicinin sordugu "limitim mi bitti" ayrimi budur.
@@ -287,13 +343,16 @@ function classifyCliError(error) {
287
343
  return of("REGION_BLOCKED", "Model bulunduğunuz bölgede kullanıma kapalı.", "Desteklenen bir bölge/hesap kullanın veya başka bir sağlayıcının agent'ını seçin.");
288
344
  }
289
345
  if (/ConnectionRefused|Unable to connect|provider hatasi|provider error|ECONNREFUSED/i.test(raw)) {
290
- return of("PROVIDER_UNAVAILABLE", "Seçilen model sağlayıcısına bağlanılamadı.", "Sağlayıcı bağlantısını kontrol edin veya başka bir model seçin.");
346
+ return of("PROVIDER_UNAVAILABLE", "Seçilen model sağlayıcısına bağlanılamadı veya model çözümlenemedi.",
347
+ adapter === "opencode"
348
+ ? "Ayarlar → Agent'lar bölümünde bu agent için açık bir model seçin (`opencode models` listesinden). Model seçili değilken OpenCode kendi son kullandığı modele düşer ve bu model erişilemez olabilir."
349
+ : "Sağlayıcı bağlantısını kontrol edin veya başka bir model seçin.");
291
350
  }
292
351
  if (/ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|getaddrinfo|fetch failed|socket hang up|network (error|unreachable)|proxy/i.test(raw)) {
293
352
  return of("NETWORK_ERROR", "Ağ bağlantısı kurulamadı (DNS/proxy/internet).", "İnternet veya kurumsal proxy ayarlarınızı kontrol edip görevi tekrar çalıştırın.");
294
353
  }
295
354
  if (/not recognized as an internal|is not recognized|command not found|spawn .*ENOENT|\bENOENT\b|no such file or directory/i.test(raw)) {
296
- return of("CLI_NOT_FOUND", "CLI komutu bulunamadı; kurulu değil veya PATH üzerinde görünmüyor.", "CLI'yi kurun (Gemini: `npm install -g @google/gemini-cli`) ve Ayarlar → Agent'lar bölümünde komut adını doğrulayın.");
355
+ return of("CLI_NOT_FOUND", "CLI komutu bulunamadı; kurulu değil veya PATH üzerinde görünmüyor.", `${adapter && adapter !== "custom" ? `${adapter} CLI'sini kurun` : "CLI'yi kurun"} ve Ayarlar → Agent'lar bölümünde komut adını doğrulayın (kurulum komutları için Doctor ekranına bakın).`);
297
356
  }
298
357
  if (/timeout|timed out|zaman asim|zaman aşım/i.test(raw)) {
299
358
  return of("TIMEOUT", "CLI ayrılan süre içinde tamamlanamadı.", "Agent ayarlarından timeout değerini artırın veya görevi daha küçük parçalara bölün.");
@@ -305,6 +364,21 @@ const RECOVERABLE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "RATE_L
305
364
  // Karantina = bu oturumda tekrar denemek anlamsiz. Kota/bolge kisiti beklemekle gecmez;
306
365
  // hiz siniri (RATE_LIMIT) ve gecici saglayici hatasi (MODEL_OVERLOADED) gecer, karantinaya alinmaz.
307
366
  const QUARANTINE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "QUOTA_EXCEEDED", "REGION_BLOCKED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "CLI_STALLED", "VERSION_INCOMPATIBLE"]);
367
+ // Birkac saniyede kendiliginden gecer; operatore geri tasiyip planlama turu harcamak yerine
368
+ // ayni agent ustel bekleme ile yeniden denenir.
369
+ const TRANSIENT_CLI_ERRORS = new Set(["RATE_LIMIT", "MODEL_OVERLOADED", "NETWORK_ERROR"]);
370
+ // Karantina suresi (sn). 0 = beklemekle duzelmez, kalici. Digerleri sure dolunca yeniden
371
+ // denenir (half-open); tekrar duserse sure katlanir (bkz. quarantineAgent).
372
+ const QUARANTINE_COOLDOWN_SECONDS = {
373
+ AUTH_REQUIRED: 180,
374
+ AUTH_INVALID: 180,
375
+ PROVIDER_UNAVAILABLE: 180,
376
+ CLI_STALLED: 300,
377
+ QUOTA_EXCEEDED: 0,
378
+ REGION_BLOCKED: 0,
379
+ CLI_NOT_FOUND: 0,
380
+ VERSION_INCOMPATIBLE: 0,
381
+ };
308
382
 
309
383
  function resolveExecutionMode(task) {
310
384
  if (["fast", "balanced", "deep"].includes(task.executionMode)) return task.executionMode;
@@ -379,7 +453,6 @@ function normalizeCliOutput(agent, stdout) {
379
453
  const event = JSON.parse(line);
380
454
  if (event.type === "text" && event.part?.text) texts.push(String(event.part.text));
381
455
  if (event.type === "error") errors.push(String(event.error?.message || event.error || "OpenCode model/provider hatasi"));
382
- // Bir kosumda birden fazla adim olabilir; hepsi toplanir.
383
456
  if (event.type === "step_finish") {
384
457
  const step = readStepUsage(event);
385
458
  if (step) usage = addUsage(usage, step);
@@ -592,6 +665,15 @@ function compatibleAgentForKind(cfg, operatorName, kind) {
592
665
  )?.[0] || "";
593
666
  }
594
667
 
668
+ // Devretme adaylari. Karantina filtresi cagirana birakilir: sureli oldugu icin karar canli
669
+ // durumdan (isQuarantined) okunmalidir.
670
+ function compatibleAgentsForKind(cfg, operatorName, kind) {
671
+ return Object.entries(cfg.agents || {})
672
+ .filter(([name, agent]) =>
673
+ name !== operatorName && agent.enabled !== false && agentUsable(agent) && supportsKind(agent, kind))
674
+ .map(([name]) => name);
675
+ }
676
+
595
677
  function nextAssignmentId(base, usedIds) {
596
678
  let id = base;
597
679
  let suffix = 2;
@@ -672,6 +754,7 @@ class Engine extends EventEmitter {
672
754
  // Kalici config'i degistirmeyiz; kullanici kimlik bilgisini duzeltip motoru yeniden
673
755
  // baslattiginda agent tekrar denenebilir.
674
756
  this.unhealthyAgents = new Map();
757
+ this._retrySleepers = new Set();
675
758
  // Canli kodlama akisi: gorev calisirken calisma klasorunu periyodik tarayan zamanlayici
676
759
  // ve son yayinlanan degisiklik imzasi (ayni durum tekrar tekrar yayinlanmasin diye).
677
760
  this._liveTimer = null;
@@ -818,6 +901,7 @@ class Engine extends EventEmitter {
818
901
  stop() {
819
902
  this.running = false;
820
903
  this.wake();
904
+ this.cancelRetrySleeps();
821
905
  this.stopLiveDiff();
822
906
  if (this.activeChild) {
823
907
  try { this.activeChild.kill(); } catch {}
@@ -842,6 +926,58 @@ class Engine extends EventEmitter {
842
926
  });
843
927
  }
844
928
 
929
+ // sleepWake'ten AYRIDIR: o bos donguye aittir ve yeni gorev eklendiginde uyanir. Bu bekleme
930
+ // yalnizca motor durdurulunca kesilmeli.
931
+ sleepRetry(ms) {
932
+ return new Promise((resolve) => {
933
+ const sleeper = { resolve, timer: null };
934
+ sleeper.timer = setTimeout(() => { this._retrySleepers.delete(sleeper); resolve(); }, ms);
935
+ this._retrySleepers.add(sleeper);
936
+ });
937
+ }
938
+
939
+ cancelRetrySleeps() {
940
+ for (const sleeper of this._retrySleepers) {
941
+ clearTimeout(sleeper.timer);
942
+ sleeper.resolve();
943
+ }
944
+ this._retrySleepers.clear();
945
+ }
946
+
947
+ // Sureli karantina: kullanici oturum acar veya saglayici toparlanirsa agent kendiliginden
948
+ // doner. Tekrar duserse ceza katlanir (en fazla 15 dk); suresi 0 olanlar kalici kalir.
949
+ quarantineAgent(name, code, cfg) {
950
+ const strikes = (this.unhealthyAgents.get(name)?.strikes || 0) + 1;
951
+ const baseSeconds = QUARANTINE_COOLDOWN_SECONDS[code] ?? 120;
952
+ const cooldownMs = baseSeconds > 0 ? Math.min(900, baseSeconds * strikes) * 1000 : 0;
953
+ this.unhealthyAgents.set(name, {
954
+ code, strikes, at: new Date().toISOString(),
955
+ until: cooldownMs ? Date.now() + cooldownMs : 0,
956
+ });
957
+ if (cfg) cfg.runtimeUnavailableAgents = this.quarantinedAgents();
958
+ this.publish("log", {
959
+ level: "warn",
960
+ msg: cooldownMs
961
+ ? `${name} [${code}] nedeniyle ${Math.round(cooldownMs / 1000)} sn karantinada; sure dolunca otomatik yeniden denenecek.`
962
+ : `${name} [${code}] nedeniyle bu oturumda kullanim disi (beklemekle duzelmez).`,
963
+ });
964
+ }
965
+
966
+ isQuarantined(name) {
967
+ const entry = this.unhealthyAgents.get(name);
968
+ if (!entry) return false;
969
+ if (entry.until && Date.now() >= entry.until) {
970
+ this.unhealthyAgents.delete(name);
971
+ this.publish("log", { level: "info", msg: `${name} karantina suresi doldu; yeniden deneme hakki verildi.` });
972
+ return false;
973
+ }
974
+ return true;
975
+ }
976
+
977
+ quarantinedAgents() {
978
+ return [...this.unhealthyAgents.keys()].filter((name) => this.isQuarantined(name));
979
+ }
980
+
845
981
  async loop() {
846
982
  while (this.running) {
847
983
  const task = store.nextPending();
@@ -852,7 +988,7 @@ class Engine extends EventEmitter {
852
988
  try {
853
989
  await this.runTask(task);
854
990
  } catch (error) {
855
- if (!this.salvage(task, error)) {
991
+ if (!(await this.salvage(task, error))) {
856
992
  const found = store.findTask(task.id);
857
993
  const failure = classifyCliError(error);
858
994
  task.status = "failed";
@@ -883,7 +1019,6 @@ class Engine extends EventEmitter {
883
1019
  }
884
1020
  }
885
1021
 
886
- // Uzman agent'i (operatorun altindaki ekip) cfg.agents'ten cozup calistirir.
887
1022
  invokeAgent(agentName, prompt, cfg, meta = {}) {
888
1023
  const configuredAgent = cfg.agents[agentName];
889
1024
  const agent = configuredAgent && cliRegistry.effectiveAgent(configuredAgent, cfg);
@@ -900,7 +1035,15 @@ class Engine extends EventEmitter {
900
1035
  }
901
1036
 
902
1037
  runCli(displayName, agent, prompt, cfg, meta = {}) {
903
- return new Promise((resolve, reject) => {
1038
+ return new Promise((resolve, rawReject) => {
1039
+ // Her hata hangi CLI'dan geldigini tasir; tavsiye metni buna gore uretilir. Etiket yokken
1040
+ // opencode hatasinda kullaniciya gemini komutu oneriliyordu. Bkz. classifyCliError.
1041
+ const reject = (cause) => {
1042
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1043
+ if (!error.agentName) error.agentName = displayName;
1044
+ if (!error.adapter) error.adapter = agent.adapter || cliRegistry.adapterId(agent.cmd);
1045
+ rawReject(error);
1046
+ };
904
1047
  const count = store.bumpCallCount();
905
1048
  if (count > (cfg.dailyCallBudget || Number.MAX_SAFE_INTEGER)) {
906
1049
  return reject(new Error(`Gunluk cagri butcesi asildi (${cfg.dailyCallBudget}).`));
@@ -928,10 +1071,23 @@ class Engine extends EventEmitter {
928
1071
  }
929
1072
  return String(arg);
930
1073
  });
1074
+ const cwd = this._cwd || path.resolve(store.WORK_BASE, cfg.workingDir || ".");
1075
+ // Path-aware hapis: codex sandbox'i disariyi zaten OS-native kapatir (effectiveAgent).
1076
+ // Claude'un OS sandbox'i her platformda (ozellikle Windows'ta) olmadigi icin en azindan
1077
+ // orkestratorun kendi topragini (config/memory/kurulum dizini) permissions.deny ile gizler.
1078
+ // JSON'u arguman olarak gecmek Windows cmd.exe'de tirnaklari bozar; bu yuzden dosyaya yazip
1079
+ // yolunu veriyoruz (prompt dosyasiyla ayni desen).
1080
+ const sandboxSettings = claudeSandboxSettings(agent, cwd, cfg);
1081
+ if (sandboxSettings) {
1082
+ const sbDir = path.join(store.ROOT, "state", "prompts");
1083
+ fs.mkdirSync(sbDir, { recursive: true });
1084
+ const sbFile = path.join(sbDir, `${callId}.settings.json`);
1085
+ fs.writeFileSync(sbFile, JSON.stringify(sandboxSettings), "utf8");
1086
+ rawArgs.push("--settings", sbFile);
1087
+ }
931
1088
  const command = cliRegistry.buildCommand(agent.cmd, rawArgs);
932
1089
  const file = command.file;
933
1090
  const args = command.args;
934
- const cwd = this._cwd || path.resolve(store.WORK_BASE, cfg.workingDir || ".");
935
1091
  const started = Date.now();
936
1092
  let stdout = "", stderr = "", settled = false, timedOut = false, silenceTimedOut = false;
937
1093
  let timer, silenceTimer, progressTimer;
@@ -1047,12 +1203,27 @@ class Engine extends EventEmitter {
1047
1203
  hasUsableImplementAgent(cfg, operatorName) {
1048
1204
  return Object.entries(cfg.agents || {}).some(([name, agent]) =>
1049
1205
  name !== operatorName && agent.enabled !== false && agentUsable(agent)
1050
- && !this.unhealthyAgents.has(name) && supportsKind(agent, "implement"));
1206
+ && !this.isQuarantined(name) && supportsKind(agent, "implement"));
1207
+ }
1208
+
1209
+ // Sureli karantinadaki implement agentinin donusune kalan sure (ms). Geri donen yoksa ya da
1210
+ // bekleme makul siniri asiyorsa null; cagiran bunu "gorevi durdur" olarak yorumlar.
1211
+ msUntilImplementAgentReturns(cfg, operatorName, maxWaitMs = 5 * 60 * 1000) {
1212
+ const now = Date.now();
1213
+ const returns = Object.entries(cfg.agents || {})
1214
+ .filter(([name, agent]) => name !== operatorName && agent.enabled !== false
1215
+ && agentUsable(agent) && supportsKind(agent, "implement"))
1216
+ .map(([name]) => this.unhealthyAgents.get(name)?.until || 0)
1217
+ .filter((until) => until > now)
1218
+ .sort((a, b) => a - b);
1219
+ if (!returns.length) return null;
1220
+ const waitMs = returns[0] - now;
1221
+ return waitMs <= maxWaitMs ? waitMs : null;
1051
1222
  }
1052
1223
 
1053
1224
  agentCatalog(cfg, operatorName) {
1054
1225
  return Object.entries(cfg.agents)
1055
- .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.unhealthyAgents.has(name))
1226
+ .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.isQuarantined(name))
1056
1227
  .map(([name, a]) => ({
1057
1228
  name,
1058
1229
  description: a.description || "",
@@ -1168,7 +1339,8 @@ class Engine extends EventEmitter {
1168
1339
  skillSection +
1169
1340
  `## Kullanici gorevi\n${task.prompt}\n` +
1170
1341
  `## Calisma klasoru\n${this._cwd}\n` +
1171
- `## Proje hafizasi (gecmis baglam gecmis teslimatlar mevcut gorevi TAMAMLANMIS SAYDIRMAZ; kanit olarak degil ipucu olarak kullan)\n${clip(memory, cfg.memoryCharBudget || 8000)}\n` +
1342
+ `SANDBOX: Butun is YALNIZCA bu klasorde yapilir. Bu klasor disina (ust dizinler, orkestrator/kurulum dizini, sistem dosyalari) dosya YAZMA/DEGISTIRME/SILME ve gerekmedikce disariyi OKUMA. Delegasyon talimatlarini da bu sinira gore yaz.\n` +
1343
+ `## Proje profili (bu klasorun birikmis baglami — tum kodu bastan taramak zorunda kalmayasin diye. IPUCUDUR, kanit degil: kritik kararlarda DOSYADAN dogrula, celiski gorursen profile guvenme. Gecmis teslimatlar mevcut gorevi TAMAMLANMIS SAYDIRMAZ.)\n${clip(memory || "(henuz proje profili yok — ilk gorevde olusturulacak)", cfg.projectContextCharBudget || cfg.memoryCharBudget || 8000)}\n` +
1172
1344
  (phase === "review"
1173
1345
  ? `## Son bagimsiz denetim\n${(() => { const review = this.latestReview(state); return review ? `${review.id} (${review.agent}) → VERDICT: ${review.verdict}` : "Henuz tamamlanmis denetim yok."; })()}\n` +
1174
1346
  `## Takim calisma kaydi\n${this.teamDigest(state, cfg)}\n`
@@ -1197,11 +1369,64 @@ class Engine extends EventEmitter {
1197
1369
  return `${role}\n\n---\n## Takim agenti protokolu\n` +
1198
1370
  `Operator sana asagidaki isi devretti. ${instructionByKind[assignment.kind] || instructionByKind.implement} ` +
1199
1371
  `Planda olmayan riskli bir is gerekiyorsa yapma; BLOCKED olarak bildir. Yalnizca gercekten gozlemledigin veya dogruladigin sonuclari yaz.\n` +
1372
+ `SANDBOX: Butun is YALNIZCA calisma klasorunde (${this._cwd}) yapilir. Bu klasor disina dosya YAZMA/DEGISTIRME/SILME ve gerekmedikce disariyi OKUMA.\n` +
1200
1373
  (skillRefs ? `## Uygulanacak beceriler\nBu is icin asagidaki beceri rehberleri secildi. Her satirda becerinin OZETI ve TAM rehberin DOSYA YOLU var. Ozet yeterliyse dogrudan uygula; daha fazla ayrinti gerekiyorsa ilgili dosyayi OKU ve prosedure uy. Ilgisiz bir sey varsa gormezden gel.\n${skillRefs}\n` : "") +
1201
1374
  `## Ana hedef\n${task.prompt}\n## Delegasyon\nID: ${assignment.id}\n${assignment.instruction}\n` +
1202
1375
  `## Onceki tamamlanan takim isleri\n${clip(JSON.stringify(completed, null, 2), cfg.teamContextCharBudget || 30000)}`;
1203
1376
  }
1204
1377
 
1378
+ // Iki kademeli kurtarma: (1) gecici hata -> ayni agent, ustel bekleme; (2) kalici hata ->
1379
+ // ayni turu yapabilen saglikli baska agente devret. Ikisi de tukenirse firlatir ve operator
1380
+ // yeniden planlar. Amac, saniyelik bir 429 icin tam bir operator turu harcamamak.
1381
+ async runAssignmentWithRecovery(task, cfg, assignment, state) {
1382
+ const retries = Math.max(0, cfg.resilience?.transientRetries ?? 2);
1383
+ const maxFailovers = Math.max(0, cfg.resilience?.maxFailoverAgents ?? 1);
1384
+ const baseDelayMs = Math.max(250, (cfg.resilience?.retryBaseSeconds ?? 3) * 1000);
1385
+ const tried = new Set();
1386
+ let agentName = assignment.agent;
1387
+ let failure = null;
1388
+
1389
+ for (let failover = 0; ; failover++) {
1390
+ tried.add(agentName);
1391
+ for (let attempt = 0; ; attempt++) {
1392
+ try {
1393
+ const response = await this.invokeAgent(
1394
+ agentName,
1395
+ this.specialistPrompt(task, cfg, { ...assignment, agent: agentName }, state),
1396
+ cfg,
1397
+ { stage: "delegate", assignmentId: assignment.id }
1398
+ );
1399
+ return { response, agent: agentName };
1400
+ } catch (error) {
1401
+ if (!this.running) throw error;
1402
+ failure = classifyCliError(error);
1403
+ if (!TRANSIENT_CLI_ERRORS.has(failure.code) || attempt >= retries) break;
1404
+ // Jitter: ayni anda dusen birden fazla delegasyon saglayiciya es zamanli geri donmesin.
1405
+ const waitMs = Math.min(30000, baseDelayMs * 2 ** attempt) + Math.floor(Math.random() * 750);
1406
+ this.publish("log", { level: "warn", msg: `${agentName} gecici hata verdi [${failure.code}]; ${Math.round(waitMs / 1000)} sn sonra yeniden deneniyor (${attempt + 2}/${retries + 1}).` }, task.id);
1407
+ this.publish("activity", { kind: "delegation.retry", assignmentId: assignment.id, agent: agentName, code: failure.code, attempt: attempt + 1, waitMs });
1408
+ await this.sleepRetry(waitMs);
1409
+ if (!this.running) throw error;
1410
+ }
1411
+ }
1412
+ if (QUARANTINE_CLI_ERRORS.has(failure.code)) this.quarantineAgent(agentName, failure.code, cfg);
1413
+ if (failover >= maxFailovers) break;
1414
+ const next = compatibleAgentsForKind(cfg, task.operatorCli, assignment.kind)
1415
+ .find((name) => !tried.has(name) && !this.isQuarantined(name));
1416
+ if (!next) break;
1417
+ this.publish("log", { level: "warn", msg: `${agentName} basarisiz [${failure.code}]; ayni is ${next} agentine devrediliyor (operator turu harcanmadan).` }, task.id);
1418
+ this.publish("activity", { kind: "delegation.failover", assignmentId: assignment.id, from: agentName, to: next, code: failure.code });
1419
+ agentName = next;
1420
+ }
1421
+ // Siniflandirici disaridaki catch'te ayni sonucu uretsin diye ham metinle firlatiyoruz.
1422
+ // CLI etiketi de tasinmali; yoksa oturum/kurulum tavsiyesi jenerige duser.
1423
+ const error = new Error(failure?.raw || "Delegasyon calistirilamadi.");
1424
+ error.agentName = failure?.agent || agentName;
1425
+ error.adapter = failure?.adapter || "";
1426
+ error.triedAgents = [...tried];
1427
+ throw error;
1428
+ }
1429
+
1205
1430
  async runAssignments(task, cfg, assignments, state) {
1206
1431
  const pending = assignments.slice();
1207
1432
  while (pending.length) {
@@ -1229,32 +1454,30 @@ class Engine extends EventEmitter {
1229
1454
  this.publish("log", { level: "stage", msg: `DELEGE ${assignment.id} -> ${assignment.agent}` }, task.id);
1230
1455
  if (assignment.skills?.length) this.publish("log", { level: "info", msg: `Beceriler: ${assignment.skills.join(", ")}` }, task.id);
1231
1456
  try {
1232
- const response = await this.invokeAgent(
1233
- assignment.agent,
1234
- this.specialistPrompt(task, cfg, assignment, state),
1235
- cfg,
1236
- { stage: "delegate", assignmentId: assignment.id }
1237
- );
1238
- const result = { ...assignment, status: "completed", result: response.text, durationMs: response.durationMs, callId: response.callId };
1457
+ const { response, agent: usedAgent } = await this.runAssignmentWithRecovery(task, cfg, assignment, state);
1458
+ const result = { ...assignment, agent: usedAgent, status: "completed", result: response.text, durationMs: response.durationMs, callId: response.callId };
1459
+ if (usedAgent !== assignment.agent) {
1460
+ result.failoverFrom = assignment.agent;
1461
+ this.publish("log", { level: "info", msg: `${assignment.id} devralan agent tarafindan tamamlandi: ${assignment.agent} -> ${usedAgent}.` }, task.id);
1462
+ }
1239
1463
  if (assignment.kind === "review") {
1240
1464
  result.verdict = extractVerdict(response.text);
1241
1465
  this.publish("log", { level: "info", msg: `Denetim ${assignment.id}: VERDICT ${result.verdict || "BELIRSIZ"}` }, task.id);
1242
1466
  }
1243
1467
  state.results[assignment.id] = result;
1244
- const message = { from: assignment.agent, to: task.operatorCli, messageType: "result", assignmentId: assignment.id, body: response.text, at: new Date().toISOString() };
1468
+ const message = { from: usedAgent, to: task.operatorCli, messageType: "result", assignmentId: assignment.id, body: response.text, at: new Date().toISOString() };
1245
1469
  state.messages.push(message);
1246
1470
  this.publish("message", message, task.id);
1247
1471
  } catch (error) {
1472
+ // Karantina ve devretme runAssignmentWithRecovery'de tuketildi; buraya dusen gercekten
1473
+ // kurtarilamayandir.
1248
1474
  const failure = classifyCliError(error);
1249
- if (QUARANTINE_CLI_ERRORS.has(failure.code)) {
1250
- this.unhealthyAgents.set(assignment.agent, { code: failure.code, at: new Date().toISOString() });
1251
- cfg.runtimeUnavailableAgents = [...this.unhealthyAgents.keys()];
1252
- }
1253
- state.results[assignment.id] = { ...assignment, status: "failed", result: failure.summary, error: failure };
1254
- const message = { from: assignment.agent, to: task.operatorCli, messageType: "failure", assignmentId: assignment.id, body: `${failure.summary}\n${failure.action}`, errorCode: failure.code, at: new Date().toISOString() };
1475
+ const tried = error.triedAgents?.length ? error.triedAgents : [assignment.agent];
1476
+ state.results[assignment.id] = { ...assignment, status: "failed", result: failure.summary, error: failure, triedAgents: tried };
1477
+ const message = { from: tried[tried.length - 1], to: task.operatorCli, messageType: "failure", assignmentId: assignment.id, body: `${failure.summary}\n${failure.action}`, errorCode: failure.code, at: new Date().toISOString() };
1255
1478
  state.messages.push(message);
1256
1479
  this.publish("message", message, task.id);
1257
- this.publish("log", { level: "warn", msg: `${assignment.agent} kullanilamadi [${failure.code}]. Operator alternatif agent sececek.` }, task.id);
1480
+ this.publish("log", { level: "warn", msg: `${tried.join(" > ")} kullanilamadi [${failure.code}]. Operator alternatif plan uretecek.` }, task.id);
1258
1481
  }
1259
1482
  task.teamState = state;
1260
1483
  store.saveTask("pending", task);
@@ -1300,8 +1523,13 @@ class Engine extends EventEmitter {
1300
1523
  this.emit("status", this.status());
1301
1524
  this.emit("queue");
1302
1525
 
1303
- cfg.runtimeUnavailableAgents = [...this.unhealthyAgents.keys()];
1304
- const memory = store.getMemory(cfg.memoryCharBudget);
1526
+ cfg.runtimeUnavailableAgents = this.quarantinedAgents();
1527
+ // Proje context: varsayilan olarak bu calisma klasorunun .crewctl/CONTEXT.md profilini
1528
+ // yukleriz (path'e ozel, tum kodu bastan taramaya gerek kalmasin). task.freshContext ise
1529
+ // temiz sayfa. projectContext:false ise eski GLOBAL hafiza davranisi korunur.
1530
+ const memory = cfg.projectContext === false
1531
+ ? store.getMemory(cfg.memoryCharBudget)
1532
+ : (task.freshContext ? "" : store.readProjectContext(this._cwd));
1305
1533
  const state = task.teamState || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1306
1534
  const usedIds = new Set(state.usedIds || []);
1307
1535
 
@@ -1395,8 +1623,24 @@ class Engine extends EventEmitter {
1395
1623
  if (taskRequiresDelegation(task.prompt)
1396
1624
  && !this.hasUsableImplementAgent(cfg, operatorCli)
1397
1625
  && !Object.values(state.results).some((result) => result.kind === "implement" && result.status === "completed")) {
1398
- const dead = [...this.unhealthyAgents.keys()];
1399
- throw new Error(
1626
+ // Implement agenti yalnizca SURELI karantinadaysa gorevi oldurmek yerine bekleriz.
1627
+ // Kalici kisitlarda (kota bitti, CLI yok) null doner.
1628
+ const waitMs = this.msUntilImplementAgentReturns(cfg, operatorCli);
1629
+ let recovered = false;
1630
+ if (waitMs !== null) {
1631
+ this.publish("log", { level: "warn", msg: `Kullanilabilir implement agenti gecici olarak yok; karantina bitene kadar ${Math.ceil(waitMs / 1000)} sn bekleniyor.` }, task.id);
1632
+ await this.sleepRetry(waitMs + 250);
1633
+ // Bekleme sirasinda motor durdurulduysa gorevi "implement agenti yok" hatasiyla
1634
+ // basarisiz isaretlemek yaniltici olur; dongu kosulu zaten temiz cikisi saglar.
1635
+ if (!this.running) break;
1636
+ recovered = this.hasUsableImplementAgent(cfg, operatorCli);
1637
+ if (recovered) {
1638
+ cfg.runtimeUnavailableAgents = this.quarantinedAgents();
1639
+ this.publish("log", { level: "info", msg: "Implement agenti karantinadan dondu; gorev surduruluyor." }, task.id);
1640
+ }
1641
+ }
1642
+ const dead = this.quarantinedAgents();
1643
+ if (!recovered) throw new Error(
1400
1644
  "Bu gorev dosya olusturma/degistirme gerektiriyor fakat kullanilabilir bir uygulama (implement) agenti yok" +
1401
1645
  (dead.length ? ` (${dead.join(", ")} bu oturumda kullanim disi kaldi)` : "") +
1402
1646
  ". 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."
@@ -1479,7 +1723,7 @@ class Engine extends EventEmitter {
1479
1723
  // Gorev beklenmedik bir hatayla kesildiginde (protokol hatasi, butce asimi vb.) somut is
1480
1724
  // uretilmisse gorevi failed yerine uyarili kismi teslimatla kapatir. Basarili olamazsa
1481
1725
  // false doner ve normal hata akisi calisir.
1482
- salvage(task, error) {
1726
+ async salvage(task, error) {
1483
1727
  try {
1484
1728
  if (task.kind === "operator-chat") return false;
1485
1729
  if (!this._cwd || !this._snapBefore) return false;
@@ -1504,7 +1748,7 @@ class Engine extends EventEmitter {
1504
1748
  changedFiles.slice(0, 40).map((file) => `- ${file}`).join("\n")
1505
1749
  : `KISMI TESLIMAT: Gorev bir altyapi/protokol hatasiyla kesildi fakat tamamlanan is korundu.\n` +
1506
1750
  completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1507
- this.complete(task, rescueState, "done", final, {
1751
+ await this.complete(task, rescueState, "done", final, {
1508
1752
  warnings: [
1509
1753
  `Gorev su hatayla kesildi: ${failure.summary}`,
1510
1754
  ...(operatorDirect ? [`Operator CLI (${task.operatorCli || "?"}) plan JSON'i yerine isi dogrudan yapti. Daha guvenilir orkestrasyon icin operator olarak JSON planlamaya uygun bir CLI (or. Codex veya Claude) secmeyi dusunun.`] : []),
@@ -1556,7 +1800,7 @@ class Engine extends EventEmitter {
1556
1800
  this.emit("queue");
1557
1801
  }
1558
1802
 
1559
- complete(task, state, status, finalText, decision = {}) {
1803
+ async complete(task, state, status, finalText, decision = {}) {
1560
1804
  const changes = diffSnapshots(this._snapBefore || new Map(), snapshotDir(this._cwd));
1561
1805
  // Son periyodik taramadan sonra yapilan degisiklikleri de replay kaydina ve dashboard'a
1562
1806
  // aktar; ardindan run --once yolunda da timer'in acik kalmamasini garanti et.
@@ -1601,6 +1845,45 @@ class Engine extends EventEmitter {
1601
1845
  this.notifyOutcome(task, status, { summary: task.summary, rounds: state.round, fileCount: files.length, verification: task.delivery.verification });
1602
1846
  this.emit("queue");
1603
1847
  this._textBefore = null;
1848
+ // Proje profilini guncelle (sonuc zaten kullaniciya iletildi; motor mesgul kaldigindan
1849
+ // sonraki gorevle es zamanli operator cagrisi olmaz). Basarisizsa gorevi ASLA etkilemez.
1850
+ await this.reviseProjectContext(task, status, files);
1851
+ }
1852
+
1853
+ // Gorev bitiminde bu calisma klasorunun .crewctl/CONTEXT.md proje profilini operatore REVIZE
1854
+ // ettirir (append degil): mimari/yapi/konvansiyon/komutlar guncel kalir, bayatlamaz. Yalnizca
1855
+ // basarili ve DOSYA DEGISTIREN gorevlerde calisir (bilgi sorulari profili degistirmez).
1856
+ async reviseProjectContext(task, status, files) {
1857
+ try {
1858
+ const cfg = this.cfg();
1859
+ if (cfg.projectContext === false) return;
1860
+ if (status !== "done" || !this._cwd) return;
1861
+ if (!Array.isArray(files) || !files.length) return;
1862
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
1863
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) return;
1864
+ const budget = cfg.projectContextCharBudget || 6000;
1865
+ const current = store.readProjectContext(this._cwd);
1866
+ const changed = files.map((f) => `${f.action}: ${f.path}`).join("\n");
1867
+ const prompt =
1868
+ `Sen bir projenin YASAYAN bilgi tabanini (proje profili) guncelliyorsun. Amac: sonraki gorevlerde ` +
1869
+ `ajan tum kodu bastan taramadan projeyi anlasin.\n` +
1870
+ `Asagida MEVCUT profil ve SON tamamlanan gorevin ozeti + degisen dosyalar var. Guncel, oz ve DOGRU ` +
1871
+ `bir proje profili YAZ (Markdown). Kurallar:\n` +
1872
+ `- Degisen gerceklere gore REVIZE et; silinen/tasinan/degisen seyleri guncelle, bayat bilgi birakma.\n` +
1873
+ `- Dayanikli bilgiyi tut: amac, mimari, klasor/dosya yapisi, giris noktalari, onemli moduller, ` +
1874
+ `konvansiyonlar, kurulum/calistirma/test komutlari, dikkat noktalari.\n` +
1875
+ `- Task-by-task changelog YAZMA; tek bir GUNCEL DURUM profili olsun.\n` +
1876
+ `- En fazla ~${budget} karakter. YALNIZCA profil metnini dondur; aciklama/selamlama/JSON/kod-bloğu isareti YAZMA.\n\n` +
1877
+ `## Mevcut profil\n${current || "(henuz yok — sifirdan olustur)"}\n\n` +
1878
+ `## Son tamamlanan gorev\n${task.prompt}\n\n## Sonuc ozeti\n${task.summary || ""}\n\n## Degisen dosyalar\n${changed}`;
1879
+ const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-context" });
1880
+ const profile = stripCodeFence(String(response?.text || "")).trim();
1881
+ if (profile && store.writeProjectContext(this._cwd, clip(profile, budget * 2))) {
1882
+ this.publish("log", { level: "info", msg: "Proje profili (.crewctl/CONTEXT.md) guncellendi." }, task.id);
1883
+ }
1884
+ } catch (error) {
1885
+ this.publish("log", { level: "warn", msg: `Proje profili guncellenemedi: ${error.message}` }, task.id);
1886
+ }
1604
1887
  }
1605
1888
 
1606
1889
  // Dis bildirim icin tek cikis noktasi: server.js bu event'i dinleyip (varsa) webhook'a POST atar.
@@ -1626,5 +1909,6 @@ module.exports._internals = {
1626
1909
  normalizeAssignments, ensureBalancedRoleChain, extractVerdict, clipMiddle, snapshotDir, diffSnapshots,
1627
1910
  captureTextSnapshot, buildLineDiff, describeFileDiff, isSensitiveDiffPath, taskRequiresDelegation,
1628
1911
  parseJson, conversationalAnswer, classifyCliError, RECOVERABLE_CLI_ERRORS, QUARANTINE_CLI_ERRORS,
1629
- normalizeCliOutput, addUsage, usageTotal,
1912
+ TRANSIENT_CLI_ERRORS, QUARANTINE_COOLDOWN_SECONDS, compatibleAgentsForKind,
1913
+ normalizeCliOutput, addUsage, usageTotal, claudeSandboxSettings,
1630
1914
  };
@@ -27,7 +27,6 @@ function pad2(n) {
27
27
  return String(n).padStart(2, "0");
28
28
  }
29
29
 
30
- // Ham girdiyi dogrulanmis bir tetik nesnesine cevir; gecersizse Error at.
31
30
  function normalizeTrigger(raw) {
32
31
  if (!isPlainObject(raw)) throw new Error("trigger nesnesi gerekli.");
33
32
  const type = String(raw.type || "").toLowerCase();
@@ -42,7 +41,6 @@ function normalizeTrigger(raw) {
42
41
  if (!time) throw new Error('trigger.at "HH:MM" (00:00–23:59) olmali.');
43
42
  const at = `${pad2(time.hours)}:${pad2(time.minutes)}`;
44
43
  if (type === "daily") return { type, at };
45
- // weekly
46
44
  const days = Array.isArray(raw.days) ? [...new Set(raw.days.map(Number))].filter((d) => Number.isInteger(d) && d >= 0 && d <= 6).sort((a, b) => a - b) : [];
47
45
  if (!days.length) throw new Error("trigger.days en az bir gun (0=Pazar .. 6=Cumartesi) icermeli.");
48
46
  return { type, at, days };