@omerrgocmen/crewctl 1.1.1 → 1.3.0

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.
Files changed (91) hide show
  1. package/README.md +193 -193
  2. package/orchestrator/README.md +473 -462
  3. package/orchestrator/config.default.json +79 -71
  4. package/orchestrator/roles/executor.md +57 -57
  5. package/orchestrator/roles/operator.md +77 -77
  6. package/orchestrator/roles/planner.md +58 -58
  7. package/orchestrator/roles/reviewer.md +63 -63
  8. package/orchestrator/skills/acceptance-criteria.md +18 -18
  9. package/orchestrator/skills/accessibility-audit.md +16 -16
  10. package/orchestrator/skills/accessible-forms.md +16 -16
  11. package/orchestrator/skills/api-design.md +27 -27
  12. package/orchestrator/skills/api-documentation.md +16 -16
  13. package/orchestrator/skills/architecture-decision-record.md +18 -18
  14. package/orchestrator/skills/authentication-design.md +16 -16
  15. package/orchestrator/skills/authorization-review.md +16 -16
  16. package/orchestrator/skills/backward-compatibility.md +17 -17
  17. package/orchestrator/skills/changelog-writing.md +16 -16
  18. package/orchestrator/skills/ci-pipeline-design.md +16 -16
  19. package/orchestrator/skills/cli-design.md +16 -16
  20. package/orchestrator/skills/code-review.md +27 -27
  21. package/orchestrator/skills/configuration-management.md +16 -16
  22. package/orchestrator/skills/container-review.md +16 -16
  23. package/orchestrator/skills/contract-testing.md +16 -16
  24. package/orchestrator/skills/dashboard-design.md +16 -16
  25. package/orchestrator/skills/database-migration.md +16 -16
  26. package/orchestrator/skills/database-schema-design.md +16 -16
  27. package/orchestrator/skills/debugging.md +26 -26
  28. package/orchestrator/skills/dependency-review.md +16 -16
  29. package/orchestrator/skills/design-review.md +29 -29
  30. package/orchestrator/skills/design-system.md +16 -16
  31. package/orchestrator/skills/docs-api-reference.md +16 -16
  32. package/orchestrator/skills/docs-troubleshooting.md +16 -16
  33. package/orchestrator/skills/docs-tutorial.md +16 -16
  34. package/orchestrator/skills/docs-writing.md +25 -25
  35. package/orchestrator/skills/empty-error-loading-states.md +16 -16
  36. package/orchestrator/skills/end-to-end-testing.md +16 -16
  37. package/orchestrator/skills/error-handling.md +16 -16
  38. package/orchestrator/skills/frontend-design.md +28 -28
  39. package/orchestrator/skills/git-commit-writing.md +16 -16
  40. package/orchestrator/skills/graphql-design.md +16 -16
  41. package/orchestrator/skills/incident-runbook.md +18 -18
  42. package/orchestrator/skills/input-validation.md +16 -16
  43. package/orchestrator/skills/integration-testing.md +16 -16
  44. package/orchestrator/skills/interaction-design.md +16 -16
  45. package/orchestrator/skills/landing-page-design.md +16 -16
  46. package/orchestrator/skills/observability-design.md +16 -16
  47. package/orchestrator/skills/openapi-contract.md +16 -16
  48. package/orchestrator/skills/performance-profiling.md +16 -16
  49. package/orchestrator/skills/privacy-review.md +16 -16
  50. package/orchestrator/skills/property-based-testing.md +16 -16
  51. package/orchestrator/skills/pull-request-writing.md +16 -16
  52. package/orchestrator/skills/refactoring.md +16 -16
  53. package/orchestrator/skills/release-readiness.md +16 -16
  54. package/orchestrator/skills/responsive-design.md +16 -16
  55. package/orchestrator/skills/secrets-management.md +16 -16
  56. package/orchestrator/skills/secure-file-upload.md +16 -16
  57. package/orchestrator/skills/security-review.md +26 -26
  58. package/orchestrator/skills/semantic-versioning.md +17 -17
  59. package/orchestrator/skills/seo-on-page.md +16 -16
  60. package/orchestrator/skills/seo-structured-data.md +16 -16
  61. package/orchestrator/skills/seo-technical-audit.md +16 -16
  62. package/orchestrator/skills/sql-query-review.md +16 -16
  63. package/orchestrator/skills/supply-chain-security.md +16 -16
  64. package/orchestrator/skills/test-strategy.md +16 -16
  65. package/orchestrator/skills/threat-modeling.md +16 -16
  66. package/orchestrator/skills/unit-testing.md +16 -16
  67. package/orchestrator/skills/write-tests.md +28 -28
  68. package/orchestrator/src/cli-registry.js +740 -682
  69. package/orchestrator/src/cli.js +140 -135
  70. package/orchestrator/src/doctor.js +64 -64
  71. package/orchestrator/src/engine.js +1735 -1630
  72. package/orchestrator/src/schedule.js +126 -126
  73. package/orchestrator/src/server.js +818 -795
  74. package/orchestrator/src/skill-registry.js +272 -272
  75. package/orchestrator/src/store.js +470 -426
  76. package/orchestrator/web/OrbitControls.js +1417 -1417
  77. package/orchestrator/web/board.html +146 -146
  78. package/orchestrator/web/code.html +213 -213
  79. package/orchestrator/web/flow.html +741 -741
  80. package/orchestrator/web/index.html +602 -602
  81. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -231
  82. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -104
  83. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -95
  84. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -99
  85. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -77
  86. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -415
  87. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -45
  88. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -66
  89. package/orchestrator/web/site.webmanifest +12 -12
  90. package/orchestrator/web/three.module.min.js +6 -6
  91. package/package.json +51 -51
@@ -1,1630 +1,1735 @@
1
- const { spawn } = require("child_process");
2
- const fs = require("fs");
3
- const path = require("path");
4
- const { EventEmitter } = require("events");
5
- const store = require("./store");
6
- const cliRegistry = require("./cli-registry");
7
- const skillRegistry = require("./skill-registry");
8
- const checkpoints = require("./checkpoints");
9
-
10
- const isWin = process.platform === "win32";
11
-
12
- function snapshotDir(dir) {
13
- const map = new Map();
14
- const ignored = (rel) => {
15
- const r = rel.replace(/\\/g, "/");
16
- return r.includes("node_modules") || r.includes(".git/") || r.startsWith(".git") ||
17
- r.startsWith("orchestrator/queue") || r.startsWith("orchestrator/state") ||
18
- r.startsWith("orchestrator/memory") || r.startsWith("orchestrator/node_modules");
19
- };
20
- let count = 0;
21
- const walk = (abs, rel) => {
22
- if (count > 12000) return;
23
- let entries;
24
- try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
25
- for (const entry of entries) {
26
- const childRel = rel ? `${rel}/${entry.name}` : entry.name;
27
- if (ignored(childRel)) continue;
28
- const childAbs = path.join(abs, entry.name);
29
- if (entry.isDirectory()) walk(childAbs, childRel);
30
- else {
31
- try {
32
- const s = fs.statSync(childAbs);
33
- map.set(childRel, `${s.mtimeMs}:${s.size}`);
34
- count++;
35
- } catch {}
36
- }
37
- }
38
- };
39
- walk(dir, "");
40
- return map;
41
- }
42
-
43
- function diffSnapshots(before, after) {
44
- const created = [], modified = [], deleted = [];
45
- for (const [file, stamp] of after) {
46
- if (!before.has(file)) created.push(file);
47
- else if (before.get(file) !== stamp) modified.push(file);
48
- }
49
- for (const file of before.keys()) if (!after.has(file)) deleted.push(file);
50
- return { created: created.sort(), modified: modified.sort(), deleted: deleted.sort() };
51
- }
52
-
53
- const DIFF_MAX_FILE_BYTES = 256 * 1024;
54
- const DIFF_MAX_BASELINE_BYTES = 24 * 1024 * 1024;
55
- const DIFF_MAX_BASELINE_FILES = 2000;
56
- const DIFF_MAX_MATRIX_CELLS = 250000;
57
- const DIFF_CONTEXT_LINES = 3;
58
- const DIFF_MAX_RENDERED_LINES = 240;
59
- const DIFF_MAX_EVENT_LINES = 1000;
60
- const DIFF_MAX_SOURCE_LINES = 5000;
61
-
62
- function isSensitiveDiffPath(file) {
63
- const normalized = String(file || "").replace(/\\/g, "/").toLowerCase();
64
- const base = path.posix.basename(normalized);
65
- return /^\.env(?:\.|$)/.test(base) || [".npmrc", ".pypirc", ".netrc"].includes(base) ||
66
- /(^|[._-])(secret|secrets|credential|credentials)([._-]|$)/.test(base) ||
67
- /\.(pem|key|p12|pfx)$/.test(base) || /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/.test(base);
68
- }
69
-
70
- function readDiffText(file, maxBytes = DIFF_MAX_FILE_BYTES) {
71
- try {
72
- const stat = fs.statSync(file);
73
- if (!stat.isFile()) return { ok: false, status: "unavailable" };
74
- if (stat.size > maxBytes) return { ok: false, status: "too-large", size: stat.size };
75
- const buffer = fs.readFileSync(file);
76
- if (buffer.includes(0)) return { ok: false, status: "binary", size: stat.size };
77
- let controls = 0;
78
- for (const byte of buffer) {
79
- if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) controls++;
80
- }
81
- if (buffer.length && controls / buffer.length > 0.02) return { ok: false, status: "binary", size: stat.size };
82
- return { ok: true, text: buffer.toString("utf8"), size: stat.size };
83
- } catch {
84
- return { ok: false, status: "unavailable" };
85
- }
86
- }
87
-
88
- // Gorev basindaki metin iceriklerini sinirli bir butceyle saklar. Bu taban, Git deposu
89
- // olmayan klasorlerde de modified/deleted dosyalar icin satir diff'i uretebilmemizi saglar.
90
- function captureTextSnapshot(dir, snapshot, options = {}) {
91
- const maxBytes = Math.max(1024, Number(options.maxBytes) || DIFF_MAX_BASELINE_BYTES);
92
- const maxFiles = Math.max(1, Number(options.maxFiles) || DIFF_MAX_BASELINE_FILES);
93
- const maxFileBytes = Math.max(1024, Number(options.maxFileBytes) || DIFF_MAX_FILE_BYTES);
94
- const result = new Map();
95
- let bytes = 0, files = 0;
96
- for (const file of [...(snapshot?.keys?.() || [])].sort()) {
97
- if (isSensitiveDiffPath(file)) {
98
- result.set(file, { ok: false, status: "redacted" });
99
- continue;
100
- }
101
- if (files >= maxFiles || bytes >= maxBytes) {
102
- result.set(file, { ok: false, status: "baseline-limit" });
103
- continue;
104
- }
105
- const value = readDiffText(path.join(dir, file), maxFileBytes);
106
- if (value.ok && bytes + value.size > maxBytes) {
107
- result.set(file, { ok: false, status: "baseline-limit" });
108
- continue;
109
- }
110
- result.set(file, value);
111
- if (value.ok) { bytes += value.size; files++; }
112
- }
113
- return result;
114
- }
115
-
116
- function splitDiffLines(text) {
117
- const value = String(text || "").replace(/\r\n/g, "\n");
118
- if (!value) return [];
119
- const lines = value.split("\n");
120
- if (lines[lines.length - 1] === "") lines.pop();
121
- return lines;
122
- }
123
-
124
- function lineOperations(beforeText, afterText) {
125
- const before = splitDiffLines(beforeText), after = splitDiffLines(afterText);
126
- const cells = before.length * after.length;
127
- let approximate = false;
128
- const operations = [];
129
- if (cells <= DIFF_MAX_MATRIX_CELLS) {
130
- const width = after.length + 1;
131
- const table = new Uint32Array((before.length + 1) * width);
132
- for (let i = before.length - 1; i >= 0; i--) {
133
- for (let j = after.length - 1; j >= 0; j--) {
134
- table[i * width + j] = before[i] === after[j]
135
- ? table[(i + 1) * width + j + 1] + 1
136
- : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
137
- }
138
- }
139
- let i = 0, j = 0;
140
- while (i < before.length || j < after.length) {
141
- if (i < before.length && j < after.length && before[i] === after[j]) {
142
- operations.push({ type: "context", text: before[i++] }); j++;
143
- } else if (i < before.length && (j >= after.length || table[(i + 1) * width + j] >= table[i * width + j + 1])) {
144
- operations.push({ type: "delete", text: before[i++] });
145
- } else {
146
- operations.push({ type: "add", text: after[j++] });
147
- }
148
- }
149
- } else {
150
- // Cok buyuk dosyalarda karesel matris kurma. Ortak bas/sonu koruyup degisen orta
151
- // bolumu tek hunk olarak goster; UI bu sonucu yaklasik olarak etiketler.
152
- approximate = true;
153
- let prefix = 0;
154
- while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) prefix++;
155
- let suffix = 0;
156
- while (suffix < before.length - prefix && suffix < after.length - prefix &&
157
- before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
158
- for (let i = 0; i < prefix; i++) operations.push({ type: "context", text: before[i] });
159
- for (let i = prefix; i < before.length - suffix; i++) operations.push({ type: "delete", text: before[i] });
160
- for (let i = prefix; i < after.length - suffix; i++) operations.push({ type: "add", text: after[i] });
161
- for (let i = suffix; i > 0; i--) operations.push({ type: "context", text: before[before.length - i] });
162
- }
163
- return { operations, approximate };
164
- }
165
-
166
- function buildLineDiff(beforeText, afterText, options = {}) {
167
- const context = Math.max(0, Number(options.context) || DIFF_CONTEXT_LINES);
168
- const maxLines = Math.max(20, Number(options.maxLines) || DIFF_MAX_RENDERED_LINES);
169
- const { operations, approximate } = lineOperations(beforeText, afterText);
170
- let oldNo = 1, newNo = 1;
171
- const annotated = operations.map((operation) => {
172
- const line = { ...operation, oldNumber: null, newNumber: null };
173
- if (operation.type !== "add") line.oldNumber = oldNo++;
174
- if (operation.type !== "delete") line.newNumber = newNo++;
175
- return line;
176
- });
177
- const additions = annotated.filter((line) => line.type === "add").length;
178
- const deletions = annotated.filter((line) => line.type === "delete").length;
179
- const ranges = [];
180
- for (let i = 0; i < annotated.length; i++) {
181
- if (annotated[i].type === "context") continue;
182
- const start = Math.max(0, i - context), end = Math.min(annotated.length, i + context + 1);
183
- const last = ranges[ranges.length - 1];
184
- if (last && start <= last.end) last.end = Math.max(last.end, end);
185
- else ranges.push({ start, end });
186
- }
187
- const hunks = [];
188
- let rendered = 0, truncated = false;
189
- for (const range of ranges) {
190
- if (rendered >= maxLines) { truncated = true; break; }
191
- const oldStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "add").length;
192
- const newStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "delete").length;
193
- const available = Math.max(0, maxLines - rendered);
194
- const selected = annotated.slice(range.start, Math.min(range.end, range.start + available));
195
- if (selected.length < range.end - range.start) truncated = true;
196
- hunks.push({
197
- oldStart,
198
- oldLines: selected.filter((line) => line.type !== "add").length,
199
- newStart,
200
- newLines: selected.filter((line) => line.type !== "delete").length,
201
- lines: selected.map((line) => ({ ...line, text: String(line.text).slice(0, 1000) })),
202
- });
203
- rendered += selected.length;
204
- }
205
- return { additions, deletions, hunks, truncated, approximate };
206
- }
207
-
208
- function describeFileDiff(root, file, action, baseline, options = {}) {
209
- const base = action === "created" ? { ok: true, text: "" } : baseline?.get(file);
210
- const current = action === "deleted" ? { ok: true, text: "" } :
211
- (isSensitiveDiffPath(file) ? { ok: false, status: "redacted" } : readDiffText(path.join(root, file)));
212
- const unavailable = !base?.ok ? base : (!current?.ok ? current : null);
213
- if (unavailable) return { path: file, action, additions: null, deletions: null, previewStatus: unavailable.status || "unavailable", hunks: [] };
214
- const lineCount = (text) => text ? (String(text).match(/\n/g) || []).length + 1 : 0;
215
- if (lineCount(base.text) > DIFF_MAX_SOURCE_LINES || lineCount(current.text) > DIFF_MAX_SOURCE_LINES) {
216
- return { path: file, action, additions: null, deletions: null, previewStatus: "too-many-lines", hunks: [] };
217
- }
218
- return { path: file, action, ...buildLineDiff(base.text, current.text, options) };
219
- }
220
-
221
- function clip(value, limit = 12000) {
222
- const text = String(value || "");
223
- return text.length <= limit ? text : `${text.slice(0, limit)}\n...[kesildi]`;
224
- }
225
-
226
- // Bas ve sonu koruyarak kirpar. Denetci raporlarinda VERDICT satiri metnin SONUNDadir;
227
- // yalnizca bastan kirpmak operatorun karari gormemesine ve gereksiz yeniden inceleme
228
- // turlarina yol acar.
229
- function clipMiddle(value, limit = 12000) {
230
- const text = String(value || "");
231
- if (text.length <= limit) return text;
232
- const head = Math.ceil(limit * 0.6);
233
- const tail = Math.max(1, limit - head);
234
- return `${text.slice(0, head)}\n...[orta kisim kesildi]...\n${text.slice(text.length - tail)}`;
235
- }
236
-
237
- function extractVerdict(text) {
238
- const matches = String(text || "").match(/VERDICT:\s*(PASS|FAIL)/gi);
239
- if (!matches || !matches.length) return null;
240
- return /PASS/i.test(matches[matches.length - 1]) ? "PASS" : "FAIL";
241
- }
242
-
243
- // Siniflandirilamayan hatada "gemini cikis kodu 1." gibi bilgisiz bir ilk satir yerine metnin
244
- // icindeki GERCEK hata cumlesini bulur; kullaniciya gosterilen tek satir buysa anlamli olmali.
245
- function meaningfulErrorLine(raw) {
246
- const all = String(raw).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
247
- // Yigin izleri ve motorun kendi sardigi "<cli> cikis kodu N." satiri teshis tasimaz;
248
- // gercek neden hemen arkalarindaki satirdadir.
249
- const useful = all.filter((line) => !/^at\s|^node:internal|^[{}[\]]+$/.test(line) && !/cikis kodu/i.test(line));
250
- const signal = useful.find((line) => /error|hata|failed|failure|denied|invalid|exceeded|limit|quota|unable|cannot|must/i.test(line) && line.length > 20);
251
- return signal || useful[0] || all[0] || String(raw).trim();
252
- }
253
-
254
- // Ham CLI ciktisini kullaniciya "ne oldu + ne yapmali" seklinde anlamli bir teshise cevirir.
255
- // SIRA ONEMLIDIR: spesifik desenler once gelmeli. Ornegin Gemini'nin "GEMINI_API_KEY ...
256
- // not found" mesaji bir PATH/kurulum sorunu DEGILDIR; genis /not found/ deseni bu yuzden
257
- // daraltildi ve kimlik kontrollerinin arkasina alindi.
258
- function classifyCliError(error) {
259
- const raw = String(error?.message || error || "Bilinmeyen CLI hatasi");
260
- const of = (code, summary, action) => ({ code, summary, action, raw: clip(raw, 5000) });
261
- if (/requires a newer version|upgrade to the latest|model metadata.*not found|unsupported.*model|model.*unsupported/i.test(raw)) {
262
- return of("VERSION_INCOMPATIBLE", "CLI sürümü seçilen modeli desteklemiyor.", "CLI aracını güncelleyin veya desteklenen bir model seçin.");
263
- }
264
- if (/sessiz kaldi|cikti uretmedi|CLI_STALLED/i.test(raw)) {
265
- 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
- }
267
- 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).");
269
- }
270
- // Gemini CLI oturum acilmadiginda "Please set an Auth method" / "GEMINI_API_KEY environment
271
- // variable not found" yazar; her ikisi de kurulum degil OTURUM sorunudur.
272
- 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.");
274
- }
275
- // Kota (gunluk/aylik hak bitti) ile hiz siniri (kisa sureli) AYRI teshislerdir: ilki
276
- // beklemekle gecmez, ikincisi gecer. Kullanicinin sordugu "limitim mi bitti" ayrimi budur.
277
- if (/RESOURCE_EXHAUSTED|quota exceeded|exceeded your current quota|daily limit|günlük limit|gunluk limit|out of credits|insufficient (credit|balance|quota)|billing/i.test(raw)) {
278
- return of("QUOTA_EXCEEDED", "Sağlayıcı kotanız doldu (günlük/aylık hak veya bakiye bitti).", "Kota yenilenene kadar bu agent kullanılamaz; başka bir agent seçin, faturalandırmayı yükseltin veya API anahtarını kotası olan bir hesapla değiştirin.");
279
- }
280
- if (/rate.?limit|too many requests|\b429\b/i.test(raw)) {
281
- return of("RATE_LIMIT", "İstek hızı sınırına takıldı (kota bitmedi, çok sık istek gönderildi).", "Kısa bir süre bekleyip tekrar deneyin; operatör bu turda alternatif bir agent kullanabilir.");
282
- }
283
- if (/overloaded|\bUNAVAILABLE\b|\b(503|500)\b|internal error|try again later/i.test(raw)) {
284
- return of("MODEL_OVERLOADED", "Model sağlayıcısı geçici olarak aşırı yüklü ya da hata döndürdü.", "Sağlayıcı kaynaklı geçici bir sorun; birkaç dakika sonra tekrar deneyin.");
285
- }
286
- if (/location is not supported|not available in your country|region.*not supported/i.test(raw)) {
287
- 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
- }
289
- 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.");
291
- }
292
- if (/ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|getaddrinfo|fetch failed|socket hang up|network (error|unreachable)|proxy/i.test(raw)) {
293
- 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
- }
295
- 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.");
297
- }
298
- if (/timeout|timed out|zaman asim|zaman aşım/i.test(raw)) {
299
- 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.");
300
- }
301
- return of("CLI_FAILED", clip(meaningfulErrorLine(raw), 500), "Ham CLI çıktısı teknik ayrıntılarda saklandı; sorun sürerse alternatif bir agent deneyin.");
302
- }
303
-
304
- const RECOVERABLE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "RATE_LIMIT", "QUOTA_EXCEEDED", "MODEL_OVERLOADED", "NETWORK_ERROR", "REGION_BLOCKED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "TIMEOUT", "CLI_STALLED", "CLI_FAILED", "VERSION_INCOMPATIBLE"]);
305
- // Karantina = bu oturumda tekrar denemek anlamsiz. Kota/bolge kisiti beklemekle gecmez;
306
- // hiz siniri (RATE_LIMIT) ve gecici saglayici hatasi (MODEL_OVERLOADED) gecer, karantinaya alinmaz.
307
- const QUARANTINE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "QUOTA_EXCEEDED", "REGION_BLOCKED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "CLI_STALLED", "VERSION_INCOMPATIBLE"]);
308
-
309
- function resolveExecutionMode(task) {
310
- if (["fast", "balanced", "deep"].includes(task.executionMode)) return task.executionMode;
311
- const prompt = String(task.prompt || "");
312
- const complex = /(mimari|migration|refactor|guvenlik|güvenlik|deploy|production|veritabani|veritabanı|authentication|entegrasyon|çoklu|multi|kapsamli|kapsamlı)/i.test(prompt);
313
- // Not: "oyun/sayfa" gibi sifirdan build isleri BASIT sayilmaz bunlar plan+uygula+incele
314
- // gerektiren gercek gorevlerdir; fast moda dusurulup ekip kullanilmadan gecilmemeli.
315
- // Not: "dosya" gibi genis kelimeler fast'a dusurmez; yorumdaki ilkeyle celisiyordu
316
- // (sifirdan build/dosya olusturma gercek bir gorevdir). Yalnizca acik "kucuk/hizli" sinyalleri.
317
- const simple = prompt.length < 350 && /(basit|ufak|küçük|kucuk|hizli|hızlı|simple|small|quick)/i.test(prompt);
318
- return simple && !complex ? "fast" : "balanced";
319
- }
320
-
321
- function applyExecutionPolicy(base, mode) {
322
- const cfg = { ...base, operator: { ...(base.operator || {}) } };
323
- if (mode === "fast") {
324
- cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 2);
325
- cfg.operator.maxDelegationsPerRound = 1;
326
- cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 2500);
327
- cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 10000);
328
- } else if (mode === "balanced") {
329
- // Saglikli bir balanced gorev tek turda biter (uygulama + zincirli inceleme);
330
- // 3 tur, iki duzeltme/yeniden dogrulama turuna yer birakir.
331
- cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 3);
332
- cfg.operator.maxDelegationsPerRound = Math.min(cfg.operator.maxDelegationsPerRound || 8, 3);
333
- cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 6000);
334
- cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 24000);
335
- }
336
- return cfg;
337
- }
338
-
339
- const EMPTY_USAGE = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
340
-
341
- function addUsage(target, extra) {
342
- const sum = { ...(target || EMPTY_USAGE) };
343
- for (const key of Object.keys(EMPTY_USAGE)) sum[key] = (sum[key] || 0) + (Number(extra?.[key]) || 0);
344
- return sum;
345
- }
346
-
347
- function usageTotal(usage) {
348
- return (Number(usage?.input) || 0) + (Number(usage?.output) || 0);
349
- }
350
-
351
- // OpenCode'un step_finish olayindaki token/maliyet alanlarini normalize eder. Alan adlari
352
- // surumler arasinda degisebildigi icin eksik alan 0 sayilir; hicbir sayi yoksa null doner
353
- // ki "veri yok" ile "sifir maliyet" ayirt edilebilsin.
354
- function readStepUsage(event) {
355
- const tokens = event.part?.tokens || event.tokens;
356
- const cost = Number(event.part?.cost ?? event.cost);
357
- if (!tokens && !Number.isFinite(cost)) return null;
358
- return {
359
- input: Number(tokens?.input) || 0,
360
- output: Number(tokens?.output) || 0,
361
- reasoning: Number(tokens?.reasoning) || 0,
362
- cacheRead: Number(tokens?.cache?.read) || 0,
363
- cacheWrite: Number(tokens?.cache?.write) || 0,
364
- cost: Number.isFinite(cost) ? cost : 0,
365
- };
366
- }
367
-
368
- // DIKKAT: text/error sozlesmesi degismemeli operatorun JSON protokolu bu metni okuyor.
369
- // usage yalnizca EK bir alandir; cikarilamadiginda null kalir ve hicbir akisi etkilemez.
370
- function normalizeCliOutput(agent, stdout) {
371
- if (agent.adapter !== "opencode" || !(agent.args || []).includes("json")) {
372
- return { text: String(stdout || "").trim(), error: "", usage: null };
373
- }
374
- const texts = [], errors = [];
375
- let usage = null;
376
- for (const line of String(stdout || "").split(/\r?\n/)) {
377
- if (!line.trim()) continue;
378
- try {
379
- const event = JSON.parse(line);
380
- if (event.type === "text" && event.part?.text) texts.push(String(event.part.text));
381
- 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
- if (event.type === "step_finish") {
384
- const step = readStepUsage(event);
385
- if (step) usage = addUsage(usage, step);
386
- }
387
- } catch {}
388
- }
389
- return { text: texts.join("\n").trim(), error: errors.join("\n").trim(), usage };
390
- }
391
-
392
- // Metindeki DENGELI suslu parantezli en dis JSON nesnesi adaylarini cikarir. String icindeki
393
- // parantez/kacis karakterleri sayilmaz; boylece "final" metninde gecen bir { veya } eski
394
- // "ilk-{ .. son-}" kestirmesini bozmaz. Ayrica model aciklama + nesne + ek aciklama yazdiginda
395
- // (ya da birden fazla nesne dondurdugunde) her nesne ayri aday olur.
396
- function jsonObjectCandidates(text) {
397
- const src = String(text);
398
- const found = [];
399
- const stack = [];
400
- let inString = false, escaped = false;
401
- for (let i = 0; i < src.length; i++) {
402
- const ch = src[i];
403
- if (inString) {
404
- if (escaped) escaped = false;
405
- else if (ch === "\\") escaped = true;
406
- else if (ch === '"') inString = false;
407
- continue;
408
- }
409
- if (ch === '"') inString = true;
410
- else if (ch === "{") stack.push(i);
411
- else if (ch === "}" && stack.length) {
412
- const start = stack.pop();
413
- if (!stack.length) found.push(src.slice(start, i + 1));
414
- }
415
- }
416
- return found;
417
- }
418
-
419
- // Model bazen JSON'u tek tirnak, sondaki virgul veya akilli tirnakla yazar. Kati parse
420
- // basarisiz oldugunda son care olarak bu yaygin sapmalari onarip bir kez daha dener.
421
- function repairJsonText(candidate) {
422
- return String(candidate)
423
- .replace(/[“”]/g, '"')
424
- .replace(/[‘’]/g, "'")
425
- .replace(/,\s*([}\]])/g, "$1");
426
- }
427
-
428
- const PROTOCOL_KEYS = /"(status|assignments|summary|completionCriteria|final)"\s*:/;
429
-
430
- function parseJson(text, label) {
431
- const raw = String(text);
432
- const candidates = [];
433
- for (const fence of raw.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(fence[1]);
434
- candidates.push(raw.trim());
435
- // Protokol anahtari iceren nesneler once denenir; ayni onem sirasinda SONDAKI nesne kazanir
436
- // (modeller once dusunce/aciklama, en sona nihai protokol nesnesini yazar).
437
- const objects = jsonObjectCandidates(raw).reverse();
438
- candidates.push(...objects.filter((o) => PROTOCOL_KEYS.test(o)), ...objects.filter((o) => !PROTOCOL_KEYS.test(o)));
439
- for (const candidate of candidates) {
440
- for (const attempt of [candidate, repairJsonText(candidate)]) {
441
- try {
442
- const parsed = JSON.parse(attempt);
443
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
444
- } catch {}
445
- }
446
- }
447
- const error = new Error(`${label} gecerli JSON dondurmedi.`);
448
- error.rawText = raw;
449
- throw error;
450
- }
451
-
452
- // Protokol hatasinda operatorun ham metnini kullaniciya gosterilebilir bir cevaba cevirir.
453
- // Yarim kalmis/kirik JSON parcalarini ve kod bloklarini atar; geriye anlamli duz metin
454
- // kalmiyorsa null doner (o zaman normal hata akisi calisir).
455
- function conversationalAnswer(error) {
456
- if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) return null;
457
- const raw = String(error.rawText || "");
458
- if (!raw.trim()) return null;
459
- const stripped = raw
460
- .replace(/```[\s\S]*?```/g, " ")
461
- .replace(/\{[\s\S]*\}/g, " ")
462
- .replace(/[\s\S]*?^\s*\{[\s\S]*$/m, (match) => match.split("{")[0])
463
- .replace(/\n{3,}/g, "\n\n")
464
- .trim();
465
- // Cok kisa artiklar (or. tek noktalama) cevap sayilmaz.
466
- return stripped.length >= 8 ? stripped : null;
467
- }
468
-
469
- function capabilitiesFor(agent) {
470
- // Rol dosyasi bir uzmanlik dayatiyorsa yetenekler DOGRUDAN rolden turer. Aksi halde
471
- // config'teki eski/genis capabilities listesi (or. planner'da "implementation") katalogda
472
- // allowedKinds ile celisir ve operatore yaniltici sinyal verirdi. Rol yoksa config'e duseriz.
473
- const role = String(agent.roleFile || "").toLowerCase();
474
- if (role.includes("executor")) return new Set(["implementation", "debugging", "testing"]);
475
- if (role.includes("review")) return new Set(["review", "testing", "analysis"]);
476
- if (role.includes("planner")) return new Set(["planning", "analysis"]);
477
- if (role.includes("operator")) return new Set(["planning", "delegation"]);
478
- return new Set(Array.isArray(agent.capabilities) ? agent.capabilities.map((x) => String(x).toLowerCase()) : []);
479
- }
480
-
481
- function inferAssignmentKind(raw, instruction) {
482
- const explicit = String(raw.kind || "").toLowerCase();
483
- if (["implement", "review", "research", "plan"].includes(explicit)) return explicit;
484
- if (/\b(olustur|oluştur|yaz|uygula|kur|gelistir|geliştir|duzelt|düzelt|implement|build|create|edit|fix)\b/i.test(instruction)) return "implement";
485
- if (/\b(incele|denetle|dogrula|doğrula|review|audit|verify|test et)\b/i.test(instruction)) return "review";
486
- if (/\b(arastir|araştır|research|web)\b/i.test(instruction)) return "research";
487
- if (/\b(planla|plan|tasarla|design)\b/i.test(instruction)) return "plan";
488
- return "implement";
489
- }
490
-
491
- // Gorev metni gercek bir yapim/degisiklik isi mi? Operator, plan evresinde "bilgi sorusu
492
- // kestirmesi" ile (delegasyon acmadan complete) yaniti dogrudan verebilir. Ama prompt acikca
493
- // bir uygulama/olusturma istiyorsa bu kestirme kullaniciya HIC is uretmeden sahte basari doner
494
- // (or. proje hafizasindaki gecmis teslimati "zaten yapildi" sanmak). Bu durumda kestirmeyi
495
- // engelleyip yeniden planlama isteriz. inferAssignmentKind'in implement fiil kumesiyle uyumlu.
496
- function taskRequiresDelegation(prompt) {
497
- return /\b(olustur|oluştur|yaz|uygula|kur|gelistir|geliştir|duzelt|düzelt|refactor|implement|build|create|edit|fix|generate|ekle|degistir|değiştir)\b/i.test(String(prompt || ""));
498
- }
499
-
500
- function roleAllowedKinds(agent) {
501
- const role = path.basename(String(agent?.roleFile || "")).toLowerCase();
502
- if (role.includes("executor")) return ["implement"];
503
- if (role.includes("review")) return ["review"];
504
- if (role.includes("planner")) return ["plan"];
505
- return null;
506
- }
507
-
508
- function supportsKind(agent, kind) {
509
- const roleKinds = roleAllowedKinds(agent);
510
- if (roleKinds) return roleKinds.includes(kind);
511
- const caps = capabilitiesFor(agent);
512
- const wanted = {
513
- implement: ["implementation", "coding", "development", "debugging"],
514
- review: ["review", "testing", "security", "analysis"],
515
- research: ["research", "web", "analysis"],
516
- plan: ["planning", "analysis", "architecture"],
517
- }[kind] || [];
518
- return wanted.some((cap) => caps.has(cap));
519
- }
520
-
521
- function agentUsable(agent) {
522
- return agent?.health?.status ? agent.health.status === "ready" : true;
523
- }
524
-
525
- function normalizeAssignments(value, cfg, operatorName, usedIds, taskText = "") {
526
- if (!Array.isArray(value)) throw new Error("Operator assignments dizisi dondurmedi.");
527
- const max = Math.max(1, cfg.operator?.maxDelegationsPerRound || 8);
528
- // Operator daha once kullanilan bir kimligi yinelerse gorevi oldurmek yerine kimligi
529
- // otomatik yeniden adlandiririz; saatlerce uretilmis teslimat bir ID cakismasi yuzunden
530
- // basarisiz sayilmamalidir. Ayni turdaki dependsOn referanslari da yeni ada tasinir.
531
- const renames = new Map();
532
- return value.slice(0, max).map((raw, index) => {
533
- let id = String(raw.id || `task-${Date.now()}-${index + 1}`).replace(/[^a-zA-Z0-9_.-]/g, "-");
534
- let renamedFrom;
535
- if (usedIds.has(id)) {
536
- let n = 2;
537
- while (usedIds.has(`${id}-r${n}`)) n++;
538
- renamedFrom = id;
539
- renames.set(id, `${id}-r${n}`);
540
- id = `${id}-r${n}`;
541
- }
542
- usedIds.add(id);
543
- const requestedAgent = String(raw.agent || "");
544
- if (!cfg.agents[requestedAgent]) throw new Error(`Operator tanimsiz agent secti: ${requestedAgent}`);
545
- if (cfg.agents[requestedAgent].enabled === false) throw new Error(`Operator devre disi agent secti: ${requestedAgent}`);
546
- if (!agentUsable(cfg.agents[requestedAgent])) throw new Error(`Operator kullanilamaz agent secti: ${requestedAgent} (${cfg.agents[requestedAgent].health?.label || "saglik testi basarisiz"})`);
547
- if (requestedAgent === operatorName) throw new Error("Operator kendisine uzman gorevi atayamaz.");
548
- const instruction = String(raw.instruction || raw.task || "").trim();
549
- if (!instruction) throw new Error(`${id} delegasyonunda instruction eksik.`);
550
- const kind = inferAssignmentKind(raw, instruction);
551
- // Beceriler kullanici-kapilidir: operator yalnizca cfg.skills.enabled icindekileri iliştirebilir.
552
- // Tanimsiz/etkin olmayan beceri sessizce dusurulur; asla gorevi olduren bir hata degildir.
553
- const hasExplicitSkills = Object.prototype.hasOwnProperty.call(raw, "skills");
554
- const autoMatchOn = cfg.skills?.autoMatch !== false;
555
- const requestedSkills = Array.isArray(raw.skills)
556
- ? raw.skills
557
- : (!hasExplicitSkills && autoMatchOn
558
- ? skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind)
559
- : []);
560
- let skills = skillRegistry.resolveForAssignment(requestedSkills, cfg).map((skill) => skill.name);
561
- // Operator kodlama/planlama/review isinde beceri iliştirmeyi atlarsa (or. skills:[] dönerse),
562
- // kullanicinin etkin becerilerinden goreve GERCEKTEN uyanlari otomatik ekle. suggest() yalnizca
563
- // lexical eslesme oldugunda skill döner; alakasiz beceri asla iliştirilmez. Boylece "operator
564
- // becerileri kullanmiyor" durumu ortadan kalkar.
565
- if (!skills.length && autoMatchOn && ["implement", "plan", "review"].includes(kind)) {
566
- skills = skillRegistry.resolveForAssignment(
567
- skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind), cfg
568
- ).map((skill) => skill.name);
569
- }
570
- let agent = requestedAgent;
571
- const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
572
- if (unavailable.has(agent) || !supportsKind(cfg.agents[agent], kind)) {
573
- const compatible = Object.entries(cfg.agents).find(([name, candidate]) => name !== operatorName && candidate.enabled !== false && agentUsable(candidate) && !unavailable.has(name) && supportsKind(candidate, kind));
574
- if (compatible) agent = compatible[0];
575
- else if (unavailable.has(agent)) throw new Error(`Operator bu oturumda kullanilamaz agent secti: ${requestedAgent}`);
576
- }
577
- return {
578
- id, agent, kind, instruction, skills,
579
- renamedFrom,
580
- requestedAgent: agent === requestedAgent ? undefined : requestedAgent,
581
- routingReason: agent === requestedAgent ? undefined : `${requestedAgent} ${unavailable.has(requestedAgent) ? "bu oturumda kullanilamaz" : `${kind} yetenegine sahip degil`}; ${agent} secildi.`,
582
- dependsOn: Array.isArray(raw.dependsOn) ? raw.dependsOn.map((dep) => renames.get(String(dep)) || String(dep)) : [],
583
- };
584
- });
585
- }
586
-
587
- function compatibleAgentForKind(cfg, operatorName, kind) {
588
- const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
589
- return Object.entries(cfg.agents || {}).find(([name, agent]) =>
590
- name !== operatorName && agent.enabled !== false && agentUsable(agent) &&
591
- !unavailable.has(name) && supportsKind(agent, kind)
592
- )?.[0] || "";
593
- }
594
-
595
- function nextAssignmentId(base, usedIds) {
596
- let id = base;
597
- let suffix = 2;
598
- while (usedIds.has(id)) id = `${base}-${suffix++}`;
599
- usedIds.add(id);
600
- return id;
601
- }
602
-
603
- // Balanced bir uygulama gorevinde katalogda standart roller varsa ilk turu
604
- // PLAN -> IMPLEMENT -> REVIEW olarak garanti eder. Operatorun bir rolu yanlislikla
605
- // atlamasi, kullanicinin etkinlestirdigi uzmani sessizce devre disi birakamaz.
606
- function ensureBalancedRoleChain(assignments, cfg, operatorName, task, usedIds) {
607
- if (task.executionMode !== "balanced" || !assignments.some((item) => item.kind === "implement")) return assignments;
608
- const chained = assignments.map((item) => ({ ...item, dependsOn: [...item.dependsOn] }));
609
- let plan = chained.find((item) => item.kind === "plan");
610
- if (!plan) {
611
- const planner = compatibleAgentForKind(cfg, operatorName, "plan");
612
- if (planner) {
613
- const instruction = "Kullanici hedefini ve mevcut calisma klasorunu salt okunur incele. Executor icin dosya kapsamini, uygulama siralamasini, riskleri ve dogrulama adimlarini iceren somut bir plan hazirla; dosyalari degistirme.";
614
- plan = {
615
- id: nextAssignmentId("balanced-plan", usedIds),
616
- agent: planner,
617
- kind: "plan",
618
- instruction,
619
- dependsOn: [],
620
- skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "plan"),
621
- routingReason: "Balanced rol zinciri: kullanilabilir planner ilk tura otomatik eklendi.",
622
- };
623
- chained.unshift(plan);
624
- }
625
- }
626
- const implementations = chained.filter((item) => item.kind === "implement");
627
- if (plan) {
628
- for (const implementation of implementations) {
629
- if (!implementation.dependsOn.includes(plan.id)) implementation.dependsOn.push(plan.id);
630
- }
631
- }
632
- let reviews = chained.filter((item) => item.kind === "review");
633
- if (!reviews.length) {
634
- const reviewer = compatibleAgentForKind(cfg, operatorName, "review");
635
- if (reviewer) {
636
- const instruction = "Tamamlanan uygulamayi kullanici hedefi ve kabul kriterlerine gore bagimsiz, salt okunur olarak denetle. Dosya ve test kanitlarini raporla; sonunda VERDICT: PASS veya VERDICT: FAIL yaz.";
637
- const review = {
638
- id: nextAssignmentId("balanced-review", usedIds),
639
- agent: reviewer,
640
- kind: "review",
641
- instruction,
642
- dependsOn: implementations.map((item) => item.id),
643
- skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "review"),
644
- routingReason: "Balanced rol zinciri: kullanilabilir reviewer ilk tura otomatik eklendi.",
645
- };
646
- chained.push(review);
647
- reviews = [review];
648
- }
649
- }
650
- for (const review of reviews) {
651
- // Operator belirli bir uygulamayi incelemeye bagladiysa bu kapsami genisletme;
652
- // ayni turdaki bagimsiz/yardimci bir implement hatasi ana incelemeyi bloke etmemeli.
653
- if (!review.dependsOn.length) {
654
- for (const implementation of implementations) review.dependsOn.push(implementation.id);
655
- }
656
- }
657
- const rank = { plan: 0, research: 1, implement: 2, review: 3 };
658
- return chained.map((item, index) => ({ item, index }))
659
- .sort((a, b) => (rank[a.item.kind] ?? 2) - (rank[b.item.kind] ?? 2) || a.index - b.index)
660
- .map(({ item }) => item);
661
- }
662
-
663
- class Engine extends EventEmitter {
664
- constructor() {
665
- super();
666
- this.running = false;
667
- this.busy = false;
668
- this.current = null;
669
- this.activeChild = null;
670
- this.callSequence = 0;
671
- // Kesin kimlik/kurulum hatasi veren agent'lari ayni motor oturumunda tekrar secme.
672
- // Kalici config'i degistirmeyiz; kullanici kimlik bilgisini duzeltip motoru yeniden
673
- // baslattiginda agent tekrar denenebilir.
674
- this.unhealthyAgents = new Map();
675
- // Canli kodlama akisi: gorev calisirken calisma klasorunu periyodik tarayan zamanlayici
676
- // ve son yayinlanan degisiklik imzasi (ayni durum tekrar tekrar yayinlanmasin diye).
677
- this._liveTimer = null;
678
- this._lastFileChangeKey = "";
679
- this._textBefore = null;
680
- }
681
-
682
- cfg() { return store.loadConfig(); }
683
-
684
- status() {
685
- const cfg = this.cfg();
686
- return {
687
- running: this.running,
688
- busy: this.busy,
689
- current: this.current,
690
- mode: cfg.approvalMode,
691
- defaultOperator: cfg.operator?.cli || "",
692
- callsToday: store.getCallCount(),
693
- budget: cfg.dailyCallBudget,
694
- usageToday: store.getDailyUsage(),
695
- };
696
- }
697
-
698
- // KULLANIM TELEMETRISI: gorev suresince agent bazinda token/maliyet toplar. Tamamen
699
- // additive ve hata toleranslidir — veri veremeyen CLI'lar (metin modunda calisan
700
- // codex/claude/gemini) icin sayaclar bos kalir, hicbir akis bundan etkilenmez.
701
- resetUsage() {
702
- this._usage = { total: { ...EMPTY_USAGE }, byAgent: {}, calls: 0, reportingCalls: 0 };
703
- }
704
-
705
- recordUsage(agentName, usage) {
706
- if (!this._usage) this.resetUsage();
707
- this._usage.total = addUsage(this._usage.total, usage);
708
- this._usage.byAgent[agentName] = addUsage(this._usage.byAgent[agentName], usage);
709
- this._usage.reportingCalls++;
710
- try { store.addDailyUsage(usage); } catch {}
711
- }
712
-
713
- // Gorev kapanisinda saklanacak sekil. Hicbir cagri veri vermediyse null doner ki arayuz
714
- // "0 token" gibi yaniltici bir sayi yerine "veri yok" gosterebilsin.
715
- usageSummary() {
716
- const usage = this._usage;
717
- if (!usage || !usage.reportingCalls) return null;
718
- return {
719
- total: usage.total,
720
- byAgent: usage.byAgent,
721
- calls: usage.calls,
722
- reportingCalls: usage.reportingCalls,
723
- };
724
- }
725
-
726
- publish(type, data, taskId = this.current?.id) {
727
- const event = { at: new Date().toISOString(), taskId: taskId || null, ...data, type };
728
- if (taskId) store.appendRunEvent(taskId, event);
729
- this.emit(type, event);
730
- return event;
731
- }
732
-
733
- // CANLI KODLAMA: gorev calisirken calisma klasorunu _snapBefore'a gore tarar ve olusan/
734
- // degisen/silinen dosyalari "filechange" olayiyla yayinlar; complete()'teki nihai diff'in
735
- // canli on-izlemesidir. Yalnizca durum degistiginde olay basar (ayni diff tekrarlanmaz).
736
- // Tamamen additive ve salt-okunurdur; hata verirse gorevi etkilemeden sessizce atlar.
737
- publishFileChanges(taskId = this.current?.id) {
738
- if (!taskId || !this._cwd || !this._snapBefore) return null;
739
- let changes, currentSnapshot;
740
- try {
741
- currentSnapshot = snapshotDir(this._cwd);
742
- changes = diffSnapshots(this._snapBefore, currentSnapshot);
743
- }
744
- catch { return null; }
745
- // Yalnizca dosya adlarini degil guncel stamp'i da imzaya kat. Ayni dosya gorev
746
- // sirasinda ikinci kez degistiginde yeni satir diff'i mutlaka yayinlanmalidir.
747
- const stampKey = [...changes.created, ...changes.modified]
748
- .map((file) => `${file}:${currentSnapshot.get(file) || ""}`).join("|");
749
- const key = `${stampKey}#${changes.deleted.join("|")}`;
750
- if (key === this._lastFileChangeKey) return null;
751
- const previouslyHadChanges = this._lastFileChangeKey !== "" && this._lastFileChangeKey !== "#";
752
- const changedFiles = [
753
- ...changes.created.map((file) => ({ file, action: "created" })),
754
- ...changes.modified.map((file) => ({ file, action: "modified" })),
755
- ...changes.deleted.map((file) => ({ file, action: "deleted" })),
756
- ];
757
- let remainingLines = DIFF_MAX_EVENT_LINES;
758
- const files = changedFiles.map(({ file, action }) => {
759
- if (remainingLines <= 0) return { path: file, action, additions: null, deletions: null, previewStatus: "event-limit", hunks: [] };
760
- const described = describeFileDiff(this._cwd, file, action, this._textBefore, { maxLines: Math.min(DIFF_MAX_RENDERED_LINES, remainingLines) });
761
- remainingLines -= (described.hunks || []).reduce((sum, hunk) => sum + (hunk.lines || []).length, 0);
762
- return described;
763
- });
764
- // Bos degisim setini (or. baslangictaki taban veya tum degisikliklerin geri alinmasi)
765
- // imzasini hatirla. Gorev basinda gurultu uretme; ancak daha once gorunen tum degisiklikler
766
- // geri alindiysa bos olayi yayinla ki dashboard bayat diff'i temizlesin.
767
- this._lastFileChangeKey = key;
768
- if (!files.length && !previouslyHadChanges) return null;
769
- return this.publish("filechange", {
770
- files,
771
- counts: { created: changes.created.length, modified: changes.modified.length, deleted: changes.deleted.length },
772
- lineCounts: {
773
- additions: files.reduce((sum, file) => sum + (Number(file.additions) || 0), 0),
774
- deletions: files.reduce((sum, file) => sum + (Number(file.deletions) || 0), 0),
775
- unavailable: files.filter((file) => file.additions == null || file.deletions == null).length,
776
- },
777
- }, taskId);
778
- }
779
-
780
- startLiveDiff(task) {
781
- this.stopLiveDiff();
782
- this._lastFileChangeKey = "";
783
- // liveDiff kesin olarak false yapilmadikca aciktir (mevcut davranisi degistirmeyen additive
784
- // ozellik). Tarama araligi config'ten ayarlanabilir; en dusuk 500ms ile sinirli.
785
- const cfg = this.cfg();
786
- if (cfg.liveDiff === false) { this._textBefore = null; return; }
787
- this._textBefore = captureTextSnapshot(this._cwd, this._snapBefore);
788
- const intervalMs = Math.max(500, Number(cfg.liveDiffIntervalMs) || 2500);
789
- this._liveTimer = setInterval(() => this.publishFileChanges(task.id), intervalMs);
790
- // unref: bekleyen bir tarama zamanlayicisi surecin kapanmasini asla engellemesin.
791
- if (this._liveTimer && typeof this._liveTimer.unref === "function") this._liveTimer.unref();
792
- }
793
-
794
- stopLiveDiff() {
795
- if (this._liveTimer) { clearInterval(this._liveTimer); this._liveTimer = null; }
796
- }
797
-
798
- setMode(mode) {
799
- const cfg = this.cfg();
800
- cfg.approvalMode = mode === "auto" ? "auto" : "ask";
801
- store.saveConfig(cfg);
802
- this.emit("status", this.status());
803
- }
804
-
805
- start() {
806
- if (this.running) return;
807
- if (!this.cfg().autonomousConsentAcceptedAt) {
808
- const error = new Error("Otonom CLI calistirma kosullari henuz kabul edilmedi. Dashboard'daki ilk kullanim uyarisini onaylayin.");
809
- error.code = "AUTONOMOUS_CONSENT_REQUIRED";
810
- throw error;
811
- }
812
- this.running = true;
813
- this.publish("log", { level: "info", msg: `Motor basladi. Mod=${this.cfg().approvalMode}` }, null);
814
- this.emit("status", this.status());
815
- this.loop();
816
- }
817
-
818
- stop() {
819
- this.running = false;
820
- this.wake();
821
- this.stopLiveDiff();
822
- if (this.activeChild) {
823
- try { this.activeChild.kill(); } catch {}
824
- }
825
- this.publish("log", { level: "warn", msg: "Motor durduruluyor; aktif CLI islemi sonlandirildi." });
826
- this.emit("status", this.status());
827
- }
828
-
829
- wake() {
830
- if (this._wakeResolve) {
831
- clearTimeout(this._wakeTimer);
832
- const resolve = this._wakeResolve;
833
- this._wakeResolve = null;
834
- resolve();
835
- }
836
- }
837
-
838
- sleepWake(ms) {
839
- return new Promise((resolve) => {
840
- this._wakeResolve = resolve;
841
- this._wakeTimer = setTimeout(() => { this._wakeResolve = null; resolve(); }, ms);
842
- });
843
- }
844
-
845
- async loop() {
846
- while (this.running) {
847
- const task = store.nextPending();
848
- if (!task) {
849
- await this.sleepWake((this.cfg().pollSeconds || 15) * 1000);
850
- continue;
851
- }
852
- try {
853
- await this.runTask(task);
854
- } catch (error) {
855
- if (!this.salvage(task, error)) {
856
- const found = store.findTask(task.id);
857
- const failure = classifyCliError(error);
858
- task.status = "failed";
859
- task.error = error.message;
860
- // Siniflandirilmis teshis gorevle birlikte saklanir; arayuz "ne oldu / ne yapmali"
861
- // ayrimini ham hata metnini kirparak tahmin etmek yerine buradan okur.
862
- task.failure = failure;
863
- // Basarisiz gorev de para harcar; telemetri burada da saklanmali.
864
- task.usage = this.usageSummary();
865
- task.finishedAt = new Date().toISOString();
866
- if (found) store.moveTask(found.state, "failed", task);
867
- this.publish("log", { level: "error", msg: `HATA (${failure.code}): ${failure.summary}` }, task.id);
868
- this.publish("log", { level: "warn", msg: `Ne yapmali: ${failure.action}` }, task.id);
869
- if (task.kind === "operator-chat") {
870
- this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: task.parentTaskId, status: "failed", error: failure.summary, failure }, task.id);
871
- }
872
- this.notifyOutcome(task, "failed", { error: failure.summary, failureCode: failure.code, action: failure.action });
873
- this.emit("queue");
874
- }
875
- } finally {
876
- this.stopLiveDiff();
877
- this._textBefore = null;
878
- this.busy = false;
879
- this.current = null;
880
- this.activeChild = null;
881
- this.emit("status", this.status());
882
- }
883
- }
884
- }
885
-
886
- // Uzman agent'i (operatorun altindaki ekip) cfg.agents'ten cozup calistirir.
887
- invokeAgent(agentName, prompt, cfg, meta = {}) {
888
- const configuredAgent = cfg.agents[agentName];
889
- const agent = configuredAgent && cliRegistry.effectiveAgent(configuredAgent, cfg);
890
- if (!agent) return Promise.reject(new Error(`Agent tanimsiz: ${agentName}`));
891
- return this.runCli(agentName, agent, prompt, cfg, meta);
892
- }
893
-
894
- // Operatoru, uzman agent'lardan BAGIMSIZ olarak dogrudan CLI'dan (claude/codex/gemini/opencode)
895
- // calistirir. operator.md rolunu bu CLI giyer.
896
- invokeOperator(cli, prompt, cfg, meta = {}) {
897
- const agent = cliRegistry.operatorSpec(cli, cfg);
898
- if (!agent) return Promise.reject(new Error(`Operator CLI tanimsiz veya kurulu degil: ${cli}`));
899
- return this.runCli(cli, agent, prompt, cfg, meta);
900
- }
901
-
902
- runCli(displayName, agent, prompt, cfg, meta = {}) {
903
- return new Promise((resolve, reject) => {
904
- const count = store.bumpCallCount();
905
- if (count > (cfg.dailyCallBudget || Number.MAX_SAFE_INTEGER)) {
906
- return reject(new Error(`Gunluk cagri butcesi asildi (${cfg.dailyCallBudget}).`));
907
- }
908
- if (!this._usage) this.resetUsage();
909
- this._usage.calls++;
910
-
911
- const callId = `${this.current?.id || "run"}-${++this.callSequence}`;
912
- let promptFile = "";
913
- let useStdin = true;
914
- const rawArgs = (agent.args || []).map((arg) => {
915
- if (String(arg).includes("{PROMPT}")) {
916
- useStdin = false;
917
- return String(arg).replaceAll("{PROMPT}", prompt);
918
- }
919
- if (String(arg).includes("{PROMPT_FILE}")) {
920
- useStdin = false;
921
- if (!promptFile) {
922
- const promptDir = path.join(store.ROOT, "state", "prompts");
923
- fs.mkdirSync(promptDir, { recursive: true });
924
- promptFile = path.join(promptDir, `${callId}.md`);
925
- fs.writeFileSync(promptFile, prompt, "utf8");
926
- }
927
- return String(arg).replaceAll("{PROMPT_FILE}", promptFile);
928
- }
929
- return String(arg);
930
- });
931
- const command = cliRegistry.buildCommand(agent.cmd, rawArgs);
932
- const file = command.file;
933
- const args = command.args;
934
- const cwd = this._cwd || path.resolve(store.WORK_BASE, cfg.workingDir || ".");
935
- const started = Date.now();
936
- let stdout = "", stderr = "", settled = false, timedOut = false, silenceTimedOut = false;
937
- let timer, silenceTimer, progressTimer;
938
- const base = { callId, agent: displayName, stage: meta.stage || "agent", assignmentId: meta.assignmentId || null };
939
-
940
- this.current = { id: this.current?.id, stage: base.stage, agent: displayName, callId };
941
- this.emit("status", this.status());
942
- this.publish("activity", { ...base, kind: "process.started", cmd: agent.cmd, args: rawArgs, cwd });
943
-
944
- // Motor onceliklidir: canli CLI saglik/model probe'lari (or. codex app-server / codex exec)
945
- // ayni CLI'yi ayni anda calistirirsa codex ikinci exec oturumunu SIGTERM ile keser. Bu yuzden
946
- // operatör/agent kosumuna girmeden uctaki tum probe'lari sonlandiriyoruz.
947
- cliRegistry.abortActiveProbes();
948
- let child;
949
- try { child = spawn(file, args, { cwd, env: cliRegistry.agentEnvironment(agent), windowsHide: true, shell: command.shell, windowsVerbatimArguments: !!command.verbatim }); }
950
- catch (error) { return reject(error); }
951
- this.activeChild = child;
952
- child.stdout.setEncoding("utf8");
953
- child.stderr.setEncoding("utf8");
954
-
955
- const timeoutMs = Math.max(10, agent.timeoutSeconds || cfg.agentTimeoutSeconds || 900) * 1000;
956
- const silenceTimeoutMs = Math.max(1, agent.silenceTimeoutSeconds || cfg.cliSilenceTimeoutSeconds || 300) * 1000;
957
- let lastOutputAt = Date.now();
958
- const terminateTree = () => {
959
- if (isWin && child.pid) {
960
- try { spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" }); } catch {}
961
- } else {
962
- try { child.kill("SIGKILL"); } catch {}
963
- }
964
- };
965
- const armSilenceTimer = () => {
966
- clearTimeout(silenceTimer);
967
- silenceTimer = setTimeout(() => {
968
- if (settled) return;
969
- silenceTimedOut = true;
970
- this.publish("activity", { ...base, kind: "process.silence-timeout", silenceTimeoutMs, elapsedMs: Date.now() - started });
971
- terminateTree();
972
- }, silenceTimeoutMs);
973
- };
974
- armSilenceTimer();
975
- progressTimer = setInterval(() => {
976
- if (settled) return;
977
- this.publish("activity", { ...base, kind: "process.progress", elapsedMs: Date.now() - started, silentMs: Date.now() - lastOutputAt, silenceTimeoutMs });
978
- }, 15000);
979
- timer = setTimeout(() => {
980
- if (settled) return;
981
- timedOut = true;
982
- this.publish("activity", { ...base, kind: "process.timeout", timeoutMs });
983
- terminateTree();
984
- }, timeoutMs);
985
-
986
- child.stdout.on("data", (data) => {
987
- const text = String(data);
988
- stdout += text;
989
- lastOutputAt = Date.now();
990
- armSilenceTimer();
991
- this.publish("activity", { ...base, kind: "stdout", text });
992
- });
993
- child.stderr.on("data", (data) => {
994
- const text = String(data);
995
- stderr += text;
996
- lastOutputAt = Date.now();
997
- armSilenceTimer();
998
- this.publish("activity", { ...base, kind: "stderr", text });
999
- });
1000
- child.on("error", (error) => {
1001
- if (settled) return;
1002
- settled = true;
1003
- clearTimeout(timer);
1004
- clearTimeout(silenceTimer);
1005
- clearInterval(progressTimer);
1006
- this.activeChild = null;
1007
- if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1008
- this.publish("activity", { ...base, kind: "process.failed", error: error.message, durationMs: Date.now() - started });
1009
- reject(error);
1010
- });
1011
- child.on("close", (code, signal) => {
1012
- if (settled) return;
1013
- settled = true;
1014
- clearTimeout(timer);
1015
- clearTimeout(silenceTimer);
1016
- clearInterval(progressTimer);
1017
- this.activeChild = null;
1018
- if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1019
- const durationMs = Date.now() - started;
1020
- // Cikti tek kez ayristirilir; hem kullanim telemetrisi hem metin ayni sonuctan okunur.
1021
- const normalized = normalizeCliOutput(agent, stdout);
1022
- if (normalized.usage) this.recordUsage(displayName, normalized.usage);
1023
- this.publish("activity", { ...base, kind: "process.finished", exitCode: code, signal, durationMs, usage: normalized.usage, reason: silenceTimedOut ? "silence-timeout" : (timedOut ? "timeout" : null) });
1024
- if (silenceTimedOut) return reject(new Error(`CLI_STALLED: ${agent.cmd} ${Math.round(silenceTimeoutMs / 1000)} saniye boyunca cikti uretmedi ve otomatik durduruldu.`));
1025
- if (timedOut) return reject(new Error(`${agent.cmd} ${Math.round(timeoutMs / 1000)} saniyede zaman asimina ugradi${signal ? ` (signal=${signal})` : ""}. ${clip(stdout || stderr, 500)}`));
1026
- // Teshis icin HER IKI akis da gerekir: bazi CLI'lar (Gemini dahil) kota/oturum hatasini
1027
- // stdout'a, yigin izini stderr'e yazar. Yalnizca birini almak siniflandiriciyi kor birakiyordu.
1028
- if (code !== 0) {
1029
- const detail = [stderr, stdout].map((stream) => String(stream || "").trim()).filter(Boolean).join("\n");
1030
- return reject(new Error(`${agent.cmd} cikis kodu ${code ?? "yok"}${signal ? ` (signal=${signal})` : ""}.\n${clip(detail, 2000)}`));
1031
- }
1032
- if (normalized.error) return reject(new Error(`OpenCode model/provider hatasi: ${clip(normalized.error, 500)}`));
1033
- if (!normalized.text) return reject(new Error(`${agent.cmd} kullanilabilir cikti dondurmedi.${stderr ? ` ${clip(stderr, 300)}` : ""}`));
1034
- resolve({ text: normalized.text, stderr: stderr.trim(), exitCode: code, durationMs, callId, usage: normalized.usage });
1035
- });
1036
-
1037
- // Prompt argumanla/dosyayla verilse bile stdin MUTLAKA kapatilmali. OpenCode gibi CLI'lar
1038
- // stdin TTY degilse borudan mesaj okur; EOF gelmezse model cagrisina hic gecmeden bloke olur.
1039
- child.stdin.on("error", () => {});
1040
- child.stdin.end(useStdin ? prompt : "");
1041
- });
1042
- }
1043
-
1044
- // Katalogda dosya yazabilen (implement) kullanilabilir bir uzman var mi? Operatorun kendisi ve
1045
- // bu oturumda karantinaya alinan agentler haric. Tek executor (or. opencode) sessizlik nedeniyle
1046
- // dustugunde takim fiilen dosya uretemez hale gelir; bu kontrol o durumu erken yakalar.
1047
- hasUsableImplementAgent(cfg, operatorName) {
1048
- return Object.entries(cfg.agents || {}).some(([name, agent]) =>
1049
- name !== operatorName && agent.enabled !== false && agentUsable(agent)
1050
- && !this.unhealthyAgents.has(name) && supportsKind(agent, "implement"));
1051
- }
1052
-
1053
- agentCatalog(cfg, operatorName) {
1054
- return Object.entries(cfg.agents)
1055
- .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.unhealthyAgents.has(name))
1056
- .map(([name, a]) => ({
1057
- name,
1058
- description: a.description || "",
1059
- capabilities: [...capabilitiesFor(a)],
1060
- allowedKinds: roleAllowedKinds(a) || ["implement", "review", "research", "plan"].filter((kind) => supportsKind(a, kind)),
1061
- roleFile: a.roleFile || "",
1062
- costTier: a.costTier || "standard",
1063
- }));
1064
- }
1065
-
1066
- async invokeOperatorJson(operatorCli, prompt, cfg, stage, label) {
1067
- const retries = Math.max(0, cfg.operator?.protocolRetries ?? 1);
1068
- let lastError;
1069
- let correction = "";
1070
- for (let attempt = 0; attempt <= retries; attempt++) {
1071
- try {
1072
- const response = await this.invokeOperator(operatorCli, prompt + correction, cfg, { stage });
1073
- return parseJson(response.text, label);
1074
- } catch (error) {
1075
- lastError = error;
1076
- // Yalnizca gercek JSON protokol hatasi yeniden denenir. CLI'nin sessiz kalmasi,
1077
- // auth/timeout veya proses hatasi ayni operatoru tekrar calistirmamalidir.
1078
- if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) throw error;
1079
- if (attempt >= retries) break;
1080
- // Modelin ne dondurdugunu duzeltme istegine geri veriyoruz: "yalnizca JSON dondur"
1081
- // talimatini tekrarlamak, hatayi goremeyen model icin cogu zaman ayni cevabi uretiyordu.
1082
- correction = `\n\nONCEKI CEVAP PROTOKOLE UYMADI: ${error.message}\n` +
1083
- `Senin onceki cevabin (kirpilmis):\n"""\n${clip(String(error.rawText || ""), 1200)}\n"""\n` +
1084
- `Bu cevap gecerli bir JSON nesnesi degildi. Simdi SADECE tek bir JSON nesnesi dondur: ` +
1085
- `ilk karakter { ve son karakter } olsun; oncesinde/sonrasinda selamlama, aciklama, dusunce, ` +
1086
- `Markdown veya \`\`\` kod bloğu OLMASIN. Metin alanlarindaki cift tirnaklari \\" ile kacir.`;
1087
- this.publish("log", { level: "warn", msg: `${label} protokol hatasi; operator yeniden deneniyor (${attempt + 2}/${retries + 1}).` });
1088
- }
1089
- }
1090
- throw lastError;
1091
- }
1092
-
1093
- // Insertion sirasi tamamlanma sirasidir; en son biten denetim en gecerli karardir.
1094
- latestReview(state) {
1095
- return Object.values(state.results || {}).reverse()
1096
- .find((result) => result.kind === "review" && result.status === "completed" && result.verdict) || null;
1097
- }
1098
-
1099
- // Operatorun karar evresine verilen takim kaydi. Ham state dokumu yerine sonuc odakli
1100
- // bir ozet uretir; her sonucu kendi icinde ortadan kirpar ki en yeni denetim karari
1101
- // (metnin sonundaki VERDICT satiri) butce asiminda kaybolmasin.
1102
- teamDigest(state, cfg) {
1103
- const results = Object.values(state.results || {});
1104
- const budget = cfg.teamContextCharBudget || 30000;
1105
- const perResult = Math.max(900, Math.floor(budget / Math.max(1, results.length)));
1106
- const digest = {
1107
- round: state.round,
1108
- completionCriteria: state.criteria,
1109
- results: results.map((result) => ({
1110
- id: result.id, agent: result.agent, kind: result.kind, status: result.status,
1111
- ...(result.verdict ? { verdict: result.verdict } : {}),
1112
- result: clipMiddle(result.result, perResult),
1113
- })),
1114
- };
1115
- return clipMiddle(JSON.stringify(digest, null, 2), budget);
1116
- }
1117
-
1118
- operatorPrompt(task, cfg, memory, state, phase) {
1119
- const operatorCli = task.operatorCli || cfg.operator?.cli;
1120
- // Operatör rolü daima operator.md'dir; operatör bir CLI'dir, uzman agent'lar onun altında çalışır.
1121
- const roleFile = cfg.operator?.roleFile || "roles/operator.md";
1122
- const role = store.readRole(path.basename(roleFile));
1123
- const catalog = this.agentCatalog(cfg, operatorCli);
1124
- const skillContext = `${task.prompt}\n${phase === "review" ? Object.values(state.results || {}).map((result) => result.instruction || "").join("\n") : ""}`;
1125
- const skillDiscovery = skillRegistry.discover(cfg, skillContext);
1126
- const skillCatalog = skillDiscovery.catalog;
1127
- const skillStats = skillDiscovery.stats;
1128
- const enabledSkillNames = skillRegistry.enabledSkills(cfg).map((skill) => skill.name);
1129
- // Beceri envanteri motor tarafindan uretilen OTORITER veridir. Operatore TAM etkin listeyi
1130
- // (sayi + tum adlar) DAIMA veririz; boylece "kac beceri var" gibi sorular icin operatorun
1131
- // kendi CLI'sinin dahili becerilerini veya bir alt agent'in iddiasini kullanmasi gerekmez.
1132
- // Eslesen kisa liste yalnizca DELEGASYONA iliştirme icindir, envanterin tamami degildir.
1133
- const skillSection = skillStats.enabled
1134
- ? `## Beceriler (OTORITER kaynak)\nSistemde tam olarak ${skillStats.enabled} etkin beceri var: ${enabledSkillNames.join(", ")}.\n` +
1135
- `Beceri sayisi/adi/varligi sorulursa YALNIZCA bu listeyi esas al ve dogrudan kendin cevapla; calistigin CLI'nin dahili becerilerini bu sisteme KATMA, bir alt agent farkli bir sayi soylerse bu listeyi degil onu YOK say.\n` +
1136
- (skillCatalog.length
1137
- ? `Bu goreve en ilgili ${skillCatalog.length} beceri (delegasyonun skills alanina yalnizca bunlardan uygun olanlari ekle):\n${JSON.stringify(skillCatalog, null, 2)}\n`
1138
- : `Bu goreve gore taramada eslesme cikmadi; bu delegasyonlarda beceri iliştirme.\n`)
1139
- : "";
1140
- // Beceriler tamamen opsiyoneldir ve YALNIZCA kullanici etkinlestirdiginde katalogda gorunur.
1141
- // Operator, assignment'a yalnizca bu kataloktaki adlari "skills" dizisinde iliştirebilir.
1142
- const skillField = skillCatalog.length ? `, "skills":["beceri-adi"]` : "";
1143
- const skillProtocol = skillCatalog.length
1144
- ? ` BECERI: Kisa listedeki gercekten ilgili adlari skills dizisine ekle; ilgisiz veya liste disi ad kullanma, uygun yoksa alani bos birak.`
1145
- : "";
1146
- const protocol = phase === "plan"
1147
- ? `Yalnizca gecerli JSON dondur. Gorev bir is/degisiklik/arastirma gerektiriyorsa delege et: {"summary":"yaklasim", "completionCriteria":["..."], "assignments":[{"id":"benzersiz-id", "agent":"catalog-name", "kind":"implement|review|research|plan", "instruction":"net gorev ve teslimat", "dependsOn":[]${skillField}}]} (en az bir assignment). ` +
1148
- `Gorev yalnizca bir bilgi/soru VEYA bir selamlasma/sohbet mesaji ise (or. "merhaba", "nasilsin", "kac beceri var") ve yaniti dogrudan verebiliyorsan, HICBIR delegasyon acmadan: {"status":"complete", "final":"kullaniciya gosterilecek CEVABIN TAM METNI", "verification":"cevabin dayandigi kaynak"}. ` +
1149
- `Selamlasmalarda bile cevabi JSON DISINA yazma: kullaniciya gosterilen tek alan "final"dir, nesnenin disindaki her metin atilir. Gercek is gerektiren gorevi bu kestirmeyle atlatma.${skillProtocol}`
1150
- : `Yalnizca gecerli JSON dondur. Is tamamlanmadiysa {"status":"continue", "reason":"...", "assignments":[{"id":"...", "agent":"...", "kind":"implement|review|research|plan", "instruction":"...", "dependsOn":[]${skillField}}]}; tum kabul kriterleri karsilandiysa {"status":"complete", "final":"en fazla 5 kisa maddeyle ne yapildi", "verification":"tek satir dogrulama"}. Dosya listesini motor ekleyecek; final icinde uzun log veya ham agent cevabi tekrarlama. Continue icin en az bir yeni assignment zorunlu.${skillProtocol}\n` +
1151
- `INCELEME DISIPLINI: Bagimsiz denetim VERDICT: PASS verdiyse ayni teslimat icin YENI review delegasyonu acma; complete dondur ve dusuk/orta onemdeki kalan notlari final metninde kalan risk olarak belirt. Ekipte bulunmayan dogrulama yetenegini (or. canli tarayici) tamamlanma sarti yapma; NOT RUN kalan dusuk riskli kontroller teslimati engellemez.`;
1152
- const strategy = task.executionMode === "fast"
1153
- ? "HIZLI MOD: Kucuk bir is. Tek bir implementation agenti kullan. Ayri planlama veya review delegasyonu acma; uzman raporu hedefi karsiliyorsa ilk degerlendirmede tamamla."
1154
- : task.executionMode === "deep"
1155
- ? "DERIN MOD: Isi uygun uzmanliklar arasinda dagit planlama, uygulama, test ve bagimsiz inceleme icin AYRI ve dogru uzmanlari kullan."
1156
- : "DENGELI MOD: Katalogda planner, executor ve reviewer varsa ucunu da ILK planda kullan. PLANLAMA -> UYGULAMA -> BAGIMSIZ INCELEME delegasyonlarini dependsOn ile ayni turda zincirle ve isi tek turda bitirmeyi hedefle. Ayni rolde birden fazla esdeger agent varsa yalnizca en uygun olani sec.";
1157
- return `${role}\n\n---\n## Operator protokolu\n${protocol}\n` +
1158
- // OpenCode gibi sohbet odakli CLI'lar varsayilan olarak once bir cumle yazip JSON'u
1159
- // arkasina ekliyor; motor bunu ayiklayabilse de belirsizlik protokol hatasi uretiyordu.
1160
- `## Cikti bicimi (KATI)\nCevabin TAMAMI tek bir JSON nesnesi olmali: ilk karakter { , son karakter } . ` +
1161
- `Oncesinde/sonrasinda selamlama, aciklama, dusunce notu, Markdown basligi veya \`\`\` kod bloğu YAZMA. ` +
1162
- `Kullaniciya iletmek istedigin her seyi ilgili JSON alanina ("final" veya "summary") koy; nesnenin disindaki metin kullaniciya ULASMAZ.\n` +
1163
- `Yalnizca katalogdaki agent adlarini kullan. Her assignment kind degeri secilen agentin allowedKinds listesinde olmali. Kendine gorev atama. Ayni delegasyon id'sini tekrar kullanma.\n` +
1164
- `Bir agent basarisiz veya kullanilamaz raporlandiysa ayni isi ona tekrar verme; katalogdaki alternatif bir uzmani sec.\n` +
1165
- `AJAN SECIMI: Her alt gorevi, katalogdaki YETENEKLERE ve role gore o ise EN UYGUN uzmana ata; tum isi tek bir CLI'ye yigma. Isin ihtiyac duydugu her uzmanlik icin dogru uzmani sec (plan/tasarim -> planlama yetenegi; uygulama -> implementation; inceleme/dogrulama -> review/test; arastirma -> research/web). Ayni KIND icin birden fazla eşdeğer agent varsa YALNIZCA birini (en uygun ve en dusuk maliyetli) kullan; ayni isi iki eşdeğer agente verme. Farkli roller (or. bir uygulayici + bir inceleyici) FARKLI islerdir, tekrar sayilmaz.\n` +
1166
- `## Calisma stratejisi\n${strategy}\n` +
1167
- `## Agent katalogu\n${JSON.stringify(catalog, null, 2)}\n` +
1168
- skillSection +
1169
- `## Kullanici gorevi\n${task.prompt}\n` +
1170
- `## 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` +
1172
- (phase === "review"
1173
- ? `## Son bagimsiz denetim\n${(() => { const review = this.latestReview(state); return review ? `${review.id} (${review.agent}) → VERDICT: ${review.verdict}` : "Henuz tamamlanmis denetim yok."; })()}\n` +
1174
- `## Takim calisma kaydi\n${this.teamDigest(state, cfg)}\n`
1175
- : "");
1176
- }
1177
-
1178
- specialistPrompt(task, cfg, assignment, state) {
1179
- const agent = cfg.agents[assignment.agent];
1180
- const roleByKind = { implement: "executor.md", review: "reviewer.md", plan: "planner.md" };
1181
- const selectedRole = roleByKind[assignment.kind] || (agent.roleFile ? path.basename(agent.roleFile) : "executor.md");
1182
- const role = store.readRole(selectedRole);
1183
- const instructionByKind = {
1184
- implement: "Delegasyonu calisma klasorunde uygula, degisiklikleri dogrula ve rolundeki teslimat formatinda raporla.",
1185
- review: "Teslimati bagimsiz ve salt okunur olarak incele; gerekli kontrolleri calistir, dosyalari degistirme ve rolundeki karar formatinda raporla.",
1186
- plan: "Calisma klasorunu salt okunur olarak incele; hicbir dosyayi degistirmeden uygulanabilir plani rolundeki formatta raporla.",
1187
- research: "Delegasyon kapsaminda kanit topla; kaynaklari ve belirsizlikleri ayirarak kisa bir sonuc raporla.",
1188
- };
1189
- const completed = Object.values(state.results).map((r) => ({
1190
- id: r.id, agent: r.agent, instruction: r.instruction, ...(r.verdict ? { verdict: r.verdict } : {}), result: clipMiddle(r.result, 5000),
1191
- }));
1192
- // Progressive disclosure: operatorun bu delegasyona sectigi becerilerin tam govdesini YIGMAYIZ;
1193
- // yalnizca ad + kisa ozet + rehber dosya yolunu veririz. Uzman gercekten ihtiyac duyarsa dosyayi
1194
- // kendisi okur. Operator hicbir beceri secmediyse (veya beceriler kapali ise) blok bostur.
1195
- const skills = skillRegistry.resolveForAssignment(assignment.skills, cfg);
1196
- const skillRefs = skillRegistry.toPromptRefs(skills, cfg.skills?.referenceCharBudget || 1200);
1197
- return `${role}\n\n---\n## Takim agenti protokolu\n` +
1198
- `Operator sana asagidaki isi devretti. ${instructionByKind[assignment.kind] || instructionByKind.implement} ` +
1199
- `Planda olmayan riskli bir is gerekiyorsa yapma; BLOCKED olarak bildir. Yalnizca gercekten gozlemledigin veya dogruladigin sonuclari yaz.\n` +
1200
- (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
- `## Ana hedef\n${task.prompt}\n## Delegasyon\nID: ${assignment.id}\n${assignment.instruction}\n` +
1202
- `## Onceki tamamlanan takim isleri\n${clip(JSON.stringify(completed, null, 2), cfg.teamContextCharBudget || 30000)}`;
1203
- }
1204
-
1205
- async runAssignments(task, cfg, assignments, state) {
1206
- const pending = assignments.slice();
1207
- while (pending.length) {
1208
- let index = pending.findIndex((a) => a.dependsOn.every((id) => state.results[id]?.status === "completed"));
1209
- if (index < 0) {
1210
- index = pending.findIndex((a) => a.dependsOn.some((id) => state.results[id] && state.results[id].status !== "completed"));
1211
- if (index >= 0) {
1212
- const blocked = pending.splice(index, 1)[0];
1213
- state.usedIds.push(blocked.id);
1214
- const failedDeps = blocked.dependsOn.filter((id) => state.results[id]?.status !== "completed");
1215
- state.results[blocked.id] = { ...blocked, status: "blocked", result: `Bagimli gorev basarisiz: ${failedDeps.join(", ")}` };
1216
- const blockedMessage = { from: "system", to: task.operatorCli, messageType: "blocked", assignmentId: blocked.id, body: `${blocked.id} calistirilmadi; bagimli gorev basarisiz: ${failedDeps.join(", ")}`, at: new Date().toISOString() };
1217
- state.messages.push(blockedMessage);
1218
- this.publish("message", blockedMessage, task.id);
1219
- continue;
1220
- }
1221
- throw new Error(`Delegasyon bagimliliklari cozumlenemedi: ${pending.map((a) => a.id).join(", ")}`);
1222
- }
1223
- const assignment = pending.splice(index, 1)[0];
1224
- state.usedIds.push(assignment.id);
1225
- state.messages.push({ from: task.operatorCli, to: assignment.agent, messageType: "delegation", assignmentId: assignment.id, body: assignment.instruction, at: new Date().toISOString() });
1226
- this.publish("message", state.messages[state.messages.length - 1], task.id);
1227
- if (assignment.routingReason) this.publish("log", { level: "info", msg: `Otomatik yonlendirme: ${assignment.routingReason}` }, task.id);
1228
- if (assignment.renamedFrom) this.publish("log", { level: "warn", msg: `Operator delegasyon kimligini yineledi; ${assignment.renamedFrom} otomatik olarak ${assignment.id} yapildi.` }, task.id);
1229
- this.publish("log", { level: "stage", msg: `DELEGE ${assignment.id} -> ${assignment.agent}` }, task.id);
1230
- if (assignment.skills?.length) this.publish("log", { level: "info", msg: `Beceriler: ${assignment.skills.join(", ")}` }, task.id);
1231
- 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 };
1239
- if (assignment.kind === "review") {
1240
- result.verdict = extractVerdict(response.text);
1241
- this.publish("log", { level: "info", msg: `Denetim ${assignment.id}: VERDICT ${result.verdict || "BELIRSIZ"}` }, task.id);
1242
- }
1243
- 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() };
1245
- state.messages.push(message);
1246
- this.publish("message", message, task.id);
1247
- } catch (error) {
1248
- 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() };
1255
- state.messages.push(message);
1256
- this.publish("message", message, task.id);
1257
- this.publish("log", { level: "warn", msg: `${assignment.agent} kullanilamadi [${failure.code}]. Operator alternatif agent sececek.` }, task.id);
1258
- }
1259
- task.teamState = state;
1260
- store.saveTask("pending", task);
1261
- }
1262
- }
1263
-
1264
- isRisky(text, cfg) {
1265
- const lower = String(text).toLowerCase();
1266
- return (cfg.riskyPatterns || []).some((pattern) => lower.includes(String(pattern).toLowerCase()));
1267
- }
1268
-
1269
- async runTask(task) {
1270
- const baseCfg = this.cfg();
1271
- if (!baseCfg.autonomousConsentAcceptedAt) throw new Error("Otonom CLI calistirma kosullari kabul edilmeden gorev calistirilamaz.");
1272
- if (task.kind === "operator-chat") return this.runChatTask(task, baseCfg);
1273
- task.executionMode = resolveExecutionMode(task);
1274
- const cfg = applyExecutionPolicy(baseCfg, task.executionMode);
1275
- const operatorCli = task.operatorCli || cfg.operator?.cli;
1276
- if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Gecerli bir operator CLI secilmedi (claude/codex/gemini/opencode).");
1277
- task.operatorCli = operatorCli;
1278
- this.busy = true;
1279
- this.current = { id: task.id, stage: "operator", agent: operatorCli };
1280
- this._cwd = path.resolve(store.WORK_BASE, task.targetDir || cfg.workingDir || ".");
1281
- this._snapBefore = snapshotDir(this._cwd);
1282
- this.startLiveDiff(task);
1283
- // Onay sonrasi devam eden gorevde sayaci sifirlamayiz; ilk girise ozgu.
1284
- if (!task.approved || !this._usage) this.resetUsage();
1285
- this.publish("log", { level: "task", msg: `GOREV ${task.id}: ${task.prompt}` }, task.id);
1286
- // Otomatik surumleme: gorev CALISMADAN ONCE calisma klasorunun bir surumunu al ki ajan
1287
- // mevcut kodu bozarsa bu gorev oncesine tek tikla donulebilsin. Yalnizca ilk girus'te
1288
- // (approval sonrasi devam da ayni checkpoint'i korur). Basarisizsa gorevi asla oldurmez.
1289
- if (baseCfg.versioning !== false && !task.checkpointId) {
1290
- const cp = checkpoints.createCheckpoint(this._cwd, { taskId: task.id, label: task.prompt, kind: "pre-task", retention: baseCfg.versioningRetention });
1291
- if (cp.ok) {
1292
- task.checkpointId = cp.id;
1293
- store.saveTask("pending", task);
1294
- this.publish("log", { level: "info", msg: `Surum alindi (${cp.fileCount} dosya · ${cp.backend}); bu gorev oncesine geri donulebilir.` }, task.id);
1295
- } else if (cp.skipped) {
1296
- this.publish("log", { level: "warn", msg: `Surum alinamadi: ${cp.reason}` }, task.id);
1297
- }
1298
- }
1299
- this.publish("log", { level: "info", msg: `Calisma modu: ${task.executionMode.toUpperCase()} · en fazla ${cfg.operator.maxRounds} tur / ${cfg.operator.maxDelegationsPerRound} delegasyon` }, task.id);
1300
- this.emit("status", this.status());
1301
- this.emit("queue");
1302
-
1303
- cfg.runtimeUnavailableAgents = [...this.unhealthyAgents.keys()];
1304
- const memory = store.getMemory(cfg.memoryCharBudget);
1305
- const state = task.teamState || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1306
- const usedIds = new Set(state.usedIds || []);
1307
-
1308
- let assignments;
1309
- if (state.plan && task.approved) {
1310
- assignments = normalizeAssignments(state.plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1311
- assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1312
- this.publish("log", { level: "info", msg: "Onaylanmis operator plani zorunlu rol zinciri korunarak devam ettiriliyor." }, task.id);
1313
- } else {
1314
- // Bilgi sorusu kestirmesi: operator, delege edilecek bir is olmadan yaniti dogrudan
1315
- // verdiginde (or. "kac beceri var") tek turda tamamla. Boylece motorun otoriter verisi
1316
- // bir alt agente aktarilirken kaybolmaz/carpitilmaz ve zorunlu assignment semasi bu
1317
- // tur sorularda gorevi bosuna oldurmez. ANCAK gorev acikca bir yapim/degisiklik isiyse
1318
- // kestirmeye izin verilmez: operator (cogunlukla proje hafizasindaki gecmis teslimati
1319
- // "zaten yapildi" sanip) is uretmeden kapatmaya calisir. Once yeniden planlama isteriz;
1320
- // israr ederse gorevi sahte basari yerine net bir hatayla dururuz.
1321
- const requiresWork = taskRequiresDelegation(task.prompt);
1322
- const shortcutRetries = requiresWork ? Math.max(1, cfg.operator?.protocolRetries ?? 1) : 0;
1323
- let plan, shortcutAttempt = 0, planCorrection = "";
1324
- while (true) {
1325
- try {
1326
- plan = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "plan") + planCorrection, cfg, "operator-plan", "Operator plani");
1327
- } catch (error) {
1328
- // Sohbet kurtarma: gorev delegasyon gerektirmiyorsa (selamlasma, kisa soru) ve operator
1329
- // protokol nesnesi yerine duz metinle cevap verdiyse, kullaniciyi "gecerli JSON
1330
- // dondurmedi" hatasiyla karsilamak yanlistir — istenen sey zaten o metindi. Yalnizca
1331
- // is gerektirmeyen gorevlerde ve elde gercek bir metin varken devreye girer.
1332
- const prose = conversationalAnswer(error);
1333
- if (requiresWork || !prose) throw error;
1334
- this.publish("log", { level: "warn", msg: "Operator protokol nesnesi dondurmedi; gorev is gerektirmedigi icin duz metin cevabi teslimat sayildi." }, task.id);
1335
- task.teamState = state;
1336
- return this.complete(task, state, "done", prose, { verification: "Operatorun dogrudan yaniti (protokol nesnesi yerine duz metin)." });
1337
- }
1338
- const isShortcut = plan && String(plan.status).toLowerCase() === "complete" && !Array.isArray(plan.assignments);
1339
- if (!isShortcut) break;
1340
- if (!requiresWork) {
1341
- if (!String(plan.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1342
- this.publish("log", { level: "info", msg: "Operator gorevi delegasyona gerek gormeden dogrudan yanitladi." }, task.id);
1343
- task.teamState = state;
1344
- return this.complete(task, state, "done", String(plan.final), plan);
1345
- }
1346
- if (shortcutAttempt++ >= shortcutRetries) {
1347
- throw new Error("Operator bu yapim gorevini delege etmeden kapatmaya calisti. Proje hafizasindaki gecmis teslimatlar gorevi tamamlanmis saymaz; en az bir uygulama delegasyonu gerekir.");
1348
- }
1349
- this.publish("log", { level: "warn", msg: "Operator yapim gorevini delegasyonsuz kapatmaya calisti; hafiza gecmisi kanit sayilmaz, yeniden planlama isteniyor." }, task.id);
1350
- planCorrection = "\n\nUYARI: Bu GERCEK bir yapim/degisiklik gorevidir. Proje hafizasindaki gecmis teslimatlar (or. onceki turlarda uretilmis dosyalar) bu gorevi TAMAMLANMIS SAYDIRMAZ; kullanici isin bu oturumda yeniden uretilip dogrulanmasini istiyor. status=complete DONME; en az bir 'implement' delegasyonu iceren bir plan uret.";
1351
- }
1352
- assignments = normalizeAssignments(plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1353
- assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1354
- state.plan = { summary: String(plan.summary || ""), completionCriteria: Array.isArray(plan.completionCriteria) ? plan.completionCriteria.map(String) : [], assignments };
1355
- state.criteria = state.plan.completionCriteria;
1356
- state.operatorDecisions.push({ round: 0, ...state.plan });
1357
- task.teamState = state;
1358
- task.planPreview = JSON.stringify(state.plan, null, 2);
1359
- store.saveTask("pending", task);
1360
- if (cfg.approvalMode === "ask" && this.isRisky(task.planPreview, cfg)) {
1361
- task.status = "awaiting-approval";
1362
- task.planHash = store.hashText(task.planPreview);
1363
- store.moveTask("pending", "approval", task);
1364
- this.publish("log", { level: "warn", msg: "Riskli operator plani insan onayina alindi." }, task.id);
1365
- this.busy = false;
1366
- this.current = null;
1367
- this.emit("queue");
1368
- this.emit("status", this.status());
1369
- this.stopLiveDiff();
1370
- this._textBefore = null;
1371
- return;
1372
- }
1373
- }
1374
-
1375
- const maxRounds = Math.max(1, cfg.operator?.maxRounds || 6);
1376
- const maxRecoveryRounds = Math.max(0, cfg.operator?.maxInfrastructureRecoveryRounds ?? 2);
1377
- let recoveryRounds = 0;
1378
- while (this.running && state.round < maxRounds + recoveryRounds) {
1379
- state.round++;
1380
- await this.runAssignments(task, cfg, assignments, state);
1381
- const infrastructureFailures = assignments.filter((assignment) => {
1382
- const result = state.results[assignment.id];
1383
- return result?.status === "failed" && RECOVERABLE_CLI_ERRORS.has(result.error?.code);
1384
- });
1385
- if (infrastructureFailures.length && recoveryRounds < maxRecoveryRounds) {
1386
- recoveryRounds++;
1387
- this.publish("log", { level: "warn", msg: `CLI altyapi hatasi tur butcesinden dusulmedi; ${maxRecoveryRounds - recoveryRounds} ek kurtarma turu kaldi.` }, task.id);
1388
- }
1389
- // Uygulama agenti kalmadi guvenligi: Gorev acikca dosya olusturma/degistirme gerektiriyor
1390
- // fakat katalogda 'implement' yapabilen kullanilabilir hicbir agent kalmadiysa (or. tek
1391
- // executor opencode sessizlik nedeniyle karantinaya alindi) ve simdiye kadar tamamlanmis bir
1392
- // uygulama isi da yoksa, HICBIR ek tur dosya uretemez. Operatoru planlayici/gap-analizi
1393
- // dongusunde bosuna dondurup tur ve cagri butcesini yakmak yerine net, eyleme donuk bir
1394
- // hatayla dur (kullanicinin gordugu "circle seklinde uzayip gitme" sorununu bu keser).
1395
- if (taskRequiresDelegation(task.prompt)
1396
- && !this.hasUsableImplementAgent(cfg, operatorCli)
1397
- && !Object.values(state.results).some((result) => result.kind === "implement" && result.status === "completed")) {
1398
- const dead = [...this.unhealthyAgents.keys()];
1399
- throw new Error(
1400
- "Bu gorev dosya olusturma/degistirme gerektiriyor fakat kullanilabilir bir uygulama (implement) agenti yok" +
1401
- (dead.length ? ` (${dead.join(", ")} bu oturumda kullanim disi kaldi)` : "") +
1402
- ". 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."
1403
- );
1404
- }
1405
- if (task.executionMode === "fast" && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")) {
1406
- const reports = assignments.map((assignment) => state.results[assignment.id].result).join("\n\n");
1407
- this.publish("log", { level: "info", msg: "FAST mod: uzman teslimati basarili; ikinci operator degerlendirme cagrisi atlandi." }, task.id);
1408
- return this.complete(task, state, "done", reports, {});
1409
- }
1410
- // PASS hizli yolu: turdaki tum delegasyonlar tamamlandi ve turun kendi bagimsiz
1411
- // denetimi PASS verdiyse operatorun ikinci degerlendirme cagrisi bilgi eklemez,
1412
- // yalnizca dakikalar kaybettirir. Bayat bir PASS'in yeni turu kapatmamasi icin
1413
- // karar en guncel denetim olmalidir. operator.passFastPath=false ile kapatilabilir.
1414
- const roundReview = assignments
1415
- .map((assignment) => state.results[assignment.id])
1416
- .filter((result) => result?.kind === "review" && result.status === "completed" && result.verdict)
1417
- .pop();
1418
- if (cfg.operator?.passFastPath !== false
1419
- && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")
1420
- && roundReview?.verdict === "PASS"
1421
- && roundReview === this.latestReview(state)) {
1422
- this.publish("log", { level: "ok", msg: `Denetim ${roundReview.id} PASS verdi; operator degerlendirme cagrisi atlandi ve teslimat tamamlandi.` }, task.id);
1423
- const completedWork = Object.values(state.results).filter((result) => result.status === "completed");
1424
- const final = `Teslimat bagimsiz denetimden gecti (${roundReview.id} VERDICT: PASS).\n` +
1425
- completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1426
- return this.complete(task, state, "done", final, { verification: `${roundReview.agent} → VERDICT: PASS` });
1427
- }
1428
- const decision = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "review"), cfg, "operator-review", "Operator karari");
1429
- state.operatorDecisions.push({ round: state.round, ...decision });
1430
- task.teamState = state;
1431
- store.saveTask("pending", task);
1432
- if (String(decision.status).toLowerCase() === "complete") {
1433
- if (!String(decision.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1434
- return this.complete(task, state, "done", String(decision.final), decision);
1435
- }
1436
- if (String(decision.status).toLowerCase() !== "continue") throw new Error("Operator status alani continue veya complete olmali.");
1437
- assignments = normalizeAssignments(decision.assignments, cfg, operatorCli, new Set(state.usedIds), task.prompt);
1438
- // Inceleme dongusu valisi: bagimsiz denetim PASS verdiyse ve operator yalnizca yeni
1439
- // inceleme turlari acmak istiyorsa dongu burada kesilir. Sonsuz review ping-pong'u
1440
- // hem tur butcesini tuketiyor hem de basarili teslimati kullaniciya geciktiriyordu.
1441
- const lastReview = this.latestReview(state);
1442
- if (lastReview?.verdict === "PASS" && assignments.every((assignment) => assignment.kind === "review")) {
1443
- this.publish("log", { level: "warn", msg: `Denetim ${lastReview.id} PASS verdi; yalnizca yeni inceleme iceren tur acilmadi ve teslimat tamamlandi.` }, task.id);
1444
- const final = `Teslimat bagimsiz denetimden gecti (${lastReview.id} → VERDICT: PASS). ` +
1445
- `Operatorun ek inceleme talebi tur butcesini korumak icin motor tarafindan sonlandirildi.` +
1446
- (String(decision.reason || "").trim() ? `\nOperator notu: ${clip(decision.reason, 600)}` : "");
1447
- return this.complete(task, state, "done", final, { verification: `${lastReview.agent} → VERDICT: PASS` });
1448
- }
1449
- }
1450
- if (!this.running) throw new Error("Motor durduruldu; gorev tamamlanmadan kesildi.");
1451
- return this.finishExhausted(task, state, recoveryRounds);
1452
- }
1453
-
1454
- // Tur butcesi doldugunda tamamlanmis isi cope atmak yerine uyarili kismi teslimat yapar.
1455
- // Kullanicinin gordugu sonuc "saatlerce bekledim ve hata aldim" degil, "teslimat hazir,
1456
- // su riskler acik kaldi" olmalidir. Hic tamamlanan is yoksa eski davranis korunur.
1457
- finishExhausted(task, state, recoveryRounds) {
1458
- const completedWork = Object.values(state.results || {}).filter((result) => result.status === "completed");
1459
- if (!completedWork.length) {
1460
- throw new Error(`Operator ${state.round} tur sonunda gorevi tamamlayamadi${recoveryRounds ? ` (${recoveryRounds} CLI kurtarma turu kullanildi)` : ""}.`);
1461
- }
1462
- const lastReview = this.latestReview(state);
1463
- const lastDecision = [...(state.operatorDecisions || [])].reverse().find((decision) => String(decision.status).toLowerCase() === "continue");
1464
- const warnings = [
1465
- `Tur butcesi doldu (${state.round} tur); operator complete karari veremeden teslimat kapatildi.`,
1466
- ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} VERDICT: ${lastReview.verdict}.`] : []),
1467
- ...(lastDecision && String(lastDecision.reason || "").trim() ? [`Operatorun acik biraktigi konu: ${clip(lastDecision.reason, 400)}`] : []),
1468
- ];
1469
- this.publish("log", { level: "warn", msg: `Tur butcesi doldu; tamamlanan is kismi teslimat olarak kapatiliyor (${completedWork.length} tamamlanmis delegasyon).` }, task.id);
1470
- const final = `KISMI TESLIMAT: Tur butcesi doldugu icin gorev, tamamlanan isle kapatildi.\n` +
1471
- completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n") +
1472
- `\nAcik kalan konular teslimat uyarilarinda listelendi.`;
1473
- return this.complete(task, state, "done", final, {
1474
- warnings,
1475
- verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1476
- });
1477
- }
1478
-
1479
- // Gorev beklenmedik bir hatayla kesildiginde (protokol hatasi, butce asimi vb.) somut is
1480
- // uretilmisse gorevi failed yerine uyarili kismi teslimatla kapatir. Basarili olamazsa
1481
- // false doner ve normal hata akisi calisir.
1482
- salvage(task, error) {
1483
- try {
1484
- if (task.kind === "operator-chat") return false;
1485
- if (!this._cwd || !this._snapBefore) return false;
1486
- const state = task.teamState;
1487
- const completedWork = Object.values(state?.results || {}).filter((result) => result.status === "completed");
1488
- const changes = diffSnapshots(this._snapBefore, snapshotDir(this._cwd));
1489
- const hasFileChanges = Boolean(changes.created.length || changes.modified.length || changes.deleted.length);
1490
- const hasImplementation = completedWork.some((result) => result.kind === "implement");
1491
- // Kurtarma kosulu: YA tamamlanmis bir uygulama delegasyonu var (ekip uretti) YA DA calisma
1492
- // klasoru fiilen degisti. Ikincisi, operatorun kendisi standart JSON protokolune uymadan
1493
- // isi dogrudan yaptigi durumu da kapsar (or. OpenCode operator rolunde JSON plan yerine
1494
- // dosyalari kendisi degistirip duz metin doner). Hicbir is yoksa normal hata akisina birak.
1495
- if (!hasFileChanges && !hasImplementation) return false;
1496
- const failure = classifyCliError(error);
1497
- const rescueState = state || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1498
- const operatorDirect = !completedWork.length && hasFileChanges;
1499
- this.publish("log", { level: "warn", msg: `Gorev hatayla kesildi (${failure.summary}); ${operatorDirect ? "operatorun dogrudan yaptigi degisiklikler" : "tamamlanan is"} kismi teslimat olarak korunuyor.` }, task.id);
1500
- const lastReview = this.latestReview(rescueState);
1501
- const changedFiles = [...changes.created, ...changes.modified, ...changes.deleted];
1502
- const final = operatorDirect
1503
- ? `KISMI TESLIMAT: Operator (${task.operatorCli || "?"}) standart delegasyon protokolune uymadan isi dogrudan uyguladi; degisiklikler korundu.\n` +
1504
- changedFiles.slice(0, 40).map((file) => `- ${file}`).join("\n")
1505
- : `KISMI TESLIMAT: Gorev bir altyapi/protokol hatasiyla kesildi fakat tamamlanan is korundu.\n` +
1506
- completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1507
- this.complete(task, rescueState, "done", final, {
1508
- warnings: [
1509
- `Gorev su hatayla kesildi: ${failure.summary}`,
1510
- ...(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.`] : []),
1511
- ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} VERDICT: ${lastReview.verdict}.`] : []),
1512
- ],
1513
- verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1514
- });
1515
- return true;
1516
- } catch {
1517
- return false;
1518
- }
1519
- }
1520
-
1521
- async runChatTask(task, cfg) {
1522
- const parentFound = store.findTask(task.parentTaskId);
1523
- if (!parentFound) throw new Error("Sohbetin bagli oldugu tamamlanmis gorev bulunamadi.");
1524
- const parent = parentFound.task;
1525
- let operatorCli = parent.operatorCli;
1526
- if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) operatorCli = cfg.operator?.cli;
1527
- if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Sohbet icin operator CLI bulunamadi.");
1528
- this.busy = true;
1529
- this.current = { id: task.id, stage: "operator-chat", agent: operatorCli };
1530
- this._cwd = path.resolve(store.WORK_BASE, parent.targetDir || cfg.workingDir || ".");
1531
- this.publish("log", { level: "task", msg: `OPERATOR SOHBETI ${parent.id}: ${task.prompt}` }, task.id);
1532
- this.emit("status", this.status());
1533
-
1534
- const role = store.readRole("operator-chat.md");
1535
- const resultContext = Object.values(parent.teamState?.results || {}).map((result) => ({
1536
- id: result.id, agent: result.agent, status: result.status, result: clip(result.result, 1800),
1537
- }));
1538
- const prompt = `${role}\n\n---\n## Tamamlanmis gorev hakkinda takip sohbeti\n` +
1539
- `Kullanici daha once tamamladiginiz gorev hakkinda soru soruyor. Yeni dosya degistirme, arac kullanma veya delegasyon yapma. ` +
1540
- `Asagidaki kayitlara dayanarak dogrudan, kisa ve acik cevap ver. Bilmiyorsan bunu belirt.\n` +
1541
- `## Gorev kimligi\n${parent.id}\n## Gorev\n${parent.prompt}\n## Teslimat\n${JSON.stringify(parent.delivery || { summary: parent.summary, changes: parent.changes }, null, 2)}\n` +
1542
- `## Takim raporlari\n${clip(JSON.stringify(resultContext, null, 2), 12000)}\n` +
1543
- `## Onceki sohbet\n${clip(JSON.stringify(parent.conversation || [], null, 2), 8000)}\n## Yeni soru\n${task.prompt}`;
1544
- const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-chat" });
1545
- const entry = { question: task.prompt, answer: response.text, at: new Date().toISOString(), taskId: task.id };
1546
- parent.conversation ||= [];
1547
- parent.conversation.push(entry);
1548
- store.saveTask(parentFound.state, parent);
1549
- task.status = "done";
1550
- task.finishedAt = new Date().toISOString();
1551
- task.final = response.text;
1552
- task.summary = clip(response.text, 1000);
1553
- store.moveTask("pending", "done", task);
1554
- this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: parent.id, answer: response.text, status: "done", operator: operatorCli }, task.id);
1555
- this.publish("log", { level: "ok", msg: `Operator soruyu yanitladi: ${parent.id}` }, task.id);
1556
- this.emit("queue");
1557
- }
1558
-
1559
- complete(task, state, status, finalText, decision = {}) {
1560
- const changes = diffSnapshots(this._snapBefore || new Map(), snapshotDir(this._cwd));
1561
- // Son periyodik taramadan sonra yapilan degisiklikleri de replay kaydina ve dashboard'a
1562
- // aktar; ardindan run --once yolunda da timer'in acik kalmamasini garanti et.
1563
- this.publishFileChanges(task.id);
1564
- this.stopLiveDiff();
1565
- task.status = status;
1566
- task.finishedAt = new Date().toISOString();
1567
- const files = [
1568
- ...changes.created.map((file) => ({ path: file, action: "created" })),
1569
- ...changes.modified.map((file) => ({ path: file, action: "modified" })),
1570
- ...changes.deleted.map((file) => ({ path: file, action: "deleted" })),
1571
- ];
1572
- const concise = clip(String(finalText).replace(/\n{3,}/g, "\n\n"), 700);
1573
- let verification = String(decision.verification || "").trim();
1574
- if (!verification) {
1575
- for (const result of Object.values(state.results)) {
1576
- const match = String(result.result || "").match(/DO(?:Ğ|G\u0306|G)RULAMA:\s*([^\n`]+)/i);
1577
- if (match) { verification = match[1].trim(); break; }
1578
- }
1579
- }
1580
- task.summary = concise;
1581
- task.final = finalText;
1582
- task.changes = changes;
1583
- task.usage = this.usageSummary();
1584
- const warnings = (Array.isArray(decision.warnings) ? decision.warnings : []).map(String).filter(Boolean);
1585
- task.delivery = {
1586
- summary: concise,
1587
- files,
1588
- location: this._cwd,
1589
- verification,
1590
- mode: task.executionMode,
1591
- rounds: state.round,
1592
- agents: [...new Set(Object.values(state.results).map((result) => result.agent))],
1593
- ...(warnings.length ? { warnings } : {}),
1594
- };
1595
- task.teamState = state;
1596
- const found = store.findTask(task.id);
1597
- store.moveTask(found?.state || "pending", status, task);
1598
- store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
1599
- this.publish("result", { id: task.id, prompt: task.prompt, status, dir: this._cwd, changes, summary: task.summary, delivery: task.delivery, usage: task.usage, operator: task.operatorCli, rounds: state.round }, task.id);
1600
- this.publish("log", { level: "ok", msg: `GOREV tamamlandi: ${task.id}` }, task.id);
1601
- this.notifyOutcome(task, status, { summary: task.summary, rounds: state.round, fileCount: files.length, verification: task.delivery.verification });
1602
- this.emit("queue");
1603
- this._textBefore = null;
1604
- }
1605
-
1606
- // Dis bildirim icin tek cikis noktasi: server.js bu event'i dinleyip (varsa) webhook'a POST atar.
1607
- // Motoru HTTP'den bagimsiz tutar; bildirim basarisiz olsa bile gorev akisini etkilemez.
1608
- notifyOutcome(task, status, extra = {}) {
1609
- try {
1610
- this.emit("notify", {
1611
- status,
1612
- id: task.id,
1613
- prompt: task.prompt,
1614
- operator: task.operatorCli || null,
1615
- dir: this._cwd || task.targetDir || null,
1616
- at: new Date().toISOString(),
1617
- ...extra,
1618
- });
1619
- } catch {}
1620
- }
1621
- }
1622
-
1623
- module.exports = new Engine();
1624
- // Testlerin dis davranisla birlikte kritik saf kurallari da dogrudan dogrulayabilmesi icin.
1625
- module.exports._internals = {
1626
- normalizeAssignments, ensureBalancedRoleChain, extractVerdict, clipMiddle, snapshotDir, diffSnapshots,
1627
- captureTextSnapshot, buildLineDiff, describeFileDiff, isSensitiveDiffPath, taskRequiresDelegation,
1628
- parseJson, conversationalAnswer, classifyCliError, RECOVERABLE_CLI_ERRORS, QUARANTINE_CLI_ERRORS,
1629
- normalizeCliOutput, addUsage, usageTotal,
1630
- };
1
+ const { spawn } = require("child_process");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { EventEmitter } = require("events");
5
+ const store = require("./store");
6
+ const cliRegistry = require("./cli-registry");
7
+ const skillRegistry = require("./skill-registry");
8
+ const checkpoints = require("./checkpoints");
9
+
10
+ const isWin = process.platform === "win32";
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
+
47
+ function snapshotDir(dir) {
48
+ const map = new Map();
49
+ const ignored = (rel) => {
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).
53
+ return r.includes("node_modules") || r.includes(".git/") || r.startsWith(".git") ||
54
+ r === ".crewctl" || r.startsWith(".crewctl/") ||
55
+ r.startsWith("orchestrator/queue") || r.startsWith("orchestrator/state") ||
56
+ r.startsWith("orchestrator/memory") || r.startsWith("orchestrator/node_modules");
57
+ };
58
+ let count = 0;
59
+ const walk = (abs, rel) => {
60
+ if (count > 12000) return;
61
+ let entries;
62
+ try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
63
+ for (const entry of entries) {
64
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
65
+ if (ignored(childRel)) continue;
66
+ const childAbs = path.join(abs, entry.name);
67
+ if (entry.isDirectory()) walk(childAbs, childRel);
68
+ else {
69
+ try {
70
+ const s = fs.statSync(childAbs);
71
+ map.set(childRel, `${s.mtimeMs}:${s.size}`);
72
+ count++;
73
+ } catch {}
74
+ }
75
+ }
76
+ };
77
+ walk(dir, "");
78
+ return map;
79
+ }
80
+
81
+ function diffSnapshots(before, after) {
82
+ const created = [], modified = [], deleted = [];
83
+ for (const [file, stamp] of after) {
84
+ if (!before.has(file)) created.push(file);
85
+ else if (before.get(file) !== stamp) modified.push(file);
86
+ }
87
+ for (const file of before.keys()) if (!after.has(file)) deleted.push(file);
88
+ return { created: created.sort(), modified: modified.sort(), deleted: deleted.sort() };
89
+ }
90
+
91
+ const DIFF_MAX_FILE_BYTES = 256 * 1024;
92
+ const DIFF_MAX_BASELINE_BYTES = 24 * 1024 * 1024;
93
+ const DIFF_MAX_BASELINE_FILES = 2000;
94
+ const DIFF_MAX_MATRIX_CELLS = 250000;
95
+ const DIFF_CONTEXT_LINES = 3;
96
+ const DIFF_MAX_RENDERED_LINES = 240;
97
+ const DIFF_MAX_EVENT_LINES = 1000;
98
+ const DIFF_MAX_SOURCE_LINES = 5000;
99
+
100
+ function isSensitiveDiffPath(file) {
101
+ const normalized = String(file || "").replace(/\\/g, "/").toLowerCase();
102
+ const base = path.posix.basename(normalized);
103
+ return /^\.env(?:\.|$)/.test(base) || [".npmrc", ".pypirc", ".netrc"].includes(base) ||
104
+ /(^|[._-])(secret|secrets|credential|credentials)([._-]|$)/.test(base) ||
105
+ /\.(pem|key|p12|pfx)$/.test(base) || /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/.test(base);
106
+ }
107
+
108
+ function readDiffText(file, maxBytes = DIFF_MAX_FILE_BYTES) {
109
+ try {
110
+ const stat = fs.statSync(file);
111
+ if (!stat.isFile()) return { ok: false, status: "unavailable" };
112
+ if (stat.size > maxBytes) return { ok: false, status: "too-large", size: stat.size };
113
+ const buffer = fs.readFileSync(file);
114
+ if (buffer.includes(0)) return { ok: false, status: "binary", size: stat.size };
115
+ let controls = 0;
116
+ for (const byte of buffer) {
117
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) controls++;
118
+ }
119
+ if (buffer.length && controls / buffer.length > 0.02) return { ok: false, status: "binary", size: stat.size };
120
+ return { ok: true, text: buffer.toString("utf8"), size: stat.size };
121
+ } catch {
122
+ return { ok: false, status: "unavailable" };
123
+ }
124
+ }
125
+
126
+ // Gorev basindaki metin iceriklerini sinirli bir butceyle saklar. Bu taban, Git deposu
127
+ // olmayan klasorlerde de modified/deleted dosyalar icin satir diff'i uretebilmemizi saglar.
128
+ function captureTextSnapshot(dir, snapshot, options = {}) {
129
+ const maxBytes = Math.max(1024, Number(options.maxBytes) || DIFF_MAX_BASELINE_BYTES);
130
+ const maxFiles = Math.max(1, Number(options.maxFiles) || DIFF_MAX_BASELINE_FILES);
131
+ const maxFileBytes = Math.max(1024, Number(options.maxFileBytes) || DIFF_MAX_FILE_BYTES);
132
+ const result = new Map();
133
+ let bytes = 0, files = 0;
134
+ for (const file of [...(snapshot?.keys?.() || [])].sort()) {
135
+ if (isSensitiveDiffPath(file)) {
136
+ result.set(file, { ok: false, status: "redacted" });
137
+ continue;
138
+ }
139
+ if (files >= maxFiles || bytes >= maxBytes) {
140
+ result.set(file, { ok: false, status: "baseline-limit" });
141
+ continue;
142
+ }
143
+ const value = readDiffText(path.join(dir, file), maxFileBytes);
144
+ if (value.ok && bytes + value.size > maxBytes) {
145
+ result.set(file, { ok: false, status: "baseline-limit" });
146
+ continue;
147
+ }
148
+ result.set(file, value);
149
+ if (value.ok) { bytes += value.size; files++; }
150
+ }
151
+ return result;
152
+ }
153
+
154
+ function splitDiffLines(text) {
155
+ const value = String(text || "").replace(/\r\n/g, "\n");
156
+ if (!value) return [];
157
+ const lines = value.split("\n");
158
+ if (lines[lines.length - 1] === "") lines.pop();
159
+ return lines;
160
+ }
161
+
162
+ function lineOperations(beforeText, afterText) {
163
+ const before = splitDiffLines(beforeText), after = splitDiffLines(afterText);
164
+ const cells = before.length * after.length;
165
+ let approximate = false;
166
+ const operations = [];
167
+ if (cells <= DIFF_MAX_MATRIX_CELLS) {
168
+ const width = after.length + 1;
169
+ const table = new Uint32Array((before.length + 1) * width);
170
+ for (let i = before.length - 1; i >= 0; i--) {
171
+ for (let j = after.length - 1; j >= 0; j--) {
172
+ table[i * width + j] = before[i] === after[j]
173
+ ? table[(i + 1) * width + j + 1] + 1
174
+ : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
175
+ }
176
+ }
177
+ let i = 0, j = 0;
178
+ while (i < before.length || j < after.length) {
179
+ if (i < before.length && j < after.length && before[i] === after[j]) {
180
+ operations.push({ type: "context", text: before[i++] }); j++;
181
+ } else if (i < before.length && (j >= after.length || table[(i + 1) * width + j] >= table[i * width + j + 1])) {
182
+ operations.push({ type: "delete", text: before[i++] });
183
+ } else {
184
+ operations.push({ type: "add", text: after[j++] });
185
+ }
186
+ }
187
+ } else {
188
+ // Cok buyuk dosyalarda karesel matris kurma. Ortak bas/sonu koruyup degisen orta
189
+ // bolumu tek hunk olarak goster; UI bu sonucu yaklasik olarak etiketler.
190
+ approximate = true;
191
+ let prefix = 0;
192
+ while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) prefix++;
193
+ let suffix = 0;
194
+ while (suffix < before.length - prefix && suffix < after.length - prefix &&
195
+ before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
196
+ for (let i = 0; i < prefix; i++) operations.push({ type: "context", text: before[i] });
197
+ for (let i = prefix; i < before.length - suffix; i++) operations.push({ type: "delete", text: before[i] });
198
+ for (let i = prefix; i < after.length - suffix; i++) operations.push({ type: "add", text: after[i] });
199
+ for (let i = suffix; i > 0; i--) operations.push({ type: "context", text: before[before.length - i] });
200
+ }
201
+ return { operations, approximate };
202
+ }
203
+
204
+ function buildLineDiff(beforeText, afterText, options = {}) {
205
+ const context = Math.max(0, Number(options.context) || DIFF_CONTEXT_LINES);
206
+ const maxLines = Math.max(20, Number(options.maxLines) || DIFF_MAX_RENDERED_LINES);
207
+ const { operations, approximate } = lineOperations(beforeText, afterText);
208
+ let oldNo = 1, newNo = 1;
209
+ const annotated = operations.map((operation) => {
210
+ const line = { ...operation, oldNumber: null, newNumber: null };
211
+ if (operation.type !== "add") line.oldNumber = oldNo++;
212
+ if (operation.type !== "delete") line.newNumber = newNo++;
213
+ return line;
214
+ });
215
+ const additions = annotated.filter((line) => line.type === "add").length;
216
+ const deletions = annotated.filter((line) => line.type === "delete").length;
217
+ const ranges = [];
218
+ for (let i = 0; i < annotated.length; i++) {
219
+ if (annotated[i].type === "context") continue;
220
+ const start = Math.max(0, i - context), end = Math.min(annotated.length, i + context + 1);
221
+ const last = ranges[ranges.length - 1];
222
+ if (last && start <= last.end) last.end = Math.max(last.end, end);
223
+ else ranges.push({ start, end });
224
+ }
225
+ const hunks = [];
226
+ let rendered = 0, truncated = false;
227
+ for (const range of ranges) {
228
+ if (rendered >= maxLines) { truncated = true; break; }
229
+ const oldStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "add").length;
230
+ const newStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "delete").length;
231
+ const available = Math.max(0, maxLines - rendered);
232
+ const selected = annotated.slice(range.start, Math.min(range.end, range.start + available));
233
+ if (selected.length < range.end - range.start) truncated = true;
234
+ hunks.push({
235
+ oldStart,
236
+ oldLines: selected.filter((line) => line.type !== "add").length,
237
+ newStart,
238
+ newLines: selected.filter((line) => line.type !== "delete").length,
239
+ lines: selected.map((line) => ({ ...line, text: String(line.text).slice(0, 1000) })),
240
+ });
241
+ rendered += selected.length;
242
+ }
243
+ return { additions, deletions, hunks, truncated, approximate };
244
+ }
245
+
246
+ function describeFileDiff(root, file, action, baseline, options = {}) {
247
+ const base = action === "created" ? { ok: true, text: "" } : baseline?.get(file);
248
+ const current = action === "deleted" ? { ok: true, text: "" } :
249
+ (isSensitiveDiffPath(file) ? { ok: false, status: "redacted" } : readDiffText(path.join(root, file)));
250
+ const unavailable = !base?.ok ? base : (!current?.ok ? current : null);
251
+ if (unavailable) return { path: file, action, additions: null, deletions: null, previewStatus: unavailable.status || "unavailable", hunks: [] };
252
+ const lineCount = (text) => text ? (String(text).match(/\n/g) || []).length + 1 : 0;
253
+ if (lineCount(base.text) > DIFF_MAX_SOURCE_LINES || lineCount(current.text) > DIFF_MAX_SOURCE_LINES) {
254
+ return { path: file, action, additions: null, deletions: null, previewStatus: "too-many-lines", hunks: [] };
255
+ }
256
+ return { path: file, action, ...buildLineDiff(base.text, current.text, options) };
257
+ }
258
+
259
+ function clip(value, limit = 12000) {
260
+ const text = String(value || "");
261
+ return text.length <= limit ? text : `${text.slice(0, limit)}\n...[kesildi]`;
262
+ }
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
+
272
+ // Bas ve sonu koruyarak kirpar. Denetci raporlarinda VERDICT satiri metnin SONUNDadir;
273
+ // yalnizca bastan kirpmak operatorun karari gormemesine ve gereksiz yeniden inceleme
274
+ // turlarina yol acar.
275
+ function clipMiddle(value, limit = 12000) {
276
+ const text = String(value || "");
277
+ if (text.length <= limit) return text;
278
+ const head = Math.ceil(limit * 0.6);
279
+ const tail = Math.max(1, limit - head);
280
+ return `${text.slice(0, head)}\n...[orta kisim kesildi]...\n${text.slice(text.length - tail)}`;
281
+ }
282
+
283
+ function extractVerdict(text) {
284
+ const matches = String(text || "").match(/VERDICT:\s*(PASS|FAIL)/gi);
285
+ if (!matches || !matches.length) return null;
286
+ return /PASS/i.test(matches[matches.length - 1]) ? "PASS" : "FAIL";
287
+ }
288
+
289
+ // Siniflandirilamayan hatada "gemini cikis kodu 1." gibi bilgisiz bir ilk satir yerine metnin
290
+ // icindeki GERCEK hata cumlesini bulur; kullaniciya gosterilen tek satir buysa anlamli olmali.
291
+ function meaningfulErrorLine(raw) {
292
+ const all = String(raw).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
293
+ // Yigin izleri ve motorun kendi sardigi "<cli> cikis kodu N." satiri teshis tasimaz;
294
+ // gercek neden hemen arkalarindaki satirdadir.
295
+ const useful = all.filter((line) => !/^at\s|^node:internal|^[{}[\]]+$/.test(line) && !/cikis kodu/i.test(line));
296
+ const signal = useful.find((line) => /error|hata|failed|failure|denied|invalid|exceeded|limit|quota|unable|cannot|must/i.test(line) && line.length > 20);
297
+ return signal || useful[0] || all[0] || String(raw).trim();
298
+ }
299
+
300
+ // Ham CLI ciktisini kullaniciya "ne oldu + ne yapmali" seklinde anlamli bir teshise cevirir.
301
+ // SIRA ONEMLIDIR: spesifik desenler once gelmeli. Ornegin Gemini'nin "GEMINI_API_KEY ...
302
+ // not found" mesaji bir PATH/kurulum sorunu DEGILDIR; genis /not found/ deseni bu yuzden
303
+ // daraltildi ve kimlik kontrollerinin arkasina alindi.
304
+ function classifyCliError(error) {
305
+ const raw = String(error?.message || error || "Bilinmeyen CLI hatasi");
306
+ const of = (code, summary, action) => ({ code, summary, action, raw: clip(raw, 5000) });
307
+ if (/requires a newer version|upgrade to the latest|model metadata.*not found|unsupported.*model|model.*unsupported/i.test(raw)) {
308
+ return of("VERSION_INCOMPATIBLE", "CLI sürümü seçilen modeli desteklemiyor.", "CLI aracını güncelleyin veya desteklenen bir model seçin.");
309
+ }
310
+ if (/sessiz kaldi|cikti uretmedi|CLI_STALLED/i.test(raw)) {
311
+ 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.");
312
+ }
313
+ if (/API key not valid|API_KEY_INVALID|invalid[_ ]api[_ ]key|incorrect api key/i.test(raw)) {
314
+ 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).");
315
+ }
316
+ // Gemini CLI oturum acilmadiginda "Please set an Auth method" / "GEMINI_API_KEY environment
317
+ // variable not found" yazar; her ikisi de kurulum degil OTURUM sorunudur.
318
+ 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)) {
319
+ 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.");
320
+ }
321
+ // Kota (gunluk/aylik hak bitti) ile hiz siniri (kisa sureli) AYRI teshislerdir: ilki
322
+ // beklemekle gecmez, ikincisi gecer. Kullanicinin sordugu "limitim mi bitti" ayrimi budur.
323
+ if (/RESOURCE_EXHAUSTED|quota exceeded|exceeded your current quota|daily limit|günlük limit|gunluk limit|out of credits|insufficient (credit|balance|quota)|billing/i.test(raw)) {
324
+ return of("QUOTA_EXCEEDED", "Sağlayıcı kotanız doldu (günlük/aylık hak veya bakiye bitti).", "Kota yenilenene kadar bu agent kullanılamaz; başka bir agent seçin, faturalandırmayı yükseltin veya API anahtarını kotası olan bir hesapla değiştirin.");
325
+ }
326
+ if (/rate.?limit|too many requests|\b429\b/i.test(raw)) {
327
+ return of("RATE_LIMIT", "İstek hızı sınırına takıldı (kota bitmedi, çok sık istek gönderildi).", "Kısa bir süre bekleyip tekrar deneyin; operatör bu turda alternatif bir agent kullanabilir.");
328
+ }
329
+ if (/overloaded|\bUNAVAILABLE\b|\b(503|500)\b|internal error|try again later/i.test(raw)) {
330
+ return of("MODEL_OVERLOADED", "Model sağlayıcısı geçici olarak aşırı yüklü ya da hata döndürdü.", "Sağlayıcı kaynaklı geçici bir sorun; birkaç dakika sonra tekrar deneyin.");
331
+ }
332
+ if (/location is not supported|not available in your country|region.*not supported/i.test(raw)) {
333
+ 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.");
334
+ }
335
+ if (/ConnectionRefused|Unable to connect|provider hatasi|provider error|ECONNREFUSED/i.test(raw)) {
336
+ 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.");
337
+ }
338
+ if (/ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|getaddrinfo|fetch failed|socket hang up|network (error|unreachable)|proxy/i.test(raw)) {
339
+ 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.");
340
+ }
341
+ if (/not recognized as an internal|is not recognized|command not found|spawn .*ENOENT|\bENOENT\b|no such file or directory/i.test(raw)) {
342
+ 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.");
343
+ }
344
+ if (/timeout|timed out|zaman asim|zaman aşım/i.test(raw)) {
345
+ 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.");
346
+ }
347
+ return of("CLI_FAILED", clip(meaningfulErrorLine(raw), 500), "Ham CLI çıktısı teknik ayrıntılarda saklandı; sorun sürerse alternatif bir agent deneyin.");
348
+ }
349
+
350
+ const RECOVERABLE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "RATE_LIMIT", "QUOTA_EXCEEDED", "MODEL_OVERLOADED", "NETWORK_ERROR", "REGION_BLOCKED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "TIMEOUT", "CLI_STALLED", "CLI_FAILED", "VERSION_INCOMPATIBLE"]);
351
+ // Karantina = bu oturumda tekrar denemek anlamsiz. Kota/bolge kisiti beklemekle gecmez;
352
+ // hiz siniri (RATE_LIMIT) ve gecici saglayici hatasi (MODEL_OVERLOADED) gecer, karantinaya alinmaz.
353
+ const QUARANTINE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "QUOTA_EXCEEDED", "REGION_BLOCKED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "CLI_STALLED", "VERSION_INCOMPATIBLE"]);
354
+
355
+ function resolveExecutionMode(task) {
356
+ if (["fast", "balanced", "deep"].includes(task.executionMode)) return task.executionMode;
357
+ const prompt = String(task.prompt || "");
358
+ const complex = /(mimari|migration|refactor|guvenlik|güvenlik|deploy|production|veritabani|veritabanı|authentication|entegrasyon|çoklu|multi|kapsamli|kapsamlı)/i.test(prompt);
359
+ // Not: "oyun/sayfa" gibi sifirdan build isleri BASIT sayilmaz — bunlar plan+uygula+incele
360
+ // gerektiren gercek gorevlerdir; fast moda dusurulup ekip kullanilmadan gecilmemeli.
361
+ // Not: "dosya" gibi genis kelimeler fast'a dusurmez; yorumdaki ilkeyle celisiyordu
362
+ // (sifirdan build/dosya olusturma gercek bir gorevdir). Yalnizca acik "kucuk/hizli" sinyalleri.
363
+ const simple = prompt.length < 350 && /(basit|ufak|küçük|kucuk|hizli|hızlı|simple|small|quick)/i.test(prompt);
364
+ return simple && !complex ? "fast" : "balanced";
365
+ }
366
+
367
+ function applyExecutionPolicy(base, mode) {
368
+ const cfg = { ...base, operator: { ...(base.operator || {}) } };
369
+ if (mode === "fast") {
370
+ cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 2);
371
+ cfg.operator.maxDelegationsPerRound = 1;
372
+ cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 2500);
373
+ cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 10000);
374
+ } else if (mode === "balanced") {
375
+ // Saglikli bir balanced gorev tek turda biter (uygulama + zincirli inceleme);
376
+ // 3 tur, iki duzeltme/yeniden dogrulama turuna yer birakir.
377
+ cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 3);
378
+ cfg.operator.maxDelegationsPerRound = Math.min(cfg.operator.maxDelegationsPerRound || 8, 3);
379
+ cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 6000);
380
+ cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 24000);
381
+ }
382
+ return cfg;
383
+ }
384
+
385
+ const EMPTY_USAGE = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
386
+
387
+ function addUsage(target, extra) {
388
+ const sum = { ...(target || EMPTY_USAGE) };
389
+ for (const key of Object.keys(EMPTY_USAGE)) sum[key] = (sum[key] || 0) + (Number(extra?.[key]) || 0);
390
+ return sum;
391
+ }
392
+
393
+ function usageTotal(usage) {
394
+ return (Number(usage?.input) || 0) + (Number(usage?.output) || 0);
395
+ }
396
+
397
+ // OpenCode'un step_finish olayindaki token/maliyet alanlarini normalize eder. Alan adlari
398
+ // surumler arasinda degisebildigi icin eksik alan 0 sayilir; hicbir sayi yoksa null doner
399
+ // ki "veri yok" ile "sifir maliyet" ayirt edilebilsin.
400
+ function readStepUsage(event) {
401
+ const tokens = event.part?.tokens || event.tokens;
402
+ const cost = Number(event.part?.cost ?? event.cost);
403
+ if (!tokens && !Number.isFinite(cost)) return null;
404
+ return {
405
+ input: Number(tokens?.input) || 0,
406
+ output: Number(tokens?.output) || 0,
407
+ reasoning: Number(tokens?.reasoning) || 0,
408
+ cacheRead: Number(tokens?.cache?.read) || 0,
409
+ cacheWrite: Number(tokens?.cache?.write) || 0,
410
+ cost: Number.isFinite(cost) ? cost : 0,
411
+ };
412
+ }
413
+
414
+ // DIKKAT: text/error sozlesmesi degismemeli — operatorun JSON protokolu bu metni okuyor.
415
+ // usage yalnizca EK bir alandir; cikarilamadiginda null kalir ve hicbir akisi etkilemez.
416
+ function normalizeCliOutput(agent, stdout) {
417
+ if (agent.adapter !== "opencode" || !(agent.args || []).includes("json")) {
418
+ return { text: String(stdout || "").trim(), error: "", usage: null };
419
+ }
420
+ const texts = [], errors = [];
421
+ let usage = null;
422
+ for (const line of String(stdout || "").split(/\r?\n/)) {
423
+ if (!line.trim()) continue;
424
+ try {
425
+ const event = JSON.parse(line);
426
+ if (event.type === "text" && event.part?.text) texts.push(String(event.part.text));
427
+ if (event.type === "error") errors.push(String(event.error?.message || event.error || "OpenCode model/provider hatasi"));
428
+ // Bir kosumda birden fazla adim olabilir; hepsi toplanir.
429
+ if (event.type === "step_finish") {
430
+ const step = readStepUsage(event);
431
+ if (step) usage = addUsage(usage, step);
432
+ }
433
+ } catch {}
434
+ }
435
+ return { text: texts.join("\n").trim(), error: errors.join("\n").trim(), usage };
436
+ }
437
+
438
+ // Metindeki DENGELI suslu parantezli en dis JSON nesnesi adaylarini cikarir. String icindeki
439
+ // parantez/kacis karakterleri sayilmaz; boylece "final" metninde gecen bir { veya } eski
440
+ // "ilk-{ .. son-}" kestirmesini bozmaz. Ayrica model aciklama + nesne + ek aciklama yazdiginda
441
+ // (ya da birden fazla nesne dondurdugunde) her nesne ayri aday olur.
442
+ function jsonObjectCandidates(text) {
443
+ const src = String(text);
444
+ const found = [];
445
+ const stack = [];
446
+ let inString = false, escaped = false;
447
+ for (let i = 0; i < src.length; i++) {
448
+ const ch = src[i];
449
+ if (inString) {
450
+ if (escaped) escaped = false;
451
+ else if (ch === "\\") escaped = true;
452
+ else if (ch === '"') inString = false;
453
+ continue;
454
+ }
455
+ if (ch === '"') inString = true;
456
+ else if (ch === "{") stack.push(i);
457
+ else if (ch === "}" && stack.length) {
458
+ const start = stack.pop();
459
+ if (!stack.length) found.push(src.slice(start, i + 1));
460
+ }
461
+ }
462
+ return found;
463
+ }
464
+
465
+ // Model bazen JSON'u tek tirnak, sondaki virgul veya akilli tirnakla yazar. Kati parse
466
+ // basarisiz oldugunda son care olarak bu yaygin sapmalari onarip bir kez daha dener.
467
+ function repairJsonText(candidate) {
468
+ return String(candidate)
469
+ .replace(/[“”]/g, '"')
470
+ .replace(/[‘’]/g, "'")
471
+ .replace(/,\s*([}\]])/g, "$1");
472
+ }
473
+
474
+ const PROTOCOL_KEYS = /"(status|assignments|summary|completionCriteria|final)"\s*:/;
475
+
476
+ function parseJson(text, label) {
477
+ const raw = String(text);
478
+ const candidates = [];
479
+ for (const fence of raw.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(fence[1]);
480
+ candidates.push(raw.trim());
481
+ // Protokol anahtari iceren nesneler once denenir; ayni onem sirasinda SONDAKI nesne kazanir
482
+ // (modeller once dusunce/aciklama, en sona nihai protokol nesnesini yazar).
483
+ const objects = jsonObjectCandidates(raw).reverse();
484
+ candidates.push(...objects.filter((o) => PROTOCOL_KEYS.test(o)), ...objects.filter((o) => !PROTOCOL_KEYS.test(o)));
485
+ for (const candidate of candidates) {
486
+ for (const attempt of [candidate, repairJsonText(candidate)]) {
487
+ try {
488
+ const parsed = JSON.parse(attempt);
489
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
490
+ } catch {}
491
+ }
492
+ }
493
+ const error = new Error(`${label} gecerli JSON dondurmedi.`);
494
+ error.rawText = raw;
495
+ throw error;
496
+ }
497
+
498
+ // Protokol hatasinda operatorun ham metnini kullaniciya gosterilebilir bir cevaba cevirir.
499
+ // Yarim kalmis/kirik JSON parcalarini ve kod bloklarini atar; geriye anlamli duz metin
500
+ // kalmiyorsa null doner (o zaman normal hata akisi calisir).
501
+ function conversationalAnswer(error) {
502
+ if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) return null;
503
+ const raw = String(error.rawText || "");
504
+ if (!raw.trim()) return null;
505
+ const stripped = raw
506
+ .replace(/```[\s\S]*?```/g, " ")
507
+ .replace(/\{[\s\S]*\}/g, " ")
508
+ .replace(/[\s\S]*?^\s*\{[\s\S]*$/m, (match) => match.split("{")[0])
509
+ .replace(/\n{3,}/g, "\n\n")
510
+ .trim();
511
+ // Cok kisa artiklar (or. tek noktalama) cevap sayilmaz.
512
+ return stripped.length >= 8 ? stripped : null;
513
+ }
514
+
515
+ function capabilitiesFor(agent) {
516
+ // Rol dosyasi bir uzmanlik dayatiyorsa yetenekler DOGRUDAN rolden turer. Aksi halde
517
+ // config'teki eski/genis capabilities listesi (or. planner'da "implementation") katalogda
518
+ // allowedKinds ile celisir ve operatore yaniltici sinyal verirdi. Rol yoksa config'e duseriz.
519
+ const role = String(agent.roleFile || "").toLowerCase();
520
+ if (role.includes("executor")) return new Set(["implementation", "debugging", "testing"]);
521
+ if (role.includes("review")) return new Set(["review", "testing", "analysis"]);
522
+ if (role.includes("planner")) return new Set(["planning", "analysis"]);
523
+ if (role.includes("operator")) return new Set(["planning", "delegation"]);
524
+ return new Set(Array.isArray(agent.capabilities) ? agent.capabilities.map((x) => String(x).toLowerCase()) : []);
525
+ }
526
+
527
+ function inferAssignmentKind(raw, instruction) {
528
+ const explicit = String(raw.kind || "").toLowerCase();
529
+ if (["implement", "review", "research", "plan"].includes(explicit)) return explicit;
530
+ if (/\b(olustur|oluştur|yaz|uygula|kur|gelistir|geliştir|duzelt|düzelt|implement|build|create|edit|fix)\b/i.test(instruction)) return "implement";
531
+ if (/\b(incele|denetle|dogrula|doğrula|review|audit|verify|test et)\b/i.test(instruction)) return "review";
532
+ if (/\b(arastir|araştır|research|web)\b/i.test(instruction)) return "research";
533
+ if (/\b(planla|plan|tasarla|design)\b/i.test(instruction)) return "plan";
534
+ return "implement";
535
+ }
536
+
537
+ // Gorev metni gercek bir yapim/degisiklik isi mi? Operator, plan evresinde "bilgi sorusu
538
+ // kestirmesi" ile (delegasyon acmadan complete) yaniti dogrudan verebilir. Ama prompt acikca
539
+ // bir uygulama/olusturma istiyorsa bu kestirme kullaniciya HIC is uretmeden sahte basari doner
540
+ // (or. proje hafizasindaki gecmis teslimati "zaten yapildi" sanmak). Bu durumda kestirmeyi
541
+ // engelleyip yeniden planlama isteriz. inferAssignmentKind'in implement fiil kumesiyle uyumlu.
542
+ function taskRequiresDelegation(prompt) {
543
+ return /\b(olustur|oluştur|yaz|uygula|kur|gelistir|geliştir|duzelt|düzelt|refactor|implement|build|create|edit|fix|generate|ekle|degistir|değiştir)\b/i.test(String(prompt || ""));
544
+ }
545
+
546
+ function roleAllowedKinds(agent) {
547
+ const role = path.basename(String(agent?.roleFile || "")).toLowerCase();
548
+ if (role.includes("executor")) return ["implement"];
549
+ if (role.includes("review")) return ["review"];
550
+ if (role.includes("planner")) return ["plan"];
551
+ return null;
552
+ }
553
+
554
+ function supportsKind(agent, kind) {
555
+ const roleKinds = roleAllowedKinds(agent);
556
+ if (roleKinds) return roleKinds.includes(kind);
557
+ const caps = capabilitiesFor(agent);
558
+ const wanted = {
559
+ implement: ["implementation", "coding", "development", "debugging"],
560
+ review: ["review", "testing", "security", "analysis"],
561
+ research: ["research", "web", "analysis"],
562
+ plan: ["planning", "analysis", "architecture"],
563
+ }[kind] || [];
564
+ return wanted.some((cap) => caps.has(cap));
565
+ }
566
+
567
+ function agentUsable(agent) {
568
+ return agent?.health?.status ? agent.health.status === "ready" : true;
569
+ }
570
+
571
+ function normalizeAssignments(value, cfg, operatorName, usedIds, taskText = "") {
572
+ if (!Array.isArray(value)) throw new Error("Operator assignments dizisi dondurmedi.");
573
+ const max = Math.max(1, cfg.operator?.maxDelegationsPerRound || 8);
574
+ // Operator daha once kullanilan bir kimligi yinelerse gorevi oldurmek yerine kimligi
575
+ // otomatik yeniden adlandiririz; saatlerce uretilmis teslimat bir ID cakismasi yuzunden
576
+ // basarisiz sayilmamalidir. Ayni turdaki dependsOn referanslari da yeni ada tasinir.
577
+ const renames = new Map();
578
+ return value.slice(0, max).map((raw, index) => {
579
+ let id = String(raw.id || `task-${Date.now()}-${index + 1}`).replace(/[^a-zA-Z0-9_.-]/g, "-");
580
+ let renamedFrom;
581
+ if (usedIds.has(id)) {
582
+ let n = 2;
583
+ while (usedIds.has(`${id}-r${n}`)) n++;
584
+ renamedFrom = id;
585
+ renames.set(id, `${id}-r${n}`);
586
+ id = `${id}-r${n}`;
587
+ }
588
+ usedIds.add(id);
589
+ const requestedAgent = String(raw.agent || "");
590
+ if (!cfg.agents[requestedAgent]) throw new Error(`Operator tanimsiz agent secti: ${requestedAgent}`);
591
+ if (cfg.agents[requestedAgent].enabled === false) throw new Error(`Operator devre disi agent secti: ${requestedAgent}`);
592
+ if (!agentUsable(cfg.agents[requestedAgent])) throw new Error(`Operator kullanilamaz agent secti: ${requestedAgent} (${cfg.agents[requestedAgent].health?.label || "saglik testi basarisiz"})`);
593
+ if (requestedAgent === operatorName) throw new Error("Operator kendisine uzman gorevi atayamaz.");
594
+ const instruction = String(raw.instruction || raw.task || "").trim();
595
+ if (!instruction) throw new Error(`${id} delegasyonunda instruction eksik.`);
596
+ const kind = inferAssignmentKind(raw, instruction);
597
+ // Beceriler kullanici-kapilidir: operator yalnizca cfg.skills.enabled icindekileri iliştirebilir.
598
+ // Tanimsiz/etkin olmayan beceri sessizce dusurulur; asla gorevi olduren bir hata degildir.
599
+ const hasExplicitSkills = Object.prototype.hasOwnProperty.call(raw, "skills");
600
+ const autoMatchOn = cfg.skills?.autoMatch !== false;
601
+ const requestedSkills = Array.isArray(raw.skills)
602
+ ? raw.skills
603
+ : (!hasExplicitSkills && autoMatchOn
604
+ ? skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind)
605
+ : []);
606
+ let skills = skillRegistry.resolveForAssignment(requestedSkills, cfg).map((skill) => skill.name);
607
+ // Operator kodlama/planlama/review isinde beceri iliştirmeyi atlarsa (or. skills:[] dönerse),
608
+ // kullanicinin etkin becerilerinden goreve GERCEKTEN uyanlari otomatik ekle. suggest() yalnizca
609
+ // lexical eslesme oldugunda skill döner; alakasiz beceri asla iliştirilmez. Boylece "operator
610
+ // becerileri kullanmiyor" durumu ortadan kalkar.
611
+ if (!skills.length && autoMatchOn && ["implement", "plan", "review"].includes(kind)) {
612
+ skills = skillRegistry.resolveForAssignment(
613
+ skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind), cfg
614
+ ).map((skill) => skill.name);
615
+ }
616
+ let agent = requestedAgent;
617
+ const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
618
+ if (unavailable.has(agent) || !supportsKind(cfg.agents[agent], kind)) {
619
+ const compatible = Object.entries(cfg.agents).find(([name, candidate]) => name !== operatorName && candidate.enabled !== false && agentUsable(candidate) && !unavailable.has(name) && supportsKind(candidate, kind));
620
+ if (compatible) agent = compatible[0];
621
+ else if (unavailable.has(agent)) throw new Error(`Operator bu oturumda kullanilamaz agent secti: ${requestedAgent}`);
622
+ }
623
+ return {
624
+ id, agent, kind, instruction, skills,
625
+ renamedFrom,
626
+ requestedAgent: agent === requestedAgent ? undefined : requestedAgent,
627
+ routingReason: agent === requestedAgent ? undefined : `${requestedAgent} ${unavailable.has(requestedAgent) ? "bu oturumda kullanilamaz" : `${kind} yetenegine sahip degil`}; ${agent} secildi.`,
628
+ dependsOn: Array.isArray(raw.dependsOn) ? raw.dependsOn.map((dep) => renames.get(String(dep)) || String(dep)) : [],
629
+ };
630
+ });
631
+ }
632
+
633
+ function compatibleAgentForKind(cfg, operatorName, kind) {
634
+ const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
635
+ return Object.entries(cfg.agents || {}).find(([name, agent]) =>
636
+ name !== operatorName && agent.enabled !== false && agentUsable(agent) &&
637
+ !unavailable.has(name) && supportsKind(agent, kind)
638
+ )?.[0] || "";
639
+ }
640
+
641
+ function nextAssignmentId(base, usedIds) {
642
+ let id = base;
643
+ let suffix = 2;
644
+ while (usedIds.has(id)) id = `${base}-${suffix++}`;
645
+ usedIds.add(id);
646
+ return id;
647
+ }
648
+
649
+ // Balanced bir uygulama gorevinde katalogda standart roller varsa ilk turu
650
+ // PLAN -> IMPLEMENT -> REVIEW olarak garanti eder. Operatorun bir rolu yanlislikla
651
+ // atlamasi, kullanicinin etkinlestirdigi uzmani sessizce devre disi birakamaz.
652
+ function ensureBalancedRoleChain(assignments, cfg, operatorName, task, usedIds) {
653
+ if (task.executionMode !== "balanced" || !assignments.some((item) => item.kind === "implement")) return assignments;
654
+ const chained = assignments.map((item) => ({ ...item, dependsOn: [...item.dependsOn] }));
655
+ let plan = chained.find((item) => item.kind === "plan");
656
+ if (!plan) {
657
+ const planner = compatibleAgentForKind(cfg, operatorName, "plan");
658
+ if (planner) {
659
+ const instruction = "Kullanici hedefini ve mevcut calisma klasorunu salt okunur incele. Executor icin dosya kapsamini, uygulama siralamasini, riskleri ve dogrulama adimlarini iceren somut bir plan hazirla; dosyalari degistirme.";
660
+ plan = {
661
+ id: nextAssignmentId("balanced-plan", usedIds),
662
+ agent: planner,
663
+ kind: "plan",
664
+ instruction,
665
+ dependsOn: [],
666
+ skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "plan"),
667
+ routingReason: "Balanced rol zinciri: kullanilabilir planner ilk tura otomatik eklendi.",
668
+ };
669
+ chained.unshift(plan);
670
+ }
671
+ }
672
+ const implementations = chained.filter((item) => item.kind === "implement");
673
+ if (plan) {
674
+ for (const implementation of implementations) {
675
+ if (!implementation.dependsOn.includes(plan.id)) implementation.dependsOn.push(plan.id);
676
+ }
677
+ }
678
+ let reviews = chained.filter((item) => item.kind === "review");
679
+ if (!reviews.length) {
680
+ const reviewer = compatibleAgentForKind(cfg, operatorName, "review");
681
+ if (reviewer) {
682
+ const instruction = "Tamamlanan uygulamayi kullanici hedefi ve kabul kriterlerine gore bagimsiz, salt okunur olarak denetle. Dosya ve test kanitlarini raporla; sonunda VERDICT: PASS veya VERDICT: FAIL yaz.";
683
+ const review = {
684
+ id: nextAssignmentId("balanced-review", usedIds),
685
+ agent: reviewer,
686
+ kind: "review",
687
+ instruction,
688
+ dependsOn: implementations.map((item) => item.id),
689
+ skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "review"),
690
+ routingReason: "Balanced rol zinciri: kullanilabilir reviewer ilk tura otomatik eklendi.",
691
+ };
692
+ chained.push(review);
693
+ reviews = [review];
694
+ }
695
+ }
696
+ for (const review of reviews) {
697
+ // Operator belirli bir uygulamayi incelemeye bagladiysa bu kapsami genisletme;
698
+ // ayni turdaki bagimsiz/yardimci bir implement hatasi ana incelemeyi bloke etmemeli.
699
+ if (!review.dependsOn.length) {
700
+ for (const implementation of implementations) review.dependsOn.push(implementation.id);
701
+ }
702
+ }
703
+ const rank = { plan: 0, research: 1, implement: 2, review: 3 };
704
+ return chained.map((item, index) => ({ item, index }))
705
+ .sort((a, b) => (rank[a.item.kind] ?? 2) - (rank[b.item.kind] ?? 2) || a.index - b.index)
706
+ .map(({ item }) => item);
707
+ }
708
+
709
+ class Engine extends EventEmitter {
710
+ constructor() {
711
+ super();
712
+ this.running = false;
713
+ this.busy = false;
714
+ this.current = null;
715
+ this.activeChild = null;
716
+ this.callSequence = 0;
717
+ // Kesin kimlik/kurulum hatasi veren agent'lari ayni motor oturumunda tekrar secme.
718
+ // Kalici config'i degistirmeyiz; kullanici kimlik bilgisini duzeltip motoru yeniden
719
+ // baslattiginda agent tekrar denenebilir.
720
+ this.unhealthyAgents = new Map();
721
+ // Canli kodlama akisi: gorev calisirken calisma klasorunu periyodik tarayan zamanlayici
722
+ // ve son yayinlanan degisiklik imzasi (ayni durum tekrar tekrar yayinlanmasin diye).
723
+ this._liveTimer = null;
724
+ this._lastFileChangeKey = "";
725
+ this._textBefore = null;
726
+ }
727
+
728
+ cfg() { return store.loadConfig(); }
729
+
730
+ status() {
731
+ const cfg = this.cfg();
732
+ return {
733
+ running: this.running,
734
+ busy: this.busy,
735
+ current: this.current,
736
+ mode: cfg.approvalMode,
737
+ defaultOperator: cfg.operator?.cli || "",
738
+ callsToday: store.getCallCount(),
739
+ budget: cfg.dailyCallBudget,
740
+ usageToday: store.getDailyUsage(),
741
+ };
742
+ }
743
+
744
+ // KULLANIM TELEMETRISI: gorev suresince agent bazinda token/maliyet toplar. Tamamen
745
+ // additive ve hata toleranslidir veri veremeyen CLI'lar (metin modunda calisan
746
+ // codex/claude/gemini) icin sayaclar bos kalir, hicbir akis bundan etkilenmez.
747
+ resetUsage() {
748
+ this._usage = { total: { ...EMPTY_USAGE }, byAgent: {}, calls: 0, reportingCalls: 0 };
749
+ }
750
+
751
+ recordUsage(agentName, usage) {
752
+ if (!this._usage) this.resetUsage();
753
+ this._usage.total = addUsage(this._usage.total, usage);
754
+ this._usage.byAgent[agentName] = addUsage(this._usage.byAgent[agentName], usage);
755
+ this._usage.reportingCalls++;
756
+ try { store.addDailyUsage(usage); } catch {}
757
+ }
758
+
759
+ // Gorev kapanisinda saklanacak sekil. Hicbir cagri veri vermediyse null doner ki arayuz
760
+ // "0 token" gibi yaniltici bir sayi yerine "veri yok" gosterebilsin.
761
+ usageSummary() {
762
+ const usage = this._usage;
763
+ if (!usage || !usage.reportingCalls) return null;
764
+ return {
765
+ total: usage.total,
766
+ byAgent: usage.byAgent,
767
+ calls: usage.calls,
768
+ reportingCalls: usage.reportingCalls,
769
+ };
770
+ }
771
+
772
+ publish(type, data, taskId = this.current?.id) {
773
+ const event = { at: new Date().toISOString(), taskId: taskId || null, ...data, type };
774
+ if (taskId) store.appendRunEvent(taskId, event);
775
+ this.emit(type, event);
776
+ return event;
777
+ }
778
+
779
+ // CANLI KODLAMA: gorev calisirken calisma klasorunu _snapBefore'a gore tarar ve olusan/
780
+ // degisen/silinen dosyalari "filechange" olayiyla yayinlar; complete()'teki nihai diff'in
781
+ // canli on-izlemesidir. Yalnizca durum degistiginde olay basar (ayni diff tekrarlanmaz).
782
+ // Tamamen additive ve salt-okunurdur; hata verirse gorevi etkilemeden sessizce atlar.
783
+ publishFileChanges(taskId = this.current?.id) {
784
+ if (!taskId || !this._cwd || !this._snapBefore) return null;
785
+ let changes, currentSnapshot;
786
+ try {
787
+ currentSnapshot = snapshotDir(this._cwd);
788
+ changes = diffSnapshots(this._snapBefore, currentSnapshot);
789
+ }
790
+ catch { return null; }
791
+ // Yalnizca dosya adlarini degil guncel stamp'i da imzaya kat. Ayni dosya gorev
792
+ // sirasinda ikinci kez degistiginde yeni satir diff'i mutlaka yayinlanmalidir.
793
+ const stampKey = [...changes.created, ...changes.modified]
794
+ .map((file) => `${file}:${currentSnapshot.get(file) || ""}`).join("|");
795
+ const key = `${stampKey}#${changes.deleted.join("|")}`;
796
+ if (key === this._lastFileChangeKey) return null;
797
+ const previouslyHadChanges = this._lastFileChangeKey !== "" && this._lastFileChangeKey !== "#";
798
+ const changedFiles = [
799
+ ...changes.created.map((file) => ({ file, action: "created" })),
800
+ ...changes.modified.map((file) => ({ file, action: "modified" })),
801
+ ...changes.deleted.map((file) => ({ file, action: "deleted" })),
802
+ ];
803
+ let remainingLines = DIFF_MAX_EVENT_LINES;
804
+ const files = changedFiles.map(({ file, action }) => {
805
+ if (remainingLines <= 0) return { path: file, action, additions: null, deletions: null, previewStatus: "event-limit", hunks: [] };
806
+ const described = describeFileDiff(this._cwd, file, action, this._textBefore, { maxLines: Math.min(DIFF_MAX_RENDERED_LINES, remainingLines) });
807
+ remainingLines -= (described.hunks || []).reduce((sum, hunk) => sum + (hunk.lines || []).length, 0);
808
+ return described;
809
+ });
810
+ // Bos degisim setini (or. baslangictaki taban veya tum degisikliklerin geri alinmasi)
811
+ // imzasini hatirla. Gorev basinda gurultu uretme; ancak daha once gorunen tum degisiklikler
812
+ // geri alindiysa bos olayi yayinla ki dashboard bayat diff'i temizlesin.
813
+ this._lastFileChangeKey = key;
814
+ if (!files.length && !previouslyHadChanges) return null;
815
+ return this.publish("filechange", {
816
+ files,
817
+ counts: { created: changes.created.length, modified: changes.modified.length, deleted: changes.deleted.length },
818
+ lineCounts: {
819
+ additions: files.reduce((sum, file) => sum + (Number(file.additions) || 0), 0),
820
+ deletions: files.reduce((sum, file) => sum + (Number(file.deletions) || 0), 0),
821
+ unavailable: files.filter((file) => file.additions == null || file.deletions == null).length,
822
+ },
823
+ }, taskId);
824
+ }
825
+
826
+ startLiveDiff(task) {
827
+ this.stopLiveDiff();
828
+ this._lastFileChangeKey = "";
829
+ // liveDiff kesin olarak false yapilmadikca aciktir (mevcut davranisi degistirmeyen additive
830
+ // ozellik). Tarama araligi config'ten ayarlanabilir; en dusuk 500ms ile sinirli.
831
+ const cfg = this.cfg();
832
+ if (cfg.liveDiff === false) { this._textBefore = null; return; }
833
+ this._textBefore = captureTextSnapshot(this._cwd, this._snapBefore);
834
+ const intervalMs = Math.max(500, Number(cfg.liveDiffIntervalMs) || 2500);
835
+ this._liveTimer = setInterval(() => this.publishFileChanges(task.id), intervalMs);
836
+ // unref: bekleyen bir tarama zamanlayicisi surecin kapanmasini asla engellemesin.
837
+ if (this._liveTimer && typeof this._liveTimer.unref === "function") this._liveTimer.unref();
838
+ }
839
+
840
+ stopLiveDiff() {
841
+ if (this._liveTimer) { clearInterval(this._liveTimer); this._liveTimer = null; }
842
+ }
843
+
844
+ setMode(mode) {
845
+ const cfg = this.cfg();
846
+ cfg.approvalMode = mode === "auto" ? "auto" : "ask";
847
+ store.saveConfig(cfg);
848
+ this.emit("status", this.status());
849
+ }
850
+
851
+ start() {
852
+ if (this.running) return;
853
+ if (!this.cfg().autonomousConsentAcceptedAt) {
854
+ const error = new Error("Otonom CLI calistirma kosullari henuz kabul edilmedi. Dashboard'daki ilk kullanim uyarisini onaylayin.");
855
+ error.code = "AUTONOMOUS_CONSENT_REQUIRED";
856
+ throw error;
857
+ }
858
+ this.running = true;
859
+ this.publish("log", { level: "info", msg: `Motor basladi. Mod=${this.cfg().approvalMode}` }, null);
860
+ this.emit("status", this.status());
861
+ this.loop();
862
+ }
863
+
864
+ stop() {
865
+ this.running = false;
866
+ this.wake();
867
+ this.stopLiveDiff();
868
+ if (this.activeChild) {
869
+ try { this.activeChild.kill(); } catch {}
870
+ }
871
+ this.publish("log", { level: "warn", msg: "Motor durduruluyor; aktif CLI islemi sonlandirildi." });
872
+ this.emit("status", this.status());
873
+ }
874
+
875
+ wake() {
876
+ if (this._wakeResolve) {
877
+ clearTimeout(this._wakeTimer);
878
+ const resolve = this._wakeResolve;
879
+ this._wakeResolve = null;
880
+ resolve();
881
+ }
882
+ }
883
+
884
+ sleepWake(ms) {
885
+ return new Promise((resolve) => {
886
+ this._wakeResolve = resolve;
887
+ this._wakeTimer = setTimeout(() => { this._wakeResolve = null; resolve(); }, ms);
888
+ });
889
+ }
890
+
891
+ async loop() {
892
+ while (this.running) {
893
+ const task = store.nextPending();
894
+ if (!task) {
895
+ await this.sleepWake((this.cfg().pollSeconds || 15) * 1000);
896
+ continue;
897
+ }
898
+ try {
899
+ await this.runTask(task);
900
+ } catch (error) {
901
+ if (!(await this.salvage(task, error))) {
902
+ const found = store.findTask(task.id);
903
+ const failure = classifyCliError(error);
904
+ task.status = "failed";
905
+ task.error = error.message;
906
+ // Siniflandirilmis teshis gorevle birlikte saklanir; arayuz "ne oldu / ne yapmali"
907
+ // ayrimini ham hata metnini kirparak tahmin etmek yerine buradan okur.
908
+ task.failure = failure;
909
+ // Basarisiz gorev de para harcar; telemetri burada da saklanmali.
910
+ task.usage = this.usageSummary();
911
+ task.finishedAt = new Date().toISOString();
912
+ if (found) store.moveTask(found.state, "failed", task);
913
+ this.publish("log", { level: "error", msg: `HATA (${failure.code}): ${failure.summary}` }, task.id);
914
+ this.publish("log", { level: "warn", msg: `Ne yapmali: ${failure.action}` }, task.id);
915
+ if (task.kind === "operator-chat") {
916
+ this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: task.parentTaskId, status: "failed", error: failure.summary, failure }, task.id);
917
+ }
918
+ this.notifyOutcome(task, "failed", { error: failure.summary, failureCode: failure.code, action: failure.action });
919
+ this.emit("queue");
920
+ }
921
+ } finally {
922
+ this.stopLiveDiff();
923
+ this._textBefore = null;
924
+ this.busy = false;
925
+ this.current = null;
926
+ this.activeChild = null;
927
+ this.emit("status", this.status());
928
+ }
929
+ }
930
+ }
931
+
932
+ // Uzman agent'i (operatorun altindaki ekip) cfg.agents'ten cozup calistirir.
933
+ invokeAgent(agentName, prompt, cfg, meta = {}) {
934
+ const configuredAgent = cfg.agents[agentName];
935
+ const agent = configuredAgent && cliRegistry.effectiveAgent(configuredAgent, cfg);
936
+ if (!agent) return Promise.reject(new Error(`Agent tanimsiz: ${agentName}`));
937
+ return this.runCli(agentName, agent, prompt, cfg, meta);
938
+ }
939
+
940
+ // Operatoru, uzman agent'lardan BAGIMSIZ olarak dogrudan CLI'dan (claude/codex/gemini/opencode)
941
+ // calistirir. operator.md rolunu bu CLI giyer.
942
+ invokeOperator(cli, prompt, cfg, meta = {}) {
943
+ const agent = cliRegistry.operatorSpec(cli, cfg);
944
+ if (!agent) return Promise.reject(new Error(`Operator CLI tanimsiz veya kurulu degil: ${cli}`));
945
+ return this.runCli(cli, agent, prompt, cfg, meta);
946
+ }
947
+
948
+ runCli(displayName, agent, prompt, cfg, meta = {}) {
949
+ return new Promise((resolve, reject) => {
950
+ const count = store.bumpCallCount();
951
+ if (count > (cfg.dailyCallBudget || Number.MAX_SAFE_INTEGER)) {
952
+ return reject(new Error(`Gunluk cagri butcesi asildi (${cfg.dailyCallBudget}).`));
953
+ }
954
+ if (!this._usage) this.resetUsage();
955
+ this._usage.calls++;
956
+
957
+ const callId = `${this.current?.id || "run"}-${++this.callSequence}`;
958
+ let promptFile = "";
959
+ let useStdin = true;
960
+ const rawArgs = (agent.args || []).map((arg) => {
961
+ if (String(arg).includes("{PROMPT}")) {
962
+ useStdin = false;
963
+ return String(arg).replaceAll("{PROMPT}", prompt);
964
+ }
965
+ if (String(arg).includes("{PROMPT_FILE}")) {
966
+ useStdin = false;
967
+ if (!promptFile) {
968
+ const promptDir = path.join(store.ROOT, "state", "prompts");
969
+ fs.mkdirSync(promptDir, { recursive: true });
970
+ promptFile = path.join(promptDir, `${callId}.md`);
971
+ fs.writeFileSync(promptFile, prompt, "utf8");
972
+ }
973
+ return String(arg).replaceAll("{PROMPT_FILE}", promptFile);
974
+ }
975
+ return String(arg);
976
+ });
977
+ const cwd = this._cwd || path.resolve(store.WORK_BASE, cfg.workingDir || ".");
978
+ // Path-aware hapis: codex sandbox'i disariyi zaten OS-native kapatir (effectiveAgent).
979
+ // Claude'un OS sandbox'i her platformda (ozellikle Windows'ta) olmadigi icin en azindan
980
+ // orkestratorun kendi topragini (config/memory/kurulum dizini) permissions.deny ile gizler.
981
+ // JSON'u arguman olarak gecmek Windows cmd.exe'de tirnaklari bozar; bu yuzden dosyaya yazip
982
+ // yolunu veriyoruz (prompt dosyasiyla ayni desen).
983
+ const sandboxSettings = claudeSandboxSettings(agent, cwd, cfg);
984
+ if (sandboxSettings) {
985
+ const sbDir = path.join(store.ROOT, "state", "prompts");
986
+ fs.mkdirSync(sbDir, { recursive: true });
987
+ const sbFile = path.join(sbDir, `${callId}.settings.json`);
988
+ fs.writeFileSync(sbFile, JSON.stringify(sandboxSettings), "utf8");
989
+ rawArgs.push("--settings", sbFile);
990
+ }
991
+ const command = cliRegistry.buildCommand(agent.cmd, rawArgs);
992
+ const file = command.file;
993
+ const args = command.args;
994
+ const started = Date.now();
995
+ let stdout = "", stderr = "", settled = false, timedOut = false, silenceTimedOut = false;
996
+ let timer, silenceTimer, progressTimer;
997
+ const base = { callId, agent: displayName, stage: meta.stage || "agent", assignmentId: meta.assignmentId || null };
998
+
999
+ this.current = { id: this.current?.id, stage: base.stage, agent: displayName, callId };
1000
+ this.emit("status", this.status());
1001
+ this.publish("activity", { ...base, kind: "process.started", cmd: agent.cmd, args: rawArgs, cwd });
1002
+
1003
+ // Motor onceliklidir: canli CLI saglik/model probe'lari (or. codex app-server / codex exec)
1004
+ // ayni CLI'yi ayni anda calistirirsa codex ikinci exec oturumunu SIGTERM ile keser. Bu yuzden
1005
+ // operatör/agent kosumuna girmeden uctaki tum probe'lari sonlandiriyoruz.
1006
+ cliRegistry.abortActiveProbes();
1007
+ let child;
1008
+ try { child = spawn(file, args, { cwd, env: cliRegistry.agentEnvironment(agent), windowsHide: true, shell: command.shell, windowsVerbatimArguments: !!command.verbatim }); }
1009
+ catch (error) { return reject(error); }
1010
+ this.activeChild = child;
1011
+ child.stdout.setEncoding("utf8");
1012
+ child.stderr.setEncoding("utf8");
1013
+
1014
+ const timeoutMs = Math.max(10, agent.timeoutSeconds || cfg.agentTimeoutSeconds || 900) * 1000;
1015
+ const silenceTimeoutMs = Math.max(1, agent.silenceTimeoutSeconds || cfg.cliSilenceTimeoutSeconds || 300) * 1000;
1016
+ let lastOutputAt = Date.now();
1017
+ const terminateTree = () => {
1018
+ if (isWin && child.pid) {
1019
+ try { spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" }); } catch {}
1020
+ } else {
1021
+ try { child.kill("SIGKILL"); } catch {}
1022
+ }
1023
+ };
1024
+ const armSilenceTimer = () => {
1025
+ clearTimeout(silenceTimer);
1026
+ silenceTimer = setTimeout(() => {
1027
+ if (settled) return;
1028
+ silenceTimedOut = true;
1029
+ this.publish("activity", { ...base, kind: "process.silence-timeout", silenceTimeoutMs, elapsedMs: Date.now() - started });
1030
+ terminateTree();
1031
+ }, silenceTimeoutMs);
1032
+ };
1033
+ armSilenceTimer();
1034
+ progressTimer = setInterval(() => {
1035
+ if (settled) return;
1036
+ this.publish("activity", { ...base, kind: "process.progress", elapsedMs: Date.now() - started, silentMs: Date.now() - lastOutputAt, silenceTimeoutMs });
1037
+ }, 15000);
1038
+ timer = setTimeout(() => {
1039
+ if (settled) return;
1040
+ timedOut = true;
1041
+ this.publish("activity", { ...base, kind: "process.timeout", timeoutMs });
1042
+ terminateTree();
1043
+ }, timeoutMs);
1044
+
1045
+ child.stdout.on("data", (data) => {
1046
+ const text = String(data);
1047
+ stdout += text;
1048
+ lastOutputAt = Date.now();
1049
+ armSilenceTimer();
1050
+ this.publish("activity", { ...base, kind: "stdout", text });
1051
+ });
1052
+ child.stderr.on("data", (data) => {
1053
+ const text = String(data);
1054
+ stderr += text;
1055
+ lastOutputAt = Date.now();
1056
+ armSilenceTimer();
1057
+ this.publish("activity", { ...base, kind: "stderr", text });
1058
+ });
1059
+ child.on("error", (error) => {
1060
+ if (settled) return;
1061
+ settled = true;
1062
+ clearTimeout(timer);
1063
+ clearTimeout(silenceTimer);
1064
+ clearInterval(progressTimer);
1065
+ this.activeChild = null;
1066
+ if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1067
+ this.publish("activity", { ...base, kind: "process.failed", error: error.message, durationMs: Date.now() - started });
1068
+ reject(error);
1069
+ });
1070
+ child.on("close", (code, signal) => {
1071
+ if (settled) return;
1072
+ settled = true;
1073
+ clearTimeout(timer);
1074
+ clearTimeout(silenceTimer);
1075
+ clearInterval(progressTimer);
1076
+ this.activeChild = null;
1077
+ if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1078
+ const durationMs = Date.now() - started;
1079
+ // Cikti tek kez ayristirilir; hem kullanim telemetrisi hem metin ayni sonuctan okunur.
1080
+ const normalized = normalizeCliOutput(agent, stdout);
1081
+ if (normalized.usage) this.recordUsage(displayName, normalized.usage);
1082
+ this.publish("activity", { ...base, kind: "process.finished", exitCode: code, signal, durationMs, usage: normalized.usage, reason: silenceTimedOut ? "silence-timeout" : (timedOut ? "timeout" : null) });
1083
+ if (silenceTimedOut) return reject(new Error(`CLI_STALLED: ${agent.cmd} ${Math.round(silenceTimeoutMs / 1000)} saniye boyunca cikti uretmedi ve otomatik durduruldu.`));
1084
+ if (timedOut) return reject(new Error(`${agent.cmd} ${Math.round(timeoutMs / 1000)} saniyede zaman asimina ugradi${signal ? ` (signal=${signal})` : ""}. ${clip(stdout || stderr, 500)}`));
1085
+ // Teshis icin HER IKI akis da gerekir: bazi CLI'lar (Gemini dahil) kota/oturum hatasini
1086
+ // stdout'a, yigin izini stderr'e yazar. Yalnizca birini almak siniflandiriciyi kor birakiyordu.
1087
+ if (code !== 0) {
1088
+ const detail = [stderr, stdout].map((stream) => String(stream || "").trim()).filter(Boolean).join("\n");
1089
+ return reject(new Error(`${agent.cmd} cikis kodu ${code ?? "yok"}${signal ? ` (signal=${signal})` : ""}.\n${clip(detail, 2000)}`));
1090
+ }
1091
+ if (normalized.error) return reject(new Error(`OpenCode model/provider hatasi: ${clip(normalized.error, 500)}`));
1092
+ if (!normalized.text) return reject(new Error(`${agent.cmd} kullanilabilir cikti dondurmedi.${stderr ? ` ${clip(stderr, 300)}` : ""}`));
1093
+ resolve({ text: normalized.text, stderr: stderr.trim(), exitCode: code, durationMs, callId, usage: normalized.usage });
1094
+ });
1095
+
1096
+ // Prompt argumanla/dosyayla verilse bile stdin MUTLAKA kapatilmali. OpenCode gibi CLI'lar
1097
+ // stdin TTY degilse borudan mesaj okur; EOF gelmezse model cagrisina hic gecmeden bloke olur.
1098
+ child.stdin.on("error", () => {});
1099
+ child.stdin.end(useStdin ? prompt : "");
1100
+ });
1101
+ }
1102
+
1103
+ // Katalogda dosya yazabilen (implement) kullanilabilir bir uzman var mi? Operatorun kendisi ve
1104
+ // bu oturumda karantinaya alinan agentler haric. Tek executor (or. opencode) sessizlik nedeniyle
1105
+ // dustugunde takim fiilen dosya uretemez hale gelir; bu kontrol o durumu erken yakalar.
1106
+ hasUsableImplementAgent(cfg, operatorName) {
1107
+ return Object.entries(cfg.agents || {}).some(([name, agent]) =>
1108
+ name !== operatorName && agent.enabled !== false && agentUsable(agent)
1109
+ && !this.unhealthyAgents.has(name) && supportsKind(agent, "implement"));
1110
+ }
1111
+
1112
+ agentCatalog(cfg, operatorName) {
1113
+ return Object.entries(cfg.agents)
1114
+ .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.unhealthyAgents.has(name))
1115
+ .map(([name, a]) => ({
1116
+ name,
1117
+ description: a.description || "",
1118
+ capabilities: [...capabilitiesFor(a)],
1119
+ allowedKinds: roleAllowedKinds(a) || ["implement", "review", "research", "plan"].filter((kind) => supportsKind(a, kind)),
1120
+ roleFile: a.roleFile || "",
1121
+ costTier: a.costTier || "standard",
1122
+ }));
1123
+ }
1124
+
1125
+ async invokeOperatorJson(operatorCli, prompt, cfg, stage, label) {
1126
+ const retries = Math.max(0, cfg.operator?.protocolRetries ?? 1);
1127
+ let lastError;
1128
+ let correction = "";
1129
+ for (let attempt = 0; attempt <= retries; attempt++) {
1130
+ try {
1131
+ const response = await this.invokeOperator(operatorCli, prompt + correction, cfg, { stage });
1132
+ return parseJson(response.text, label);
1133
+ } catch (error) {
1134
+ lastError = error;
1135
+ // Yalnizca gercek JSON protokol hatasi yeniden denenir. CLI'nin sessiz kalmasi,
1136
+ // auth/timeout veya proses hatasi ayni operatoru tekrar calistirmamalidir.
1137
+ if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) throw error;
1138
+ if (attempt >= retries) break;
1139
+ // Modelin ne dondurdugunu duzeltme istegine geri veriyoruz: "yalnizca JSON dondur"
1140
+ // talimatini tekrarlamak, hatayi goremeyen model icin cogu zaman ayni cevabi uretiyordu.
1141
+ correction = `\n\nONCEKI CEVAP PROTOKOLE UYMADI: ${error.message}\n` +
1142
+ `Senin onceki cevabin (kirpilmis):\n"""\n${clip(String(error.rawText || ""), 1200)}\n"""\n` +
1143
+ `Bu cevap gecerli bir JSON nesnesi degildi. Simdi SADECE tek bir JSON nesnesi dondur: ` +
1144
+ `ilk karakter { ve son karakter } olsun; oncesinde/sonrasinda selamlama, aciklama, dusunce, ` +
1145
+ `Markdown veya \`\`\` kod bloğu OLMASIN. Metin alanlarindaki cift tirnaklari \\" ile kacir.`;
1146
+ this.publish("log", { level: "warn", msg: `${label} protokol hatasi; operator yeniden deneniyor (${attempt + 2}/${retries + 1}).` });
1147
+ }
1148
+ }
1149
+ throw lastError;
1150
+ }
1151
+
1152
+ // Insertion sirasi tamamlanma sirasidir; en son biten denetim en gecerli karardir.
1153
+ latestReview(state) {
1154
+ return Object.values(state.results || {}).reverse()
1155
+ .find((result) => result.kind === "review" && result.status === "completed" && result.verdict) || null;
1156
+ }
1157
+
1158
+ // Operatorun karar evresine verilen takim kaydi. Ham state dokumu yerine sonuc odakli
1159
+ // bir ozet uretir; her sonucu kendi icinde ortadan kirpar ki en yeni denetim karari
1160
+ // (metnin sonundaki VERDICT satiri) butce asiminda kaybolmasin.
1161
+ teamDigest(state, cfg) {
1162
+ const results = Object.values(state.results || {});
1163
+ const budget = cfg.teamContextCharBudget || 30000;
1164
+ const perResult = Math.max(900, Math.floor(budget / Math.max(1, results.length)));
1165
+ const digest = {
1166
+ round: state.round,
1167
+ completionCriteria: state.criteria,
1168
+ results: results.map((result) => ({
1169
+ id: result.id, agent: result.agent, kind: result.kind, status: result.status,
1170
+ ...(result.verdict ? { verdict: result.verdict } : {}),
1171
+ result: clipMiddle(result.result, perResult),
1172
+ })),
1173
+ };
1174
+ return clipMiddle(JSON.stringify(digest, null, 2), budget);
1175
+ }
1176
+
1177
+ operatorPrompt(task, cfg, memory, state, phase) {
1178
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
1179
+ // Operatör rolü daima operator.md'dir; operatör bir CLI'dir, uzman agent'lar onun altında çalışır.
1180
+ const roleFile = cfg.operator?.roleFile || "roles/operator.md";
1181
+ const role = store.readRole(path.basename(roleFile));
1182
+ const catalog = this.agentCatalog(cfg, operatorCli);
1183
+ const skillContext = `${task.prompt}\n${phase === "review" ? Object.values(state.results || {}).map((result) => result.instruction || "").join("\n") : ""}`;
1184
+ const skillDiscovery = skillRegistry.discover(cfg, skillContext);
1185
+ const skillCatalog = skillDiscovery.catalog;
1186
+ const skillStats = skillDiscovery.stats;
1187
+ const enabledSkillNames = skillRegistry.enabledSkills(cfg).map((skill) => skill.name);
1188
+ // Beceri envanteri motor tarafindan uretilen OTORITER veridir. Operatore TAM etkin listeyi
1189
+ // (sayi + tum adlar) DAIMA veririz; boylece "kac beceri var" gibi sorular icin operatorun
1190
+ // kendi CLI'sinin dahili becerilerini veya bir alt agent'in iddiasini kullanmasi gerekmez.
1191
+ // Eslesen kisa liste yalnizca DELEGASYONA iliştirme icindir, envanterin tamami degildir.
1192
+ const skillSection = skillStats.enabled
1193
+ ? `## Beceriler (OTORITER kaynak)\nSistemde tam olarak ${skillStats.enabled} etkin beceri var: ${enabledSkillNames.join(", ")}.\n` +
1194
+ `Beceri sayisi/adi/varligi sorulursa YALNIZCA bu listeyi esas al ve dogrudan kendin cevapla; calistigin CLI'nin dahili becerilerini bu sisteme KATMA, bir alt agent farkli bir sayi soylerse bu listeyi degil onu YOK say.\n` +
1195
+ (skillCatalog.length
1196
+ ? `Bu goreve en ilgili ${skillCatalog.length} beceri (delegasyonun skills alanina yalnizca bunlardan uygun olanlari ekle):\n${JSON.stringify(skillCatalog, null, 2)}\n`
1197
+ : `Bu goreve gore taramada eslesme cikmadi; bu delegasyonlarda beceri iliştirme.\n`)
1198
+ : "";
1199
+ // Beceriler tamamen opsiyoneldir ve YALNIZCA kullanici etkinlestirdiginde katalogda gorunur.
1200
+ // Operator, assignment'a yalnizca bu kataloktaki adlari "skills" dizisinde iliştirebilir.
1201
+ const skillField = skillCatalog.length ? `, "skills":["beceri-adi"]` : "";
1202
+ const skillProtocol = skillCatalog.length
1203
+ ? ` BECERI: Kisa listedeki gercekten ilgili adlari skills dizisine ekle; ilgisiz veya liste disi ad kullanma, uygun yoksa alani bos birak.`
1204
+ : "";
1205
+ const protocol = phase === "plan"
1206
+ ? `Yalnizca gecerli JSON dondur. Gorev bir is/degisiklik/arastirma gerektiriyorsa delege et: {"summary":"yaklasim", "completionCriteria":["..."], "assignments":[{"id":"benzersiz-id", "agent":"catalog-name", "kind":"implement|review|research|plan", "instruction":"net gorev ve teslimat", "dependsOn":[]${skillField}}]} (en az bir assignment). ` +
1207
+ `Gorev yalnizca bir bilgi/soru VEYA bir selamlasma/sohbet mesaji ise (or. "merhaba", "nasilsin", "kac beceri var") ve yaniti dogrudan verebiliyorsan, HICBIR delegasyon acmadan: {"status":"complete", "final":"kullaniciya gosterilecek CEVABIN TAM METNI", "verification":"cevabin dayandigi kaynak"}. ` +
1208
+ `Selamlasmalarda bile cevabi JSON DISINA yazma: kullaniciya gosterilen tek alan "final"dir, nesnenin disindaki her metin atilir. Gercek is gerektiren gorevi bu kestirmeyle atlatma.${skillProtocol}`
1209
+ : `Yalnizca gecerli JSON dondur. Is tamamlanmadiysa {"status":"continue", "reason":"...", "assignments":[{"id":"...", "agent":"...", "kind":"implement|review|research|plan", "instruction":"...", "dependsOn":[]${skillField}}]}; tum kabul kriterleri karsilandiysa {"status":"complete", "final":"en fazla 5 kisa maddeyle ne yapildi", "verification":"tek satir dogrulama"}. Dosya listesini motor ekleyecek; final icinde uzun log veya ham agent cevabi tekrarlama. Continue icin en az bir yeni assignment zorunlu.${skillProtocol}\n` +
1210
+ `INCELEME DISIPLINI: Bagimsiz denetim VERDICT: PASS verdiyse ayni teslimat icin YENI review delegasyonu acma; complete dondur ve dusuk/orta onemdeki kalan notlari final metninde kalan risk olarak belirt. Ekipte bulunmayan dogrulama yetenegini (or. canli tarayici) tamamlanma sarti yapma; NOT RUN kalan dusuk riskli kontroller teslimati engellemez.`;
1211
+ const strategy = task.executionMode === "fast"
1212
+ ? "HIZLI MOD: Kucuk bir is. Tek bir implementation agenti kullan. Ayri planlama veya review delegasyonu acma; uzman raporu hedefi karsiliyorsa ilk degerlendirmede tamamla."
1213
+ : task.executionMode === "deep"
1214
+ ? "DERIN MOD: Isi uygun uzmanliklar arasinda dagit — planlama, uygulama, test ve bagimsiz inceleme icin AYRI ve dogru uzmanlari kullan."
1215
+ : "DENGELI MOD: Katalogda planner, executor ve reviewer varsa ucunu da ILK planda kullan. PLANLAMA -> UYGULAMA -> BAGIMSIZ INCELEME delegasyonlarini dependsOn ile ayni turda zincirle ve isi tek turda bitirmeyi hedefle. Ayni rolde birden fazla esdeger agent varsa yalnizca en uygun olani sec.";
1216
+ return `${role}\n\n---\n## Operator protokolu\n${protocol}\n` +
1217
+ // OpenCode gibi sohbet odakli CLI'lar varsayilan olarak once bir cumle yazip JSON'u
1218
+ // arkasina ekliyor; motor bunu ayiklayabilse de belirsizlik protokol hatasi uretiyordu.
1219
+ `## Cikti bicimi (KATI)\nCevabin TAMAMI tek bir JSON nesnesi olmali: ilk karakter { , son karakter } . ` +
1220
+ `Oncesinde/sonrasinda selamlama, aciklama, dusunce notu, Markdown basligi veya \`\`\` kod bloğu YAZMA. ` +
1221
+ `Kullaniciya iletmek istedigin her seyi ilgili JSON alanina ("final" veya "summary") koy; nesnenin disindaki metin kullaniciya ULASMAZ.\n` +
1222
+ `Yalnizca katalogdaki agent adlarini kullan. Her assignment kind degeri secilen agentin allowedKinds listesinde olmali. Kendine gorev atama. Ayni delegasyon id'sini tekrar kullanma.\n` +
1223
+ `Bir agent basarisiz veya kullanilamaz raporlandiysa ayni isi ona tekrar verme; katalogdaki alternatif bir uzmani sec.\n` +
1224
+ `AJAN SECIMI: Her alt gorevi, katalogdaki YETENEKLERE ve role gore o ise EN UYGUN uzmana ata; tum isi tek bir CLI'ye yigma. Isin ihtiyac duydugu her uzmanlik icin dogru uzmani sec (plan/tasarim -> planlama yetenegi; uygulama -> implementation; inceleme/dogrulama -> review/test; arastirma -> research/web). Ayni KIND icin birden fazla eşdeğer agent varsa YALNIZCA birini (en uygun ve en dusuk maliyetli) kullan; ayni isi iki eşdeğer agente verme. Farkli roller (or. bir uygulayici + bir inceleyici) FARKLI islerdir, tekrar sayilmaz.\n` +
1225
+ `## Calisma stratejisi\n${strategy}\n` +
1226
+ `## Agent katalogu\n${JSON.stringify(catalog, null, 2)}\n` +
1227
+ skillSection +
1228
+ `## Kullanici gorevi\n${task.prompt}\n` +
1229
+ `## Calisma klasoru\n${this._cwd}\n` +
1230
+ `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` +
1231
+ `## 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` +
1232
+ (phase === "review"
1233
+ ? `## Son bagimsiz denetim\n${(() => { const review = this.latestReview(state); return review ? `${review.id} (${review.agent}) → VERDICT: ${review.verdict}` : "Henuz tamamlanmis denetim yok."; })()}\n` +
1234
+ `## Takim calisma kaydi\n${this.teamDigest(state, cfg)}\n`
1235
+ : "");
1236
+ }
1237
+
1238
+ specialistPrompt(task, cfg, assignment, state) {
1239
+ const agent = cfg.agents[assignment.agent];
1240
+ const roleByKind = { implement: "executor.md", review: "reviewer.md", plan: "planner.md" };
1241
+ const selectedRole = roleByKind[assignment.kind] || (agent.roleFile ? path.basename(agent.roleFile) : "executor.md");
1242
+ const role = store.readRole(selectedRole);
1243
+ const instructionByKind = {
1244
+ implement: "Delegasyonu calisma klasorunde uygula, degisiklikleri dogrula ve rolundeki teslimat formatinda raporla.",
1245
+ review: "Teslimati bagimsiz ve salt okunur olarak incele; gerekli kontrolleri calistir, dosyalari degistirme ve rolundeki karar formatinda raporla.",
1246
+ plan: "Calisma klasorunu salt okunur olarak incele; hicbir dosyayi degistirmeden uygulanabilir plani rolundeki formatta raporla.",
1247
+ research: "Delegasyon kapsaminda kanit topla; kaynaklari ve belirsizlikleri ayirarak kisa bir sonuc raporla.",
1248
+ };
1249
+ const completed = Object.values(state.results).map((r) => ({
1250
+ id: r.id, agent: r.agent, instruction: r.instruction, ...(r.verdict ? { verdict: r.verdict } : {}), result: clipMiddle(r.result, 5000),
1251
+ }));
1252
+ // Progressive disclosure: operatorun bu delegasyona sectigi becerilerin tam govdesini YIGMAYIZ;
1253
+ // yalnizca ad + kisa ozet + rehber dosya yolunu veririz. Uzman gercekten ihtiyac duyarsa dosyayi
1254
+ // kendisi okur. Operator hicbir beceri secmediyse (veya beceriler kapali ise) blok bostur.
1255
+ const skills = skillRegistry.resolveForAssignment(assignment.skills, cfg);
1256
+ const skillRefs = skillRegistry.toPromptRefs(skills, cfg.skills?.referenceCharBudget || 1200);
1257
+ return `${role}\n\n---\n## Takim agenti protokolu\n` +
1258
+ `Operator sana asagidaki isi devretti. ${instructionByKind[assignment.kind] || instructionByKind.implement} ` +
1259
+ `Planda olmayan riskli bir is gerekiyorsa yapma; BLOCKED olarak bildir. Yalnizca gercekten gozlemledigin veya dogruladigin sonuclari yaz.\n` +
1260
+ `SANDBOX: Butun is YALNIZCA calisma klasorunde (${this._cwd}) yapilir. Bu klasor disina dosya YAZMA/DEGISTIRME/SILME ve gerekmedikce disariyi OKUMA.\n` +
1261
+ (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` : "") +
1262
+ `## Ana hedef\n${task.prompt}\n## Delegasyon\nID: ${assignment.id}\n${assignment.instruction}\n` +
1263
+ `## Onceki tamamlanan takim isleri\n${clip(JSON.stringify(completed, null, 2), cfg.teamContextCharBudget || 30000)}`;
1264
+ }
1265
+
1266
+ async runAssignments(task, cfg, assignments, state) {
1267
+ const pending = assignments.slice();
1268
+ while (pending.length) {
1269
+ let index = pending.findIndex((a) => a.dependsOn.every((id) => state.results[id]?.status === "completed"));
1270
+ if (index < 0) {
1271
+ index = pending.findIndex((a) => a.dependsOn.some((id) => state.results[id] && state.results[id].status !== "completed"));
1272
+ if (index >= 0) {
1273
+ const blocked = pending.splice(index, 1)[0];
1274
+ state.usedIds.push(blocked.id);
1275
+ const failedDeps = blocked.dependsOn.filter((id) => state.results[id]?.status !== "completed");
1276
+ state.results[blocked.id] = { ...blocked, status: "blocked", result: `Bagimli gorev basarisiz: ${failedDeps.join(", ")}` };
1277
+ const blockedMessage = { from: "system", to: task.operatorCli, messageType: "blocked", assignmentId: blocked.id, body: `${blocked.id} calistirilmadi; bagimli gorev basarisiz: ${failedDeps.join(", ")}`, at: new Date().toISOString() };
1278
+ state.messages.push(blockedMessage);
1279
+ this.publish("message", blockedMessage, task.id);
1280
+ continue;
1281
+ }
1282
+ throw new Error(`Delegasyon bagimliliklari cozumlenemedi: ${pending.map((a) => a.id).join(", ")}`);
1283
+ }
1284
+ const assignment = pending.splice(index, 1)[0];
1285
+ state.usedIds.push(assignment.id);
1286
+ state.messages.push({ from: task.operatorCli, to: assignment.agent, messageType: "delegation", assignmentId: assignment.id, body: assignment.instruction, at: new Date().toISOString() });
1287
+ this.publish("message", state.messages[state.messages.length - 1], task.id);
1288
+ if (assignment.routingReason) this.publish("log", { level: "info", msg: `Otomatik yonlendirme: ${assignment.routingReason}` }, task.id);
1289
+ if (assignment.renamedFrom) this.publish("log", { level: "warn", msg: `Operator delegasyon kimligini yineledi; ${assignment.renamedFrom} otomatik olarak ${assignment.id} yapildi.` }, task.id);
1290
+ this.publish("log", { level: "stage", msg: `DELEGE ${assignment.id} -> ${assignment.agent}` }, task.id);
1291
+ if (assignment.skills?.length) this.publish("log", { level: "info", msg: `Beceriler: ${assignment.skills.join(", ")}` }, task.id);
1292
+ try {
1293
+ const response = await this.invokeAgent(
1294
+ assignment.agent,
1295
+ this.specialistPrompt(task, cfg, assignment, state),
1296
+ cfg,
1297
+ { stage: "delegate", assignmentId: assignment.id }
1298
+ );
1299
+ const result = { ...assignment, status: "completed", result: response.text, durationMs: response.durationMs, callId: response.callId };
1300
+ if (assignment.kind === "review") {
1301
+ result.verdict = extractVerdict(response.text);
1302
+ this.publish("log", { level: "info", msg: `Denetim ${assignment.id}: VERDICT ${result.verdict || "BELIRSIZ"}` }, task.id);
1303
+ }
1304
+ state.results[assignment.id] = result;
1305
+ const message = { from: assignment.agent, to: task.operatorCli, messageType: "result", assignmentId: assignment.id, body: response.text, at: new Date().toISOString() };
1306
+ state.messages.push(message);
1307
+ this.publish("message", message, task.id);
1308
+ } catch (error) {
1309
+ const failure = classifyCliError(error);
1310
+ if (QUARANTINE_CLI_ERRORS.has(failure.code)) {
1311
+ this.unhealthyAgents.set(assignment.agent, { code: failure.code, at: new Date().toISOString() });
1312
+ cfg.runtimeUnavailableAgents = [...this.unhealthyAgents.keys()];
1313
+ }
1314
+ state.results[assignment.id] = { ...assignment, status: "failed", result: failure.summary, error: failure };
1315
+ 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() };
1316
+ state.messages.push(message);
1317
+ this.publish("message", message, task.id);
1318
+ this.publish("log", { level: "warn", msg: `${assignment.agent} kullanilamadi [${failure.code}]. Operator alternatif agent sececek.` }, task.id);
1319
+ }
1320
+ task.teamState = state;
1321
+ store.saveTask("pending", task);
1322
+ }
1323
+ }
1324
+
1325
+ isRisky(text, cfg) {
1326
+ const lower = String(text).toLowerCase();
1327
+ return (cfg.riskyPatterns || []).some((pattern) => lower.includes(String(pattern).toLowerCase()));
1328
+ }
1329
+
1330
+ async runTask(task) {
1331
+ const baseCfg = this.cfg();
1332
+ if (!baseCfg.autonomousConsentAcceptedAt) throw new Error("Otonom CLI calistirma kosullari kabul edilmeden gorev calistirilamaz.");
1333
+ if (task.kind === "operator-chat") return this.runChatTask(task, baseCfg);
1334
+ task.executionMode = resolveExecutionMode(task);
1335
+ const cfg = applyExecutionPolicy(baseCfg, task.executionMode);
1336
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
1337
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Gecerli bir operator CLI secilmedi (claude/codex/gemini/opencode).");
1338
+ task.operatorCli = operatorCli;
1339
+ this.busy = true;
1340
+ this.current = { id: task.id, stage: "operator", agent: operatorCli };
1341
+ this._cwd = path.resolve(store.WORK_BASE, task.targetDir || cfg.workingDir || ".");
1342
+ this._snapBefore = snapshotDir(this._cwd);
1343
+ this.startLiveDiff(task);
1344
+ // Onay sonrasi devam eden gorevde sayaci sifirlamayiz; ilk girise ozgu.
1345
+ if (!task.approved || !this._usage) this.resetUsage();
1346
+ this.publish("log", { level: "task", msg: `GOREV ${task.id}: ${task.prompt}` }, task.id);
1347
+ // Otomatik surumleme: gorev CALISMADAN ONCE calisma klasorunun bir surumunu al ki ajan
1348
+ // mevcut kodu bozarsa bu gorev oncesine tek tikla donulebilsin. Yalnizca ilk girus'te
1349
+ // (approval sonrasi devam da ayni checkpoint'i korur). Basarisizsa gorevi asla oldurmez.
1350
+ if (baseCfg.versioning !== false && !task.checkpointId) {
1351
+ const cp = checkpoints.createCheckpoint(this._cwd, { taskId: task.id, label: task.prompt, kind: "pre-task", retention: baseCfg.versioningRetention });
1352
+ if (cp.ok) {
1353
+ task.checkpointId = cp.id;
1354
+ store.saveTask("pending", task);
1355
+ this.publish("log", { level: "info", msg: `Surum alindi (${cp.fileCount} dosya · ${cp.backend}); bu gorev oncesine geri donulebilir.` }, task.id);
1356
+ } else if (cp.skipped) {
1357
+ this.publish("log", { level: "warn", msg: `Surum alinamadi: ${cp.reason}` }, task.id);
1358
+ }
1359
+ }
1360
+ this.publish("log", { level: "info", msg: `Calisma modu: ${task.executionMode.toUpperCase()} · en fazla ${cfg.operator.maxRounds} tur / ${cfg.operator.maxDelegationsPerRound} delegasyon` }, task.id);
1361
+ this.emit("status", this.status());
1362
+ this.emit("queue");
1363
+
1364
+ cfg.runtimeUnavailableAgents = [...this.unhealthyAgents.keys()];
1365
+ // Proje context: varsayilan olarak bu calisma klasorunun .crewctl/CONTEXT.md profilini
1366
+ // yukleriz (path'e ozel, tum kodu bastan taramaya gerek kalmasin). task.freshContext ise
1367
+ // temiz sayfa. projectContext:false ise eski GLOBAL hafiza davranisi korunur.
1368
+ const memory = cfg.projectContext === false
1369
+ ? store.getMemory(cfg.memoryCharBudget)
1370
+ : (task.freshContext ? "" : store.readProjectContext(this._cwd));
1371
+ const state = task.teamState || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1372
+ const usedIds = new Set(state.usedIds || []);
1373
+
1374
+ let assignments;
1375
+ if (state.plan && task.approved) {
1376
+ assignments = normalizeAssignments(state.plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1377
+ assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1378
+ this.publish("log", { level: "info", msg: "Onaylanmis operator plani zorunlu rol zinciri korunarak devam ettiriliyor." }, task.id);
1379
+ } else {
1380
+ // Bilgi sorusu kestirmesi: operator, delege edilecek bir is olmadan yaniti dogrudan
1381
+ // verdiginde (or. "kac beceri var") tek turda tamamla. Boylece motorun otoriter verisi
1382
+ // bir alt agente aktarilirken kaybolmaz/carpitilmaz ve zorunlu assignment semasi bu
1383
+ // tur sorularda gorevi bosuna oldurmez. ANCAK gorev acikca bir yapim/degisiklik isiyse
1384
+ // kestirmeye izin verilmez: operator (cogunlukla proje hafizasindaki gecmis teslimati
1385
+ // "zaten yapildi" sanip) is uretmeden kapatmaya calisir. Once yeniden planlama isteriz;
1386
+ // israr ederse gorevi sahte basari yerine net bir hatayla dururuz.
1387
+ const requiresWork = taskRequiresDelegation(task.prompt);
1388
+ const shortcutRetries = requiresWork ? Math.max(1, cfg.operator?.protocolRetries ?? 1) : 0;
1389
+ let plan, shortcutAttempt = 0, planCorrection = "";
1390
+ while (true) {
1391
+ try {
1392
+ plan = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "plan") + planCorrection, cfg, "operator-plan", "Operator plani");
1393
+ } catch (error) {
1394
+ // Sohbet kurtarma: gorev delegasyon gerektirmiyorsa (selamlasma, kisa soru) ve operator
1395
+ // protokol nesnesi yerine duz metinle cevap verdiyse, kullaniciyi "gecerli JSON
1396
+ // dondurmedi" hatasiyla karsilamak yanlistir — istenen sey zaten o metindi. Yalnizca
1397
+ // is gerektirmeyen gorevlerde ve elde gercek bir metin varken devreye girer.
1398
+ const prose = conversationalAnswer(error);
1399
+ if (requiresWork || !prose) throw error;
1400
+ this.publish("log", { level: "warn", msg: "Operator protokol nesnesi dondurmedi; gorev is gerektirmedigi icin duz metin cevabi teslimat sayildi." }, task.id);
1401
+ task.teamState = state;
1402
+ return this.complete(task, state, "done", prose, { verification: "Operatorun dogrudan yaniti (protokol nesnesi yerine duz metin)." });
1403
+ }
1404
+ const isShortcut = plan && String(plan.status).toLowerCase() === "complete" && !Array.isArray(plan.assignments);
1405
+ if (!isShortcut) break;
1406
+ if (!requiresWork) {
1407
+ if (!String(plan.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1408
+ this.publish("log", { level: "info", msg: "Operator gorevi delegasyona gerek gormeden dogrudan yanitladi." }, task.id);
1409
+ task.teamState = state;
1410
+ return this.complete(task, state, "done", String(plan.final), plan);
1411
+ }
1412
+ if (shortcutAttempt++ >= shortcutRetries) {
1413
+ throw new Error("Operator bu yapim gorevini delege etmeden kapatmaya calisti. Proje hafizasindaki gecmis teslimatlar gorevi tamamlanmis saymaz; en az bir uygulama delegasyonu gerekir.");
1414
+ }
1415
+ this.publish("log", { level: "warn", msg: "Operator yapim gorevini delegasyonsuz kapatmaya calisti; hafiza gecmisi kanit sayilmaz, yeniden planlama isteniyor." }, task.id);
1416
+ planCorrection = "\n\nUYARI: Bu GERCEK bir yapim/degisiklik gorevidir. Proje hafizasindaki gecmis teslimatlar (or. onceki turlarda uretilmis dosyalar) bu gorevi TAMAMLANMIS SAYDIRMAZ; kullanici isin bu oturumda yeniden uretilip dogrulanmasini istiyor. status=complete DONME; en az bir 'implement' delegasyonu iceren bir plan uret.";
1417
+ }
1418
+ assignments = normalizeAssignments(plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1419
+ assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1420
+ state.plan = { summary: String(plan.summary || ""), completionCriteria: Array.isArray(plan.completionCriteria) ? plan.completionCriteria.map(String) : [], assignments };
1421
+ state.criteria = state.plan.completionCriteria;
1422
+ state.operatorDecisions.push({ round: 0, ...state.plan });
1423
+ task.teamState = state;
1424
+ task.planPreview = JSON.stringify(state.plan, null, 2);
1425
+ store.saveTask("pending", task);
1426
+ if (cfg.approvalMode === "ask" && this.isRisky(task.planPreview, cfg)) {
1427
+ task.status = "awaiting-approval";
1428
+ task.planHash = store.hashText(task.planPreview);
1429
+ store.moveTask("pending", "approval", task);
1430
+ this.publish("log", { level: "warn", msg: "Riskli operator plani insan onayina alindi." }, task.id);
1431
+ this.busy = false;
1432
+ this.current = null;
1433
+ this.emit("queue");
1434
+ this.emit("status", this.status());
1435
+ this.stopLiveDiff();
1436
+ this._textBefore = null;
1437
+ return;
1438
+ }
1439
+ }
1440
+
1441
+ const maxRounds = Math.max(1, cfg.operator?.maxRounds || 6);
1442
+ const maxRecoveryRounds = Math.max(0, cfg.operator?.maxInfrastructureRecoveryRounds ?? 2);
1443
+ let recoveryRounds = 0;
1444
+ while (this.running && state.round < maxRounds + recoveryRounds) {
1445
+ state.round++;
1446
+ await this.runAssignments(task, cfg, assignments, state);
1447
+ const infrastructureFailures = assignments.filter((assignment) => {
1448
+ const result = state.results[assignment.id];
1449
+ return result?.status === "failed" && RECOVERABLE_CLI_ERRORS.has(result.error?.code);
1450
+ });
1451
+ if (infrastructureFailures.length && recoveryRounds < maxRecoveryRounds) {
1452
+ recoveryRounds++;
1453
+ this.publish("log", { level: "warn", msg: `CLI altyapi hatasi tur butcesinden dusulmedi; ${maxRecoveryRounds - recoveryRounds} ek kurtarma turu kaldi.` }, task.id);
1454
+ }
1455
+ // Uygulama agenti kalmadi guvenligi: Gorev acikca dosya olusturma/degistirme gerektiriyor
1456
+ // fakat katalogda 'implement' yapabilen kullanilabilir hicbir agent kalmadiysa (or. tek
1457
+ // executor opencode sessizlik nedeniyle karantinaya alindi) ve simdiye kadar tamamlanmis bir
1458
+ // uygulama isi da yoksa, HICBIR ek tur dosya uretemez. Operatoru planlayici/gap-analizi
1459
+ // dongusunde bosuna dondurup tur ve cagri butcesini yakmak yerine net, eyleme donuk bir
1460
+ // hatayla dur (kullanicinin gordugu "circle seklinde uzayip gitme" sorununu bu keser).
1461
+ if (taskRequiresDelegation(task.prompt)
1462
+ && !this.hasUsableImplementAgent(cfg, operatorCli)
1463
+ && !Object.values(state.results).some((result) => result.kind === "implement" && result.status === "completed")) {
1464
+ const dead = [...this.unhealthyAgents.keys()];
1465
+ throw new Error(
1466
+ "Bu gorev dosya olusturma/degistirme gerektiriyor fakat kullanilabilir bir uygulama (implement) agenti yok" +
1467
+ (dead.length ? ` (${dead.join(", ")} bu oturumda kullanim disi kaldi)` : "") +
1468
+ ". 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."
1469
+ );
1470
+ }
1471
+ if (task.executionMode === "fast" && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")) {
1472
+ const reports = assignments.map((assignment) => state.results[assignment.id].result).join("\n\n");
1473
+ this.publish("log", { level: "info", msg: "FAST mod: uzman teslimati basarili; ikinci operator degerlendirme cagrisi atlandi." }, task.id);
1474
+ return this.complete(task, state, "done", reports, {});
1475
+ }
1476
+ // PASS hizli yolu: turdaki tum delegasyonlar tamamlandi ve turun kendi bagimsiz
1477
+ // denetimi PASS verdiyse operatorun ikinci degerlendirme cagrisi bilgi eklemez,
1478
+ // yalnizca dakikalar kaybettirir. Bayat bir PASS'in yeni turu kapatmamasi icin
1479
+ // karar en guncel denetim olmalidir. operator.passFastPath=false ile kapatilabilir.
1480
+ const roundReview = assignments
1481
+ .map((assignment) => state.results[assignment.id])
1482
+ .filter((result) => result?.kind === "review" && result.status === "completed" && result.verdict)
1483
+ .pop();
1484
+ if (cfg.operator?.passFastPath !== false
1485
+ && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")
1486
+ && roundReview?.verdict === "PASS"
1487
+ && roundReview === this.latestReview(state)) {
1488
+ this.publish("log", { level: "ok", msg: `Denetim ${roundReview.id} PASS verdi; operator degerlendirme cagrisi atlandi ve teslimat tamamlandi.` }, task.id);
1489
+ const completedWork = Object.values(state.results).filter((result) => result.status === "completed");
1490
+ const final = `Teslimat bagimsiz denetimden gecti (${roundReview.id} VERDICT: PASS).\n` +
1491
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1492
+ return this.complete(task, state, "done", final, { verification: `${roundReview.agent} VERDICT: PASS` });
1493
+ }
1494
+ const decision = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "review"), cfg, "operator-review", "Operator karari");
1495
+ state.operatorDecisions.push({ round: state.round, ...decision });
1496
+ task.teamState = state;
1497
+ store.saveTask("pending", task);
1498
+ if (String(decision.status).toLowerCase() === "complete") {
1499
+ if (!String(decision.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1500
+ return this.complete(task, state, "done", String(decision.final), decision);
1501
+ }
1502
+ if (String(decision.status).toLowerCase() !== "continue") throw new Error("Operator status alani continue veya complete olmali.");
1503
+ assignments = normalizeAssignments(decision.assignments, cfg, operatorCli, new Set(state.usedIds), task.prompt);
1504
+ // Inceleme dongusu valisi: bagimsiz denetim PASS verdiyse ve operator yalnizca yeni
1505
+ // inceleme turlari acmak istiyorsa dongu burada kesilir. Sonsuz review ping-pong'u
1506
+ // hem tur butcesini tuketiyor hem de basarili teslimati kullaniciya geciktiriyordu.
1507
+ const lastReview = this.latestReview(state);
1508
+ if (lastReview?.verdict === "PASS" && assignments.every((assignment) => assignment.kind === "review")) {
1509
+ this.publish("log", { level: "warn", msg: `Denetim ${lastReview.id} PASS verdi; yalnizca yeni inceleme iceren tur acilmadi ve teslimat tamamlandi.` }, task.id);
1510
+ const final = `Teslimat bagimsiz denetimden gecti (${lastReview.id} VERDICT: PASS). ` +
1511
+ `Operatorun ek inceleme talebi tur butcesini korumak icin motor tarafindan sonlandirildi.` +
1512
+ (String(decision.reason || "").trim() ? `\nOperator notu: ${clip(decision.reason, 600)}` : "");
1513
+ return this.complete(task, state, "done", final, { verification: `${lastReview.agent} → VERDICT: PASS` });
1514
+ }
1515
+ }
1516
+ if (!this.running) throw new Error("Motor durduruldu; gorev tamamlanmadan kesildi.");
1517
+ return this.finishExhausted(task, state, recoveryRounds);
1518
+ }
1519
+
1520
+ // Tur butcesi doldugunda tamamlanmis isi cope atmak yerine uyarili kismi teslimat yapar.
1521
+ // Kullanicinin gordugu sonuc "saatlerce bekledim ve hata aldim" degil, "teslimat hazir,
1522
+ // su riskler acik kaldi" olmalidir. Hic tamamlanan is yoksa eski davranis korunur.
1523
+ finishExhausted(task, state, recoveryRounds) {
1524
+ const completedWork = Object.values(state.results || {}).filter((result) => result.status === "completed");
1525
+ if (!completedWork.length) {
1526
+ throw new Error(`Operator ${state.round} tur sonunda gorevi tamamlayamadi${recoveryRounds ? ` (${recoveryRounds} CLI kurtarma turu kullanildi)` : ""}.`);
1527
+ }
1528
+ const lastReview = this.latestReview(state);
1529
+ const lastDecision = [...(state.operatorDecisions || [])].reverse().find((decision) => String(decision.status).toLowerCase() === "continue");
1530
+ const warnings = [
1531
+ `Tur butcesi doldu (${state.round} tur); operator complete karari veremeden teslimat kapatildi.`,
1532
+ ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} → VERDICT: ${lastReview.verdict}.`] : []),
1533
+ ...(lastDecision && String(lastDecision.reason || "").trim() ? [`Operatorun acik biraktigi konu: ${clip(lastDecision.reason, 400)}`] : []),
1534
+ ];
1535
+ this.publish("log", { level: "warn", msg: `Tur butcesi doldu; tamamlanan is kismi teslimat olarak kapatiliyor (${completedWork.length} tamamlanmis delegasyon).` }, task.id);
1536
+ const final = `KISMI TESLIMAT: Tur butcesi doldugu icin gorev, tamamlanan isle kapatildi.\n` +
1537
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n") +
1538
+ `\nAcik kalan konular teslimat uyarilarinda listelendi.`;
1539
+ return this.complete(task, state, "done", final, {
1540
+ warnings,
1541
+ verification: lastReview ? `${lastReview.agent} VERDICT: ${lastReview.verdict}` : "",
1542
+ });
1543
+ }
1544
+
1545
+ // Gorev beklenmedik bir hatayla kesildiginde (protokol hatasi, butce asimi vb.) somut is
1546
+ // uretilmisse gorevi failed yerine uyarili kismi teslimatla kapatir. Basarili olamazsa
1547
+ // false doner ve normal hata akisi calisir.
1548
+ async salvage(task, error) {
1549
+ try {
1550
+ if (task.kind === "operator-chat") return false;
1551
+ if (!this._cwd || !this._snapBefore) return false;
1552
+ const state = task.teamState;
1553
+ const completedWork = Object.values(state?.results || {}).filter((result) => result.status === "completed");
1554
+ const changes = diffSnapshots(this._snapBefore, snapshotDir(this._cwd));
1555
+ const hasFileChanges = Boolean(changes.created.length || changes.modified.length || changes.deleted.length);
1556
+ const hasImplementation = completedWork.some((result) => result.kind === "implement");
1557
+ // Kurtarma kosulu: YA tamamlanmis bir uygulama delegasyonu var (ekip uretti) YA DA calisma
1558
+ // klasoru fiilen degisti. Ikincisi, operatorun kendisi standart JSON protokolune uymadan
1559
+ // isi dogrudan yaptigi durumu da kapsar (or. OpenCode operator rolunde JSON plan yerine
1560
+ // dosyalari kendisi degistirip duz metin doner). Hicbir is yoksa normal hata akisina birak.
1561
+ if (!hasFileChanges && !hasImplementation) return false;
1562
+ const failure = classifyCliError(error);
1563
+ const rescueState = state || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1564
+ const operatorDirect = !completedWork.length && hasFileChanges;
1565
+ this.publish("log", { level: "warn", msg: `Gorev hatayla kesildi (${failure.summary}); ${operatorDirect ? "operatorun dogrudan yaptigi degisiklikler" : "tamamlanan is"} kismi teslimat olarak korunuyor.` }, task.id);
1566
+ const lastReview = this.latestReview(rescueState);
1567
+ const changedFiles = [...changes.created, ...changes.modified, ...changes.deleted];
1568
+ const final = operatorDirect
1569
+ ? `KISMI TESLIMAT: Operator (${task.operatorCli || "?"}) standart delegasyon protokolune uymadan isi dogrudan uyguladi; degisiklikler korundu.\n` +
1570
+ changedFiles.slice(0, 40).map((file) => `- ${file}`).join("\n")
1571
+ : `KISMI TESLIMAT: Gorev bir altyapi/protokol hatasiyla kesildi fakat tamamlanan is korundu.\n` +
1572
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1573
+ await this.complete(task, rescueState, "done", final, {
1574
+ warnings: [
1575
+ `Gorev su hatayla kesildi: ${failure.summary}`,
1576
+ ...(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.`] : []),
1577
+ ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} VERDICT: ${lastReview.verdict}.`] : []),
1578
+ ],
1579
+ verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1580
+ });
1581
+ return true;
1582
+ } catch {
1583
+ return false;
1584
+ }
1585
+ }
1586
+
1587
+ async runChatTask(task, cfg) {
1588
+ const parentFound = store.findTask(task.parentTaskId);
1589
+ if (!parentFound) throw new Error("Sohbetin bagli oldugu tamamlanmis gorev bulunamadi.");
1590
+ const parent = parentFound.task;
1591
+ let operatorCli = parent.operatorCli;
1592
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) operatorCli = cfg.operator?.cli;
1593
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Sohbet icin operator CLI bulunamadi.");
1594
+ this.busy = true;
1595
+ this.current = { id: task.id, stage: "operator-chat", agent: operatorCli };
1596
+ this._cwd = path.resolve(store.WORK_BASE, parent.targetDir || cfg.workingDir || ".");
1597
+ this.publish("log", { level: "task", msg: `OPERATOR SOHBETI ${parent.id}: ${task.prompt}` }, task.id);
1598
+ this.emit("status", this.status());
1599
+
1600
+ const role = store.readRole("operator-chat.md");
1601
+ const resultContext = Object.values(parent.teamState?.results || {}).map((result) => ({
1602
+ id: result.id, agent: result.agent, status: result.status, result: clip(result.result, 1800),
1603
+ }));
1604
+ const prompt = `${role}\n\n---\n## Tamamlanmis gorev hakkinda takip sohbeti\n` +
1605
+ `Kullanici daha once tamamladiginiz gorev hakkinda soru soruyor. Yeni dosya degistirme, arac kullanma veya delegasyon yapma. ` +
1606
+ `Asagidaki kayitlara dayanarak dogrudan, kisa ve acik cevap ver. Bilmiyorsan bunu belirt.\n` +
1607
+ `## Gorev kimligi\n${parent.id}\n## Gorev\n${parent.prompt}\n## Teslimat\n${JSON.stringify(parent.delivery || { summary: parent.summary, changes: parent.changes }, null, 2)}\n` +
1608
+ `## Takim raporlari\n${clip(JSON.stringify(resultContext, null, 2), 12000)}\n` +
1609
+ `## Onceki sohbet\n${clip(JSON.stringify(parent.conversation || [], null, 2), 8000)}\n## Yeni soru\n${task.prompt}`;
1610
+ const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-chat" });
1611
+ const entry = { question: task.prompt, answer: response.text, at: new Date().toISOString(), taskId: task.id };
1612
+ parent.conversation ||= [];
1613
+ parent.conversation.push(entry);
1614
+ store.saveTask(parentFound.state, parent);
1615
+ task.status = "done";
1616
+ task.finishedAt = new Date().toISOString();
1617
+ task.final = response.text;
1618
+ task.summary = clip(response.text, 1000);
1619
+ store.moveTask("pending", "done", task);
1620
+ this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: parent.id, answer: response.text, status: "done", operator: operatorCli }, task.id);
1621
+ this.publish("log", { level: "ok", msg: `Operator soruyu yanitladi: ${parent.id}` }, task.id);
1622
+ this.emit("queue");
1623
+ }
1624
+
1625
+ async complete(task, state, status, finalText, decision = {}) {
1626
+ const changes = diffSnapshots(this._snapBefore || new Map(), snapshotDir(this._cwd));
1627
+ // Son periyodik taramadan sonra yapilan degisiklikleri de replay kaydina ve dashboard'a
1628
+ // aktar; ardindan run --once yolunda da timer'in acik kalmamasini garanti et.
1629
+ this.publishFileChanges(task.id);
1630
+ this.stopLiveDiff();
1631
+ task.status = status;
1632
+ task.finishedAt = new Date().toISOString();
1633
+ const files = [
1634
+ ...changes.created.map((file) => ({ path: file, action: "created" })),
1635
+ ...changes.modified.map((file) => ({ path: file, action: "modified" })),
1636
+ ...changes.deleted.map((file) => ({ path: file, action: "deleted" })),
1637
+ ];
1638
+ const concise = clip(String(finalText).replace(/\n{3,}/g, "\n\n"), 700);
1639
+ let verification = String(decision.verification || "").trim();
1640
+ if (!verification) {
1641
+ for (const result of Object.values(state.results)) {
1642
+ const match = String(result.result || "").match(/DO(?:Ğ|G\u0306|G)RULAMA:\s*([^\n`]+)/i);
1643
+ if (match) { verification = match[1].trim(); break; }
1644
+ }
1645
+ }
1646
+ task.summary = concise;
1647
+ task.final = finalText;
1648
+ task.changes = changes;
1649
+ task.usage = this.usageSummary();
1650
+ const warnings = (Array.isArray(decision.warnings) ? decision.warnings : []).map(String).filter(Boolean);
1651
+ task.delivery = {
1652
+ summary: concise,
1653
+ files,
1654
+ location: this._cwd,
1655
+ verification,
1656
+ mode: task.executionMode,
1657
+ rounds: state.round,
1658
+ agents: [...new Set(Object.values(state.results).map((result) => result.agent))],
1659
+ ...(warnings.length ? { warnings } : {}),
1660
+ };
1661
+ task.teamState = state;
1662
+ const found = store.findTask(task.id);
1663
+ store.moveTask(found?.state || "pending", status, task);
1664
+ store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
1665
+ this.publish("result", { id: task.id, prompt: task.prompt, status, dir: this._cwd, changes, summary: task.summary, delivery: task.delivery, usage: task.usage, operator: task.operatorCli, rounds: state.round }, task.id);
1666
+ this.publish("log", { level: "ok", msg: `GOREV tamamlandi: ${task.id}` }, task.id);
1667
+ this.notifyOutcome(task, status, { summary: task.summary, rounds: state.round, fileCount: files.length, verification: task.delivery.verification });
1668
+ this.emit("queue");
1669
+ this._textBefore = null;
1670
+ // Proje profilini guncelle (sonuc zaten kullaniciya iletildi; motor mesgul kaldigindan
1671
+ // sonraki gorevle es zamanli operator cagrisi olmaz). Basarisizsa gorevi ASLA etkilemez.
1672
+ await this.reviseProjectContext(task, status, files);
1673
+ }
1674
+
1675
+ // Gorev bitiminde bu calisma klasorunun .crewctl/CONTEXT.md proje profilini operatore REVIZE
1676
+ // ettirir (append degil): mimari/yapi/konvansiyon/komutlar guncel kalir, bayatlamaz. Yalnizca
1677
+ // basarili ve DOSYA DEGISTIREN gorevlerde calisir (bilgi sorulari profili degistirmez).
1678
+ async reviseProjectContext(task, status, files) {
1679
+ try {
1680
+ const cfg = this.cfg();
1681
+ if (cfg.projectContext === false) return;
1682
+ if (status !== "done" || !this._cwd) return;
1683
+ if (!Array.isArray(files) || !files.length) return;
1684
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
1685
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) return;
1686
+ const budget = cfg.projectContextCharBudget || 6000;
1687
+ const current = store.readProjectContext(this._cwd);
1688
+ const changed = files.map((f) => `${f.action}: ${f.path}`).join("\n");
1689
+ const prompt =
1690
+ `Sen bir projenin YASAYAN bilgi tabanini (proje profili) guncelliyorsun. Amac: sonraki gorevlerde ` +
1691
+ `ajan tum kodu bastan taramadan projeyi anlasin.\n` +
1692
+ `Asagida MEVCUT profil ve SON tamamlanan gorevin ozeti + degisen dosyalar var. Guncel, oz ve DOGRU ` +
1693
+ `bir proje profili YAZ (Markdown). Kurallar:\n` +
1694
+ `- Degisen gerceklere gore REVIZE et; silinen/tasinan/degisen seyleri guncelle, bayat bilgi birakma.\n` +
1695
+ `- Dayanikli bilgiyi tut: amac, mimari, klasor/dosya yapisi, giris noktalari, onemli moduller, ` +
1696
+ `konvansiyonlar, kurulum/calistirma/test komutlari, dikkat noktalari.\n` +
1697
+ `- Task-by-task changelog YAZMA; tek bir GUNCEL DURUM profili olsun.\n` +
1698
+ `- En fazla ~${budget} karakter. YALNIZCA profil metnini dondur; aciklama/selamlama/JSON/kod-bloğu isareti YAZMA.\n\n` +
1699
+ `## Mevcut profil\n${current || "(henuz yok — sifirdan olustur)"}\n\n` +
1700
+ `## Son tamamlanan gorev\n${task.prompt}\n\n## Sonuc ozeti\n${task.summary || ""}\n\n## Degisen dosyalar\n${changed}`;
1701
+ const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-context" });
1702
+ const profile = stripCodeFence(String(response?.text || "")).trim();
1703
+ if (profile && store.writeProjectContext(this._cwd, clip(profile, budget * 2))) {
1704
+ this.publish("log", { level: "info", msg: "Proje profili (.crewctl/CONTEXT.md) guncellendi." }, task.id);
1705
+ }
1706
+ } catch (error) {
1707
+ this.publish("log", { level: "warn", msg: `Proje profili guncellenemedi: ${error.message}` }, task.id);
1708
+ }
1709
+ }
1710
+
1711
+ // Dis bildirim icin tek cikis noktasi: server.js bu event'i dinleyip (varsa) webhook'a POST atar.
1712
+ // Motoru HTTP'den bagimsiz tutar; bildirim basarisiz olsa bile gorev akisini etkilemez.
1713
+ notifyOutcome(task, status, extra = {}) {
1714
+ try {
1715
+ this.emit("notify", {
1716
+ status,
1717
+ id: task.id,
1718
+ prompt: task.prompt,
1719
+ operator: task.operatorCli || null,
1720
+ dir: this._cwd || task.targetDir || null,
1721
+ at: new Date().toISOString(),
1722
+ ...extra,
1723
+ });
1724
+ } catch {}
1725
+ }
1726
+ }
1727
+
1728
+ module.exports = new Engine();
1729
+ // Testlerin dis davranisla birlikte kritik saf kurallari da dogrudan dogrulayabilmesi icin.
1730
+ module.exports._internals = {
1731
+ normalizeAssignments, ensureBalancedRoleChain, extractVerdict, clipMiddle, snapshotDir, diffSnapshots,
1732
+ captureTextSnapshot, buildLineDiff, describeFileDiff, isSensitiveDiffPath, taskRequiresDelegation,
1733
+ parseJson, conversationalAnswer, classifyCliError, RECOVERABLE_CLI_ERRORS, QUARANTINE_CLI_ERRORS,
1734
+ normalizeCliOutput, addUsage, usageTotal, claudeSandboxSettings,
1735
+ };