@omerrgocmen/crewctl 1.3.1 → 1.3.3

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 (92) hide show
  1. package/README.md +193 -193
  2. package/orchestrator/README.md +473 -473
  3. package/orchestrator/config.default.json +96 -90
  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/checkpoints.js +183 -183
  69. package/orchestrator/src/cli-registry.js +125 -3
  70. package/orchestrator/src/cli.js +140 -140
  71. package/orchestrator/src/doctor.js +64 -64
  72. package/orchestrator/src/engine.js +2112 -1914
  73. package/orchestrator/src/schedule.js +124 -124
  74. package/orchestrator/src/server.js +942 -817
  75. package/orchestrator/src/skill-registry.js +272 -272
  76. package/orchestrator/src/store.js +583 -478
  77. package/orchestrator/web/OrbitControls.js +1417 -1417
  78. package/orchestrator/web/board.html +146 -146
  79. package/orchestrator/web/code.html +213 -213
  80. package/orchestrator/web/flow.html +741 -741
  81. package/orchestrator/web/index.html +784 -622
  82. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -231
  83. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -104
  84. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -95
  85. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -99
  86. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -77
  87. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -415
  88. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -45
  89. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -66
  90. package/orchestrator/web/site.webmanifest +12 -12
  91. package/orchestrator/web/three.module.min.js +6 -6
  92. package/package.json +51 -51
@@ -1,1914 +1,2112 @@
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
- // Hatanin geldigi CLI biliniyorsa tavsiye ona gore uretilir (runCli her hatayi etiketler).
307
- const adapter = error?.adapter || "";
308
- const agentName = error?.agentName || "";
309
- const of = (code, summary, action) => ({ code, summary, action, agent: agentName, adapter, raw: clip(raw, 5000) });
310
- if (/requires a newer version|upgrade to the latest|model metadata.*not found|unsupported.*model|model.*unsupported/i.test(raw)) {
311
- return of("VERSION_INCOMPATIBLE", "CLI sürümü seçilen modeli desteklemiyor.", "CLI aracını güncelleyin veya desteklenen bir model seçin.");
312
- }
313
- if (/sessiz kaldi|cikti uretmedi|CLI_STALLED/i.test(raw)) {
314
- return of("CLI_STALLED", "CLI uzun süre yeni çıktı üretmeyince otomatik durduruldu.", "Önceki ilerleme kayıtları korundu; operatör bu oturumda farklı bir agent kullanacak.");
315
- }
316
- // "No provider available" 401 ile gelir ama OTURUM sorunu degildir: secili model hicbir
317
- // saglayiciya cozulemiyordur. Genel 401/403 kuralindan ONCE gelmeli, yoksa zaten girisli
318
- // kullaniciya bosuna "oturum acin" denir.
319
- if (/no provider available/i.test(raw)) {
320
- return of("PROVIDER_UNAVAILABLE", "Seçili model hiçbir yapılandırılmış sağlayıcıya çözümlenemedi (oturum sorunu değil).",
321
- "Ayarlar Agent'lar bölümünde bu agent için açık bir model seçin. Model boşken OpenCode kendi son kullandığı modele düşer; o model erişilemezse görev bu hatayla durur.");
322
- }
323
- if (/API key not valid|API_KEY_INVALID|invalid[_ ]api[_ ]key|incorrect api key/i.test(raw)) {
324
- return of("AUTH_INVALID", "API anahtarı geçersiz veya reddedildi.", `Bu agent'ın API anahtarını yenileyin. ${cliRegistry.authHint(adapter)}`);
325
- }
326
- // Gemini CLI oturum acilmadiginda "Please set an Auth method" / "GEMINI_API_KEY environment
327
- // variable not found" yazar; her ikisi de kurulum degil OTURUM sorunudur.
328
- if (/set an auth method|GEMINI_API_KEY|GOOGLE_API_KEY|unauthorized|unauthenticated|authentication|login required|not logged in|please (run )?login|credentials? (not found|missing|expired)|PERMISSION_DENIED|\b40[13]\b/i.test(raw)) {
329
- return of("AUTH_REQUIRED", "CLI oturum açmamış; kimlik doğrulaması gerekiyor.", `${cliRegistry.authHint(adapter)} Sonra görevi tekrar deneyin.`);
330
- }
331
- // Kota (gunluk/aylik hak bitti) ile hiz siniri (kisa sureli) AYRI teshislerdir: ilki
332
- // beklemekle gecmez, ikincisi gecer. Kullanicinin sordugu "limitim mi bitti" ayrimi budur.
333
- 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)) {
334
- 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.");
335
- }
336
- if (/rate.?limit|too many requests|\b429\b/i.test(raw)) {
337
- 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.");
338
- }
339
- if (/overloaded|\bUNAVAILABLE\b|\b(503|500)\b|internal error|try again later/i.test(raw)) {
340
- 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.");
341
- }
342
- if (/location is not supported|not available in your country|region.*not supported/i.test(raw)) {
343
- return of("REGION_BLOCKED", "Model bulunduğunuz bölgede kullanıma kapalı.", "Desteklenen bir bölge/hesap kullanın veya başka bir sağlayıcının agent'ını seçin.");
344
- }
345
- if (/ConnectionRefused|Unable to connect|provider hatasi|provider error|ECONNREFUSED/i.test(raw)) {
346
- return of("PROVIDER_UNAVAILABLE", "Seçilen model sağlayıcısına bağlanılamadı veya model çözümlenemedi.",
347
- adapter === "opencode"
348
- ? "Ayarlar Agent'lar bölümünde bu agent için açık bir model seçin (`opencode models` listesinden). Model seçili değilken OpenCode kendi son kullandığı modele düşer ve bu model erişilemez olabilir."
349
- : "Sağlayıcı bağlantısını kontrol edin veya başka bir model seçin.");
350
- }
351
- if (/ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|getaddrinfo|fetch failed|socket hang up|network (error|unreachable)|proxy/i.test(raw)) {
352
- return of("NETWORK_ERROR", " bağlantısı kurulamadı (DNS/proxy/internet).", "İnternet veya kurumsal proxy ayarlarınızı kontrol edip görevi tekrar çalıştırın.");
353
- }
354
- if (/not recognized as an internal|is not recognized|command not found|spawn .*ENOENT|\bENOENT\b|no such file or directory/i.test(raw)) {
355
- return of("CLI_NOT_FOUND", "CLI komutu bulunamadı; kurulu değil veya PATH üzerinde görünmüyor.", `${adapter && adapter !== "custom" ? `${adapter} CLI'sini kurun` : "CLI'yi kurun"} ve Ayarlar → Agent'lar bölümünde komut adını doğrulayın (kurulum komutları için Doctor ekranına bakın).`);
356
- }
357
- if (/timeout|timed out|zaman asim|zaman aşım/i.test(raw)) {
358
- return of("TIMEOUT", "CLI ayrılan süre içinde tamamlanamadı.", "Agent ayarlarından timeout değerini artırın veya görevi daha küçük parçalara bölün.");
359
- }
360
- return of("CLI_FAILED", clip(meaningfulErrorLine(raw), 500), "Ham CLI çıktısı teknik ayrıntılarda saklandı; sorun sürerse alternatif bir agent deneyin.");
361
- }
362
-
363
- 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"]);
364
- // Karantina = bu oturumda tekrar denemek anlamsiz. Kota/bolge kisiti beklemekle gecmez;
365
- // hiz siniri (RATE_LIMIT) ve gecici saglayici hatasi (MODEL_OVERLOADED) gecer, karantinaya alinmaz.
366
- const QUARANTINE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "QUOTA_EXCEEDED", "REGION_BLOCKED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "CLI_STALLED", "VERSION_INCOMPATIBLE"]);
367
- // Birkac saniyede kendiliginden gecer; operatore geri tasiyip planlama turu harcamak yerine
368
- // ayni agent ustel bekleme ile yeniden denenir.
369
- const TRANSIENT_CLI_ERRORS = new Set(["RATE_LIMIT", "MODEL_OVERLOADED", "NETWORK_ERROR"]);
370
- // Karantina suresi (sn). 0 = beklemekle duzelmez, kalici. Digerleri sure dolunca yeniden
371
- // denenir (half-open); tekrar duserse sure katlanir (bkz. quarantineAgent).
372
- const QUARANTINE_COOLDOWN_SECONDS = {
373
- AUTH_REQUIRED: 180,
374
- AUTH_INVALID: 180,
375
- PROVIDER_UNAVAILABLE: 180,
376
- CLI_STALLED: 300,
377
- QUOTA_EXCEEDED: 0,
378
- REGION_BLOCKED: 0,
379
- CLI_NOT_FOUND: 0,
380
- VERSION_INCOMPATIBLE: 0,
381
- };
382
-
383
- function resolveExecutionMode(task) {
384
- if (["fast", "balanced", "deep"].includes(task.executionMode)) return task.executionMode;
385
- const prompt = String(task.prompt || "");
386
- const complex = /(mimari|migration|refactor|guvenlik|güvenlik|deploy|production|veritabani|veritabanı|authentication|entegrasyon|çoklu|multi|kapsamli|kapsamlı)/i.test(prompt);
387
- // Not: "oyun/sayfa" gibi sifirdan build isleri BASIT sayilmaz — bunlar plan+uygula+incele
388
- // gerektiren gercek gorevlerdir; fast moda dusurulup ekip kullanilmadan gecilmemeli.
389
- // Not: "dosya" gibi genis kelimeler fast'a dusurmez; yorumdaki ilkeyle celisiyordu
390
- // (sifirdan build/dosya olusturma gercek bir gorevdir). Yalnizca acik "kucuk/hizli" sinyalleri.
391
- const simple = prompt.length < 350 && /(basit|ufak|küçük|kucuk|hizli|hızlı|simple|small|quick)/i.test(prompt);
392
- return simple && !complex ? "fast" : "balanced";
393
- }
394
-
395
- function applyExecutionPolicy(base, mode) {
396
- const cfg = { ...base, operator: { ...(base.operator || {}) } };
397
- if (mode === "fast") {
398
- cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 2);
399
- cfg.operator.maxDelegationsPerRound = 1;
400
- cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 2500);
401
- cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 10000);
402
- } else if (mode === "balanced") {
403
- // Saglikli bir balanced gorev tek turda biter (uygulama + zincirli inceleme);
404
- // 3 tur, iki duzeltme/yeniden dogrulama turuna yer birakir.
405
- cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 3);
406
- cfg.operator.maxDelegationsPerRound = Math.min(cfg.operator.maxDelegationsPerRound || 8, 3);
407
- cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 6000);
408
- cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 24000);
409
- }
410
- return cfg;
411
- }
412
-
413
- const EMPTY_USAGE = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
414
-
415
- function addUsage(target, extra) {
416
- const sum = { ...(target || EMPTY_USAGE) };
417
- for (const key of Object.keys(EMPTY_USAGE)) sum[key] = (sum[key] || 0) + (Number(extra?.[key]) || 0);
418
- return sum;
419
- }
420
-
421
- function usageTotal(usage) {
422
- return (Number(usage?.input) || 0) + (Number(usage?.output) || 0);
423
- }
424
-
425
- // OpenCode'un step_finish olayindaki token/maliyet alanlarini normalize eder. Alan adlari
426
- // surumler arasinda degisebildigi icin eksik alan 0 sayilir; hicbir sayi yoksa null doner
427
- // ki "veri yok" ile "sifir maliyet" ayirt edilebilsin.
428
- function readStepUsage(event) {
429
- const tokens = event.part?.tokens || event.tokens;
430
- const cost = Number(event.part?.cost ?? event.cost);
431
- if (!tokens && !Number.isFinite(cost)) return null;
432
- return {
433
- input: Number(tokens?.input) || 0,
434
- output: Number(tokens?.output) || 0,
435
- reasoning: Number(tokens?.reasoning) || 0,
436
- cacheRead: Number(tokens?.cache?.read) || 0,
437
- cacheWrite: Number(tokens?.cache?.write) || 0,
438
- cost: Number.isFinite(cost) ? cost : 0,
439
- };
440
- }
441
-
442
- // DIKKAT: text/error sozlesmesi degismemeli — operatorun JSON protokolu bu metni okuyor.
443
- // usage yalnizca EK bir alandir; cikarilamadiginda null kalir ve hicbir akisi etkilemez.
444
- function normalizeCliOutput(agent, stdout) {
445
- if (agent.adapter !== "opencode" || !(agent.args || []).includes("json")) {
446
- return { text: String(stdout || "").trim(), error: "", usage: null };
447
- }
448
- const texts = [], errors = [];
449
- let usage = null;
450
- for (const line of String(stdout || "").split(/\r?\n/)) {
451
- if (!line.trim()) continue;
452
- try {
453
- const event = JSON.parse(line);
454
- if (event.type === "text" && event.part?.text) texts.push(String(event.part.text));
455
- if (event.type === "error") errors.push(String(event.error?.message || event.error || "OpenCode model/provider hatasi"));
456
- if (event.type === "step_finish") {
457
- const step = readStepUsage(event);
458
- if (step) usage = addUsage(usage, step);
459
- }
460
- } catch {}
461
- }
462
- return { text: texts.join("\n").trim(), error: errors.join("\n").trim(), usage };
463
- }
464
-
465
- // Metindeki DENGELI suslu parantezli en dis JSON nesnesi adaylarini cikarir. String icindeki
466
- // parantez/kacis karakterleri sayilmaz; boylece "final" metninde gecen bir { veya } eski
467
- // "ilk-{ .. son-}" kestirmesini bozmaz. Ayrica model aciklama + nesne + ek aciklama yazdiginda
468
- // (ya da birden fazla nesne dondurdugunde) her nesne ayri aday olur.
469
- function jsonObjectCandidates(text) {
470
- const src = String(text);
471
- const found = [];
472
- const stack = [];
473
- let inString = false, escaped = false;
474
- for (let i = 0; i < src.length; i++) {
475
- const ch = src[i];
476
- if (inString) {
477
- if (escaped) escaped = false;
478
- else if (ch === "\\") escaped = true;
479
- else if (ch === '"') inString = false;
480
- continue;
481
- }
482
- if (ch === '"') inString = true;
483
- else if (ch === "{") stack.push(i);
484
- else if (ch === "}" && stack.length) {
485
- const start = stack.pop();
486
- if (!stack.length) found.push(src.slice(start, i + 1));
487
- }
488
- }
489
- return found;
490
- }
491
-
492
- // Model bazen JSON'u tek tirnak, sondaki virgul veya akilli tirnakla yazar. Kati parse
493
- // basarisiz oldugunda son care olarak bu yaygin sapmalari onarip bir kez daha dener.
494
- function repairJsonText(candidate) {
495
- return String(candidate)
496
- .replace(/[“”]/g, '"')
497
- .replace(/[‘’]/g, "'")
498
- .replace(/,\s*([}\]])/g, "$1");
499
- }
500
-
501
- const PROTOCOL_KEYS = /"(status|assignments|summary|completionCriteria|final)"\s*:/;
502
-
503
- function parseJson(text, label) {
504
- const raw = String(text);
505
- const candidates = [];
506
- for (const fence of raw.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(fence[1]);
507
- candidates.push(raw.trim());
508
- // Protokol anahtari iceren nesneler once denenir; ayni onem sirasinda SONDAKI nesne kazanir
509
- // (modeller once dusunce/aciklama, en sona nihai protokol nesnesini yazar).
510
- const objects = jsonObjectCandidates(raw).reverse();
511
- candidates.push(...objects.filter((o) => PROTOCOL_KEYS.test(o)), ...objects.filter((o) => !PROTOCOL_KEYS.test(o)));
512
- for (const candidate of candidates) {
513
- for (const attempt of [candidate, repairJsonText(candidate)]) {
514
- try {
515
- const parsed = JSON.parse(attempt);
516
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
517
- } catch {}
518
- }
519
- }
520
- const error = new Error(`${label} gecerli JSON dondurmedi.`);
521
- error.rawText = raw;
522
- throw error;
523
- }
524
-
525
- // Protokol hatasinda operatorun ham metnini kullaniciya gosterilebilir bir cevaba cevirir.
526
- // Yarim kalmis/kirik JSON parcalarini ve kod bloklarini atar; geriye anlamli duz metin
527
- // kalmiyorsa null doner (o zaman normal hata akisi calisir).
528
- function conversationalAnswer(error) {
529
- if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) return null;
530
- const raw = String(error.rawText || "");
531
- if (!raw.trim()) return null;
532
- const stripped = raw
533
- .replace(/```[\s\S]*?```/g, " ")
534
- .replace(/\{[\s\S]*\}/g, " ")
535
- .replace(/[\s\S]*?^\s*\{[\s\S]*$/m, (match) => match.split("{")[0])
536
- .replace(/\n{3,}/g, "\n\n")
537
- .trim();
538
- // Cok kisa artiklar (or. tek noktalama) cevap sayilmaz.
539
- return stripped.length >= 8 ? stripped : null;
540
- }
541
-
542
- function capabilitiesFor(agent) {
543
- // Rol dosyasi bir uzmanlik dayatiyorsa yetenekler DOGRUDAN rolden turer. Aksi halde
544
- // config'teki eski/genis capabilities listesi (or. planner'da "implementation") katalogda
545
- // allowedKinds ile celisir ve operatore yaniltici sinyal verirdi. Rol yoksa config'e duseriz.
546
- const role = String(agent.roleFile || "").toLowerCase();
547
- if (role.includes("executor")) return new Set(["implementation", "debugging", "testing"]);
548
- if (role.includes("review")) return new Set(["review", "testing", "analysis"]);
549
- if (role.includes("planner")) return new Set(["planning", "analysis"]);
550
- if (role.includes("operator")) return new Set(["planning", "delegation"]);
551
- return new Set(Array.isArray(agent.capabilities) ? agent.capabilities.map((x) => String(x).toLowerCase()) : []);
552
- }
553
-
554
- function inferAssignmentKind(raw, instruction) {
555
- const explicit = String(raw.kind || "").toLowerCase();
556
- if (["implement", "review", "research", "plan"].includes(explicit)) return explicit;
557
- 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";
558
- if (/\b(incele|denetle|dogrula|doğrula|review|audit|verify|test et)\b/i.test(instruction)) return "review";
559
- if (/\b(arastir|araştır|research|web)\b/i.test(instruction)) return "research";
560
- if (/\b(planla|plan|tasarla|design)\b/i.test(instruction)) return "plan";
561
- return "implement";
562
- }
563
-
564
- // Gorev metni gercek bir yapim/degisiklik isi mi? Operator, plan evresinde "bilgi sorusu
565
- // kestirmesi" ile (delegasyon acmadan complete) yaniti dogrudan verebilir. Ama prompt acikca
566
- // bir uygulama/olusturma istiyorsa bu kestirme kullaniciya HIC is uretmeden sahte basari doner
567
- // (or. proje hafizasindaki gecmis teslimati "zaten yapildi" sanmak). Bu durumda kestirmeyi
568
- // engelleyip yeniden planlama isteriz. inferAssignmentKind'in implement fiil kumesiyle uyumlu.
569
- function taskRequiresDelegation(prompt) {
570
- 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 || ""));
571
- }
572
-
573
- function roleAllowedKinds(agent) {
574
- const role = path.basename(String(agent?.roleFile || "")).toLowerCase();
575
- if (role.includes("executor")) return ["implement"];
576
- if (role.includes("review")) return ["review"];
577
- if (role.includes("planner")) return ["plan"];
578
- return null;
579
- }
580
-
581
- function supportsKind(agent, kind) {
582
- const roleKinds = roleAllowedKinds(agent);
583
- if (roleKinds) return roleKinds.includes(kind);
584
- const caps = capabilitiesFor(agent);
585
- const wanted = {
586
- implement: ["implementation", "coding", "development", "debugging"],
587
- review: ["review", "testing", "security", "analysis"],
588
- research: ["research", "web", "analysis"],
589
- plan: ["planning", "analysis", "architecture"],
590
- }[kind] || [];
591
- return wanted.some((cap) => caps.has(cap));
592
- }
593
-
594
- function agentUsable(agent) {
595
- return agent?.health?.status ? agent.health.status === "ready" : true;
596
- }
597
-
598
- function normalizeAssignments(value, cfg, operatorName, usedIds, taskText = "") {
599
- if (!Array.isArray(value)) throw new Error("Operator assignments dizisi dondurmedi.");
600
- const max = Math.max(1, cfg.operator?.maxDelegationsPerRound || 8);
601
- // Operator daha once kullanilan bir kimligi yinelerse gorevi oldurmek yerine kimligi
602
- // otomatik yeniden adlandiririz; saatlerce uretilmis teslimat bir ID cakismasi yuzunden
603
- // basarisiz sayilmamalidir. Ayni turdaki dependsOn referanslari da yeni ada tasinir.
604
- const renames = new Map();
605
- return value.slice(0, max).map((raw, index) => {
606
- let id = String(raw.id || `task-${Date.now()}-${index + 1}`).replace(/[^a-zA-Z0-9_.-]/g, "-");
607
- let renamedFrom;
608
- if (usedIds.has(id)) {
609
- let n = 2;
610
- while (usedIds.has(`${id}-r${n}`)) n++;
611
- renamedFrom = id;
612
- renames.set(id, `${id}-r${n}`);
613
- id = `${id}-r${n}`;
614
- }
615
- usedIds.add(id);
616
- const requestedAgent = String(raw.agent || "");
617
- if (!cfg.agents[requestedAgent]) throw new Error(`Operator tanimsiz agent secti: ${requestedAgent}`);
618
- if (cfg.agents[requestedAgent].enabled === false) throw new Error(`Operator devre disi agent secti: ${requestedAgent}`);
619
- if (!agentUsable(cfg.agents[requestedAgent])) throw new Error(`Operator kullanilamaz agent secti: ${requestedAgent} (${cfg.agents[requestedAgent].health?.label || "saglik testi basarisiz"})`);
620
- if (requestedAgent === operatorName) throw new Error("Operator kendisine uzman gorevi atayamaz.");
621
- const instruction = String(raw.instruction || raw.task || "").trim();
622
- if (!instruction) throw new Error(`${id} delegasyonunda instruction eksik.`);
623
- const kind = inferAssignmentKind(raw, instruction);
624
- // Beceriler kullanici-kapilidir: operator yalnizca cfg.skills.enabled icindekileri iliştirebilir.
625
- // Tanimsiz/etkin olmayan beceri sessizce dusurulur; asla gorevi olduren bir hata degildir.
626
- const hasExplicitSkills = Object.prototype.hasOwnProperty.call(raw, "skills");
627
- const autoMatchOn = cfg.skills?.autoMatch !== false;
628
- const requestedSkills = Array.isArray(raw.skills)
629
- ? raw.skills
630
- : (!hasExplicitSkills && autoMatchOn
631
- ? skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind)
632
- : []);
633
- let skills = skillRegistry.resolveForAssignment(requestedSkills, cfg).map((skill) => skill.name);
634
- // Operator kodlama/planlama/review isinde beceri iliştirmeyi atlarsa (or. skills:[] dönerse),
635
- // kullanicinin etkin becerilerinden goreve GERCEKTEN uyanlari otomatik ekle. suggest() yalnizca
636
- // lexical eslesme oldugunda skill döner; alakasiz beceri asla iliştirilmez. Boylece "operator
637
- // becerileri kullanmiyor" durumu ortadan kalkar.
638
- if (!skills.length && autoMatchOn && ["implement", "plan", "review"].includes(kind)) {
639
- skills = skillRegistry.resolveForAssignment(
640
- skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind), cfg
641
- ).map((skill) => skill.name);
642
- }
643
- let agent = requestedAgent;
644
- const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
645
- if (unavailable.has(agent) || !supportsKind(cfg.agents[agent], kind)) {
646
- const compatible = Object.entries(cfg.agents).find(([name, candidate]) => name !== operatorName && candidate.enabled !== false && agentUsable(candidate) && !unavailable.has(name) && supportsKind(candidate, kind));
647
- if (compatible) agent = compatible[0];
648
- else if (unavailable.has(agent)) throw new Error(`Operator bu oturumda kullanilamaz agent secti: ${requestedAgent}`);
649
- }
650
- return {
651
- id, agent, kind, instruction, skills,
652
- renamedFrom,
653
- requestedAgent: agent === requestedAgent ? undefined : requestedAgent,
654
- routingReason: agent === requestedAgent ? undefined : `${requestedAgent} ${unavailable.has(requestedAgent) ? "bu oturumda kullanilamaz" : `${kind} yetenegine sahip degil`}; ${agent} secildi.`,
655
- dependsOn: Array.isArray(raw.dependsOn) ? raw.dependsOn.map((dep) => renames.get(String(dep)) || String(dep)) : [],
656
- };
657
- });
658
- }
659
-
660
- function compatibleAgentForKind(cfg, operatorName, kind) {
661
- const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
662
- return Object.entries(cfg.agents || {}).find(([name, agent]) =>
663
- name !== operatorName && agent.enabled !== false && agentUsable(agent) &&
664
- !unavailable.has(name) && supportsKind(agent, kind)
665
- )?.[0] || "";
666
- }
667
-
668
- // Devretme adaylari. Karantina filtresi cagirana birakilir: sureli oldugu icin karar canli
669
- // durumdan (isQuarantined) okunmalidir.
670
- function compatibleAgentsForKind(cfg, operatorName, kind) {
671
- return Object.entries(cfg.agents || {})
672
- .filter(([name, agent]) =>
673
- name !== operatorName && agent.enabled !== false && agentUsable(agent) && supportsKind(agent, kind))
674
- .map(([name]) => name);
675
- }
676
-
677
- function nextAssignmentId(base, usedIds) {
678
- let id = base;
679
- let suffix = 2;
680
- while (usedIds.has(id)) id = `${base}-${suffix++}`;
681
- usedIds.add(id);
682
- return id;
683
- }
684
-
685
- // Balanced bir uygulama gorevinde katalogda standart roller varsa ilk turu
686
- // PLAN -> IMPLEMENT -> REVIEW olarak garanti eder. Operatorun bir rolu yanlislikla
687
- // atlamasi, kullanicinin etkinlestirdigi uzmani sessizce devre disi birakamaz.
688
- function ensureBalancedRoleChain(assignments, cfg, operatorName, task, usedIds) {
689
- if (task.executionMode !== "balanced" || !assignments.some((item) => item.kind === "implement")) return assignments;
690
- const chained = assignments.map((item) => ({ ...item, dependsOn: [...item.dependsOn] }));
691
- let plan = chained.find((item) => item.kind === "plan");
692
- if (!plan) {
693
- const planner = compatibleAgentForKind(cfg, operatorName, "plan");
694
- if (planner) {
695
- 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.";
696
- plan = {
697
- id: nextAssignmentId("balanced-plan", usedIds),
698
- agent: planner,
699
- kind: "plan",
700
- instruction,
701
- dependsOn: [],
702
- skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "plan"),
703
- routingReason: "Balanced rol zinciri: kullanilabilir planner ilk tura otomatik eklendi.",
704
- };
705
- chained.unshift(plan);
706
- }
707
- }
708
- const implementations = chained.filter((item) => item.kind === "implement");
709
- if (plan) {
710
- for (const implementation of implementations) {
711
- if (!implementation.dependsOn.includes(plan.id)) implementation.dependsOn.push(plan.id);
712
- }
713
- }
714
- let reviews = chained.filter((item) => item.kind === "review");
715
- if (!reviews.length) {
716
- const reviewer = compatibleAgentForKind(cfg, operatorName, "review");
717
- if (reviewer) {
718
- 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.";
719
- const review = {
720
- id: nextAssignmentId("balanced-review", usedIds),
721
- agent: reviewer,
722
- kind: "review",
723
- instruction,
724
- dependsOn: implementations.map((item) => item.id),
725
- skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "review"),
726
- routingReason: "Balanced rol zinciri: kullanilabilir reviewer ilk tura otomatik eklendi.",
727
- };
728
- chained.push(review);
729
- reviews = [review];
730
- }
731
- }
732
- for (const review of reviews) {
733
- // Operator belirli bir uygulamayi incelemeye bagladiysa bu kapsami genisletme;
734
- // ayni turdaki bagimsiz/yardimci bir implement hatasi ana incelemeyi bloke etmemeli.
735
- if (!review.dependsOn.length) {
736
- for (const implementation of implementations) review.dependsOn.push(implementation.id);
737
- }
738
- }
739
- const rank = { plan: 0, research: 1, implement: 2, review: 3 };
740
- return chained.map((item, index) => ({ item, index }))
741
- .sort((a, b) => (rank[a.item.kind] ?? 2) - (rank[b.item.kind] ?? 2) || a.index - b.index)
742
- .map(({ item }) => item);
743
- }
744
-
745
- class Engine extends EventEmitter {
746
- constructor() {
747
- super();
748
- this.running = false;
749
- this.busy = false;
750
- this.current = null;
751
- this.activeChild = null;
752
- this.callSequence = 0;
753
- // Kesin kimlik/kurulum hatasi veren agent'lari ayni motor oturumunda tekrar secme.
754
- // Kalici config'i degistirmeyiz; kullanici kimlik bilgisini duzeltip motoru yeniden
755
- // baslattiginda agent tekrar denenebilir.
756
- this.unhealthyAgents = new Map();
757
- this._retrySleepers = new Set();
758
- // Canli kodlama akisi: gorev calisirken calisma klasorunu periyodik tarayan zamanlayici
759
- // ve son yayinlanan degisiklik imzasi (ayni durum tekrar tekrar yayinlanmasin diye).
760
- this._liveTimer = null;
761
- this._lastFileChangeKey = "";
762
- this._textBefore = null;
763
- }
764
-
765
- cfg() { return store.loadConfig(); }
766
-
767
- status() {
768
- const cfg = this.cfg();
769
- return {
770
- running: this.running,
771
- busy: this.busy,
772
- current: this.current,
773
- mode: cfg.approvalMode,
774
- defaultOperator: cfg.operator?.cli || "",
775
- callsToday: store.getCallCount(),
776
- budget: cfg.dailyCallBudget,
777
- usageToday: store.getDailyUsage(),
778
- };
779
- }
780
-
781
- // KULLANIM TELEMETRISI: gorev suresince agent bazinda token/maliyet toplar. Tamamen
782
- // additive ve hata toleranslidir veri veremeyen CLI'lar (metin modunda calisan
783
- // codex/claude/gemini) icin sayaclar bos kalir, hicbir akis bundan etkilenmez.
784
- resetUsage() {
785
- this._usage = { total: { ...EMPTY_USAGE }, byAgent: {}, calls: 0, reportingCalls: 0 };
786
- }
787
-
788
- recordUsage(agentName, usage) {
789
- if (!this._usage) this.resetUsage();
790
- this._usage.total = addUsage(this._usage.total, usage);
791
- this._usage.byAgent[agentName] = addUsage(this._usage.byAgent[agentName], usage);
792
- this._usage.reportingCalls++;
793
- try { store.addDailyUsage(usage); } catch {}
794
- }
795
-
796
- // Gorev kapanisinda saklanacak sekil. Hicbir cagri veri vermediyse null doner ki arayuz
797
- // "0 token" gibi yaniltici bir sayi yerine "veri yok" gosterebilsin.
798
- usageSummary() {
799
- const usage = this._usage;
800
- if (!usage || !usage.reportingCalls) return null;
801
- return {
802
- total: usage.total,
803
- byAgent: usage.byAgent,
804
- calls: usage.calls,
805
- reportingCalls: usage.reportingCalls,
806
- };
807
- }
808
-
809
- publish(type, data, taskId = this.current?.id) {
810
- const event = { at: new Date().toISOString(), taskId: taskId || null, ...data, type };
811
- if (taskId) store.appendRunEvent(taskId, event);
812
- this.emit(type, event);
813
- return event;
814
- }
815
-
816
- // CANLI KODLAMA: gorev calisirken calisma klasorunu _snapBefore'a gore tarar ve olusan/
817
- // degisen/silinen dosyalari "filechange" olayiyla yayinlar; complete()'teki nihai diff'in
818
- // canli on-izlemesidir. Yalnizca durum degistiginde olay basar (ayni diff tekrarlanmaz).
819
- // Tamamen additive ve salt-okunurdur; hata verirse gorevi etkilemeden sessizce atlar.
820
- publishFileChanges(taskId = this.current?.id) {
821
- if (!taskId || !this._cwd || !this._snapBefore) return null;
822
- let changes, currentSnapshot;
823
- try {
824
- currentSnapshot = snapshotDir(this._cwd);
825
- changes = diffSnapshots(this._snapBefore, currentSnapshot);
826
- }
827
- catch { return null; }
828
- // Yalnizca dosya adlarini degil guncel stamp'i da imzaya kat. Ayni dosya gorev
829
- // sirasinda ikinci kez degistiginde yeni satir diff'i mutlaka yayinlanmalidir.
830
- const stampKey = [...changes.created, ...changes.modified]
831
- .map((file) => `${file}:${currentSnapshot.get(file) || ""}`).join("|");
832
- const key = `${stampKey}#${changes.deleted.join("|")}`;
833
- if (key === this._lastFileChangeKey) return null;
834
- const previouslyHadChanges = this._lastFileChangeKey !== "" && this._lastFileChangeKey !== "#";
835
- const changedFiles = [
836
- ...changes.created.map((file) => ({ file, action: "created" })),
837
- ...changes.modified.map((file) => ({ file, action: "modified" })),
838
- ...changes.deleted.map((file) => ({ file, action: "deleted" })),
839
- ];
840
- let remainingLines = DIFF_MAX_EVENT_LINES;
841
- const files = changedFiles.map(({ file, action }) => {
842
- if (remainingLines <= 0) return { path: file, action, additions: null, deletions: null, previewStatus: "event-limit", hunks: [] };
843
- const described = describeFileDiff(this._cwd, file, action, this._textBefore, { maxLines: Math.min(DIFF_MAX_RENDERED_LINES, remainingLines) });
844
- remainingLines -= (described.hunks || []).reduce((sum, hunk) => sum + (hunk.lines || []).length, 0);
845
- return described;
846
- });
847
- // Bos degisim setini (or. baslangictaki taban veya tum degisikliklerin geri alinmasi)
848
- // imzasini hatirla. Gorev basinda gurultu uretme; ancak daha once gorunen tum degisiklikler
849
- // geri alindiysa bos olayi yayinla ki dashboard bayat diff'i temizlesin.
850
- this._lastFileChangeKey = key;
851
- if (!files.length && !previouslyHadChanges) return null;
852
- return this.publish("filechange", {
853
- files,
854
- counts: { created: changes.created.length, modified: changes.modified.length, deleted: changes.deleted.length },
855
- lineCounts: {
856
- additions: files.reduce((sum, file) => sum + (Number(file.additions) || 0), 0),
857
- deletions: files.reduce((sum, file) => sum + (Number(file.deletions) || 0), 0),
858
- unavailable: files.filter((file) => file.additions == null || file.deletions == null).length,
859
- },
860
- }, taskId);
861
- }
862
-
863
- startLiveDiff(task) {
864
- this.stopLiveDiff();
865
- this._lastFileChangeKey = "";
866
- // liveDiff kesin olarak false yapilmadikca aciktir (mevcut davranisi degistirmeyen additive
867
- // ozellik). Tarama araligi config'ten ayarlanabilir; en dusuk 500ms ile sinirli.
868
- const cfg = this.cfg();
869
- if (cfg.liveDiff === false) { this._textBefore = null; return; }
870
- this._textBefore = captureTextSnapshot(this._cwd, this._snapBefore);
871
- const intervalMs = Math.max(500, Number(cfg.liveDiffIntervalMs) || 2500);
872
- this._liveTimer = setInterval(() => this.publishFileChanges(task.id), intervalMs);
873
- // unref: bekleyen bir tarama zamanlayicisi surecin kapanmasini asla engellemesin.
874
- if (this._liveTimer && typeof this._liveTimer.unref === "function") this._liveTimer.unref();
875
- }
876
-
877
- stopLiveDiff() {
878
- if (this._liveTimer) { clearInterval(this._liveTimer); this._liveTimer = null; }
879
- }
880
-
881
- setMode(mode) {
882
- const cfg = this.cfg();
883
- cfg.approvalMode = mode === "auto" ? "auto" : "ask";
884
- store.saveConfig(cfg);
885
- this.emit("status", this.status());
886
- }
887
-
888
- start() {
889
- if (this.running) return;
890
- if (!this.cfg().autonomousConsentAcceptedAt) {
891
- const error = new Error("Otonom CLI calistirma kosullari henuz kabul edilmedi. Dashboard'daki ilk kullanim uyarisini onaylayin.");
892
- error.code = "AUTONOMOUS_CONSENT_REQUIRED";
893
- throw error;
894
- }
895
- this.running = true;
896
- this.publish("log", { level: "info", msg: `Motor basladi. Mod=${this.cfg().approvalMode}` }, null);
897
- this.emit("status", this.status());
898
- this.loop();
899
- }
900
-
901
- stop() {
902
- this.running = false;
903
- this.wake();
904
- this.cancelRetrySleeps();
905
- this.stopLiveDiff();
906
- if (this.activeChild) {
907
- try { this.activeChild.kill(); } catch {}
908
- }
909
- this.publish("log", { level: "warn", msg: "Motor durduruluyor; aktif CLI islemi sonlandirildi." });
910
- this.emit("status", this.status());
911
- }
912
-
913
- wake() {
914
- if (this._wakeResolve) {
915
- clearTimeout(this._wakeTimer);
916
- const resolve = this._wakeResolve;
917
- this._wakeResolve = null;
918
- resolve();
919
- }
920
- }
921
-
922
- sleepWake(ms) {
923
- return new Promise((resolve) => {
924
- this._wakeResolve = resolve;
925
- this._wakeTimer = setTimeout(() => { this._wakeResolve = null; resolve(); }, ms);
926
- });
927
- }
928
-
929
- // sleepWake'ten AYRIDIR: o bos donguye aittir ve yeni gorev eklendiginde uyanir. Bu bekleme
930
- // yalnizca motor durdurulunca kesilmeli.
931
- sleepRetry(ms) {
932
- return new Promise((resolve) => {
933
- const sleeper = { resolve, timer: null };
934
- sleeper.timer = setTimeout(() => { this._retrySleepers.delete(sleeper); resolve(); }, ms);
935
- this._retrySleepers.add(sleeper);
936
- });
937
- }
938
-
939
- cancelRetrySleeps() {
940
- for (const sleeper of this._retrySleepers) {
941
- clearTimeout(sleeper.timer);
942
- sleeper.resolve();
943
- }
944
- this._retrySleepers.clear();
945
- }
946
-
947
- // Sureli karantina: kullanici oturum acar veya saglayici toparlanirsa agent kendiliginden
948
- // doner. Tekrar duserse ceza katlanir (en fazla 15 dk); suresi 0 olanlar kalici kalir.
949
- quarantineAgent(name, code, cfg) {
950
- const strikes = (this.unhealthyAgents.get(name)?.strikes || 0) + 1;
951
- const baseSeconds = QUARANTINE_COOLDOWN_SECONDS[code] ?? 120;
952
- const cooldownMs = baseSeconds > 0 ? Math.min(900, baseSeconds * strikes) * 1000 : 0;
953
- this.unhealthyAgents.set(name, {
954
- code, strikes, at: new Date().toISOString(),
955
- until: cooldownMs ? Date.now() + cooldownMs : 0,
956
- });
957
- if (cfg) cfg.runtimeUnavailableAgents = this.quarantinedAgents();
958
- this.publish("log", {
959
- level: "warn",
960
- msg: cooldownMs
961
- ? `${name} [${code}] nedeniyle ${Math.round(cooldownMs / 1000)} sn karantinada; sure dolunca otomatik yeniden denenecek.`
962
- : `${name} [${code}] nedeniyle bu oturumda kullanim disi (beklemekle duzelmez).`,
963
- });
964
- }
965
-
966
- isQuarantined(name) {
967
- const entry = this.unhealthyAgents.get(name);
968
- if (!entry) return false;
969
- if (entry.until && Date.now() >= entry.until) {
970
- this.unhealthyAgents.delete(name);
971
- this.publish("log", { level: "info", msg: `${name} karantina suresi doldu; yeniden deneme hakki verildi.` });
972
- return false;
973
- }
974
- return true;
975
- }
976
-
977
- quarantinedAgents() {
978
- return [...this.unhealthyAgents.keys()].filter((name) => this.isQuarantined(name));
979
- }
980
-
981
- async loop() {
982
- while (this.running) {
983
- const task = store.nextPending();
984
- if (!task) {
985
- await this.sleepWake((this.cfg().pollSeconds || 15) * 1000);
986
- continue;
987
- }
988
- try {
989
- await this.runTask(task);
990
- } catch (error) {
991
- if (!(await this.salvage(task, error))) {
992
- const found = store.findTask(task.id);
993
- const failure = classifyCliError(error);
994
- task.status = "failed";
995
- task.error = error.message;
996
- // Siniflandirilmis teshis gorevle birlikte saklanir; arayuz "ne oldu / ne yapmali"
997
- // ayrimini ham hata metnini kirparak tahmin etmek yerine buradan okur.
998
- task.failure = failure;
999
- // Basarisiz gorev de para harcar; telemetri burada da saklanmali.
1000
- task.usage = this.usageSummary();
1001
- task.finishedAt = new Date().toISOString();
1002
- if (found) store.moveTask(found.state, "failed", task);
1003
- this.publish("log", { level: "error", msg: `HATA (${failure.code}): ${failure.summary}` }, task.id);
1004
- this.publish("log", { level: "warn", msg: `Ne yapmali: ${failure.action}` }, task.id);
1005
- if (task.kind === "operator-chat") {
1006
- this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: task.parentTaskId, status: "failed", error: failure.summary, failure }, task.id);
1007
- }
1008
- this.notifyOutcome(task, "failed", { error: failure.summary, failureCode: failure.code, action: failure.action });
1009
- this.emit("queue");
1010
- }
1011
- } finally {
1012
- this.stopLiveDiff();
1013
- this._textBefore = null;
1014
- this.busy = false;
1015
- this.current = null;
1016
- this.activeChild = null;
1017
- this.emit("status", this.status());
1018
- }
1019
- }
1020
- }
1021
-
1022
- invokeAgent(agentName, prompt, cfg, meta = {}) {
1023
- const configuredAgent = cfg.agents[agentName];
1024
- const agent = configuredAgent && cliRegistry.effectiveAgent(configuredAgent, cfg);
1025
- if (!agent) return Promise.reject(new Error(`Agent tanimsiz: ${agentName}`));
1026
- return this.runCli(agentName, agent, prompt, cfg, meta);
1027
- }
1028
-
1029
- // Operatoru, uzman agent'lardan BAGIMSIZ olarak dogrudan CLI'dan (claude/codex/gemini/opencode)
1030
- // calistirir. operator.md rolunu bu CLI giyer.
1031
- invokeOperator(cli, prompt, cfg, meta = {}) {
1032
- const agent = cliRegistry.operatorSpec(cli, cfg);
1033
- if (!agent) return Promise.reject(new Error(`Operator CLI tanimsiz veya kurulu degil: ${cli}`));
1034
- return this.runCli(cli, agent, prompt, cfg, meta);
1035
- }
1036
-
1037
- runCli(displayName, agent, prompt, cfg, meta = {}) {
1038
- return new Promise((resolve, rawReject) => {
1039
- // Her hata hangi CLI'dan geldigini tasir; tavsiye metni buna gore uretilir. Etiket yokken
1040
- // opencode hatasinda kullaniciya gemini komutu oneriliyordu. Bkz. classifyCliError.
1041
- const reject = (cause) => {
1042
- const error = cause instanceof Error ? cause : new Error(String(cause));
1043
- if (!error.agentName) error.agentName = displayName;
1044
- if (!error.adapter) error.adapter = agent.adapter || cliRegistry.adapterId(agent.cmd);
1045
- rawReject(error);
1046
- };
1047
- const count = store.bumpCallCount();
1048
- if (count > (cfg.dailyCallBudget || Number.MAX_SAFE_INTEGER)) {
1049
- return reject(new Error(`Gunluk cagri butcesi asildi (${cfg.dailyCallBudget}).`));
1050
- }
1051
- if (!this._usage) this.resetUsage();
1052
- this._usage.calls++;
1053
-
1054
- const callId = `${this.current?.id || "run"}-${++this.callSequence}`;
1055
- let promptFile = "";
1056
- let useStdin = true;
1057
- const rawArgs = (agent.args || []).map((arg) => {
1058
- if (String(arg).includes("{PROMPT}")) {
1059
- useStdin = false;
1060
- return String(arg).replaceAll("{PROMPT}", prompt);
1061
- }
1062
- if (String(arg).includes("{PROMPT_FILE}")) {
1063
- useStdin = false;
1064
- if (!promptFile) {
1065
- const promptDir = path.join(store.ROOT, "state", "prompts");
1066
- fs.mkdirSync(promptDir, { recursive: true });
1067
- promptFile = path.join(promptDir, `${callId}.md`);
1068
- fs.writeFileSync(promptFile, prompt, "utf8");
1069
- }
1070
- return String(arg).replaceAll("{PROMPT_FILE}", promptFile);
1071
- }
1072
- return String(arg);
1073
- });
1074
- const cwd = this._cwd || path.resolve(store.WORK_BASE, cfg.workingDir || ".");
1075
- // Path-aware hapis: codex sandbox'i disariyi zaten OS-native kapatir (effectiveAgent).
1076
- // Claude'un OS sandbox'i her platformda (ozellikle Windows'ta) olmadigi icin en azindan
1077
- // orkestratorun kendi topragini (config/memory/kurulum dizini) permissions.deny ile gizler.
1078
- // JSON'u arguman olarak gecmek Windows cmd.exe'de tirnaklari bozar; bu yuzden dosyaya yazip
1079
- // yolunu veriyoruz (prompt dosyasiyla ayni desen).
1080
- const sandboxSettings = claudeSandboxSettings(agent, cwd, cfg);
1081
- if (sandboxSettings) {
1082
- const sbDir = path.join(store.ROOT, "state", "prompts");
1083
- fs.mkdirSync(sbDir, { recursive: true });
1084
- const sbFile = path.join(sbDir, `${callId}.settings.json`);
1085
- fs.writeFileSync(sbFile, JSON.stringify(sandboxSettings), "utf8");
1086
- rawArgs.push("--settings", sbFile);
1087
- }
1088
- const command = cliRegistry.buildCommand(agent.cmd, rawArgs);
1089
- const file = command.file;
1090
- const args = command.args;
1091
- const started = Date.now();
1092
- let stdout = "", stderr = "", settled = false, timedOut = false, silenceTimedOut = false;
1093
- let timer, silenceTimer, progressTimer;
1094
- const base = { callId, agent: displayName, stage: meta.stage || "agent", assignmentId: meta.assignmentId || null };
1095
-
1096
- this.current = { id: this.current?.id, stage: base.stage, agent: displayName, callId };
1097
- this.emit("status", this.status());
1098
- this.publish("activity", { ...base, kind: "process.started", cmd: agent.cmd, args: rawArgs, cwd });
1099
-
1100
- // Motor onceliklidir: canli CLI saglik/model probe'lari (or. codex app-server / codex exec)
1101
- // ayni CLI'yi ayni anda calistirirsa codex ikinci exec oturumunu SIGTERM ile keser. Bu yuzden
1102
- // operatör/agent kosumuna girmeden uctaki tum probe'lari sonlandiriyoruz.
1103
- cliRegistry.abortActiveProbes();
1104
- let child;
1105
- try { child = spawn(file, args, { cwd, env: cliRegistry.agentEnvironment(agent), windowsHide: true, shell: command.shell, windowsVerbatimArguments: !!command.verbatim }); }
1106
- catch (error) { return reject(error); }
1107
- this.activeChild = child;
1108
- child.stdout.setEncoding("utf8");
1109
- child.stderr.setEncoding("utf8");
1110
-
1111
- const timeoutMs = Math.max(10, agent.timeoutSeconds || cfg.agentTimeoutSeconds || 900) * 1000;
1112
- const silenceTimeoutMs = Math.max(1, agent.silenceTimeoutSeconds || cfg.cliSilenceTimeoutSeconds || 300) * 1000;
1113
- let lastOutputAt = Date.now();
1114
- const terminateTree = () => {
1115
- if (isWin && child.pid) {
1116
- try { spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" }); } catch {}
1117
- } else {
1118
- try { child.kill("SIGKILL"); } catch {}
1119
- }
1120
- };
1121
- const armSilenceTimer = () => {
1122
- clearTimeout(silenceTimer);
1123
- silenceTimer = setTimeout(() => {
1124
- if (settled) return;
1125
- silenceTimedOut = true;
1126
- this.publish("activity", { ...base, kind: "process.silence-timeout", silenceTimeoutMs, elapsedMs: Date.now() - started });
1127
- terminateTree();
1128
- }, silenceTimeoutMs);
1129
- };
1130
- armSilenceTimer();
1131
- progressTimer = setInterval(() => {
1132
- if (settled) return;
1133
- this.publish("activity", { ...base, kind: "process.progress", elapsedMs: Date.now() - started, silentMs: Date.now() - lastOutputAt, silenceTimeoutMs });
1134
- }, 15000);
1135
- timer = setTimeout(() => {
1136
- if (settled) return;
1137
- timedOut = true;
1138
- this.publish("activity", { ...base, kind: "process.timeout", timeoutMs });
1139
- terminateTree();
1140
- }, timeoutMs);
1141
-
1142
- child.stdout.on("data", (data) => {
1143
- const text = String(data);
1144
- stdout += text;
1145
- lastOutputAt = Date.now();
1146
- armSilenceTimer();
1147
- this.publish("activity", { ...base, kind: "stdout", text });
1148
- });
1149
- child.stderr.on("data", (data) => {
1150
- const text = String(data);
1151
- stderr += text;
1152
- lastOutputAt = Date.now();
1153
- armSilenceTimer();
1154
- this.publish("activity", { ...base, kind: "stderr", text });
1155
- });
1156
- child.on("error", (error) => {
1157
- if (settled) return;
1158
- settled = true;
1159
- clearTimeout(timer);
1160
- clearTimeout(silenceTimer);
1161
- clearInterval(progressTimer);
1162
- this.activeChild = null;
1163
- if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1164
- this.publish("activity", { ...base, kind: "process.failed", error: error.message, durationMs: Date.now() - started });
1165
- reject(error);
1166
- });
1167
- child.on("close", (code, signal) => {
1168
- if (settled) return;
1169
- settled = true;
1170
- clearTimeout(timer);
1171
- clearTimeout(silenceTimer);
1172
- clearInterval(progressTimer);
1173
- this.activeChild = null;
1174
- if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1175
- const durationMs = Date.now() - started;
1176
- // Cikti tek kez ayristirilir; hem kullanim telemetrisi hem metin ayni sonuctan okunur.
1177
- const normalized = normalizeCliOutput(agent, stdout);
1178
- if (normalized.usage) this.recordUsage(displayName, normalized.usage);
1179
- this.publish("activity", { ...base, kind: "process.finished", exitCode: code, signal, durationMs, usage: normalized.usage, reason: silenceTimedOut ? "silence-timeout" : (timedOut ? "timeout" : null) });
1180
- if (silenceTimedOut) return reject(new Error(`CLI_STALLED: ${agent.cmd} ${Math.round(silenceTimeoutMs / 1000)} saniye boyunca cikti uretmedi ve otomatik durduruldu.`));
1181
- if (timedOut) return reject(new Error(`${agent.cmd} ${Math.round(timeoutMs / 1000)} saniyede zaman asimina ugradi${signal ? ` (signal=${signal})` : ""}. ${clip(stdout || stderr, 500)}`));
1182
- // Teshis icin HER IKI akis da gerekir: bazi CLI'lar (Gemini dahil) kota/oturum hatasini
1183
- // stdout'a, yigin izini stderr'e yazar. Yalnizca birini almak siniflandiriciyi kor birakiyordu.
1184
- if (code !== 0) {
1185
- const detail = [stderr, stdout].map((stream) => String(stream || "").trim()).filter(Boolean).join("\n");
1186
- return reject(new Error(`${agent.cmd} cikis kodu ${code ?? "yok"}${signal ? ` (signal=${signal})` : ""}.\n${clip(detail, 2000)}`));
1187
- }
1188
- if (normalized.error) return reject(new Error(`OpenCode model/provider hatasi: ${clip(normalized.error, 500)}`));
1189
- if (!normalized.text) return reject(new Error(`${agent.cmd} kullanilabilir cikti dondurmedi.${stderr ? ` ${clip(stderr, 300)}` : ""}`));
1190
- resolve({ text: normalized.text, stderr: stderr.trim(), exitCode: code, durationMs, callId, usage: normalized.usage });
1191
- });
1192
-
1193
- // Prompt argumanla/dosyayla verilse bile stdin MUTLAKA kapatilmali. OpenCode gibi CLI'lar
1194
- // stdin TTY degilse borudan mesaj okur; EOF gelmezse model cagrisina hic gecmeden bloke olur.
1195
- child.stdin.on("error", () => {});
1196
- child.stdin.end(useStdin ? prompt : "");
1197
- });
1198
- }
1199
-
1200
- // Katalogda dosya yazabilen (implement) kullanilabilir bir uzman var mi? Operatorun kendisi ve
1201
- // bu oturumda karantinaya alinan agentler haric. Tek executor (or. opencode) sessizlik nedeniyle
1202
- // dustugunde takim fiilen dosya uretemez hale gelir; bu kontrol o durumu erken yakalar.
1203
- hasUsableImplementAgent(cfg, operatorName) {
1204
- return Object.entries(cfg.agents || {}).some(([name, agent]) =>
1205
- name !== operatorName && agent.enabled !== false && agentUsable(agent)
1206
- && !this.isQuarantined(name) && supportsKind(agent, "implement"));
1207
- }
1208
-
1209
- // Sureli karantinadaki implement agentinin donusune kalan sure (ms). Geri donen yoksa ya da
1210
- // bekleme makul siniri asiyorsa null; cagiran bunu "gorevi durdur" olarak yorumlar.
1211
- msUntilImplementAgentReturns(cfg, operatorName, maxWaitMs = 5 * 60 * 1000) {
1212
- const now = Date.now();
1213
- const returns = Object.entries(cfg.agents || {})
1214
- .filter(([name, agent]) => name !== operatorName && agent.enabled !== false
1215
- && agentUsable(agent) && supportsKind(agent, "implement"))
1216
- .map(([name]) => this.unhealthyAgents.get(name)?.until || 0)
1217
- .filter((until) => until > now)
1218
- .sort((a, b) => a - b);
1219
- if (!returns.length) return null;
1220
- const waitMs = returns[0] - now;
1221
- return waitMs <= maxWaitMs ? waitMs : null;
1222
- }
1223
-
1224
- agentCatalog(cfg, operatorName) {
1225
- return Object.entries(cfg.agents)
1226
- .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.isQuarantined(name))
1227
- .map(([name, a]) => ({
1228
- name,
1229
- description: a.description || "",
1230
- capabilities: [...capabilitiesFor(a)],
1231
- allowedKinds: roleAllowedKinds(a) || ["implement", "review", "research", "plan"].filter((kind) => supportsKind(a, kind)),
1232
- roleFile: a.roleFile || "",
1233
- costTier: a.costTier || "standard",
1234
- }));
1235
- }
1236
-
1237
- async invokeOperatorJson(operatorCli, prompt, cfg, stage, label) {
1238
- const retries = Math.max(0, cfg.operator?.protocolRetries ?? 1);
1239
- let lastError;
1240
- let correction = "";
1241
- for (let attempt = 0; attempt <= retries; attempt++) {
1242
- try {
1243
- const response = await this.invokeOperator(operatorCli, prompt + correction, cfg, { stage });
1244
- return parseJson(response.text, label);
1245
- } catch (error) {
1246
- lastError = error;
1247
- // Yalnizca gercek JSON protokol hatasi yeniden denenir. CLI'nin sessiz kalmasi,
1248
- // auth/timeout veya proses hatasi ayni operatoru tekrar calistirmamalidir.
1249
- if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) throw error;
1250
- if (attempt >= retries) break;
1251
- // Modelin ne dondurdugunu duzeltme istegine geri veriyoruz: "yalnizca JSON dondur"
1252
- // talimatini tekrarlamak, hatayi goremeyen model icin cogu zaman ayni cevabi uretiyordu.
1253
- correction = `\n\nONCEKI CEVAP PROTOKOLE UYMADI: ${error.message}\n` +
1254
- `Senin onceki cevabin (kirpilmis):\n"""\n${clip(String(error.rawText || ""), 1200)}\n"""\n` +
1255
- `Bu cevap gecerli bir JSON nesnesi degildi. Simdi SADECE tek bir JSON nesnesi dondur: ` +
1256
- `ilk karakter { ve son karakter } olsun; oncesinde/sonrasinda selamlama, aciklama, dusunce, ` +
1257
- `Markdown veya \`\`\` kod bloğu OLMASIN. Metin alanlarindaki cift tirnaklari \\" ile kacir.`;
1258
- this.publish("log", { level: "warn", msg: `${label} protokol hatasi; operator yeniden deneniyor (${attempt + 2}/${retries + 1}).` });
1259
- }
1260
- }
1261
- throw lastError;
1262
- }
1263
-
1264
- // Insertion sirasi tamamlanma sirasidir; en son biten denetim en gecerli karardir.
1265
- latestReview(state) {
1266
- return Object.values(state.results || {}).reverse()
1267
- .find((result) => result.kind === "review" && result.status === "completed" && result.verdict) || null;
1268
- }
1269
-
1270
- // Operatorun karar evresine verilen takim kaydi. Ham state dokumu yerine sonuc odakli
1271
- // bir ozet uretir; her sonucu kendi icinde ortadan kirpar ki en yeni denetim karari
1272
- // (metnin sonundaki VERDICT satiri) butce asiminda kaybolmasin.
1273
- teamDigest(state, cfg) {
1274
- const results = Object.values(state.results || {});
1275
- const budget = cfg.teamContextCharBudget || 30000;
1276
- const perResult = Math.max(900, Math.floor(budget / Math.max(1, results.length)));
1277
- const digest = {
1278
- round: state.round,
1279
- completionCriteria: state.criteria,
1280
- results: results.map((result) => ({
1281
- id: result.id, agent: result.agent, kind: result.kind, status: result.status,
1282
- ...(result.verdict ? { verdict: result.verdict } : {}),
1283
- result: clipMiddle(result.result, perResult),
1284
- })),
1285
- };
1286
- return clipMiddle(JSON.stringify(digest, null, 2), budget);
1287
- }
1288
-
1289
- operatorPrompt(task, cfg, memory, state, phase) {
1290
- const operatorCli = task.operatorCli || cfg.operator?.cli;
1291
- // Operatör rolü daima operator.md'dir; operatör bir CLI'dir, uzman agent'lar onun altında çalışır.
1292
- const roleFile = cfg.operator?.roleFile || "roles/operator.md";
1293
- const role = store.readRole(path.basename(roleFile));
1294
- const catalog = this.agentCatalog(cfg, operatorCli);
1295
- const skillContext = `${task.prompt}\n${phase === "review" ? Object.values(state.results || {}).map((result) => result.instruction || "").join("\n") : ""}`;
1296
- const skillDiscovery = skillRegistry.discover(cfg, skillContext);
1297
- const skillCatalog = skillDiscovery.catalog;
1298
- const skillStats = skillDiscovery.stats;
1299
- const enabledSkillNames = skillRegistry.enabledSkills(cfg).map((skill) => skill.name);
1300
- // Beceri envanteri motor tarafindan uretilen OTORITER veridir. Operatore TAM etkin listeyi
1301
- // (sayi + tum adlar) DAIMA veririz; boylece "kac beceri var" gibi sorular icin operatorun
1302
- // kendi CLI'sinin dahili becerilerini veya bir alt agent'in iddiasini kullanmasi gerekmez.
1303
- // Eslesen kisa liste yalnizca DELEGASYONA iliştirme icindir, envanterin tamami degildir.
1304
- const skillSection = skillStats.enabled
1305
- ? `## Beceriler (OTORITER kaynak)\nSistemde tam olarak ${skillStats.enabled} etkin beceri var: ${enabledSkillNames.join(", ")}.\n` +
1306
- `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` +
1307
- (skillCatalog.length
1308
- ? `Bu goreve en ilgili ${skillCatalog.length} beceri (delegasyonun skills alanina yalnizca bunlardan uygun olanlari ekle):\n${JSON.stringify(skillCatalog, null, 2)}\n`
1309
- : `Bu goreve gore taramada eslesme cikmadi; bu delegasyonlarda beceri iliştirme.\n`)
1310
- : "";
1311
- // Beceriler tamamen opsiyoneldir ve YALNIZCA kullanici etkinlestirdiginde katalogda gorunur.
1312
- // Operator, assignment'a yalnizca bu kataloktaki adlari "skills" dizisinde iliştirebilir.
1313
- const skillField = skillCatalog.length ? `, "skills":["beceri-adi"]` : "";
1314
- const skillProtocol = skillCatalog.length
1315
- ? ` BECERI: Kisa listedeki gercekten ilgili adlari skills dizisine ekle; ilgisiz veya liste disi ad kullanma, uygun yoksa alani bos birak.`
1316
- : "";
1317
- const protocol = phase === "plan"
1318
- ? `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). ` +
1319
- `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"}. ` +
1320
- `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}`
1321
- : `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` +
1322
- `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.`;
1323
- const strategy = task.executionMode === "fast"
1324
- ? "HIZLI MOD: Kucuk bir is. Tek bir implementation agenti kullan. Ayri planlama veya review delegasyonu acma; uzman raporu hedefi karsiliyorsa ilk degerlendirmede tamamla."
1325
- : task.executionMode === "deep"
1326
- ? "DERIN MOD: Isi uygun uzmanliklar arasinda dagit — planlama, uygulama, test ve bagimsiz inceleme icin AYRI ve dogru uzmanlari kullan."
1327
- : "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.";
1328
- return `${role}\n\n---\n## Operator protokolu\n${protocol}\n` +
1329
- // OpenCode gibi sohbet odakli CLI'lar varsayilan olarak once bir cumle yazip JSON'u
1330
- // arkasina ekliyor; motor bunu ayiklayabilse de belirsizlik protokol hatasi uretiyordu.
1331
- `## Cikti bicimi (KATI)\nCevabin TAMAMI tek bir JSON nesnesi olmali: ilk karakter { , son karakter } . ` +
1332
- `Oncesinde/sonrasinda selamlama, aciklama, dusunce notu, Markdown basligi veya \`\`\` kod bloğu YAZMA. ` +
1333
- `Kullaniciya iletmek istedigin her seyi ilgili JSON alanina ("final" veya "summary") koy; nesnenin disindaki metin kullaniciya ULASMAZ.\n` +
1334
- `Yalnizca katalogdaki agent adlarini kullan. Her assignment kind degeri secilen agentin allowedKinds listesinde olmali. Kendine gorev atama. Ayni delegasyon id'sini tekrar kullanma.\n` +
1335
- `Bir agent basarisiz veya kullanilamaz raporlandiysa ayni isi ona tekrar verme; katalogdaki alternatif bir uzmani sec.\n` +
1336
- `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` +
1337
- `## Calisma stratejisi\n${strategy}\n` +
1338
- `## Agent katalogu\n${JSON.stringify(catalog, null, 2)}\n` +
1339
- skillSection +
1340
- `## Kullanici gorevi\n${task.prompt}\n` +
1341
- `## Calisma klasoru\n${this._cwd}\n` +
1342
- `SANDBOX: Butun is YALNIZCA bu klasorde yapilir. Bu klasor disina (ust dizinler, orkestrator/kurulum dizini, sistem dosyalari) dosya YAZMA/DEGISTIRME/SILME ve gerekmedikce disariyi OKUMA. Delegasyon talimatlarini da bu sinira gore yaz.\n` +
1343
- `## Proje profili (bu klasorun birikmis baglami — tum kodu bastan taramak zorunda kalmayasin diye. IPUCUDUR, kanit degil: kritik kararlarda DOSYADAN dogrula, celiski gorursen profile guvenme. Gecmis teslimatlar mevcut gorevi TAMAMLANMIS SAYDIRMAZ.)\n${clip(memory || "(henuz proje profili yok — ilk gorevde olusturulacak)", cfg.projectContextCharBudget || cfg.memoryCharBudget || 8000)}\n` +
1344
- (phase === "review"
1345
- ? `## Son bagimsiz denetim\n${(() => { const review = this.latestReview(state); return review ? `${review.id} (${review.agent}) → VERDICT: ${review.verdict}` : "Henuz tamamlanmis denetim yok."; })()}\n` +
1346
- `## Takim calisma kaydi\n${this.teamDigest(state, cfg)}\n`
1347
- : "");
1348
- }
1349
-
1350
- specialistPrompt(task, cfg, assignment, state) {
1351
- const agent = cfg.agents[assignment.agent];
1352
- const roleByKind = { implement: "executor.md", review: "reviewer.md", plan: "planner.md" };
1353
- const selectedRole = roleByKind[assignment.kind] || (agent.roleFile ? path.basename(agent.roleFile) : "executor.md");
1354
- const role = store.readRole(selectedRole);
1355
- const instructionByKind = {
1356
- implement: "Delegasyonu calisma klasorunde uygula, degisiklikleri dogrula ve rolundeki teslimat formatinda raporla.",
1357
- review: "Teslimati bagimsiz ve salt okunur olarak incele; gerekli kontrolleri calistir, dosyalari degistirme ve rolundeki karar formatinda raporla.",
1358
- plan: "Calisma klasorunu salt okunur olarak incele; hicbir dosyayi degistirmeden uygulanabilir plani rolundeki formatta raporla.",
1359
- research: "Delegasyon kapsaminda kanit topla; kaynaklari ve belirsizlikleri ayirarak kisa bir sonuc raporla.",
1360
- };
1361
- const completed = Object.values(state.results).map((r) => ({
1362
- id: r.id, agent: r.agent, instruction: r.instruction, ...(r.verdict ? { verdict: r.verdict } : {}), result: clipMiddle(r.result, 5000),
1363
- }));
1364
- // Progressive disclosure: operatorun bu delegasyona sectigi becerilerin tam govdesini YIGMAYIZ;
1365
- // yalnizca ad + kisa ozet + rehber dosya yolunu veririz. Uzman gercekten ihtiyac duyarsa dosyayi
1366
- // kendisi okur. Operator hicbir beceri secmediyse (veya beceriler kapali ise) blok bostur.
1367
- const skills = skillRegistry.resolveForAssignment(assignment.skills, cfg);
1368
- const skillRefs = skillRegistry.toPromptRefs(skills, cfg.skills?.referenceCharBudget || 1200);
1369
- return `${role}\n\n---\n## Takim agenti protokolu\n` +
1370
- `Operator sana asagidaki isi devretti. ${instructionByKind[assignment.kind] || instructionByKind.implement} ` +
1371
- `Planda olmayan riskli bir is gerekiyorsa yapma; BLOCKED olarak bildir. Yalnizca gercekten gozlemledigin veya dogruladigin sonuclari yaz.\n` +
1372
- `SANDBOX: Butun is YALNIZCA calisma klasorunde (${this._cwd}) yapilir. Bu klasor disina dosya YAZMA/DEGISTIRME/SILME ve gerekmedikce disariyi OKUMA.\n` +
1373
- (skillRefs ? `## Uygulanacak beceriler\nBu is icin asagidaki beceri rehberleri secildi. Her satirda becerinin OZETI ve TAM rehberin DOSYA YOLU var. Ozet yeterliyse dogrudan uygula; daha fazla ayrinti gerekiyorsa ilgili dosyayi OKU ve prosedure uy. Ilgisiz bir sey varsa gormezden gel.\n${skillRefs}\n` : "") +
1374
- `## Ana hedef\n${task.prompt}\n## Delegasyon\nID: ${assignment.id}\n${assignment.instruction}\n` +
1375
- `## Onceki tamamlanan takim isleri\n${clip(JSON.stringify(completed, null, 2), cfg.teamContextCharBudget || 30000)}`;
1376
- }
1377
-
1378
- // Iki kademeli kurtarma: (1) gecici hata -> ayni agent, ustel bekleme; (2) kalici hata ->
1379
- // ayni turu yapabilen saglikli baska agente devret. Ikisi de tukenirse firlatir ve operator
1380
- // yeniden planlar. Amac, saniyelik bir 429 icin tam bir operator turu harcamamak.
1381
- async runAssignmentWithRecovery(task, cfg, assignment, state) {
1382
- const retries = Math.max(0, cfg.resilience?.transientRetries ?? 2);
1383
- const maxFailovers = Math.max(0, cfg.resilience?.maxFailoverAgents ?? 1);
1384
- const baseDelayMs = Math.max(250, (cfg.resilience?.retryBaseSeconds ?? 3) * 1000);
1385
- const tried = new Set();
1386
- let agentName = assignment.agent;
1387
- let failure = null;
1388
-
1389
- for (let failover = 0; ; failover++) {
1390
- tried.add(agentName);
1391
- for (let attempt = 0; ; attempt++) {
1392
- try {
1393
- const response = await this.invokeAgent(
1394
- agentName,
1395
- this.specialistPrompt(task, cfg, { ...assignment, agent: agentName }, state),
1396
- cfg,
1397
- { stage: "delegate", assignmentId: assignment.id }
1398
- );
1399
- return { response, agent: agentName };
1400
- } catch (error) {
1401
- if (!this.running) throw error;
1402
- failure = classifyCliError(error);
1403
- if (!TRANSIENT_CLI_ERRORS.has(failure.code) || attempt >= retries) break;
1404
- // Jitter: ayni anda dusen birden fazla delegasyon saglayiciya es zamanli geri donmesin.
1405
- const waitMs = Math.min(30000, baseDelayMs * 2 ** attempt) + Math.floor(Math.random() * 750);
1406
- this.publish("log", { level: "warn", msg: `${agentName} gecici hata verdi [${failure.code}]; ${Math.round(waitMs / 1000)} sn sonra yeniden deneniyor (${attempt + 2}/${retries + 1}).` }, task.id);
1407
- this.publish("activity", { kind: "delegation.retry", assignmentId: assignment.id, agent: agentName, code: failure.code, attempt: attempt + 1, waitMs });
1408
- await this.sleepRetry(waitMs);
1409
- if (!this.running) throw error;
1410
- }
1411
- }
1412
- if (QUARANTINE_CLI_ERRORS.has(failure.code)) this.quarantineAgent(agentName, failure.code, cfg);
1413
- if (failover >= maxFailovers) break;
1414
- const next = compatibleAgentsForKind(cfg, task.operatorCli, assignment.kind)
1415
- .find((name) => !tried.has(name) && !this.isQuarantined(name));
1416
- if (!next) break;
1417
- this.publish("log", { level: "warn", msg: `${agentName} basarisiz [${failure.code}]; ayni is ${next} agentine devrediliyor (operator turu harcanmadan).` }, task.id);
1418
- this.publish("activity", { kind: "delegation.failover", assignmentId: assignment.id, from: agentName, to: next, code: failure.code });
1419
- agentName = next;
1420
- }
1421
- // Siniflandirici disaridaki catch'te ayni sonucu uretsin diye ham metinle firlatiyoruz.
1422
- // CLI etiketi de tasinmali; yoksa oturum/kurulum tavsiyesi jenerige duser.
1423
- const error = new Error(failure?.raw || "Delegasyon calistirilamadi.");
1424
- error.agentName = failure?.agent || agentName;
1425
- error.adapter = failure?.adapter || "";
1426
- error.triedAgents = [...tried];
1427
- throw error;
1428
- }
1429
-
1430
- async runAssignments(task, cfg, assignments, state) {
1431
- const pending = assignments.slice();
1432
- while (pending.length) {
1433
- let index = pending.findIndex((a) => a.dependsOn.every((id) => state.results[id]?.status === "completed"));
1434
- if (index < 0) {
1435
- index = pending.findIndex((a) => a.dependsOn.some((id) => state.results[id] && state.results[id].status !== "completed"));
1436
- if (index >= 0) {
1437
- const blocked = pending.splice(index, 1)[0];
1438
- state.usedIds.push(blocked.id);
1439
- const failedDeps = blocked.dependsOn.filter((id) => state.results[id]?.status !== "completed");
1440
- state.results[blocked.id] = { ...blocked, status: "blocked", result: `Bagimli gorev basarisiz: ${failedDeps.join(", ")}` };
1441
- 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() };
1442
- state.messages.push(blockedMessage);
1443
- this.publish("message", blockedMessage, task.id);
1444
- continue;
1445
- }
1446
- throw new Error(`Delegasyon bagimliliklari cozumlenemedi: ${pending.map((a) => a.id).join(", ")}`);
1447
- }
1448
- const assignment = pending.splice(index, 1)[0];
1449
- state.usedIds.push(assignment.id);
1450
- state.messages.push({ from: task.operatorCli, to: assignment.agent, messageType: "delegation", assignmentId: assignment.id, body: assignment.instruction, at: new Date().toISOString() });
1451
- this.publish("message", state.messages[state.messages.length - 1], task.id);
1452
- if (assignment.routingReason) this.publish("log", { level: "info", msg: `Otomatik yonlendirme: ${assignment.routingReason}` }, task.id);
1453
- if (assignment.renamedFrom) this.publish("log", { level: "warn", msg: `Operator delegasyon kimligini yineledi; ${assignment.renamedFrom} otomatik olarak ${assignment.id} yapildi.` }, task.id);
1454
- this.publish("log", { level: "stage", msg: `DELEGE ${assignment.id} -> ${assignment.agent}` }, task.id);
1455
- if (assignment.skills?.length) this.publish("log", { level: "info", msg: `Beceriler: ${assignment.skills.join(", ")}` }, task.id);
1456
- try {
1457
- const { response, agent: usedAgent } = await this.runAssignmentWithRecovery(task, cfg, assignment, state);
1458
- const result = { ...assignment, agent: usedAgent, status: "completed", result: response.text, durationMs: response.durationMs, callId: response.callId };
1459
- if (usedAgent !== assignment.agent) {
1460
- result.failoverFrom = assignment.agent;
1461
- this.publish("log", { level: "info", msg: `${assignment.id} devralan agent tarafindan tamamlandi: ${assignment.agent} -> ${usedAgent}.` }, task.id);
1462
- }
1463
- if (assignment.kind === "review") {
1464
- result.verdict = extractVerdict(response.text);
1465
- this.publish("log", { level: "info", msg: `Denetim ${assignment.id}: VERDICT ${result.verdict || "BELIRSIZ"}` }, task.id);
1466
- }
1467
- state.results[assignment.id] = result;
1468
- const message = { from: usedAgent, to: task.operatorCli, messageType: "result", assignmentId: assignment.id, body: response.text, at: new Date().toISOString() };
1469
- state.messages.push(message);
1470
- this.publish("message", message, task.id);
1471
- } catch (error) {
1472
- // Karantina ve devretme runAssignmentWithRecovery'de tuketildi; buraya dusen gercekten
1473
- // kurtarilamayandir.
1474
- const failure = classifyCliError(error);
1475
- const tried = error.triedAgents?.length ? error.triedAgents : [assignment.agent];
1476
- state.results[assignment.id] = { ...assignment, status: "failed", result: failure.summary, error: failure, triedAgents: tried };
1477
- const message = { from: tried[tried.length - 1], to: task.operatorCli, messageType: "failure", assignmentId: assignment.id, body: `${failure.summary}\n${failure.action}`, errorCode: failure.code, at: new Date().toISOString() };
1478
- state.messages.push(message);
1479
- this.publish("message", message, task.id);
1480
- this.publish("log", { level: "warn", msg: `${tried.join(" > ")} kullanilamadi [${failure.code}]. Operator alternatif plan uretecek.` }, task.id);
1481
- }
1482
- task.teamState = state;
1483
- store.saveTask("pending", task);
1484
- }
1485
- }
1486
-
1487
- isRisky(text, cfg) {
1488
- const lower = String(text).toLowerCase();
1489
- return (cfg.riskyPatterns || []).some((pattern) => lower.includes(String(pattern).toLowerCase()));
1490
- }
1491
-
1492
- async runTask(task) {
1493
- const baseCfg = this.cfg();
1494
- if (!baseCfg.autonomousConsentAcceptedAt) throw new Error("Otonom CLI calistirma kosullari kabul edilmeden gorev calistirilamaz.");
1495
- if (task.kind === "operator-chat") return this.runChatTask(task, baseCfg);
1496
- task.executionMode = resolveExecutionMode(task);
1497
- const cfg = applyExecutionPolicy(baseCfg, task.executionMode);
1498
- const operatorCli = task.operatorCli || cfg.operator?.cli;
1499
- if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Gecerli bir operator CLI secilmedi (claude/codex/gemini/opencode).");
1500
- task.operatorCli = operatorCli;
1501
- this.busy = true;
1502
- this.current = { id: task.id, stage: "operator", agent: operatorCli };
1503
- this._cwd = path.resolve(store.WORK_BASE, task.targetDir || cfg.workingDir || ".");
1504
- this._snapBefore = snapshotDir(this._cwd);
1505
- this.startLiveDiff(task);
1506
- // Onay sonrasi devam eden gorevde sayaci sifirlamayiz; ilk girise ozgu.
1507
- if (!task.approved || !this._usage) this.resetUsage();
1508
- this.publish("log", { level: "task", msg: `GOREV ${task.id}: ${task.prompt}` }, task.id);
1509
- // Otomatik surumleme: gorev CALISMADAN ONCE calisma klasorunun bir surumunu al ki ajan
1510
- // mevcut kodu bozarsa bu gorev oncesine tek tikla donulebilsin. Yalnizca ilk girus'te
1511
- // (approval sonrasi devam da ayni checkpoint'i korur). Basarisizsa gorevi asla oldurmez.
1512
- if (baseCfg.versioning !== false && !task.checkpointId) {
1513
- const cp = checkpoints.createCheckpoint(this._cwd, { taskId: task.id, label: task.prompt, kind: "pre-task", retention: baseCfg.versioningRetention });
1514
- if (cp.ok) {
1515
- task.checkpointId = cp.id;
1516
- store.saveTask("pending", task);
1517
- this.publish("log", { level: "info", msg: `Surum alindi (${cp.fileCount} dosya · ${cp.backend}); bu gorev oncesine geri donulebilir.` }, task.id);
1518
- } else if (cp.skipped) {
1519
- this.publish("log", { level: "warn", msg: `Surum alinamadi: ${cp.reason}` }, task.id);
1520
- }
1521
- }
1522
- this.publish("log", { level: "info", msg: `Calisma modu: ${task.executionMode.toUpperCase()} · en fazla ${cfg.operator.maxRounds} tur / ${cfg.operator.maxDelegationsPerRound} delegasyon` }, task.id);
1523
- this.emit("status", this.status());
1524
- this.emit("queue");
1525
-
1526
- cfg.runtimeUnavailableAgents = this.quarantinedAgents();
1527
- // Proje context: varsayilan olarak bu calisma klasorunun .crewctl/CONTEXT.md profilini
1528
- // yukleriz (path'e ozel, tum kodu bastan taramaya gerek kalmasin). task.freshContext ise
1529
- // temiz sayfa. projectContext:false ise eski GLOBAL hafiza davranisi korunur.
1530
- const memory = cfg.projectContext === false
1531
- ? store.getMemory(cfg.memoryCharBudget)
1532
- : (task.freshContext ? "" : store.readProjectContext(this._cwd));
1533
- const state = task.teamState || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1534
- const usedIds = new Set(state.usedIds || []);
1535
-
1536
- let assignments;
1537
- if (state.plan && task.approved) {
1538
- assignments = normalizeAssignments(state.plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1539
- assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1540
- this.publish("log", { level: "info", msg: "Onaylanmis operator plani zorunlu rol zinciri korunarak devam ettiriliyor." }, task.id);
1541
- } else {
1542
- // Bilgi sorusu kestirmesi: operator, delege edilecek bir is olmadan yaniti dogrudan
1543
- // verdiginde (or. "kac beceri var") tek turda tamamla. Boylece motorun otoriter verisi
1544
- // bir alt agente aktarilirken kaybolmaz/carpitilmaz ve zorunlu assignment semasi bu
1545
- // tur sorularda gorevi bosuna oldurmez. ANCAK gorev acikca bir yapim/degisiklik isiyse
1546
- // kestirmeye izin verilmez: operator (cogunlukla proje hafizasindaki gecmis teslimati
1547
- // "zaten yapildi" sanip) is uretmeden kapatmaya calisir. Once yeniden planlama isteriz;
1548
- // israr ederse gorevi sahte basari yerine net bir hatayla dururuz.
1549
- const requiresWork = taskRequiresDelegation(task.prompt);
1550
- const shortcutRetries = requiresWork ? Math.max(1, cfg.operator?.protocolRetries ?? 1) : 0;
1551
- let plan, shortcutAttempt = 0, planCorrection = "";
1552
- while (true) {
1553
- try {
1554
- plan = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "plan") + planCorrection, cfg, "operator-plan", "Operator plani");
1555
- } catch (error) {
1556
- // Sohbet kurtarma: gorev delegasyon gerektirmiyorsa (selamlasma, kisa soru) ve operator
1557
- // protokol nesnesi yerine duz metinle cevap verdiyse, kullaniciyi "gecerli JSON
1558
- // dondurmedi" hatasiyla karsilamak yanlistir istenen sey zaten o metindi. Yalnizca
1559
- // is gerektirmeyen gorevlerde ve elde gercek bir metin varken devreye girer.
1560
- const prose = conversationalAnswer(error);
1561
- if (requiresWork || !prose) throw error;
1562
- this.publish("log", { level: "warn", msg: "Operator protokol nesnesi dondurmedi; gorev is gerektirmedigi icin duz metin cevabi teslimat sayildi." }, task.id);
1563
- task.teamState = state;
1564
- return this.complete(task, state, "done", prose, { verification: "Operatorun dogrudan yaniti (protokol nesnesi yerine duz metin)." });
1565
- }
1566
- const isShortcut = plan && String(plan.status).toLowerCase() === "complete" && !Array.isArray(plan.assignments);
1567
- if (!isShortcut) break;
1568
- if (!requiresWork) {
1569
- if (!String(plan.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1570
- this.publish("log", { level: "info", msg: "Operator gorevi delegasyona gerek gormeden dogrudan yanitladi." }, task.id);
1571
- task.teamState = state;
1572
- return this.complete(task, state, "done", String(plan.final), plan);
1573
- }
1574
- if (shortcutAttempt++ >= shortcutRetries) {
1575
- throw new Error("Operator bu yapim gorevini delege etmeden kapatmaya calisti. Proje hafizasindaki gecmis teslimatlar gorevi tamamlanmis saymaz; en az bir uygulama delegasyonu gerekir.");
1576
- }
1577
- this.publish("log", { level: "warn", msg: "Operator yapim gorevini delegasyonsuz kapatmaya calisti; hafiza gecmisi kanit sayilmaz, yeniden planlama isteniyor." }, task.id);
1578
- 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.";
1579
- }
1580
- assignments = normalizeAssignments(plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1581
- assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1582
- state.plan = { summary: String(plan.summary || ""), completionCriteria: Array.isArray(plan.completionCriteria) ? plan.completionCriteria.map(String) : [], assignments };
1583
- state.criteria = state.plan.completionCriteria;
1584
- state.operatorDecisions.push({ round: 0, ...state.plan });
1585
- task.teamState = state;
1586
- task.planPreview = JSON.stringify(state.plan, null, 2);
1587
- store.saveTask("pending", task);
1588
- if (cfg.approvalMode === "ask" && this.isRisky(task.planPreview, cfg)) {
1589
- task.status = "awaiting-approval";
1590
- task.planHash = store.hashText(task.planPreview);
1591
- store.moveTask("pending", "approval", task);
1592
- this.publish("log", { level: "warn", msg: "Riskli operator plani insan onayina alindi." }, task.id);
1593
- this.busy = false;
1594
- this.current = null;
1595
- this.emit("queue");
1596
- this.emit("status", this.status());
1597
- this.stopLiveDiff();
1598
- this._textBefore = null;
1599
- return;
1600
- }
1601
- }
1602
-
1603
- const maxRounds = Math.max(1, cfg.operator?.maxRounds || 6);
1604
- const maxRecoveryRounds = Math.max(0, cfg.operator?.maxInfrastructureRecoveryRounds ?? 2);
1605
- let recoveryRounds = 0;
1606
- while (this.running && state.round < maxRounds + recoveryRounds) {
1607
- state.round++;
1608
- await this.runAssignments(task, cfg, assignments, state);
1609
- const infrastructureFailures = assignments.filter((assignment) => {
1610
- const result = state.results[assignment.id];
1611
- return result?.status === "failed" && RECOVERABLE_CLI_ERRORS.has(result.error?.code);
1612
- });
1613
- if (infrastructureFailures.length && recoveryRounds < maxRecoveryRounds) {
1614
- recoveryRounds++;
1615
- this.publish("log", { level: "warn", msg: `CLI altyapi hatasi tur butcesinden dusulmedi; ${maxRecoveryRounds - recoveryRounds} ek kurtarma turu kaldi.` }, task.id);
1616
- }
1617
- // Uygulama agenti kalmadi guvenligi: Gorev acikca dosya olusturma/degistirme gerektiriyor
1618
- // fakat katalogda 'implement' yapabilen kullanilabilir hicbir agent kalmadiysa (or. tek
1619
- // executor opencode sessizlik nedeniyle karantinaya alindi) ve simdiye kadar tamamlanmis bir
1620
- // uygulama isi da yoksa, HICBIR ek tur dosya uretemez. Operatoru planlayici/gap-analizi
1621
- // dongusunde bosuna dondurup tur ve cagri butcesini yakmak yerine net, eyleme donuk bir
1622
- // hatayla dur (kullanicinin gordugu "circle seklinde uzayip gitme" sorununu bu keser).
1623
- if (taskRequiresDelegation(task.prompt)
1624
- && !this.hasUsableImplementAgent(cfg, operatorCli)
1625
- && !Object.values(state.results).some((result) => result.kind === "implement" && result.status === "completed")) {
1626
- // Implement agenti yalnizca SURELI karantinadaysa gorevi oldurmek yerine bekleriz.
1627
- // Kalici kisitlarda (kota bitti, CLI yok) null doner.
1628
- const waitMs = this.msUntilImplementAgentReturns(cfg, operatorCli);
1629
- let recovered = false;
1630
- if (waitMs !== null) {
1631
- this.publish("log", { level: "warn", msg: `Kullanilabilir implement agenti gecici olarak yok; karantina bitene kadar ${Math.ceil(waitMs / 1000)} sn bekleniyor.` }, task.id);
1632
- await this.sleepRetry(waitMs + 250);
1633
- // Bekleme sirasinda motor durdurulduysa gorevi "implement agenti yok" hatasiyla
1634
- // basarisiz isaretlemek yaniltici olur; dongu kosulu zaten temiz cikisi saglar.
1635
- if (!this.running) break;
1636
- recovered = this.hasUsableImplementAgent(cfg, operatorCli);
1637
- if (recovered) {
1638
- cfg.runtimeUnavailableAgents = this.quarantinedAgents();
1639
- this.publish("log", { level: "info", msg: "Implement agenti karantinadan dondu; gorev surduruluyor." }, task.id);
1640
- }
1641
- }
1642
- const dead = this.quarantinedAgents();
1643
- if (!recovered) throw new Error(
1644
- "Bu gorev dosya olusturma/degistirme gerektiriyor fakat kullanilabilir bir uygulama (implement) agenti yok" +
1645
- (dead.length ? ` (${dead.join(", ")} bu oturumda kullanim disi kaldi)` : "") +
1646
- ". Ayarlar > Agent'lar'dan 'implementation' yetenekli bir agent (Codex/Claude/Gemini/OpenCode) ekleyip etkinlestirin veya opencode'un oturum/model durumunu kontrol edin, sonra gorevi tekrar calistirin."
1647
- );
1648
- }
1649
- if (task.executionMode === "fast" && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")) {
1650
- const reports = assignments.map((assignment) => state.results[assignment.id].result).join("\n\n");
1651
- this.publish("log", { level: "info", msg: "FAST mod: uzman teslimati basarili; ikinci operator degerlendirme cagrisi atlandi." }, task.id);
1652
- return this.complete(task, state, "done", reports, {});
1653
- }
1654
- // PASS hizli yolu: turdaki tum delegasyonlar tamamlandi ve turun kendi bagimsiz
1655
- // denetimi PASS verdiyse operatorun ikinci degerlendirme cagrisi bilgi eklemez,
1656
- // yalnizca dakikalar kaybettirir. Bayat bir PASS'in yeni turu kapatmamasi icin
1657
- // karar en guncel denetim olmalidir. operator.passFastPath=false ile kapatilabilir.
1658
- const roundReview = assignments
1659
- .map((assignment) => state.results[assignment.id])
1660
- .filter((result) => result?.kind === "review" && result.status === "completed" && result.verdict)
1661
- .pop();
1662
- if (cfg.operator?.passFastPath !== false
1663
- && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")
1664
- && roundReview?.verdict === "PASS"
1665
- && roundReview === this.latestReview(state)) {
1666
- this.publish("log", { level: "ok", msg: `Denetim ${roundReview.id} PASS verdi; operator degerlendirme cagrisi atlandi ve teslimat tamamlandi.` }, task.id);
1667
- const completedWork = Object.values(state.results).filter((result) => result.status === "completed");
1668
- const final = `Teslimat bagimsiz denetimden gecti (${roundReview.id} → VERDICT: PASS).\n` +
1669
- completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1670
- return this.complete(task, state, "done", final, { verification: `${roundReview.agent} → VERDICT: PASS` });
1671
- }
1672
- const decision = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "review"), cfg, "operator-review", "Operator karari");
1673
- state.operatorDecisions.push({ round: state.round, ...decision });
1674
- task.teamState = state;
1675
- store.saveTask("pending", task);
1676
- if (String(decision.status).toLowerCase() === "complete") {
1677
- if (!String(decision.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1678
- return this.complete(task, state, "done", String(decision.final), decision);
1679
- }
1680
- if (String(decision.status).toLowerCase() !== "continue") throw new Error("Operator status alani continue veya complete olmali.");
1681
- assignments = normalizeAssignments(decision.assignments, cfg, operatorCli, new Set(state.usedIds), task.prompt);
1682
- // Inceleme dongusu valisi: bagimsiz denetim PASS verdiyse ve operator yalnizca yeni
1683
- // inceleme turlari acmak istiyorsa dongu burada kesilir. Sonsuz review ping-pong'u
1684
- // hem tur butcesini tuketiyor hem de basarili teslimati kullaniciya geciktiriyordu.
1685
- const lastReview = this.latestReview(state);
1686
- if (lastReview?.verdict === "PASS" && assignments.every((assignment) => assignment.kind === "review")) {
1687
- this.publish("log", { level: "warn", msg: `Denetim ${lastReview.id} PASS verdi; yalnizca yeni inceleme iceren tur acilmadi ve teslimat tamamlandi.` }, task.id);
1688
- const final = `Teslimat bagimsiz denetimden gecti (${lastReview.id} VERDICT: PASS). ` +
1689
- `Operatorun ek inceleme talebi tur butcesini korumak icin motor tarafindan sonlandirildi.` +
1690
- (String(decision.reason || "").trim() ? `\nOperator notu: ${clip(decision.reason, 600)}` : "");
1691
- return this.complete(task, state, "done", final, { verification: `${lastReview.agent} VERDICT: PASS` });
1692
- }
1693
- }
1694
- if (!this.running) throw new Error("Motor durduruldu; gorev tamamlanmadan kesildi.");
1695
- return this.finishExhausted(task, state, recoveryRounds);
1696
- }
1697
-
1698
- // Tur butcesi doldugunda tamamlanmis isi cope atmak yerine uyarili kismi teslimat yapar.
1699
- // Kullanicinin gordugu sonuc "saatlerce bekledim ve hata aldim" degil, "teslimat hazir,
1700
- // su riskler acik kaldi" olmalidir. Hic tamamlanan is yoksa eski davranis korunur.
1701
- finishExhausted(task, state, recoveryRounds) {
1702
- const completedWork = Object.values(state.results || {}).filter((result) => result.status === "completed");
1703
- if (!completedWork.length) {
1704
- throw new Error(`Operator ${state.round} tur sonunda gorevi tamamlayamadi${recoveryRounds ? ` (${recoveryRounds} CLI kurtarma turu kullanildi)` : ""}.`);
1705
- }
1706
- const lastReview = this.latestReview(state);
1707
- const lastDecision = [...(state.operatorDecisions || [])].reverse().find((decision) => String(decision.status).toLowerCase() === "continue");
1708
- const warnings = [
1709
- `Tur butcesi doldu (${state.round} tur); operator complete karari veremeden teslimat kapatildi.`,
1710
- ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} → VERDICT: ${lastReview.verdict}.`] : []),
1711
- ...(lastDecision && String(lastDecision.reason || "").trim() ? [`Operatorun acik biraktigi konu: ${clip(lastDecision.reason, 400)}`] : []),
1712
- ];
1713
- this.publish("log", { level: "warn", msg: `Tur butcesi doldu; tamamlanan is kismi teslimat olarak kapatiliyor (${completedWork.length} tamamlanmis delegasyon).` }, task.id);
1714
- const final = `KISMI TESLIMAT: Tur butcesi doldugu icin gorev, tamamlanan isle kapatildi.\n` +
1715
- completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n") +
1716
- `\nAcik kalan konular teslimat uyarilarinda listelendi.`;
1717
- return this.complete(task, state, "done", final, {
1718
- warnings,
1719
- verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1720
- });
1721
- }
1722
-
1723
- // Gorev beklenmedik bir hatayla kesildiginde (protokol hatasi, butce asimi vb.) somut is
1724
- // uretilmisse gorevi failed yerine uyarili kismi teslimatla kapatir. Basarili olamazsa
1725
- // false doner ve normal hata akisi calisir.
1726
- async salvage(task, error) {
1727
- try {
1728
- if (task.kind === "operator-chat") return false;
1729
- if (!this._cwd || !this._snapBefore) return false;
1730
- const state = task.teamState;
1731
- const completedWork = Object.values(state?.results || {}).filter((result) => result.status === "completed");
1732
- const changes = diffSnapshots(this._snapBefore, snapshotDir(this._cwd));
1733
- const hasFileChanges = Boolean(changes.created.length || changes.modified.length || changes.deleted.length);
1734
- const hasImplementation = completedWork.some((result) => result.kind === "implement");
1735
- // Kurtarma kosulu: YA tamamlanmis bir uygulama delegasyonu var (ekip uretti) YA DA calisma
1736
- // klasoru fiilen degisti. Ikincisi, operatorun kendisi standart JSON protokolune uymadan
1737
- // isi dogrudan yaptigi durumu da kapsar (or. OpenCode operator rolunde JSON plan yerine
1738
- // dosyalari kendisi degistirip duz metin doner). Hicbir is yoksa normal hata akisina birak.
1739
- if (!hasFileChanges && !hasImplementation) return false;
1740
- const failure = classifyCliError(error);
1741
- const rescueState = state || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1742
- const operatorDirect = !completedWork.length && hasFileChanges;
1743
- this.publish("log", { level: "warn", msg: `Gorev hatayla kesildi (${failure.summary}); ${operatorDirect ? "operatorun dogrudan yaptigi degisiklikler" : "tamamlanan is"} kismi teslimat olarak korunuyor.` }, task.id);
1744
- const lastReview = this.latestReview(rescueState);
1745
- const changedFiles = [...changes.created, ...changes.modified, ...changes.deleted];
1746
- const final = operatorDirect
1747
- ? `KISMI TESLIMAT: Operator (${task.operatorCli || "?"}) standart delegasyon protokolune uymadan isi dogrudan uyguladi; degisiklikler korundu.\n` +
1748
- changedFiles.slice(0, 40).map((file) => `- ${file}`).join("\n")
1749
- : `KISMI TESLIMAT: Gorev bir altyapi/protokol hatasiyla kesildi fakat tamamlanan is korundu.\n` +
1750
- completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1751
- await this.complete(task, rescueState, "done", final, {
1752
- warnings: [
1753
- `Gorev su hatayla kesildi: ${failure.summary}`,
1754
- ...(operatorDirect ? [`Operator CLI (${task.operatorCli || "?"}) plan JSON'i yerine isi dogrudan yapti. Daha guvenilir orkestrasyon icin operator olarak JSON planlamaya uygun bir CLI (or. Codex veya Claude) secmeyi dusunun.`] : []),
1755
- ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} VERDICT: ${lastReview.verdict}.`] : []),
1756
- ],
1757
- verification: lastReview ? `${lastReview.agent} VERDICT: ${lastReview.verdict}` : "",
1758
- });
1759
- return true;
1760
- } catch {
1761
- return false;
1762
- }
1763
- }
1764
-
1765
- async runChatTask(task, cfg) {
1766
- const parentFound = store.findTask(task.parentTaskId);
1767
- if (!parentFound) throw new Error("Sohbetin bagli oldugu tamamlanmis gorev bulunamadi.");
1768
- const parent = parentFound.task;
1769
- let operatorCli = parent.operatorCli;
1770
- if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) operatorCli = cfg.operator?.cli;
1771
- if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Sohbet icin operator CLI bulunamadi.");
1772
- this.busy = true;
1773
- this.current = { id: task.id, stage: "operator-chat", agent: operatorCli };
1774
- this._cwd = path.resolve(store.WORK_BASE, parent.targetDir || cfg.workingDir || ".");
1775
- this.publish("log", { level: "task", msg: `OPERATOR SOHBETI ${parent.id}: ${task.prompt}` }, task.id);
1776
- this.emit("status", this.status());
1777
-
1778
- const role = store.readRole("operator-chat.md");
1779
- const resultContext = Object.values(parent.teamState?.results || {}).map((result) => ({
1780
- id: result.id, agent: result.agent, status: result.status, result: clip(result.result, 1800),
1781
- }));
1782
- const prompt = `${role}\n\n---\n## Tamamlanmis gorev hakkinda takip sohbeti\n` +
1783
- `Kullanici daha once tamamladiginiz gorev hakkinda soru soruyor. Yeni dosya degistirme, arac kullanma veya delegasyon yapma. ` +
1784
- `Asagidaki kayitlara dayanarak dogrudan, kisa ve acik cevap ver. Bilmiyorsan bunu belirt.\n` +
1785
- `## 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` +
1786
- `## Takim raporlari\n${clip(JSON.stringify(resultContext, null, 2), 12000)}\n` +
1787
- `## Onceki sohbet\n${clip(JSON.stringify(parent.conversation || [], null, 2), 8000)}\n## Yeni soru\n${task.prompt}`;
1788
- const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-chat" });
1789
- const entry = { question: task.prompt, answer: response.text, at: new Date().toISOString(), taskId: task.id };
1790
- parent.conversation ||= [];
1791
- parent.conversation.push(entry);
1792
- store.saveTask(parentFound.state, parent);
1793
- task.status = "done";
1794
- task.finishedAt = new Date().toISOString();
1795
- task.final = response.text;
1796
- task.summary = clip(response.text, 1000);
1797
- store.moveTask("pending", "done", task);
1798
- this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: parent.id, answer: response.text, status: "done", operator: operatorCli }, task.id);
1799
- this.publish("log", { level: "ok", msg: `Operator soruyu yanitladi: ${parent.id}` }, task.id);
1800
- this.emit("queue");
1801
- }
1802
-
1803
- async complete(task, state, status, finalText, decision = {}) {
1804
- const changes = diffSnapshots(this._snapBefore || new Map(), snapshotDir(this._cwd));
1805
- // Son periyodik taramadan sonra yapilan degisiklikleri de replay kaydina ve dashboard'a
1806
- // aktar; ardindan run --once yolunda da timer'in acik kalmamasini garanti et.
1807
- this.publishFileChanges(task.id);
1808
- this.stopLiveDiff();
1809
- task.status = status;
1810
- task.finishedAt = new Date().toISOString();
1811
- const files = [
1812
- ...changes.created.map((file) => ({ path: file, action: "created" })),
1813
- ...changes.modified.map((file) => ({ path: file, action: "modified" })),
1814
- ...changes.deleted.map((file) => ({ path: file, action: "deleted" })),
1815
- ];
1816
- const concise = clip(String(finalText).replace(/\n{3,}/g, "\n\n"), 700);
1817
- let verification = String(decision.verification || "").trim();
1818
- if (!verification) {
1819
- for (const result of Object.values(state.results)) {
1820
- const match = String(result.result || "").match(/DO(?:Ğ|G\u0306|G)RULAMA:\s*([^\n`]+)/i);
1821
- if (match) { verification = match[1].trim(); break; }
1822
- }
1823
- }
1824
- task.summary = concise;
1825
- task.final = finalText;
1826
- task.changes = changes;
1827
- task.usage = this.usageSummary();
1828
- const warnings = (Array.isArray(decision.warnings) ? decision.warnings : []).map(String).filter(Boolean);
1829
- task.delivery = {
1830
- summary: concise,
1831
- files,
1832
- location: this._cwd,
1833
- verification,
1834
- mode: task.executionMode,
1835
- rounds: state.round,
1836
- agents: [...new Set(Object.values(state.results).map((result) => result.agent))],
1837
- ...(warnings.length ? { warnings } : {}),
1838
- };
1839
- task.teamState = state;
1840
- const found = store.findTask(task.id);
1841
- store.moveTask(found?.state || "pending", status, task);
1842
- store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
1843
- 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);
1844
- this.publish("log", { level: "ok", msg: `GOREV tamamlandi: ${task.id}` }, task.id);
1845
- this.notifyOutcome(task, status, { summary: task.summary, rounds: state.round, fileCount: files.length, verification: task.delivery.verification });
1846
- this.emit("queue");
1847
- this._textBefore = null;
1848
- // Proje profilini guncelle (sonuc zaten kullaniciya iletildi; motor mesgul kaldigindan
1849
- // sonraki gorevle es zamanli operator cagrisi olmaz). Basarisizsa gorevi ASLA etkilemez.
1850
- await this.reviseProjectContext(task, status, files);
1851
- }
1852
-
1853
- // Gorev bitiminde bu calisma klasorunun .crewctl/CONTEXT.md proje profilini operatore REVIZE
1854
- // ettirir (append degil): mimari/yapi/konvansiyon/komutlar guncel kalir, bayatlamaz. Yalnizca
1855
- // basarili ve DOSYA DEGISTIREN gorevlerde calisir (bilgi sorulari profili degistirmez).
1856
- async reviseProjectContext(task, status, files) {
1857
- try {
1858
- const cfg = this.cfg();
1859
- if (cfg.projectContext === false) return;
1860
- if (status !== "done" || !this._cwd) return;
1861
- if (!Array.isArray(files) || !files.length) return;
1862
- const operatorCli = task.operatorCli || cfg.operator?.cli;
1863
- if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) return;
1864
- const budget = cfg.projectContextCharBudget || 6000;
1865
- const current = store.readProjectContext(this._cwd);
1866
- const changed = files.map((f) => `${f.action}: ${f.path}`).join("\n");
1867
- const prompt =
1868
- `Sen bir projenin YASAYAN bilgi tabanini (proje profili) guncelliyorsun. Amac: sonraki gorevlerde ` +
1869
- `ajan tum kodu bastan taramadan projeyi anlasin.\n` +
1870
- `Asagida MEVCUT profil ve SON tamamlanan gorevin ozeti + degisen dosyalar var. Guncel, oz ve DOGRU ` +
1871
- `bir proje profili YAZ (Markdown). Kurallar:\n` +
1872
- `- Degisen gerceklere gore REVIZE et; silinen/tasinan/degisen seyleri guncelle, bayat bilgi birakma.\n` +
1873
- `- Dayanikli bilgiyi tut: amac, mimari, klasor/dosya yapisi, giris noktalari, onemli moduller, ` +
1874
- `konvansiyonlar, kurulum/calistirma/test komutlari, dikkat noktalari.\n` +
1875
- `- Task-by-task changelog YAZMA; tek bir GUNCEL DURUM profili olsun.\n` +
1876
- `- En fazla ~${budget} karakter. YALNIZCA profil metnini dondur; aciklama/selamlama/JSON/kod-bloğu isareti YAZMA.\n\n` +
1877
- `## Mevcut profil\n${current || "(henuz yok sifirdan olustur)"}\n\n` +
1878
- `## Son tamamlanan gorev\n${task.prompt}\n\n## Sonuc ozeti\n${task.summary || ""}\n\n## Degisen dosyalar\n${changed}`;
1879
- const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-context" });
1880
- const profile = stripCodeFence(String(response?.text || "")).trim();
1881
- if (profile && store.writeProjectContext(this._cwd, clip(profile, budget * 2))) {
1882
- this.publish("log", { level: "info", msg: "Proje profili (.crewctl/CONTEXT.md) guncellendi." }, task.id);
1883
- }
1884
- } catch (error) {
1885
- this.publish("log", { level: "warn", msg: `Proje profili guncellenemedi: ${error.message}` }, task.id);
1886
- }
1887
- }
1888
-
1889
- // Dis bildirim icin tek cikis noktasi: server.js bu event'i dinleyip (varsa) webhook'a POST atar.
1890
- // Motoru HTTP'den bagimsiz tutar; bildirim basarisiz olsa bile gorev akisini etkilemez.
1891
- notifyOutcome(task, status, extra = {}) {
1892
- try {
1893
- this.emit("notify", {
1894
- status,
1895
- id: task.id,
1896
- prompt: task.prompt,
1897
- operator: task.operatorCli || null,
1898
- dir: this._cwd || task.targetDir || null,
1899
- at: new Date().toISOString(),
1900
- ...extra,
1901
- });
1902
- } catch {}
1903
- }
1904
- }
1905
-
1906
- module.exports = new Engine();
1907
- // Testlerin dis davranisla birlikte kritik saf kurallari da dogrudan dogrulayabilmesi icin.
1908
- module.exports._internals = {
1909
- normalizeAssignments, ensureBalancedRoleChain, extractVerdict, clipMiddle, snapshotDir, diffSnapshots,
1910
- captureTextSnapshot, buildLineDiff, describeFileDiff, isSensitiveDiffPath, taskRequiresDelegation,
1911
- parseJson, conversationalAnswer, classifyCliError, RECOVERABLE_CLI_ERRORS, QUARANTINE_CLI_ERRORS,
1912
- TRANSIENT_CLI_ERRORS, QUARANTINE_COOLDOWN_SECONDS, compatibleAgentsForKind,
1913
- normalizeCliOutput, addUsage, usageTotal, claudeSandboxSettings,
1914
- };
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
+ // Windows, ASCII-disi veya uzun adli klasorler icin 8.3 KISA AD uretir (or. kullanici "Ömer" ->
48
+ // "MER~1"). CLI'nin calisma klasoru bu kisa formda gecerse (path.resolve bazen kisa form dondurur)
49
+ // claude gibi ajanlarin izin motoru kisa-ad path'ini "supheli/olasi kacis" sayip her YAZMAYI manuel
50
+ // onaya alir; headless (-p) modda onay gelmedigi icin ajan "duzenlemek istiyor ama yapamiyor" diye
51
+ // takilir ve kullanici path secmesine ragmen "baska yere yazma yetkisi istiyor" gorunur. realpath
52
+ // kisa formu gercek uzun forma cozer; boylece ajan guvenilen tam yolu gorur. Cozemezse (dizin yok
53
+ // vb.) mevcut yolu aynen birakiriz.
54
+ function canonicalDir(dir) {
55
+ try { return fs.realpathSync.native(dir); }
56
+ catch { try { return fs.realpathSync(dir); } catch { return dir; } }
57
+ }
58
+
59
+ function snapshotDir(dir) {
60
+ const map = new Map();
61
+ const ignored = (rel) => {
62
+ const r = rel.replace(/\\/g, "/");
63
+ // .crewctl/ orkestratorun kendi proje-context'ini (CONTEXT.md) tutar; gorev diff'lerinde,
64
+ // live-diff'te ve teslimat dosya listesinde gozukmesin (kullanicinin isi degil).
65
+ return r.includes("node_modules") || r.includes(".git/") || r.startsWith(".git") ||
66
+ r === ".crewctl" || r.startsWith(".crewctl/") ||
67
+ r.startsWith("orchestrator/queue") || r.startsWith("orchestrator/state") ||
68
+ r.startsWith("orchestrator/memory") || r.startsWith("orchestrator/node_modules");
69
+ };
70
+ let count = 0;
71
+ const walk = (abs, rel) => {
72
+ if (count > 12000) return;
73
+ let entries;
74
+ try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
75
+ for (const entry of entries) {
76
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
77
+ if (ignored(childRel)) continue;
78
+ const childAbs = path.join(abs, entry.name);
79
+ if (entry.isDirectory()) walk(childAbs, childRel);
80
+ else {
81
+ try {
82
+ const s = fs.statSync(childAbs);
83
+ map.set(childRel, `${s.mtimeMs}:${s.size}`);
84
+ count++;
85
+ } catch {}
86
+ }
87
+ }
88
+ };
89
+ walk(dir, "");
90
+ return map;
91
+ }
92
+
93
+ function diffSnapshots(before, after) {
94
+ const created = [], modified = [], deleted = [];
95
+ for (const [file, stamp] of after) {
96
+ if (!before.has(file)) created.push(file);
97
+ else if (before.get(file) !== stamp) modified.push(file);
98
+ }
99
+ for (const file of before.keys()) if (!after.has(file)) deleted.push(file);
100
+ return { created: created.sort(), modified: modified.sort(), deleted: deleted.sort() };
101
+ }
102
+
103
+ const DIFF_MAX_FILE_BYTES = 256 * 1024;
104
+ const DIFF_MAX_BASELINE_BYTES = 24 * 1024 * 1024;
105
+ const DIFF_MAX_BASELINE_FILES = 2000;
106
+ const DIFF_MAX_MATRIX_CELLS = 250000;
107
+ const DIFF_CONTEXT_LINES = 3;
108
+ const DIFF_MAX_RENDERED_LINES = 240;
109
+ const DIFF_MAX_EVENT_LINES = 1000;
110
+ const DIFF_MAX_SOURCE_LINES = 5000;
111
+
112
+ function isSensitiveDiffPath(file) {
113
+ const normalized = String(file || "").replace(/\\/g, "/").toLowerCase();
114
+ const base = path.posix.basename(normalized);
115
+ return /^\.env(?:\.|$)/.test(base) || [".npmrc", ".pypirc", ".netrc"].includes(base) ||
116
+ /(^|[._-])(secret|secrets|credential|credentials)([._-]|$)/.test(base) ||
117
+ /\.(pem|key|p12|pfx)$/.test(base) || /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/.test(base);
118
+ }
119
+
120
+ function readDiffText(file, maxBytes = DIFF_MAX_FILE_BYTES) {
121
+ try {
122
+ const stat = fs.statSync(file);
123
+ if (!stat.isFile()) return { ok: false, status: "unavailable" };
124
+ if (stat.size > maxBytes) return { ok: false, status: "too-large", size: stat.size };
125
+ const buffer = fs.readFileSync(file);
126
+ if (buffer.includes(0)) return { ok: false, status: "binary", size: stat.size };
127
+ let controls = 0;
128
+ for (const byte of buffer) {
129
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) controls++;
130
+ }
131
+ if (buffer.length && controls / buffer.length > 0.02) return { ok: false, status: "binary", size: stat.size };
132
+ return { ok: true, text: buffer.toString("utf8"), size: stat.size };
133
+ } catch {
134
+ return { ok: false, status: "unavailable" };
135
+ }
136
+ }
137
+
138
+ // Gorev basindaki metin iceriklerini sinirli bir butceyle saklar. Bu taban, Git deposu
139
+ // olmayan klasorlerde de modified/deleted dosyalar icin satir diff'i uretebilmemizi saglar.
140
+ function captureTextSnapshot(dir, snapshot, options = {}) {
141
+ const maxBytes = Math.max(1024, Number(options.maxBytes) || DIFF_MAX_BASELINE_BYTES);
142
+ const maxFiles = Math.max(1, Number(options.maxFiles) || DIFF_MAX_BASELINE_FILES);
143
+ const maxFileBytes = Math.max(1024, Number(options.maxFileBytes) || DIFF_MAX_FILE_BYTES);
144
+ const result = new Map();
145
+ let bytes = 0, files = 0;
146
+ for (const file of [...(snapshot?.keys?.() || [])].sort()) {
147
+ if (isSensitiveDiffPath(file)) {
148
+ result.set(file, { ok: false, status: "redacted" });
149
+ continue;
150
+ }
151
+ if (files >= maxFiles || bytes >= maxBytes) {
152
+ result.set(file, { ok: false, status: "baseline-limit" });
153
+ continue;
154
+ }
155
+ const value = readDiffText(path.join(dir, file), maxFileBytes);
156
+ if (value.ok && bytes + value.size > maxBytes) {
157
+ result.set(file, { ok: false, status: "baseline-limit" });
158
+ continue;
159
+ }
160
+ result.set(file, value);
161
+ if (value.ok) { bytes += value.size; files++; }
162
+ }
163
+ return result;
164
+ }
165
+
166
+ function splitDiffLines(text) {
167
+ const value = String(text || "").replace(/\r\n/g, "\n");
168
+ if (!value) return [];
169
+ const lines = value.split("\n");
170
+ if (lines[lines.length - 1] === "") lines.pop();
171
+ return lines;
172
+ }
173
+
174
+ function lineOperations(beforeText, afterText) {
175
+ const before = splitDiffLines(beforeText), after = splitDiffLines(afterText);
176
+ const cells = before.length * after.length;
177
+ let approximate = false;
178
+ const operations = [];
179
+ if (cells <= DIFF_MAX_MATRIX_CELLS) {
180
+ const width = after.length + 1;
181
+ const table = new Uint32Array((before.length + 1) * width);
182
+ for (let i = before.length - 1; i >= 0; i--) {
183
+ for (let j = after.length - 1; j >= 0; j--) {
184
+ table[i * width + j] = before[i] === after[j]
185
+ ? table[(i + 1) * width + j + 1] + 1
186
+ : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
187
+ }
188
+ }
189
+ let i = 0, j = 0;
190
+ while (i < before.length || j < after.length) {
191
+ if (i < before.length && j < after.length && before[i] === after[j]) {
192
+ operations.push({ type: "context", text: before[i++] }); j++;
193
+ } else if (i < before.length && (j >= after.length || table[(i + 1) * width + j] >= table[i * width + j + 1])) {
194
+ operations.push({ type: "delete", text: before[i++] });
195
+ } else {
196
+ operations.push({ type: "add", text: after[j++] });
197
+ }
198
+ }
199
+ } else {
200
+ // Cok buyuk dosyalarda karesel matris kurma. Ortak bas/sonu koruyup degisen orta
201
+ // bolumu tek hunk olarak goster; UI bu sonucu yaklasik olarak etiketler.
202
+ approximate = true;
203
+ let prefix = 0;
204
+ while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) prefix++;
205
+ let suffix = 0;
206
+ while (suffix < before.length - prefix && suffix < after.length - prefix &&
207
+ before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
208
+ for (let i = 0; i < prefix; i++) operations.push({ type: "context", text: before[i] });
209
+ for (let i = prefix; i < before.length - suffix; i++) operations.push({ type: "delete", text: before[i] });
210
+ for (let i = prefix; i < after.length - suffix; i++) operations.push({ type: "add", text: after[i] });
211
+ for (let i = suffix; i > 0; i--) operations.push({ type: "context", text: before[before.length - i] });
212
+ }
213
+ return { operations, approximate };
214
+ }
215
+
216
+ function buildLineDiff(beforeText, afterText, options = {}) {
217
+ const context = Math.max(0, Number(options.context) || DIFF_CONTEXT_LINES);
218
+ const maxLines = Math.max(20, Number(options.maxLines) || DIFF_MAX_RENDERED_LINES);
219
+ const { operations, approximate } = lineOperations(beforeText, afterText);
220
+ let oldNo = 1, newNo = 1;
221
+ const annotated = operations.map((operation) => {
222
+ const line = { ...operation, oldNumber: null, newNumber: null };
223
+ if (operation.type !== "add") line.oldNumber = oldNo++;
224
+ if (operation.type !== "delete") line.newNumber = newNo++;
225
+ return line;
226
+ });
227
+ const additions = annotated.filter((line) => line.type === "add").length;
228
+ const deletions = annotated.filter((line) => line.type === "delete").length;
229
+ const ranges = [];
230
+ for (let i = 0; i < annotated.length; i++) {
231
+ if (annotated[i].type === "context") continue;
232
+ const start = Math.max(0, i - context), end = Math.min(annotated.length, i + context + 1);
233
+ const last = ranges[ranges.length - 1];
234
+ if (last && start <= last.end) last.end = Math.max(last.end, end);
235
+ else ranges.push({ start, end });
236
+ }
237
+ const hunks = [];
238
+ let rendered = 0, truncated = false;
239
+ for (const range of ranges) {
240
+ if (rendered >= maxLines) { truncated = true; break; }
241
+ const oldStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "add").length;
242
+ const newStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "delete").length;
243
+ const available = Math.max(0, maxLines - rendered);
244
+ const selected = annotated.slice(range.start, Math.min(range.end, range.start + available));
245
+ if (selected.length < range.end - range.start) truncated = true;
246
+ hunks.push({
247
+ oldStart,
248
+ oldLines: selected.filter((line) => line.type !== "add").length,
249
+ newStart,
250
+ newLines: selected.filter((line) => line.type !== "delete").length,
251
+ lines: selected.map((line) => ({ ...line, text: String(line.text).slice(0, 1000) })),
252
+ });
253
+ rendered += selected.length;
254
+ }
255
+ return { additions, deletions, hunks, truncated, approximate };
256
+ }
257
+
258
+ function describeFileDiff(root, file, action, baseline, options = {}) {
259
+ const base = action === "created" ? { ok: true, text: "" } : baseline?.get(file);
260
+ const current = action === "deleted" ? { ok: true, text: "" } :
261
+ (isSensitiveDiffPath(file) ? { ok: false, status: "redacted" } : readDiffText(path.join(root, file)));
262
+ const unavailable = !base?.ok ? base : (!current?.ok ? current : null);
263
+ if (unavailable) return { path: file, action, additions: null, deletions: null, previewStatus: unavailable.status || "unavailable", hunks: [] };
264
+ const lineCount = (text) => text ? (String(text).match(/\n/g) || []).length + 1 : 0;
265
+ if (lineCount(base.text) > DIFF_MAX_SOURCE_LINES || lineCount(current.text) > DIFF_MAX_SOURCE_LINES) {
266
+ return { path: file, action, additions: null, deletions: null, previewStatus: "too-many-lines", hunks: [] };
267
+ }
268
+ return { path: file, action, ...buildLineDiff(base.text, current.text, options) };
269
+ }
270
+
271
+ function clip(value, limit = 12000) {
272
+ const text = String(value || "");
273
+ return text.length <= limit ? text : `${text.slice(0, limit)}\n...[kesildi]`;
274
+ }
275
+
276
+ // Sohbet odakli CLI'lar profili bazen ``` ile sarar; profil dosyasina ham metni yazariz.
277
+ // Yalnizca tum icerigi saran TEK bir dis fence'i soyar; ic kod bloklarina dokunmaz.
278
+ function stripCodeFence(text) {
279
+ const trimmed = String(text || "").trim();
280
+ const match = trimmed.match(/^```[a-zA-Z0-9]*\s*\n([\s\S]*?)\n?```$/);
281
+ return match ? match[1] : trimmed;
282
+ }
283
+
284
+ // Bas ve sonu koruyarak kirpar. Denetci raporlarinda VERDICT satiri metnin SONUNDadir;
285
+ // yalnizca bastan kirpmak operatorun karari gormemesine ve gereksiz yeniden inceleme
286
+ // turlarina yol acar.
287
+ function clipMiddle(value, limit = 12000) {
288
+ const text = String(value || "");
289
+ if (text.length <= limit) return text;
290
+ const head = Math.ceil(limit * 0.6);
291
+ const tail = Math.max(1, limit - head);
292
+ return `${text.slice(0, head)}\n...[orta kisim kesildi]...\n${text.slice(text.length - tail)}`;
293
+ }
294
+
295
+ function extractVerdict(text) {
296
+ const matches = String(text || "").match(/VERDICT:\s*(PASS|FAIL)/gi);
297
+ if (!matches || !matches.length) return null;
298
+ return /PASS/i.test(matches[matches.length - 1]) ? "PASS" : "FAIL";
299
+ }
300
+
301
+ // Siniflandirilamayan hatada "gemini cikis kodu 1." gibi bilgisiz bir ilk satir yerine metnin
302
+ // icindeki GERCEK hata cumlesini bulur; kullaniciya gosterilen tek satir buysa anlamli olmali.
303
+ function meaningfulErrorLine(raw) {
304
+ const all = String(raw).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
305
+ // Yigin izleri ve motorun kendi sardigi "<cli> cikis kodu N." satiri teshis tasimaz;
306
+ // gercek neden hemen arkalarindaki satirdadir.
307
+ const useful = all.filter((line) => !/^at\s|^node:internal|^[{}[\]]+$/.test(line) && !/cikis kodu/i.test(line));
308
+ const signal = useful.find((line) => /error|hata|failed|failure|denied|invalid|exceeded|limit|quota|unable|cannot|must/i.test(line) && line.length > 20);
309
+ return signal || useful[0] || all[0] || String(raw).trim();
310
+ }
311
+
312
+ // Ham CLI ciktisini kullaniciya "ne oldu + ne yapmali" seklinde anlamli bir teshise cevirir.
313
+ // SIRA ONEMLIDIR: spesifik desenler once gelmeli. Ornegin Gemini'nin "GEMINI_API_KEY ...
314
+ // not found" mesaji bir PATH/kurulum sorunu DEGILDIR; genis /not found/ deseni bu yuzden
315
+ // daraltildi ve kimlik kontrollerinin arkasina alindi.
316
+ function classifyCliError(error) {
317
+ const raw = String(error?.message || error || "Bilinmeyen CLI hatasi");
318
+ // Hatanin geldigi CLI biliniyorsa tavsiye ona gore uretilir (runCli her hatayi etiketler).
319
+ const adapter = error?.adapter || "";
320
+ const agentName = error?.agentName || "";
321
+ const of = (code, summary, action) => ({ code, summary, action, agent: agentName, adapter, raw: clip(raw, 5000) });
322
+ if (/requires a newer version|upgrade to the latest|model metadata.*not found|unsupported.*model|model.*unsupported/i.test(raw)) {
323
+ return of("VERSION_INCOMPATIBLE", "CLI sürümü seçilen modeli desteklemiyor.", "CLI aracını güncelleyin veya desteklenen bir model seçin.");
324
+ }
325
+ if (/sessiz kaldi|cikti uretmedi|CLI_STALLED/i.test(raw)) {
326
+ 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.");
327
+ }
328
+ // "No provider available" 401 ile gelir ama OTURUM sorunu degildir: secili model hicbir
329
+ // saglayiciya cozulemiyordur. Genel 401/403 kuralindan ONCE gelmeli, yoksa zaten girisli
330
+ // kullaniciya bosuna "oturum acin" denir.
331
+ if (/no provider available/i.test(raw)) {
332
+ return of("PROVIDER_UNAVAILABLE", "Seçili model hiçbir yapılandırılmış sağlayıcıya çözümlenemedi (oturum sorunu değil).",
333
+ "Ayarlar Agent'lar bölümünde bu agent için açık bir model seçin. Model boşken OpenCode kendi son kullandığı modele düşer; o model erişilemezse görev bu hatayla durur.");
334
+ }
335
+ if (/API key not valid|API_KEY_INVALID|invalid[_ ]api[_ ]key|incorrect api key/i.test(raw)) {
336
+ return of("AUTH_INVALID", "API anahtarı geçersiz veya reddedildi.", `Bu agent'ın API anahtarını yenileyin. ${cliRegistry.authHint(adapter)}`);
337
+ }
338
+ // Gemini CLI oturum acilmadiginda "Please set an Auth method" / "GEMINI_API_KEY environment
339
+ // variable not found" yazar; her ikisi de kurulum degil OTURUM sorunudur.
340
+ 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)) {
341
+ return of("AUTH_REQUIRED", "CLI oturum açmamış; kimlik doğrulaması gerekiyor.", `${cliRegistry.authHint(adapter)} Sonra görevi tekrar deneyin.`);
342
+ }
343
+ // Kota (gunluk/aylik hak bitti) ile hiz siniri (kisa sureli) AYRI teshislerdir: ilki
344
+ // beklemekle gecmez, ikincisi gecer. Kullanicinin sordugu "limitim mi bitti" ayrimi budur.
345
+ 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)) {
346
+ 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.");
347
+ }
348
+ if (/rate.?limit|too many requests|\b429\b/i.test(raw)) {
349
+ 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.");
350
+ }
351
+ if (/overloaded|\bUNAVAILABLE\b|\b(503|500)\b|internal error|try again later/i.test(raw)) {
352
+ 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.");
353
+ }
354
+ if (/location is not supported|not available in your country|region.*not supported/i.test(raw)) {
355
+ 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.");
356
+ }
357
+ if (/ConnectionRefused|Unable to connect|provider hatasi|provider error|ECONNREFUSED/i.test(raw)) {
358
+ return of("PROVIDER_UNAVAILABLE", "Seçilen model sağlayıcısına bağlanılamadı veya model çözümlenemedi.",
359
+ adapter === "opencode"
360
+ ? "Ayarlar → Agent'lar bölümünde bu agent için açık bir model seçin (`opencode models` listesinden). Model seçili değilken OpenCode kendi son kullandığı modele düşer ve bu model erişilemez olabilir."
361
+ : "Sağlayıcı bağlantısını kontrol edin veya başka bir model seçin.");
362
+ }
363
+ if (/ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|getaddrinfo|fetch failed|socket hang up|network (error|unreachable)|proxy/i.test(raw)) {
364
+ 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.");
365
+ }
366
+ if (/not recognized as an internal|is not recognized|command not found|spawn .*ENOENT|\bENOENT\b|no such file or directory/i.test(raw)) {
367
+ return of("CLI_NOT_FOUND", "CLI komutu bulunamadı; kurulu değil veya PATH üzerinde görünmüyor.", `${adapter && adapter !== "custom" ? `${adapter} CLI'sini kurun` : "CLI'yi kurun"} ve Ayarlar → Agent'lar bölümünde komut adını doğrulayın (kurulum komutları için Doctor ekranına bakın).`);
368
+ }
369
+ if (/timeout|timed out|zaman asim|zaman aşım/i.test(raw)) {
370
+ 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.");
371
+ }
372
+ return of("CLI_FAILED", clip(meaningfulErrorLine(raw), 500), "Ham CLI çıktısı teknik ayrıntılarda saklandı; sorun sürerse alternatif bir agent deneyin.");
373
+ }
374
+
375
+ 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"]);
376
+ // Karantina = bu oturumda tekrar denemek anlamsiz. Kota/bolge kisiti beklemekle gecmez;
377
+ // hiz siniri (RATE_LIMIT) ve gecici saglayici hatasi (MODEL_OVERLOADED) gecer, karantinaya alinmaz.
378
+ const QUARANTINE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "QUOTA_EXCEEDED", "REGION_BLOCKED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "CLI_STALLED", "VERSION_INCOMPATIBLE"]);
379
+ // Birkac saniyede kendiliginden gecer; operatore geri tasiyip planlama turu harcamak yerine
380
+ // ayni agent ustel bekleme ile yeniden denenir.
381
+ const TRANSIENT_CLI_ERRORS = new Set(["RATE_LIMIT", "MODEL_OVERLOADED", "NETWORK_ERROR"]);
382
+ // Karantina suresi (sn). 0 = beklemekle duzelmez, kalici. Digerleri sure dolunca yeniden
383
+ // denenir (half-open); tekrar duserse sure katlanir (bkz. quarantineAgent).
384
+ const QUARANTINE_COOLDOWN_SECONDS = {
385
+ AUTH_REQUIRED: 180,
386
+ AUTH_INVALID: 180,
387
+ PROVIDER_UNAVAILABLE: 180,
388
+ CLI_STALLED: 300,
389
+ QUOTA_EXCEEDED: 0,
390
+ REGION_BLOCKED: 0,
391
+ CLI_NOT_FOUND: 0,
392
+ VERSION_INCOMPATIBLE: 0,
393
+ };
394
+
395
+ function resolveExecutionMode(task) {
396
+ if (["fast", "balanced", "deep"].includes(task.executionMode)) return task.executionMode;
397
+ const prompt = String(task.prompt || "");
398
+ const complex = /(mimari|migration|refactor|guvenlik|güvenlik|deploy|production|veritabani|veritabanı|authentication|entegrasyon|çoklu|multi|kapsamli|kapsamlı)/i.test(prompt);
399
+ // Not: "oyun/sayfa" gibi sifirdan build isleri BASIT sayilmaz — bunlar plan+uygula+incele
400
+ // gerektiren gercek gorevlerdir; fast moda dusurulup ekip kullanilmadan gecilmemeli.
401
+ // Not: "dosya" gibi genis kelimeler fast'a dusurmez; yorumdaki ilkeyle celisiyordu
402
+ // (sifirdan build/dosya olusturma gercek bir gorevdir). Yalnizca acik "kucuk/hizli" sinyalleri.
403
+ const simple = prompt.length < 350 && /(basit|ufak|küçük|kucuk|hizli|hızlı|simple|small|quick)/i.test(prompt);
404
+ return simple && !complex ? "fast" : "balanced";
405
+ }
406
+
407
+ function applyExecutionPolicy(base, mode) {
408
+ const cfg = { ...base, operator: { ...(base.operator || {}) } };
409
+ if (mode === "fast") {
410
+ cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 2);
411
+ cfg.operator.maxDelegationsPerRound = 1;
412
+ cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 2500);
413
+ cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 10000);
414
+ } else if (mode === "balanced") {
415
+ // Saglikli bir balanced gorev tek turda biter (uygulama + zincirli inceleme);
416
+ // 3 tur, iki duzeltme/yeniden dogrulama turuna yer birakir.
417
+ cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 3);
418
+ cfg.operator.maxDelegationsPerRound = Math.min(cfg.operator.maxDelegationsPerRound || 8, 3);
419
+ cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 6000);
420
+ cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 24000);
421
+ }
422
+ return cfg;
423
+ }
424
+
425
+ const EMPTY_USAGE = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
426
+
427
+ function addUsage(target, extra) {
428
+ const sum = { ...(target || EMPTY_USAGE) };
429
+ for (const key of Object.keys(EMPTY_USAGE)) sum[key] = (sum[key] || 0) + (Number(extra?.[key]) || 0);
430
+ return sum;
431
+ }
432
+
433
+ function usageTotal(usage) {
434
+ return (Number(usage?.input) || 0) + (Number(usage?.output) || 0);
435
+ }
436
+
437
+ // OpenCode'un step_finish olayindaki token/maliyet alanlarini normalize eder. Alan adlari
438
+ // surumler arasinda degisebildigi icin eksik alan 0 sayilir; hicbir sayi yoksa null doner
439
+ // ki "veri yok" ile "sifir maliyet" ayirt edilebilsin.
440
+ function readStepUsage(event) {
441
+ const tokens = event.part?.tokens || event.tokens;
442
+ const cost = Number(event.part?.cost ?? event.cost);
443
+ if (!tokens && !Number.isFinite(cost)) return null;
444
+ return {
445
+ input: Number(tokens?.input) || 0,
446
+ output: Number(tokens?.output) || 0,
447
+ reasoning: Number(tokens?.reasoning) || 0,
448
+ cacheRead: Number(tokens?.cache?.read) || 0,
449
+ cacheWrite: Number(tokens?.cache?.write) || 0,
450
+ cost: Number.isFinite(cost) ? cost : 0,
451
+ };
452
+ }
453
+
454
+ // DIKKAT: text/error sozlesmesi degismemeli — operatorun JSON protokolu bu metni okuyor.
455
+ // usage yalnizca EK bir alandir; cikarilamadiginda null kalir ve hicbir akisi etkilemez.
456
+ // Claude stream-json ciktisini ayiklar: satir-satir JSON olaylar; nihai metin ve kullanim son
457
+ // "result" olayindadir. result yoksa (surec yarida kesildiyse) toplanan assistant metnine duseriz.
458
+ function parseClaudeStream(stdout) {
459
+ let text = "", usage = null;
460
+ const assistant = [];
461
+ for (const line of String(stdout || "").split(/\r?\n/)) {
462
+ const t = line.trim();
463
+ if (!t || t[0] !== "{") continue;
464
+ let ev; try { ev = JSON.parse(t); } catch { continue; }
465
+ if (ev.type === "assistant" && Array.isArray(ev.message?.content)) {
466
+ for (const c of ev.message.content) if (c.type === "text" && c.text) assistant.push(String(c.text));
467
+ } else if (ev.type === "result") {
468
+ if (typeof ev.result === "string") text = ev.result;
469
+ const u = ev.usage || {};
470
+ usage = {
471
+ input: Number(u.input_tokens) || 0,
472
+ output: Number(u.output_tokens) || 0,
473
+ reasoning: 0,
474
+ cacheRead: Number(u.cache_read_input_tokens) || 0,
475
+ cacheWrite: Number(u.cache_creation_input_tokens) || 0,
476
+ cost: Number(ev.total_cost_usd) || 0,
477
+ };
478
+ }
479
+ }
480
+ if (!text && assistant.length) text = assistant.join("\n");
481
+ return { text: String(text).trim(), error: "", usage };
482
+ }
483
+
484
+ // Tek bir claude stream-json satirini canli terminal icin okunabilir metne cevirir. Assistant
485
+ // metnini oldugu gibi, tool cagrilarini kisa bir eylem satiri olarak gosterir; sistem/sonuc/tool
486
+ // -sonuc olaylarini atlar. Cevrilecek bir sey yoksa "" doner (o satir icin cikti yayimlanmaz).
487
+ function claudeToolBrief(c) {
488
+ const i = c.input || {};
489
+ const p = i.file_path || i.path || i.command || i.pattern || i.url || i.prompt || "";
490
+ const s = String(p).replace(/\s+/g, " ").trim();
491
+ return s ? ` ${s.length > 60 ? s.slice(0, 60) + "…" : s}` : "";
492
+ }
493
+ function claudeEventText(line) {
494
+ const t = String(line).trim();
495
+ if (!t || t[0] !== "{") return "";
496
+ let ev; try { ev = JSON.parse(t); } catch { return ""; }
497
+ if (ev.type === "assistant" && Array.isArray(ev.message?.content)) {
498
+ const out = [];
499
+ for (const c of ev.message.content) {
500
+ if (c.type === "text" && c.text) out.push(String(c.text));
501
+ else if (c.type === "tool_use") out.push(`\n ⏵ ${c.name}${claudeToolBrief(c)}`);
502
+ }
503
+ return out.join("");
504
+ }
505
+ if (ev.type === "result" && ev.is_error) return `\n✕ ${String(ev.result || ev.subtype || "hata")}`;
506
+ return "";
507
+ }
508
+ // Rate-limit olayi her adimda tekrar gelir; canli terminale her seferinde basmak gurultu olur.
509
+ // runCli bunu KOSMA BASINA BIR KEZ, net bir uyari log'una cevirir. "allowed" (sorun yok) atlanir;
510
+ // "allowed_warning" (kotaya yaklasiliyor) ve daha kotusu bir kez bildirilir.
511
+ function claudeRateLimitWarning(line) {
512
+ let ev; try { ev = JSON.parse(String(line).trim()); } catch { return ""; }
513
+ const r = ev.rate_limit_info || {};
514
+ if (!r.status || r.status === "allowed") return "";
515
+ const pct = Number.isFinite(+r.utilization) ? ` (~%${Math.round(r.utilization * 100)})` : "";
516
+ const win = r.rateLimitType ? ` [${r.rateLimitType}]` : "";
517
+ return `Claude kullanim limitine yaklasiliyor${win}${pct}; is kesintisiz devam ediyor.`;
518
+ }
519
+
520
+ function normalizeCliOutput(agent, stdout) {
521
+ if (agent.adapter === "claude" && (agent.args || []).includes("stream-json")) {
522
+ return parseClaudeStream(stdout);
523
+ }
524
+ if (agent.adapter !== "opencode" || !(agent.args || []).includes("json")) {
525
+ return { text: String(stdout || "").trim(), error: "", usage: null };
526
+ }
527
+ const texts = [], errors = [];
528
+ let usage = null;
529
+ for (const line of String(stdout || "").split(/\r?\n/)) {
530
+ if (!line.trim()) continue;
531
+ try {
532
+ const event = JSON.parse(line);
533
+ if (event.type === "text" && event.part?.text) texts.push(String(event.part.text));
534
+ if (event.type === "error") errors.push(String(event.error?.message || event.error || "OpenCode model/provider hatasi"));
535
+ if (event.type === "step_finish") {
536
+ const step = readStepUsage(event);
537
+ if (step) usage = addUsage(usage, step);
538
+ }
539
+ } catch {}
540
+ }
541
+ return { text: texts.join("\n").trim(), error: errors.join("\n").trim(), usage };
542
+ }
543
+
544
+ // Metindeki DENGELI suslu parantezli en dis JSON nesnesi adaylarini cikarir. String icindeki
545
+ // parantez/kacis karakterleri sayilmaz; boylece "final" metninde gecen bir { veya } eski
546
+ // "ilk-{ .. son-}" kestirmesini bozmaz. Ayrica model aciklama + nesne + ek aciklama yazdiginda
547
+ // (ya da birden fazla nesne dondurdugunde) her nesne ayri aday olur.
548
+ function jsonObjectCandidates(text) {
549
+ const src = String(text);
550
+ const found = [];
551
+ const stack = [];
552
+ let inString = false, escaped = false;
553
+ for (let i = 0; i < src.length; i++) {
554
+ const ch = src[i];
555
+ if (inString) {
556
+ if (escaped) escaped = false;
557
+ else if (ch === "\\") escaped = true;
558
+ else if (ch === '"') inString = false;
559
+ continue;
560
+ }
561
+ if (ch === '"') inString = true;
562
+ else if (ch === "{") stack.push(i);
563
+ else if (ch === "}" && stack.length) {
564
+ const start = stack.pop();
565
+ if (!stack.length) found.push(src.slice(start, i + 1));
566
+ }
567
+ }
568
+ return found;
569
+ }
570
+
571
+ // Model bazen JSON'u tek tirnak, sondaki virgul veya akilli tirnakla yazar. Kati parse
572
+ // basarisiz oldugunda son care olarak bu yaygin sapmalari onarip bir kez daha dener.
573
+ function repairJsonText(candidate) {
574
+ return String(candidate)
575
+ .replace(/[“”]/g, '"')
576
+ .replace(/[‘’]/g, "'")
577
+ .replace(/,\s*([}\]])/g, "$1");
578
+ }
579
+
580
+ const PROTOCOL_KEYS = /"(status|assignments|summary|completionCriteria|final)"\s*:/;
581
+
582
+ function parseJson(text, label) {
583
+ const raw = String(text);
584
+ const candidates = [];
585
+ for (const fence of raw.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(fence[1]);
586
+ candidates.push(raw.trim());
587
+ // Protokol anahtari iceren nesneler once denenir; ayni onem sirasinda SONDAKI nesne kazanir
588
+ // (modeller once dusunce/aciklama, en sona nihai protokol nesnesini yazar).
589
+ const objects = jsonObjectCandidates(raw).reverse();
590
+ candidates.push(...objects.filter((o) => PROTOCOL_KEYS.test(o)), ...objects.filter((o) => !PROTOCOL_KEYS.test(o)));
591
+ for (const candidate of candidates) {
592
+ for (const attempt of [candidate, repairJsonText(candidate)]) {
593
+ try {
594
+ const parsed = JSON.parse(attempt);
595
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
596
+ } catch {}
597
+ }
598
+ }
599
+ const error = new Error(`${label} gecerli JSON dondurmedi.`);
600
+ error.rawText = raw;
601
+ throw error;
602
+ }
603
+
604
+ // Protokol hatasinda operatorun ham metnini kullaniciya gosterilebilir bir cevaba cevirir.
605
+ // Yarim kalmis/kirik JSON parcalarini ve kod bloklarini atar; geriye anlamli duz metin
606
+ // kalmiyorsa null doner (o zaman normal hata akisi calisir).
607
+ function conversationalAnswer(error) {
608
+ if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) return null;
609
+ const raw = String(error.rawText || "");
610
+ if (!raw.trim()) return null;
611
+ const stripped = raw
612
+ .replace(/```[\s\S]*?```/g, " ")
613
+ .replace(/\{[\s\S]*\}/g, " ")
614
+ .replace(/[\s\S]*?^\s*\{[\s\S]*$/m, (match) => match.split("{")[0])
615
+ .replace(/\n{3,}/g, "\n\n")
616
+ .trim();
617
+ // Cok kisa artiklar (or. tek noktalama) cevap sayilmaz.
618
+ return stripped.length >= 8 ? stripped : null;
619
+ }
620
+
621
+ function capabilitiesFor(agent) {
622
+ // Rol dosyasi bir uzmanlik dayatiyorsa yetenekler DOGRUDAN rolden turer. Aksi halde
623
+ // config'teki eski/genis capabilities listesi (or. planner'da "implementation") katalogda
624
+ // allowedKinds ile celisir ve operatore yaniltici sinyal verirdi. Rol yoksa config'e duseriz.
625
+ const role = String(agent.roleFile || "").toLowerCase();
626
+ if (role.includes("executor")) return new Set(["implementation", "debugging", "testing"]);
627
+ if (role.includes("review")) return new Set(["review", "testing", "analysis"]);
628
+ if (role.includes("planner")) return new Set(["planning", "analysis"]);
629
+ if (role.includes("operator")) return new Set(["planning", "delegation"]);
630
+ return new Set(Array.isArray(agent.capabilities) ? agent.capabilities.map((x) => String(x).toLowerCase()) : []);
631
+ }
632
+
633
+ function inferAssignmentKind(raw, instruction) {
634
+ const explicit = String(raw.kind || "").toLowerCase();
635
+ if (["implement", "review", "research", "plan"].includes(explicit)) return explicit;
636
+ 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";
637
+ if (/\b(incele|denetle|dogrula|doğrula|review|audit|verify|test et)\b/i.test(instruction)) return "review";
638
+ if (/\b(arastir|araştır|research|web)\b/i.test(instruction)) return "research";
639
+ if (/\b(planla|plan|tasarla|design)\b/i.test(instruction)) return "plan";
640
+ return "implement";
641
+ }
642
+
643
+ // Gorev metni gercek bir yapim/degisiklik isi mi? Operator, plan evresinde "bilgi sorusu
644
+ // kestirmesi" ile (delegasyon acmadan complete) yaniti dogrudan verebilir. Ama prompt acikca
645
+ // bir uygulama/olusturma istiyorsa bu kestirme kullaniciya HIC is uretmeden sahte basari doner
646
+ // (or. proje hafizasindaki gecmis teslimati "zaten yapildi" sanmak). Bu durumda kestirmeyi
647
+ // engelleyip yeniden planlama isteriz. inferAssignmentKind'in implement fiil kumesiyle uyumlu.
648
+ function taskRequiresDelegation(prompt) {
649
+ 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 || ""));
650
+ }
651
+
652
+ function roleAllowedKinds(agent) {
653
+ const role = path.basename(String(agent?.roleFile || "")).toLowerCase();
654
+ if (role.includes("executor")) return ["implement"];
655
+ if (role.includes("review")) return ["review"];
656
+ if (role.includes("planner")) return ["plan"];
657
+ return null;
658
+ }
659
+
660
+ function supportsKind(agent, kind) {
661
+ const roleKinds = roleAllowedKinds(agent);
662
+ if (roleKinds) return roleKinds.includes(kind);
663
+ const caps = capabilitiesFor(agent);
664
+ const wanted = {
665
+ implement: ["implementation", "coding", "development", "debugging"],
666
+ review: ["review", "testing", "security", "analysis"],
667
+ research: ["research", "web", "analysis"],
668
+ plan: ["planning", "analysis", "architecture"],
669
+ }[kind] || [];
670
+ return wanted.some((cap) => caps.has(cap));
671
+ }
672
+
673
+ function agentUsable(agent) {
674
+ return agent?.health?.status ? agent.health.status === "ready" : true;
675
+ }
676
+
677
+ function normalizeAssignments(value, cfg, operatorName, usedIds, taskText = "") {
678
+ if (!Array.isArray(value)) throw new Error("Operator assignments dizisi dondurmedi.");
679
+ const max = Math.max(1, cfg.operator?.maxDelegationsPerRound || 8);
680
+ // Operator daha once kullanilan bir kimligi yinelerse gorevi oldurmek yerine kimligi
681
+ // otomatik yeniden adlandiririz; saatlerce uretilmis teslimat bir ID cakismasi yuzunden
682
+ // basarisiz sayilmamalidir. Ayni turdaki dependsOn referanslari da yeni ada tasinir.
683
+ const renames = new Map();
684
+ return value.slice(0, max).map((raw, index) => {
685
+ let id = String(raw.id || `task-${Date.now()}-${index + 1}`).replace(/[^a-zA-Z0-9_.-]/g, "-");
686
+ let renamedFrom;
687
+ if (usedIds.has(id)) {
688
+ let n = 2;
689
+ while (usedIds.has(`${id}-r${n}`)) n++;
690
+ renamedFrom = id;
691
+ renames.set(id, `${id}-r${n}`);
692
+ id = `${id}-r${n}`;
693
+ }
694
+ usedIds.add(id);
695
+ const requestedAgent = String(raw.agent || "");
696
+ if (!cfg.agents[requestedAgent]) throw new Error(`Operator tanimsiz agent secti: ${requestedAgent}`);
697
+ if (cfg.agents[requestedAgent].enabled === false) throw new Error(`Operator devre disi agent secti: ${requestedAgent}`);
698
+ if (!agentUsable(cfg.agents[requestedAgent])) throw new Error(`Operator kullanilamaz agent secti: ${requestedAgent} (${cfg.agents[requestedAgent].health?.label || "saglik testi basarisiz"})`);
699
+ if (requestedAgent === operatorName) throw new Error("Operator kendisine uzman gorevi atayamaz.");
700
+ const instruction = String(raw.instruction || raw.task || "").trim();
701
+ if (!instruction) throw new Error(`${id} delegasyonunda instruction eksik.`);
702
+ const kind = inferAssignmentKind(raw, instruction);
703
+ // Beceriler kullanici-kapilidir: operator yalnizca cfg.skills.enabled icindekileri iliştirebilir.
704
+ // Tanimsiz/etkin olmayan beceri sessizce dusurulur; asla gorevi olduren bir hata degildir.
705
+ const hasExplicitSkills = Object.prototype.hasOwnProperty.call(raw, "skills");
706
+ const autoMatchOn = cfg.skills?.autoMatch !== false;
707
+ const requestedSkills = Array.isArray(raw.skills)
708
+ ? raw.skills
709
+ : (!hasExplicitSkills && autoMatchOn
710
+ ? skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind)
711
+ : []);
712
+ let skills = skillRegistry.resolveForAssignment(requestedSkills, cfg).map((skill) => skill.name);
713
+ // Operator kodlama/planlama/review isinde beceri iliştirmeyi atlarsa (or. skills:[] dönerse),
714
+ // kullanicinin etkin becerilerinden goreve GERCEKTEN uyanlari otomatik ekle. suggest() yalnizca
715
+ // lexical eslesme oldugunda skill döner; alakasiz beceri asla iliştirilmez. Boylece "operator
716
+ // becerileri kullanmiyor" durumu ortadan kalkar.
717
+ if (!skills.length && autoMatchOn && ["implement", "plan", "review"].includes(kind)) {
718
+ skills = skillRegistry.resolveForAssignment(
719
+ skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind), cfg
720
+ ).map((skill) => skill.name);
721
+ }
722
+ let agent = requestedAgent;
723
+ const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
724
+ if (unavailable.has(agent) || !supportsKind(cfg.agents[agent], kind)) {
725
+ const compatible = Object.entries(cfg.agents).find(([name, candidate]) => name !== operatorName && candidate.enabled !== false && agentUsable(candidate) && !unavailable.has(name) && supportsKind(candidate, kind));
726
+ if (compatible) agent = compatible[0];
727
+ else if (unavailable.has(agent)) throw new Error(`Operator bu oturumda kullanilamaz agent secti: ${requestedAgent}`);
728
+ }
729
+ return {
730
+ id, agent, kind, instruction, skills,
731
+ renamedFrom,
732
+ requestedAgent: agent === requestedAgent ? undefined : requestedAgent,
733
+ routingReason: agent === requestedAgent ? undefined : `${requestedAgent} ${unavailable.has(requestedAgent) ? "bu oturumda kullanilamaz" : `${kind} yetenegine sahip degil`}; ${agent} secildi.`,
734
+ dependsOn: Array.isArray(raw.dependsOn) ? raw.dependsOn.map((dep) => renames.get(String(dep)) || String(dep)) : [],
735
+ };
736
+ });
737
+ }
738
+
739
+ function compatibleAgentForKind(cfg, operatorName, kind) {
740
+ const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
741
+ return Object.entries(cfg.agents || {}).find(([name, agent]) =>
742
+ name !== operatorName && agent.enabled !== false && agentUsable(agent) &&
743
+ !unavailable.has(name) && supportsKind(agent, kind)
744
+ )?.[0] || "";
745
+ }
746
+
747
+ // Devretme adaylari. Karantina filtresi cagirana birakilir: sureli oldugu icin karar canli
748
+ // durumdan (isQuarantined) okunmalidir.
749
+ function compatibleAgentsForKind(cfg, operatorName, kind) {
750
+ return Object.entries(cfg.agents || {})
751
+ .filter(([name, agent]) =>
752
+ name !== operatorName && agent.enabled !== false && agentUsable(agent) && supportsKind(agent, kind))
753
+ .map(([name]) => name);
754
+ }
755
+
756
+ function nextAssignmentId(base, usedIds) {
757
+ let id = base;
758
+ let suffix = 2;
759
+ while (usedIds.has(id)) id = `${base}-${suffix++}`;
760
+ usedIds.add(id);
761
+ return id;
762
+ }
763
+
764
+ // Balanced bir uygulama gorevinde katalogda standart roller varsa ilk turu
765
+ // PLAN -> IMPLEMENT -> REVIEW olarak garanti eder. Operatorun bir rolu yanlislikla
766
+ // atlamasi, kullanicinin etkinlestirdigi uzmani sessizce devre disi birakamaz.
767
+ function ensureBalancedRoleChain(assignments, cfg, operatorName, task, usedIds) {
768
+ if (task.executionMode !== "balanced" || !assignments.some((item) => item.kind === "implement")) return assignments;
769
+ const chained = assignments.map((item) => ({ ...item, dependsOn: [...item.dependsOn] }));
770
+ let plan = chained.find((item) => item.kind === "plan");
771
+ if (!plan) {
772
+ const planner = compatibleAgentForKind(cfg, operatorName, "plan");
773
+ if (planner) {
774
+ 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.";
775
+ plan = {
776
+ id: nextAssignmentId("balanced-plan", usedIds),
777
+ agent: planner,
778
+ kind: "plan",
779
+ instruction,
780
+ dependsOn: [],
781
+ skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "plan"),
782
+ routingReason: "Balanced rol zinciri: kullanilabilir planner ilk tura otomatik eklendi.",
783
+ };
784
+ chained.unshift(plan);
785
+ }
786
+ }
787
+ const implementations = chained.filter((item) => item.kind === "implement");
788
+ if (plan) {
789
+ for (const implementation of implementations) {
790
+ if (!implementation.dependsOn.includes(plan.id)) implementation.dependsOn.push(plan.id);
791
+ }
792
+ }
793
+ let reviews = chained.filter((item) => item.kind === "review");
794
+ if (!reviews.length) {
795
+ const reviewer = compatibleAgentForKind(cfg, operatorName, "review");
796
+ if (reviewer) {
797
+ 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.";
798
+ const review = {
799
+ id: nextAssignmentId("balanced-review", usedIds),
800
+ agent: reviewer,
801
+ kind: "review",
802
+ instruction,
803
+ dependsOn: implementations.map((item) => item.id),
804
+ skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "review"),
805
+ routingReason: "Balanced rol zinciri: kullanilabilir reviewer ilk tura otomatik eklendi.",
806
+ };
807
+ chained.push(review);
808
+ reviews = [review];
809
+ }
810
+ }
811
+ for (const review of reviews) {
812
+ // Operator belirli bir uygulamayi incelemeye bagladiysa bu kapsami genisletme;
813
+ // ayni turdaki bagimsiz/yardimci bir implement hatasi ana incelemeyi bloke etmemeli.
814
+ if (!review.dependsOn.length) {
815
+ for (const implementation of implementations) review.dependsOn.push(implementation.id);
816
+ }
817
+ }
818
+ const rank = { plan: 0, research: 1, implement: 2, review: 3 };
819
+ return chained.map((item, index) => ({ item, index }))
820
+ .sort((a, b) => (rank[a.item.kind] ?? 2) - (rank[b.item.kind] ?? 2) || a.index - b.index)
821
+ .map(({ item }) => item);
822
+ }
823
+
824
+ class Engine extends EventEmitter {
825
+ constructor() {
826
+ super();
827
+ this.running = false;
828
+ this.busy = false;
829
+ this.current = null;
830
+ this.activeChild = null;
831
+ this.callSequence = 0;
832
+ // Kesin kimlik/kurulum hatasi veren agent'lari ayni motor oturumunda tekrar secme.
833
+ // Kalici config'i degistirmeyiz; kullanici kimlik bilgisini duzeltip motoru yeniden
834
+ // baslattiginda agent tekrar denenebilir.
835
+ this.unhealthyAgents = new Map();
836
+ this._retrySleepers = new Set();
837
+ // Canli kodlama akisi: gorev calisirken calisma klasorunu periyodik tarayan zamanlayici
838
+ // ve son yayinlanan degisiklik imzasi (ayni durum tekrar tekrar yayinlanmasin diye).
839
+ this._liveTimer = null;
840
+ this._lastFileChangeKey = "";
841
+ this._textBefore = null;
842
+ }
843
+
844
+ cfg() { return store.loadConfig(); }
845
+
846
+ status() {
847
+ const cfg = this.cfg();
848
+ return {
849
+ running: this.running,
850
+ busy: this.busy,
851
+ current: this.current,
852
+ mode: cfg.approvalMode,
853
+ defaultOperator: cfg.operator?.cli || "",
854
+ callsToday: store.getCallCount(),
855
+ budget: cfg.dailyCallBudget,
856
+ usageToday: store.getDailyUsage(),
857
+ };
858
+ }
859
+
860
+ // KULLANIM TELEMETRISI: gorev suresince agent bazinda token/maliyet toplar. Tamamen
861
+ // additive ve hata toleranslidir — veri veremeyen CLI'lar (metin modunda calisan
862
+ // codex/claude/gemini) icin sayaclar bos kalir, hicbir akis bundan etkilenmez.
863
+ resetUsage() {
864
+ this._usage = { total: { ...EMPTY_USAGE }, byAgent: {}, calls: 0, reportingCalls: 0 };
865
+ }
866
+
867
+ recordUsage(agentName, usage) {
868
+ if (!this._usage) this.resetUsage();
869
+ this._usage.total = addUsage(this._usage.total, usage);
870
+ this._usage.byAgent[agentName] = addUsage(this._usage.byAgent[agentName], usage);
871
+ this._usage.reportingCalls++;
872
+ try { store.addDailyUsage(usage); } catch {}
873
+ }
874
+
875
+ // Gorev kapanisinda saklanacak sekil. Hicbir cagri veri vermediyse null doner ki arayuz
876
+ // "0 token" gibi yaniltici bir sayi yerine "veri yok" gosterebilsin.
877
+ usageSummary() {
878
+ const usage = this._usage;
879
+ if (!usage || !usage.reportingCalls) return null;
880
+ return {
881
+ total: usage.total,
882
+ byAgent: usage.byAgent,
883
+ calls: usage.calls,
884
+ reportingCalls: usage.reportingCalls,
885
+ };
886
+ }
887
+
888
+ publish(type, data, taskId = this.current?.id) {
889
+ const event = { at: new Date().toISOString(), taskId: taskId || null, ...data, type };
890
+ if (taskId) store.appendRunEvent(taskId, event);
891
+ this.emit(type, event);
892
+ return event;
893
+ }
894
+
895
+ // CANLI KODLAMA: gorev calisirken calisma klasorunu _snapBefore'a gore tarar ve olusan/
896
+ // degisen/silinen dosyalari "filechange" olayiyla yayinlar; complete()'teki nihai diff'in
897
+ // canli on-izlemesidir. Yalnizca durum degistiginde olay basar (ayni diff tekrarlanmaz).
898
+ // Tamamen additive ve salt-okunurdur; hata verirse gorevi etkilemeden sessizce atlar.
899
+ publishFileChanges(taskId = this.current?.id) {
900
+ if (!taskId || !this._cwd || !this._snapBefore) return null;
901
+ let changes, currentSnapshot;
902
+ try {
903
+ currentSnapshot = snapshotDir(this._cwd);
904
+ changes = diffSnapshots(this._snapBefore, currentSnapshot);
905
+ }
906
+ catch { return null; }
907
+ // Yalnizca dosya adlarini degil guncel stamp'i da imzaya kat. Ayni dosya gorev
908
+ // sirasinda ikinci kez degistiginde yeni satir diff'i mutlaka yayinlanmalidir.
909
+ const stampKey = [...changes.created, ...changes.modified]
910
+ .map((file) => `${file}:${currentSnapshot.get(file) || ""}`).join("|");
911
+ const key = `${stampKey}#${changes.deleted.join("|")}`;
912
+ if (key === this._lastFileChangeKey) return null;
913
+ const previouslyHadChanges = this._lastFileChangeKey !== "" && this._lastFileChangeKey !== "#";
914
+ const changedFiles = [
915
+ ...changes.created.map((file) => ({ file, action: "created" })),
916
+ ...changes.modified.map((file) => ({ file, action: "modified" })),
917
+ ...changes.deleted.map((file) => ({ file, action: "deleted" })),
918
+ ];
919
+ let remainingLines = DIFF_MAX_EVENT_LINES;
920
+ const files = changedFiles.map(({ file, action }) => {
921
+ if (remainingLines <= 0) return { path: file, action, additions: null, deletions: null, previewStatus: "event-limit", hunks: [] };
922
+ const described = describeFileDiff(this._cwd, file, action, this._textBefore, { maxLines: Math.min(DIFF_MAX_RENDERED_LINES, remainingLines) });
923
+ remainingLines -= (described.hunks || []).reduce((sum, hunk) => sum + (hunk.lines || []).length, 0);
924
+ return described;
925
+ });
926
+ // Bos degisim setini (or. baslangictaki taban veya tum degisikliklerin geri alinmasi)
927
+ // imzasini hatirla. Gorev basinda gurultu uretme; ancak daha once gorunen tum degisiklikler
928
+ // geri alindiysa bos olayi yayinla ki dashboard bayat diff'i temizlesin.
929
+ this._lastFileChangeKey = key;
930
+ if (!files.length && !previouslyHadChanges) return null;
931
+ return this.publish("filechange", {
932
+ files,
933
+ counts: { created: changes.created.length, modified: changes.modified.length, deleted: changes.deleted.length },
934
+ lineCounts: {
935
+ additions: files.reduce((sum, file) => sum + (Number(file.additions) || 0), 0),
936
+ deletions: files.reduce((sum, file) => sum + (Number(file.deletions) || 0), 0),
937
+ unavailable: files.filter((file) => file.additions == null || file.deletions == null).length,
938
+ },
939
+ }, taskId);
940
+ }
941
+
942
+ startLiveDiff(task) {
943
+ this.stopLiveDiff();
944
+ this._lastFileChangeKey = "";
945
+ // liveDiff kesin olarak false yapilmadikca aciktir (mevcut davranisi degistirmeyen additive
946
+ // ozellik). Tarama araligi config'ten ayarlanabilir; en dusuk 500ms ile sinirli.
947
+ const cfg = this.cfg();
948
+ if (cfg.liveDiff === false) { this._textBefore = null; return; }
949
+ this._textBefore = captureTextSnapshot(this._cwd, this._snapBefore);
950
+ const intervalMs = Math.max(500, Number(cfg.liveDiffIntervalMs) || 2500);
951
+ this._liveTimer = setInterval(() => this.publishFileChanges(task.id), intervalMs);
952
+ // unref: bekleyen bir tarama zamanlayicisi surecin kapanmasini asla engellemesin.
953
+ if (this._liveTimer && typeof this._liveTimer.unref === "function") this._liveTimer.unref();
954
+ }
955
+
956
+ stopLiveDiff() {
957
+ if (this._liveTimer) { clearInterval(this._liveTimer); this._liveTimer = null; }
958
+ }
959
+
960
+ setMode(mode) {
961
+ const cfg = this.cfg();
962
+ cfg.approvalMode = mode === "auto" ? "auto" : "ask";
963
+ store.saveConfig(cfg);
964
+ this.emit("status", this.status());
965
+ }
966
+
967
+ start() {
968
+ if (this.running) return;
969
+ if (!this.cfg().autonomousConsentAcceptedAt) {
970
+ const error = new Error("Otonom CLI calistirma kosullari henuz kabul edilmedi. Dashboard'daki ilk kullanim uyarisini onaylayin.");
971
+ error.code = "AUTONOMOUS_CONSENT_REQUIRED";
972
+ throw error;
973
+ }
974
+ this.running = true;
975
+ this.publish("log", { level: "info", msg: `Motor basladi. Mod=${this.cfg().approvalMode}` }, null);
976
+ this.emit("status", this.status());
977
+ this.loop();
978
+ }
979
+
980
+ stop() {
981
+ this.running = false;
982
+ this.wake();
983
+ this.cancelRetrySleeps();
984
+ this.stopLiveDiff();
985
+ if (this.activeChild) {
986
+ try { this.activeChild.kill(); } catch {}
987
+ }
988
+ this.publish("log", { level: "warn", msg: "Motor durduruluyor; aktif CLI islemi sonlandirildi." });
989
+ this.emit("status", this.status());
990
+ }
991
+
992
+ wake() {
993
+ if (this._wakeResolve) {
994
+ clearTimeout(this._wakeTimer);
995
+ const resolve = this._wakeResolve;
996
+ this._wakeResolve = null;
997
+ resolve();
998
+ }
999
+ }
1000
+
1001
+ sleepWake(ms) {
1002
+ return new Promise((resolve) => {
1003
+ this._wakeResolve = resolve;
1004
+ this._wakeTimer = setTimeout(() => { this._wakeResolve = null; resolve(); }, ms);
1005
+ });
1006
+ }
1007
+
1008
+ // sleepWake'ten AYRIDIR: o bos donguye aittir ve yeni gorev eklendiginde uyanir. Bu bekleme
1009
+ // yalnizca motor durdurulunca kesilmeli.
1010
+ sleepRetry(ms) {
1011
+ return new Promise((resolve) => {
1012
+ const sleeper = { resolve, timer: null };
1013
+ sleeper.timer = setTimeout(() => { this._retrySleepers.delete(sleeper); resolve(); }, ms);
1014
+ this._retrySleepers.add(sleeper);
1015
+ });
1016
+ }
1017
+
1018
+ cancelRetrySleeps() {
1019
+ for (const sleeper of this._retrySleepers) {
1020
+ clearTimeout(sleeper.timer);
1021
+ sleeper.resolve();
1022
+ }
1023
+ this._retrySleepers.clear();
1024
+ }
1025
+
1026
+ // Sureli karantina: kullanici oturum acar veya saglayici toparlanirsa agent kendiliginden
1027
+ // doner. Tekrar duserse ceza katlanir (en fazla 15 dk); suresi 0 olanlar kalici kalir.
1028
+ quarantineAgent(name, code, cfg) {
1029
+ const strikes = (this.unhealthyAgents.get(name)?.strikes || 0) + 1;
1030
+ const baseSeconds = QUARANTINE_COOLDOWN_SECONDS[code] ?? 120;
1031
+ const cooldownMs = baseSeconds > 0 ? Math.min(900, baseSeconds * strikes) * 1000 : 0;
1032
+ this.unhealthyAgents.set(name, {
1033
+ code, strikes, at: new Date().toISOString(),
1034
+ until: cooldownMs ? Date.now() + cooldownMs : 0,
1035
+ });
1036
+ if (cfg) cfg.runtimeUnavailableAgents = this.quarantinedAgents();
1037
+ this.publish("log", {
1038
+ level: "warn",
1039
+ msg: cooldownMs
1040
+ ? `${name} [${code}] nedeniyle ${Math.round(cooldownMs / 1000)} sn karantinada; sure dolunca otomatik yeniden denenecek.`
1041
+ : `${name} [${code}] nedeniyle bu oturumda kullanim disi (beklemekle duzelmez).`,
1042
+ });
1043
+ }
1044
+
1045
+ isQuarantined(name) {
1046
+ const entry = this.unhealthyAgents.get(name);
1047
+ if (!entry) return false;
1048
+ if (entry.until && Date.now() >= entry.until) {
1049
+ this.unhealthyAgents.delete(name);
1050
+ this.publish("log", { level: "info", msg: `${name} karantina suresi doldu; yeniden deneme hakki verildi.` });
1051
+ return false;
1052
+ }
1053
+ return true;
1054
+ }
1055
+
1056
+ quarantinedAgents() {
1057
+ return [...this.unhealthyAgents.keys()].filter((name) => this.isQuarantined(name));
1058
+ }
1059
+
1060
+ async loop() {
1061
+ while (this.running) {
1062
+ const task = store.nextPending();
1063
+ if (!task) {
1064
+ await this.sleepWake((this.cfg().pollSeconds || 15) * 1000);
1065
+ continue;
1066
+ }
1067
+ try {
1068
+ await this.runTask(task);
1069
+ } catch (error) {
1070
+ if (!(await this.salvage(task, error))) {
1071
+ const found = store.findTask(task.id);
1072
+ const failure = classifyCliError(error);
1073
+ task.status = "failed";
1074
+ task.error = error.message;
1075
+ // Siniflandirilmis teshis gorevle birlikte saklanir; arayuz "ne oldu / ne yapmali"
1076
+ // ayrimini ham hata metnini kirparak tahmin etmek yerine buradan okur.
1077
+ task.failure = failure;
1078
+ // Basarisiz gorev de para harcar; telemetri burada da saklanmali.
1079
+ task.usage = this.usageSummary();
1080
+ task.finishedAt = new Date().toISOString();
1081
+ if (found) store.moveTask(found.state, "failed", task);
1082
+ this.publish("log", { level: "error", msg: `HATA (${failure.code}): ${failure.summary}` }, task.id);
1083
+ this.publish("log", { level: "warn", msg: `Ne yapmali: ${failure.action}` }, task.id);
1084
+ if (task.kind === "operator-chat") {
1085
+ this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: task.parentTaskId, status: "failed", error: failure.summary, failure }, task.id);
1086
+ }
1087
+ this.notifyOutcome(task, "failed", { error: failure.summary, failureCode: failure.code, action: failure.action });
1088
+ this.emit("queue");
1089
+ }
1090
+ } finally {
1091
+ this.stopLiveDiff();
1092
+ this._textBefore = null;
1093
+ this.busy = false;
1094
+ this.current = null;
1095
+ this.activeChild = null;
1096
+ this.emit("status", this.status());
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ invokeAgent(agentName, prompt, cfg, meta = {}) {
1102
+ const configuredAgent = cfg.agents[agentName];
1103
+ const agent = configuredAgent && cliRegistry.effectiveAgent(configuredAgent, cfg);
1104
+ if (!agent) return Promise.reject(new Error(`Agent tanimsiz: ${agentName}`));
1105
+ return this.runCli(agentName, agent, prompt, cfg, meta);
1106
+ }
1107
+
1108
+ // Operatoru, uzman agent'lardan BAGIMSIZ olarak dogrudan CLI'dan (claude/codex/gemini/opencode)
1109
+ // calistirir. operator.md rolunu bu CLI giyer.
1110
+ invokeOperator(cli, prompt, cfg, meta = {}) {
1111
+ const agent = cliRegistry.operatorSpec(cli, cfg);
1112
+ if (!agent) return Promise.reject(new Error(`Operator CLI tanimsiz veya kurulu degil: ${cli}`));
1113
+ return this.runCli(cli, agent, prompt, cfg, meta);
1114
+ }
1115
+
1116
+ runCli(displayName, agent, prompt, cfg, meta = {}) {
1117
+ return new Promise((resolve, rawReject) => {
1118
+ // Her hata hangi CLI'dan geldigini tasir; tavsiye metni buna gore uretilir. Etiket yokken
1119
+ // opencode hatasinda kullaniciya gemini komutu oneriliyordu. Bkz. classifyCliError.
1120
+ const reject = (cause) => {
1121
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1122
+ if (!error.agentName) error.agentName = displayName;
1123
+ if (!error.adapter) error.adapter = agent.adapter || cliRegistry.adapterId(agent.cmd);
1124
+ rawReject(error);
1125
+ };
1126
+ const count = store.bumpCallCount();
1127
+ if (count > (cfg.dailyCallBudget || Number.MAX_SAFE_INTEGER)) {
1128
+ return reject(new Error(`Gunluk cagri butcesi asildi (${cfg.dailyCallBudget}).`));
1129
+ }
1130
+ if (!this._usage) this.resetUsage();
1131
+ this._usage.calls++;
1132
+
1133
+ const callId = `${this.current?.id || "run"}-${++this.callSequence}`;
1134
+ let promptFile = "";
1135
+ let useStdin = true;
1136
+ const rawArgs = (agent.args || []).map((arg) => {
1137
+ if (String(arg).includes("{PROMPT}")) {
1138
+ useStdin = false;
1139
+ return String(arg).replaceAll("{PROMPT}", prompt);
1140
+ }
1141
+ if (String(arg).includes("{PROMPT_FILE}")) {
1142
+ useStdin = false;
1143
+ if (!promptFile) {
1144
+ const promptDir = path.join(store.ROOT, "state", "prompts");
1145
+ fs.mkdirSync(promptDir, { recursive: true });
1146
+ promptFile = path.join(promptDir, `${callId}.md`);
1147
+ fs.writeFileSync(promptFile, prompt, "utf8");
1148
+ }
1149
+ return String(arg).replaceAll("{PROMPT_FILE}", promptFile);
1150
+ }
1151
+ return String(arg);
1152
+ });
1153
+ const cwd = this._cwd || path.resolve(store.WORK_BASE, cfg.workingDir || ".");
1154
+ // Path-aware hapis: codex sandbox'i disariyi zaten OS-native kapatir (effectiveAgent).
1155
+ // Claude'un OS sandbox'i her platformda (ozellikle Windows'ta) olmadigi icin en azindan
1156
+ // orkestratorun kendi topragini (config/memory/kurulum dizini) permissions.deny ile gizler.
1157
+ // JSON'u arguman olarak gecmek Windows cmd.exe'de tirnaklari bozar; bu yuzden dosyaya yazip
1158
+ // yolunu veriyoruz (prompt dosyasiyla ayni desen).
1159
+ const sandboxSettings = claudeSandboxSettings(agent, cwd, cfg);
1160
+ if (sandboxSettings) {
1161
+ const sbDir = path.join(store.ROOT, "state", "prompts");
1162
+ fs.mkdirSync(sbDir, { recursive: true });
1163
+ const sbFile = path.join(sbDir, `${callId}.settings.json`);
1164
+ fs.writeFileSync(sbFile, JSON.stringify(sandboxSettings), "utf8");
1165
+ rawArgs.push("--settings", sbFile);
1166
+ }
1167
+ const command = cliRegistry.buildCommand(agent.cmd, rawArgs);
1168
+ const file = command.file;
1169
+ const args = command.args;
1170
+ const started = Date.now();
1171
+ let stdout = "", stderr = "", settled = false, timedOut = false, silenceTimedOut = false;
1172
+ let timer, silenceTimer, progressTimer;
1173
+ const base = { callId, agent: displayName, stage: meta.stage || "agent", assignmentId: meta.assignmentId || null };
1174
+
1175
+ this.current = { id: this.current?.id, stage: base.stage, agent: displayName, callId };
1176
+ this.emit("status", this.status());
1177
+ this.publish("activity", { ...base, kind: "process.started", cmd: agent.cmd, args: rawArgs, cwd });
1178
+
1179
+ // Motor onceliklidir: canli CLI saglik/model probe'lari (or. codex app-server / codex exec)
1180
+ // ayni CLI'yi ayni anda calistirirsa codex ikinci exec oturumunu SIGTERM ile keser. Bu yuzden
1181
+ // operatör/agent kosumuna girmeden uctaki tum probe'lari sonlandiriyoruz.
1182
+ cliRegistry.abortActiveProbes();
1183
+ let child;
1184
+ try { child = spawn(file, args, { cwd, env: cliRegistry.agentEnvironment(agent), windowsHide: true, shell: command.shell, windowsVerbatimArguments: !!command.verbatim }); }
1185
+ catch (error) { return reject(error); }
1186
+ this.activeChild = child;
1187
+ child.stdout.setEncoding("utf8");
1188
+ child.stderr.setEncoding("utf8");
1189
+
1190
+ // BOYUT FAKTORU: buyuk gorevlerde model daha uzun sure sessiz kalabilir (dev spec'i okuma +
1191
+ // buyuk diff uretme). Sabit sessizlik penceresi bu durumda SAGLIKLI bir kosmayi SIGKILL'liyor
1192
+ // ("opencode terminated"). Orijinal gorev boyutunu (this._taskChars, taskBrief ile ayni esik)
1193
+ // vekil alip sessizlik VE sert timeout'u 1x..3x arasi kirpilmis bir katsayiyla acariz. Tavan
1194
+ // 3x: sonsuz asilmis gercek bir takilma yine yakalanir. Girdi kucukse (<=budget) katsayi 1x,
1195
+ // yani mevcut davranis ve testler AYNEN korunur. Not: gercek byte akisi (stdout/stderr) zaten
1196
+ // sessizlik sayacini sifirliyor (liveness); katsayi yalnizca HIC cikti gelmeyen sessiz dusunme
1197
+ // penceresini genisletir. Kendi ilerleme kalp atisimizi liveness saymayiz — o, takilmayi gizlerdi.
1198
+ const taskChars = Number(this._taskChars) || String(prompt || "").length;
1199
+ const promptBudget = Math.max(1200, Number(cfg.taskPromptCharBudget) || 6000);
1200
+ const sizeFactor = Math.min(3, Math.max(1, taskChars / promptBudget));
1201
+ const timeoutMs = Math.round(Math.max(10, agent.timeoutSeconds || cfg.agentTimeoutSeconds || 900) * 1000 * sizeFactor);
1202
+ const silenceTimeoutMs = Math.round(Math.max(1, agent.silenceTimeoutSeconds || cfg.cliSilenceTimeoutSeconds || 300) * 1000 * sizeFactor);
1203
+ if (sizeFactor > 1) this.publish("log", { level: "info", msg: `Buyuk gorev (${taskChars} karakter): zaman asimi ${sizeFactor.toFixed(1)}x acildi (sessizlik ${Math.round(silenceTimeoutMs / 1000)}s / sert ${Math.round(timeoutMs / 1000)}s).` }, this.current?.id);
1204
+ let lastOutputAt = Date.now();
1205
+ const terminateTree = () => {
1206
+ if (isWin && child.pid) {
1207
+ try { spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" }); } catch {}
1208
+ } else {
1209
+ try { child.kill("SIGKILL"); } catch {}
1210
+ }
1211
+ };
1212
+ const armSilenceTimer = () => {
1213
+ clearTimeout(silenceTimer);
1214
+ silenceTimer = setTimeout(() => {
1215
+ if (settled) return;
1216
+ silenceTimedOut = true;
1217
+ this.publish("activity", { ...base, kind: "process.silence-timeout", silenceTimeoutMs, elapsedMs: Date.now() - started });
1218
+ terminateTree();
1219
+ }, silenceTimeoutMs);
1220
+ };
1221
+ armSilenceTimer();
1222
+ progressTimer = setInterval(() => {
1223
+ if (settled) return;
1224
+ this.publish("activity", { ...base, kind: "process.progress", elapsedMs: Date.now() - started, silentMs: Date.now() - lastOutputAt, silenceTimeoutMs });
1225
+ }, 15000);
1226
+ timer = setTimeout(() => {
1227
+ if (settled) return;
1228
+ timedOut = true;
1229
+ this.publish("activity", { ...base, kind: "process.timeout", timeoutMs });
1230
+ terminateTree();
1231
+ }, timeoutMs);
1232
+
1233
+ // Claude stream-json ham JSON akitir; canli "CLI calisma kaydi" terminaline JSON dokmek
1234
+ // yerine her tam satiri OKUNABILIR metne cevirir (assistant metni + tool eylemleri). Boylece
1235
+ // gorunum eski 'text' modu gibi okunur kalir; ham JSON yine `stdout`da birikip sonda
1236
+ // normalizeCliOutput ile ayiklanir. Silence timer her veri parcasinda zaten sifirlanir.
1237
+ const isClaudeStream = agent.adapter === "claude" && (agent.args || []).includes("stream-json");
1238
+ let claudeLineBuf = "";
1239
+ child.stdout.on("data", (data) => {
1240
+ const text = String(data);
1241
+ stdout += text;
1242
+ lastOutputAt = Date.now();
1243
+ armSilenceTimer();
1244
+ if (isClaudeStream) {
1245
+ claudeLineBuf += text;
1246
+ let nl;
1247
+ while ((nl = claudeLineBuf.indexOf("\n")) >= 0) {
1248
+ const line = claudeLineBuf.slice(0, nl);
1249
+ claudeLineBuf = claudeLineBuf.slice(nl + 1);
1250
+ const disp = claudeEventText(line);
1251
+ if (disp) this.publish("activity", { ...base, kind: "stdout", text: disp });
1252
+ }
1253
+ } else {
1254
+ this.publish("activity", { ...base, kind: "stdout", text });
1255
+ }
1256
+ });
1257
+ child.stderr.on("data", (data) => {
1258
+ const text = String(data);
1259
+ stderr += text;
1260
+ lastOutputAt = Date.now();
1261
+ armSilenceTimer();
1262
+ this.publish("activity", { ...base, kind: "stderr", text });
1263
+ });
1264
+ child.on("error", (error) => {
1265
+ if (settled) return;
1266
+ settled = true;
1267
+ clearTimeout(timer);
1268
+ clearTimeout(silenceTimer);
1269
+ clearInterval(progressTimer);
1270
+ this.activeChild = null;
1271
+ if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1272
+ this.publish("activity", { ...base, kind: "process.failed", error: error.message, durationMs: Date.now() - started });
1273
+ reject(error);
1274
+ });
1275
+ child.on("close", (code, signal) => {
1276
+ if (settled) return;
1277
+ settled = true;
1278
+ clearTimeout(timer);
1279
+ clearTimeout(silenceTimer);
1280
+ clearInterval(progressTimer);
1281
+ this.activeChild = null;
1282
+ if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
1283
+ const durationMs = Date.now() - started;
1284
+ // Cikti tek kez ayristirilir; hem kullanim telemetrisi hem metin ayni sonuctan okunur.
1285
+ const normalized = normalizeCliOutput(agent, stdout);
1286
+ if (normalized.usage) this.recordUsage(displayName, normalized.usage);
1287
+ this.publish("activity", { ...base, kind: "process.finished", exitCode: code, signal, durationMs, usage: normalized.usage, reason: silenceTimedOut ? "silence-timeout" : (timedOut ? "timeout" : null) });
1288
+ if (silenceTimedOut) return reject(new Error(`CLI_STALLED: ${agent.cmd} ${Math.round(silenceTimeoutMs / 1000)} saniye boyunca cikti uretmedi ve otomatik durduruldu.`));
1289
+ if (timedOut) return reject(new Error(`${agent.cmd} ${Math.round(timeoutMs / 1000)} saniyede zaman asimina ugradi${signal ? ` (signal=${signal})` : ""}. ${clip(stdout || stderr, 500)}`));
1290
+ // Teshis icin HER IKI akis da gerekir: bazi CLI'lar (Gemini dahil) kota/oturum hatasini
1291
+ // stdout'a, yigin izini stderr'e yazar. Yalnizca birini almak siniflandiriciyi kor birakiyordu.
1292
+ if (code !== 0) {
1293
+ const detail = [stderr, stdout].map((stream) => String(stream || "").trim()).filter(Boolean).join("\n");
1294
+ return reject(new Error(`${agent.cmd} cikis kodu ${code ?? "yok"}${signal ? ` (signal=${signal})` : ""}.\n${clip(detail, 2000)}`));
1295
+ }
1296
+ if (normalized.error) return reject(new Error(`OpenCode model/provider hatasi: ${clip(normalized.error, 500)}`));
1297
+ if (!normalized.text) return reject(new Error(`${agent.cmd} kullanilabilir cikti dondurmedi.${stderr ? ` ${clip(stderr, 300)}` : ""}`));
1298
+ resolve({ text: normalized.text, stderr: stderr.trim(), exitCode: code, durationMs, callId, usage: normalized.usage });
1299
+ });
1300
+
1301
+ // Prompt argumanla/dosyayla verilse bile stdin MUTLAKA kapatilmali. OpenCode gibi CLI'lar
1302
+ // stdin TTY degilse borudan mesaj okur; EOF gelmezse model cagrisina hic gecmeden bloke olur.
1303
+ child.stdin.on("error", () => {});
1304
+ child.stdin.end(useStdin ? prompt : "");
1305
+ });
1306
+ }
1307
+
1308
+ // Katalogda dosya yazabilen (implement) kullanilabilir bir uzman var mi? Operatorun kendisi ve
1309
+ // bu oturumda karantinaya alinan agentler haric. Tek executor (or. opencode) sessizlik nedeniyle
1310
+ // dustugunde takim fiilen dosya uretemez hale gelir; bu kontrol o durumu erken yakalar.
1311
+ hasUsableImplementAgent(cfg, operatorName) {
1312
+ return Object.entries(cfg.agents || {}).some(([name, agent]) =>
1313
+ name !== operatorName && agent.enabled !== false && agentUsable(agent)
1314
+ && !this.isQuarantined(name) && supportsKind(agent, "implement"));
1315
+ }
1316
+
1317
+ // Sureli karantinadaki implement agentinin donusune kalan sure (ms). Geri donen yoksa ya da
1318
+ // bekleme makul siniri asiyorsa null; cagiran bunu "gorevi durdur" olarak yorumlar.
1319
+ msUntilImplementAgentReturns(cfg, operatorName, maxWaitMs = 5 * 60 * 1000) {
1320
+ const now = Date.now();
1321
+ const returns = Object.entries(cfg.agents || {})
1322
+ .filter(([name, agent]) => name !== operatorName && agent.enabled !== false
1323
+ && agentUsable(agent) && supportsKind(agent, "implement"))
1324
+ .map(([name]) => this.unhealthyAgents.get(name)?.until || 0)
1325
+ .filter((until) => until > now)
1326
+ .sort((a, b) => a - b);
1327
+ if (!returns.length) return null;
1328
+ const waitMs = returns[0] - now;
1329
+ return waitMs <= maxWaitMs ? waitMs : null;
1330
+ }
1331
+
1332
+ agentCatalog(cfg, operatorName) {
1333
+ return Object.entries(cfg.agents)
1334
+ .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.isQuarantined(name))
1335
+ .map(([name, a]) => ({
1336
+ name,
1337
+ description: a.description || "",
1338
+ capabilities: [...capabilitiesFor(a)],
1339
+ allowedKinds: roleAllowedKinds(a) || ["implement", "review", "research", "plan"].filter((kind) => supportsKind(a, kind)),
1340
+ roleFile: a.roleFile || "",
1341
+ costTier: a.costTier || "standard",
1342
+ }));
1343
+ }
1344
+
1345
+ async invokeOperatorJson(operatorCli, prompt, cfg, stage, label) {
1346
+ const retries = Math.max(0, cfg.operator?.protocolRetries ?? 1);
1347
+ let lastError;
1348
+ let correction = "";
1349
+ for (let attempt = 0; attempt <= retries; attempt++) {
1350
+ try {
1351
+ const response = await this.invokeOperator(operatorCli, prompt + correction, cfg, { stage });
1352
+ return parseJson(response.text, label);
1353
+ } catch (error) {
1354
+ lastError = error;
1355
+ // Yalnizca gercek JSON protokol hatasi yeniden denenir. CLI'nin sessiz kalmasi,
1356
+ // auth/timeout veya proses hatasi ayni operatoru tekrar calistirmamalidir.
1357
+ if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) throw error;
1358
+ if (attempt >= retries) break;
1359
+ // Modelin ne dondurdugunu duzeltme istegine geri veriyoruz: "yalnizca JSON dondur"
1360
+ // talimatini tekrarlamak, hatayi goremeyen model icin cogu zaman ayni cevabi uretiyordu.
1361
+ correction = `\n\nONCEKI CEVAP PROTOKOLE UYMADI: ${error.message}\n` +
1362
+ `Senin onceki cevabin (kirpilmis):\n"""\n${clip(String(error.rawText || ""), 1200)}\n"""\n` +
1363
+ `Bu cevap gecerli bir JSON nesnesi degildi. Simdi SADECE tek bir JSON nesnesi dondur: ` +
1364
+ `ilk karakter { ve son karakter } olsun; oncesinde/sonrasinda selamlama, aciklama, dusunce, ` +
1365
+ `Markdown veya \`\`\` kod bloğu OLMASIN. Metin alanlarindaki cift tirnaklari \\" ile kacir.`;
1366
+ this.publish("log", { level: "warn", msg: `${label} protokol hatasi; operator yeniden deneniyor (${attempt + 2}/${retries + 1}).` });
1367
+ }
1368
+ }
1369
+ throw lastError;
1370
+ }
1371
+
1372
+ // Insertion sirasi tamamlanma sirasidir; en son biten denetim en gecerli karardir.
1373
+ latestReview(state) {
1374
+ return Object.values(state.results || {}).reverse()
1375
+ .find((result) => result.kind === "review" && result.status === "completed" && result.verdict) || null;
1376
+ }
1377
+
1378
+ // Operatorun karar evresine verilen takim kaydi. Ham state dokumu yerine sonuc odakli
1379
+ // bir ozet uretir; her sonucu kendi icinde ortadan kirpar ki en yeni denetim karari
1380
+ // (metnin sonundaki VERDICT satiri) butce asiminda kaybolmasin.
1381
+ teamDigest(state, cfg) {
1382
+ const results = Object.values(state.results || {});
1383
+ const budget = cfg.teamContextCharBudget || 30000;
1384
+ const perResult = Math.max(900, Math.floor(budget / Math.max(1, results.length)));
1385
+ const digest = {
1386
+ round: state.round,
1387
+ completionCriteria: state.criteria,
1388
+ results: results.map((result) => ({
1389
+ id: result.id, agent: result.agent, kind: result.kind, status: result.status,
1390
+ ...(result.verdict ? { verdict: result.verdict } : {}),
1391
+ result: clipMiddle(result.result, perResult),
1392
+ })),
1393
+ };
1394
+ return clipMiddle(JSON.stringify(digest, null, 2), budget);
1395
+ }
1396
+
1397
+ // Cok uzun kullanici spec'leri (1000+ satir) operatore/uzmana OLDUGU GIBI gomulunce iki sorun
1398
+ // cikar: (1) kati JSON-only operator protokolu dev metinde bozulur — model prose/markdown dokup
1399
+ // gecersiz JSON uretir, delegasyon acilmaz, ongorulemez sonuc cikar; (2) ayni dev metin her
1400
+ // operator turunda ve her uzman cagrisinda tekrar baglama yigilir, model baglami tasar veya
1401
+ // sessizce kesilir. Cozum: spec'i BIR KEZ calisma klasorunun .crewctl/TASK-<id>.md dosyasina
1402
+ // yaz, prompta yalnizca bas+son OZETINI + "tam metni bu dosyadan OKU" isaretini goм. Butun
1403
+ // CLI'lar cwd altindaki dosyayi kendileri okur; hicbir sey sessizce kesilmez. Kisa promptlar
1404
+ // (butcenin altinda) aynen dondurulur; mevcut davranis ve testler degismez.
1405
+ taskBrief(task, cfg) {
1406
+ const full = String(task.prompt || "");
1407
+ const budget = Math.max(1200, Number(cfg.taskPromptCharBudget) || 6000);
1408
+ if (full.length <= budget) return full;
1409
+ let ref = "";
1410
+ try {
1411
+ const dir = path.join(this._cwd, ".crewctl");
1412
+ fs.mkdirSync(dir, { recursive: true });
1413
+ fs.writeFileSync(path.join(dir, `TASK-${task.id}.md`), full, "utf8");
1414
+ ref = `.crewctl/TASK-${task.id}.md`;
1415
+ } catch {}
1416
+ const head = Math.ceil(budget * 0.7);
1417
+ const tail = budget - head;
1418
+ const excerpt = `${full.slice(0, head)}\n\n...[GOREV METNI KIRPILDI ${full.length} karakterlik tam spec ${ref ? `\`${ref}\` dosyasinda` : "yukarida"}]...\n\n${full.slice(-tail)}`;
1419
+ const pointer = ref
1420
+ ? `\nNOT: Bu gorevin tam metni cok uzun oldugu icin calisma klasorundeki \`${ref}\` dosyasina yazildi. Yukaridaki yalnizca bas/son ozetidir; PLANLAMADAN/UYGULAMADAN ONCE tam spec icin bu dosyayi OKU ve tamamina uy.`
1421
+ : "";
1422
+ return `${excerpt}${pointer}`;
1423
+ }
1424
+
1425
+ // Kullanicinin goreve ekledigi dosyalari (server ROOT/state/attachments/<id>/ altina yazdi)
1426
+ // calisma klasorunun .crewctl/attachments/<id>/ icine kopyalar; boylece dosyalar hapsin ICINDE
1427
+ // olur ve ajanlar (claude/codex/gemini/opencode) YOLU verildiginde kendileri okuyabilir. .crewctl
1428
+ // zaten diff/snapshot'tan haric. Okuyabilme tamamen ajanin/modelin becerisine kalir (metin/kod
1429
+ // her ajanda, gorseli yalnizca gorsel okuyabilen model); biz yetenek koprusu KURMAYIZ, sadece
1430
+ // yolu prompta koyariz. [{name, rel, ext}] doner.
1431
+ stageTaskAttachments(task) {
1432
+ const list = Array.isArray(task.attachments) ? task.attachments : [];
1433
+ if (!list.length) return [];
1434
+ const cwd = this._cwd;
1435
+ const dstDir = path.join(cwd, ".crewctl", "attachments", task.id);
1436
+ let dstMade = false;
1437
+ const ensureDst = () => { if (!dstMade) { try { fs.mkdirSync(dstDir, { recursive: true }); } catch {} dstMade = true; } };
1438
+ // Mutlak yol cwd'nin ICINDE mi? Ise goreli yolu doner (kopya gerekmez); degilse null.
1439
+ const relInsideCwd = (abs) => { const r = path.relative(cwd, abs); return (r && !r.startsWith("..") && !path.isAbsolute(r)) ? r.split(path.sep).join("/") : null; };
1440
+ const refs = [];
1441
+ for (const a of list) {
1442
+ const name = path.basename(String(a && a.name || ""));
1443
+ if (!name) continue;
1444
+ const ext = (path.extname(name).replace(".", "") || "?").toLowerCase();
1445
+ if (a && a.kind === "path" && a.srcPath) {
1446
+ // Sunucu tarafi secilen dosya (projeden/klasorden). cwd ICINDEyse KOPYA YOK — ajan zaten
1447
+ // o klasoru okuyor, prompta sadece goreli yolu koyariz. cwd DISIndaysa .crewctl'e kopyalanir.
1448
+ const abs = path.resolve(String(a.srcPath));
1449
+ const inside = relInsideCwd(abs);
1450
+ if (inside && fs.existsSync(abs)) { refs.push({ name, rel: inside, ext }); continue; }
1451
+ ensureDst();
1452
+ try { if (fs.existsSync(abs)) fs.copyFileSync(abs, path.join(dstDir, name)); } catch {}
1453
+ refs.push({ name, rel: `.crewctl/attachments/${task.id}/${name}`, ext });
1454
+ } else {
1455
+ // Cihazdan yuklenen (base64) dosya: staging -> .crewctl.
1456
+ ensureDst();
1457
+ const src = path.join(store.attachmentStagingDir(task.id), name);
1458
+ try { if (fs.existsSync(src)) fs.copyFileSync(src, path.join(dstDir, name)); } catch {}
1459
+ refs.push({ name, rel: `.crewctl/attachments/${task.id}/${name}`, ext });
1460
+ }
1461
+ }
1462
+ return refs;
1463
+ }
1464
+
1465
+ // Ekli dosyalari operator/uzman prompt'una tek blok olarak koyar. Icerigi GOMMEZ; yalnizca yol +
1466
+ // uzanti verir (taskBrief'teki isaret deseniyle ayni) — ajan ilgiliyse dosyayi kendi okur.
1467
+ attachmentSection() {
1468
+ const refs = this._attachmentRefs || [];
1469
+ if (!refs.length) return "";
1470
+ const lines = refs.map((r) => `- \`${r.rel}\` (${r.ext})`).join("\n");
1471
+ return `## Ekli dosyalar\nKullanici bu goreve asagidaki dosyalari ekledi; hepsi calisma klasorunun ICINDE. Goreve ilgiliyse ilgili dosyayi OKU. Okuyabilme dosya turune ve ajanin/modelin becerisine baglidir: metin/kod/markdown/json her ajanda okunur; gorsel gibi ikili dosyalari yalnizca o turu okuyabilen model acabilir, acamayan ajan bu dosyayi atlar. Bir alt ajana is verirken ilgili dosyanin YOLUNU talimatina aynen aktar.\n${lines}\n`;
1472
+ }
1473
+
1474
+ operatorPrompt(task, cfg, memory, state, phase) {
1475
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
1476
+ // Operatör rolü daima operator.md'dir; operatör bir CLI'dir, uzman agent'lar onun altında çalışır.
1477
+ const roleFile = cfg.operator?.roleFile || "roles/operator.md";
1478
+ const role = store.readRole(path.basename(roleFile));
1479
+ const catalog = this.agentCatalog(cfg, operatorCli);
1480
+ const skillContext = `${task.prompt}\n${phase === "review" ? Object.values(state.results || {}).map((result) => result.instruction || "").join("\n") : ""}`;
1481
+ const skillDiscovery = skillRegistry.discover(cfg, skillContext);
1482
+ const skillCatalog = skillDiscovery.catalog;
1483
+ const skillStats = skillDiscovery.stats;
1484
+ const enabledSkillNames = skillRegistry.enabledSkills(cfg).map((skill) => skill.name);
1485
+ // Beceri envanteri motor tarafindan uretilen OTORITER veridir. Operatore TAM etkin listeyi
1486
+ // (sayi + tum adlar) DAIMA veririz; boylece "kac beceri var" gibi sorular icin operatorun
1487
+ // kendi CLI'sinin dahili becerilerini veya bir alt agent'in iddiasini kullanmasi gerekmez.
1488
+ // Eslesen kisa liste yalnizca DELEGASYONA iliştirme icindir, envanterin tamami degildir.
1489
+ const skillSection = skillStats.enabled
1490
+ ? `## Beceriler (OTORITER kaynak)\nSistemde tam olarak ${skillStats.enabled} etkin beceri var: ${enabledSkillNames.join(", ")}.\n` +
1491
+ `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` +
1492
+ (skillCatalog.length
1493
+ ? `Bu goreve en ilgili ${skillCatalog.length} beceri (delegasyonun skills alanina yalnizca bunlardan uygun olanlari ekle):\n${JSON.stringify(skillCatalog, null, 2)}\n`
1494
+ : `Bu goreve gore taramada eslesme cikmadi; bu delegasyonlarda beceri iliştirme.\n`)
1495
+ : "";
1496
+ // Beceriler tamamen opsiyoneldir ve YALNIZCA kullanici etkinlestirdiginde katalogda gorunur.
1497
+ // Operator, assignment'a yalnizca bu kataloktaki adlari "skills" dizisinde iliştirebilir.
1498
+ const skillField = skillCatalog.length ? `, "skills":["beceri-adi"]` : "";
1499
+ const skillProtocol = skillCatalog.length
1500
+ ? ` BECERI: Kisa listedeki gercekten ilgili adlari skills dizisine ekle; ilgisiz veya liste disi ad kullanma, uygun yoksa alani bos birak.`
1501
+ : "";
1502
+ const protocol = phase === "plan"
1503
+ ? `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). ` +
1504
+ `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"}. ` +
1505
+ `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}`
1506
+ : `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` +
1507
+ `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.`;
1508
+ const strategy = task.executionMode === "fast"
1509
+ ? "HIZLI MOD: Kucuk bir is. Tek bir implementation agenti kullan. Ayri planlama veya review delegasyonu acma; uzman raporu hedefi karsiliyorsa ilk degerlendirmede tamamla."
1510
+ : task.executionMode === "deep"
1511
+ ? "DERIN MOD: Isi uygun uzmanliklar arasinda dagit planlama, uygulama, test ve bagimsiz inceleme icin AYRI ve dogru uzmanlari kullan."
1512
+ : "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.";
1513
+ return `${role}\n\n---\n## Operator protokolu\n${protocol}\n` +
1514
+ // OpenCode gibi sohbet odakli CLI'lar varsayilan olarak once bir cumle yazip JSON'u
1515
+ // arkasina ekliyor; motor bunu ayiklayabilse de belirsizlik protokol hatasi uretiyordu.
1516
+ `## Cikti bicimi (KATI)\nCevabin TAMAMI tek bir JSON nesnesi olmali: ilk karakter { , son karakter } . ` +
1517
+ `Oncesinde/sonrasinda selamlama, aciklama, dusunce notu, Markdown basligi veya \`\`\` kod bloğu YAZMA. ` +
1518
+ `Kullaniciya iletmek istedigin her seyi ilgili JSON alanina ("final" veya "summary") koy; nesnenin disindaki metin kullaniciya ULASMAZ.\n` +
1519
+ `Yalnizca katalogdaki agent adlarini kullan. Her assignment kind degeri secilen agentin allowedKinds listesinde olmali. Kendine gorev atama. Ayni delegasyon id'sini tekrar kullanma.\n` +
1520
+ `Bir agent basarisiz veya kullanilamaz raporlandiysa ayni isi ona tekrar verme; katalogdaki alternatif bir uzmani sec.\n` +
1521
+ `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` +
1522
+ `## Calisma stratejisi\n${strategy}\n` +
1523
+ `## Agent katalogu\n${JSON.stringify(catalog, null, 2)}\n` +
1524
+ skillSection +
1525
+ `## Kullanici gorevi\n${this.taskBrief(task, cfg)}\n` +
1526
+ this.attachmentSection() +
1527
+ `## Calisma klasoru\n${this._cwd}\n` +
1528
+ `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` +
1529
+ `## 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` +
1530
+ (phase === "review"
1531
+ ? `## Son bagimsiz denetim\n${(() => { const review = this.latestReview(state); return review ? `${review.id} (${review.agent}) → VERDICT: ${review.verdict}` : "Henuz tamamlanmis denetim yok."; })()}\n` +
1532
+ `## Takim calisma kaydi\n${this.teamDigest(state, cfg)}\n`
1533
+ : "");
1534
+ }
1535
+
1536
+ specialistPrompt(task, cfg, assignment, state) {
1537
+ const agent = cfg.agents[assignment.agent];
1538
+ const roleByKind = { implement: "executor.md", review: "reviewer.md", plan: "planner.md" };
1539
+ const selectedRole = roleByKind[assignment.kind] || (agent.roleFile ? path.basename(agent.roleFile) : "executor.md");
1540
+ const role = store.readRole(selectedRole);
1541
+ const instructionByKind = {
1542
+ implement: "Delegasyonu calisma klasorunde uygula, degisiklikleri dogrula ve rolundeki teslimat formatinda raporla.",
1543
+ review: "Teslimati bagimsiz ve salt okunur olarak incele; gerekli kontrolleri calistir, dosyalari degistirme ve rolundeki karar formatinda raporla.",
1544
+ plan: "Calisma klasorunu salt okunur olarak incele; hicbir dosyayi degistirmeden uygulanabilir plani rolundeki formatta raporla.",
1545
+ research: "Delegasyon kapsaminda kanit topla; kaynaklari ve belirsizlikleri ayirarak kisa bir sonuc raporla.",
1546
+ };
1547
+ const completed = Object.values(state.results).map((r) => ({
1548
+ id: r.id, agent: r.agent, instruction: r.instruction, ...(r.verdict ? { verdict: r.verdict } : {}), result: clipMiddle(r.result, 5000),
1549
+ }));
1550
+ // Progressive disclosure: operatorun bu delegasyona sectigi becerilerin tam govdesini YIGMAYIZ;
1551
+ // yalnizca ad + kisa ozet + rehber dosya yolunu veririz. Uzman gercekten ihtiyac duyarsa dosyayi
1552
+ // kendisi okur. Operator hicbir beceri secmediyse (veya beceriler kapali ise) blok bostur.
1553
+ const skills = skillRegistry.resolveForAssignment(assignment.skills, cfg);
1554
+ const skillRefs = skillRegistry.toPromptRefs(skills, cfg.skills?.referenceCharBudget || 1200);
1555
+ return `${role}\n\n---\n## Takim agenti protokolu\n` +
1556
+ `Operator sana asagidaki isi devretti. ${instructionByKind[assignment.kind] || instructionByKind.implement} ` +
1557
+ `Planda olmayan riskli bir is gerekiyorsa yapma; BLOCKED olarak bildir. Yalnizca gercekten gozlemledigin veya dogruladigin sonuclari yaz.\n` +
1558
+ `SANDBOX: Butun is YALNIZCA calisma klasorunde (${this._cwd}) yapilir. Bu klasor disina dosya YAZMA/DEGISTIRME/SILME ve gerekmedikce disariyi OKUMA.\n` +
1559
+ (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` : "") +
1560
+ `## Ana hedef\n${this.taskBrief(task, cfg)}\n` + this.attachmentSection() + `## Delegasyon\nID: ${assignment.id}\n${assignment.instruction}\n` +
1561
+ `## Onceki tamamlanan takim isleri\n${clip(JSON.stringify(completed, null, 2), cfg.teamContextCharBudget || 30000)}`;
1562
+ }
1563
+
1564
+ // Iki kademeli kurtarma: (1) gecici hata -> ayni agent, ustel bekleme; (2) kalici hata ->
1565
+ // ayni turu yapabilen saglikli baska agente devret. Ikisi de tukenirse firlatir ve operator
1566
+ // yeniden planlar. Amac, saniyelik bir 429 icin tam bir operator turu harcamamak.
1567
+ async runAssignmentWithRecovery(task, cfg, assignment, state) {
1568
+ const retries = Math.max(0, cfg.resilience?.transientRetries ?? 2);
1569
+ const maxFailovers = Math.max(0, cfg.resilience?.maxFailoverAgents ?? 1);
1570
+ const baseDelayMs = Math.max(250, (cfg.resilience?.retryBaseSeconds ?? 3) * 1000);
1571
+ const tried = new Set();
1572
+ let agentName = assignment.agent;
1573
+ let failure = null;
1574
+
1575
+ for (let failover = 0; ; failover++) {
1576
+ tried.add(agentName);
1577
+ for (let attempt = 0; ; attempt++) {
1578
+ try {
1579
+ const response = await this.invokeAgent(
1580
+ agentName,
1581
+ this.specialistPrompt(task, cfg, { ...assignment, agent: agentName }, state),
1582
+ cfg,
1583
+ { stage: "delegate", assignmentId: assignment.id }
1584
+ );
1585
+ return { response, agent: agentName };
1586
+ } catch (error) {
1587
+ if (!this.running) throw error;
1588
+ failure = classifyCliError(error);
1589
+ if (!TRANSIENT_CLI_ERRORS.has(failure.code) || attempt >= retries) break;
1590
+ // Jitter: ayni anda dusen birden fazla delegasyon saglayiciya es zamanli geri donmesin.
1591
+ const waitMs = Math.min(30000, baseDelayMs * 2 ** attempt) + Math.floor(Math.random() * 750);
1592
+ this.publish("log", { level: "warn", msg: `${agentName} gecici hata verdi [${failure.code}]; ${Math.round(waitMs / 1000)} sn sonra yeniden deneniyor (${attempt + 2}/${retries + 1}).` }, task.id);
1593
+ this.publish("activity", { kind: "delegation.retry", assignmentId: assignment.id, agent: agentName, code: failure.code, attempt: attempt + 1, waitMs });
1594
+ await this.sleepRetry(waitMs);
1595
+ if (!this.running) throw error;
1596
+ }
1597
+ }
1598
+ if (QUARANTINE_CLI_ERRORS.has(failure.code)) this.quarantineAgent(agentName, failure.code, cfg);
1599
+ if (failover >= maxFailovers) break;
1600
+ const next = compatibleAgentsForKind(cfg, task.operatorCli, assignment.kind)
1601
+ .find((name) => !tried.has(name) && !this.isQuarantined(name));
1602
+ if (!next) break;
1603
+ this.publish("log", { level: "warn", msg: `${agentName} basarisiz [${failure.code}]; ayni is ${next} agentine devrediliyor (operator turu harcanmadan).` }, task.id);
1604
+ this.publish("activity", { kind: "delegation.failover", assignmentId: assignment.id, from: agentName, to: next, code: failure.code });
1605
+ agentName = next;
1606
+ }
1607
+ // Siniflandirici disaridaki catch'te ayni sonucu uretsin diye ham metinle firlatiyoruz.
1608
+ // CLI etiketi de tasinmali; yoksa oturum/kurulum tavsiyesi jenerige duser.
1609
+ const error = new Error(failure?.raw || "Delegasyon calistirilamadi.");
1610
+ error.agentName = failure?.agent || agentName;
1611
+ error.adapter = failure?.adapter || "";
1612
+ error.triedAgents = [...tried];
1613
+ throw error;
1614
+ }
1615
+
1616
+ async runAssignments(task, cfg, assignments, state) {
1617
+ const pending = assignments.slice();
1618
+ while (pending.length) {
1619
+ let index = pending.findIndex((a) => a.dependsOn.every((id) => state.results[id]?.status === "completed"));
1620
+ if (index < 0) {
1621
+ index = pending.findIndex((a) => a.dependsOn.some((id) => state.results[id] && state.results[id].status !== "completed"));
1622
+ if (index >= 0) {
1623
+ const blocked = pending.splice(index, 1)[0];
1624
+ state.usedIds.push(blocked.id);
1625
+ const failedDeps = blocked.dependsOn.filter((id) => state.results[id]?.status !== "completed");
1626
+ state.results[blocked.id] = { ...blocked, status: "blocked", result: `Bagimli gorev basarisiz: ${failedDeps.join(", ")}` };
1627
+ 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() };
1628
+ state.messages.push(blockedMessage);
1629
+ this.publish("message", blockedMessage, task.id);
1630
+ continue;
1631
+ }
1632
+ throw new Error(`Delegasyon bagimliliklari cozumlenemedi: ${pending.map((a) => a.id).join(", ")}`);
1633
+ }
1634
+ const assignment = pending.splice(index, 1)[0];
1635
+ state.usedIds.push(assignment.id);
1636
+ state.messages.push({ from: task.operatorCli, to: assignment.agent, messageType: "delegation", assignmentId: assignment.id, body: assignment.instruction, at: new Date().toISOString() });
1637
+ this.publish("message", state.messages[state.messages.length - 1], task.id);
1638
+ if (assignment.routingReason) this.publish("log", { level: "info", msg: `Otomatik yonlendirme: ${assignment.routingReason}` }, task.id);
1639
+ if (assignment.renamedFrom) this.publish("log", { level: "warn", msg: `Operator delegasyon kimligini yineledi; ${assignment.renamedFrom} otomatik olarak ${assignment.id} yapildi.` }, task.id);
1640
+ this.publish("log", { level: "stage", msg: `DELEGE ${assignment.id} -> ${assignment.agent}` }, task.id);
1641
+ if (assignment.skills?.length) this.publish("log", { level: "info", msg: `Beceriler: ${assignment.skills.join(", ")}` }, task.id);
1642
+ try {
1643
+ const { response, agent: usedAgent } = await this.runAssignmentWithRecovery(task, cfg, assignment, state);
1644
+ const result = { ...assignment, agent: usedAgent, status: "completed", result: response.text, durationMs: response.durationMs, callId: response.callId };
1645
+ if (usedAgent !== assignment.agent) {
1646
+ result.failoverFrom = assignment.agent;
1647
+ this.publish("log", { level: "info", msg: `${assignment.id} devralan agent tarafindan tamamlandi: ${assignment.agent} -> ${usedAgent}.` }, task.id);
1648
+ }
1649
+ if (assignment.kind === "review") {
1650
+ result.verdict = extractVerdict(response.text);
1651
+ this.publish("log", { level: "info", msg: `Denetim ${assignment.id}: VERDICT ${result.verdict || "BELIRSIZ"}` }, task.id);
1652
+ }
1653
+ state.results[assignment.id] = result;
1654
+ const message = { from: usedAgent, to: task.operatorCli, messageType: "result", assignmentId: assignment.id, body: response.text, at: new Date().toISOString() };
1655
+ state.messages.push(message);
1656
+ this.publish("message", message, task.id);
1657
+ } catch (error) {
1658
+ // Karantina ve devretme runAssignmentWithRecovery'de tuketildi; buraya dusen gercekten
1659
+ // kurtarilamayandir.
1660
+ const failure = classifyCliError(error);
1661
+ const tried = error.triedAgents?.length ? error.triedAgents : [assignment.agent];
1662
+ state.results[assignment.id] = { ...assignment, status: "failed", result: failure.summary, error: failure, triedAgents: tried };
1663
+ const message = { from: tried[tried.length - 1], to: task.operatorCli, messageType: "failure", assignmentId: assignment.id, body: `${failure.summary}\n${failure.action}`, errorCode: failure.code, at: new Date().toISOString() };
1664
+ state.messages.push(message);
1665
+ this.publish("message", message, task.id);
1666
+ this.publish("log", { level: "warn", msg: `${tried.join(" > ")} kullanilamadi [${failure.code}]. Operator alternatif plan uretecek.` }, task.id);
1667
+ }
1668
+ task.teamState = state;
1669
+ store.saveTask("pending", task);
1670
+ }
1671
+ }
1672
+
1673
+ isRisky(text, cfg) {
1674
+ const lower = String(text).toLowerCase();
1675
+ return (cfg.riskyPatterns || []).some((pattern) => lower.includes(String(pattern).toLowerCase()));
1676
+ }
1677
+
1678
+ async runTask(task) {
1679
+ const baseCfg = this.cfg();
1680
+ if (!baseCfg.autonomousConsentAcceptedAt) throw new Error("Otonom CLI calistirma kosullari kabul edilmeden gorev calistirilamaz.");
1681
+ if (task.kind === "operator-chat") return this.runChatTask(task, baseCfg);
1682
+ task.executionMode = resolveExecutionMode(task);
1683
+ const cfg = applyExecutionPolicy(baseCfg, task.executionMode);
1684
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
1685
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Gecerli bir operator CLI secilmedi (claude/codex/gemini/opencode).");
1686
+ task.operatorCli = operatorCli;
1687
+ this.busy = true;
1688
+ this.current = { id: task.id, stage: "operator", agent: operatorCli };
1689
+ this._cwd = path.resolve(store.WORK_BASE, task.targetDir || cfg.workingDir || ".");
1690
+ // Kullanicinin sectigi path GERCEKTEN calisma klasoru olmali. Cozulen dizin yoksa CLI'lar
1691
+ // spawn cwd bulunamayinca sunucunun baslatildigi dizine (WORK_BASE) veya bir ust dizine
1692
+ // dusup "baska yerleri okuyup yazma" davranisi gosteriyordu. Sectigi yolu var edip garanti
1693
+ // altina aliriz; boylece hapis (codex -s workspace-write / claude cwd) tam bu klasora oturur.
1694
+ try { fs.mkdirSync(this._cwd, { recursive: true }); } catch {}
1695
+ this._cwd = canonicalDir(this._cwd);
1696
+ // Timeout OLCEKLEME SINYALI: taskBrief buyuk spec'i dosyaya tasidigi icin runCli'ye giden
1697
+ // bileşik prompt artik KISA olabilir; gercek is boyutunu orijinal gorev metninden okuruz.
1698
+ this._taskChars = String(task.prompt || "").length;
1699
+ this._attachmentRefs = this.stageTaskAttachments(task);
1700
+ this._snapBefore = snapshotDir(this._cwd);
1701
+ this.startLiveDiff(task);
1702
+ // Onay sonrasi devam eden gorevde sayaci sifirlamayiz; ilk girise ozgu.
1703
+ if (!task.approved || !this._usage) this.resetUsage();
1704
+ this.publish("log", { level: "task", msg: `GOREV ${task.id}: ${task.prompt}` }, task.id);
1705
+ // Otomatik surumleme: gorev CALISMADAN ONCE calisma klasorunun bir surumunu al ki ajan
1706
+ // mevcut kodu bozarsa bu gorev oncesine tek tikla donulebilsin. Yalnizca ilk girus'te
1707
+ // (approval sonrasi devam da ayni checkpoint'i korur). Basarisizsa gorevi asla oldurmez.
1708
+ if (baseCfg.versioning !== false && !task.checkpointId) {
1709
+ const cp = checkpoints.createCheckpoint(this._cwd, { taskId: task.id, label: task.prompt, kind: "pre-task", retention: baseCfg.versioningRetention });
1710
+ if (cp.ok) {
1711
+ task.checkpointId = cp.id;
1712
+ store.saveTask("pending", task);
1713
+ this.publish("log", { level: "info", msg: `Surum alindi (${cp.fileCount} dosya · ${cp.backend}); bu gorev oncesine geri donulebilir.` }, task.id);
1714
+ } else if (cp.skipped) {
1715
+ this.publish("log", { level: "warn", msg: `Surum alinamadi: ${cp.reason}` }, task.id);
1716
+ }
1717
+ }
1718
+ this.publish("log", { level: "info", msg: `Calisma modu: ${task.executionMode.toUpperCase()} · en fazla ${cfg.operator.maxRounds} tur / ${cfg.operator.maxDelegationsPerRound} delegasyon` }, task.id);
1719
+ this.emit("status", this.status());
1720
+ this.emit("queue");
1721
+
1722
+ cfg.runtimeUnavailableAgents = this.quarantinedAgents();
1723
+ // Proje context: varsayilan olarak bu calisma klasorunun .crewctl/CONTEXT.md profilini
1724
+ // yukleriz (path'e ozel, tum kodu bastan taramaya gerek kalmasin). task.freshContext ise
1725
+ // temiz sayfa. projectContext:false ise eski GLOBAL hafiza davranisi korunur.
1726
+ const memory = cfg.projectContext === false
1727
+ ? store.getMemory(cfg.memoryCharBudget)
1728
+ : (task.freshContext ? "" : store.readProjectContext(this._cwd));
1729
+ const state = task.teamState || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1730
+ const usedIds = new Set(state.usedIds || []);
1731
+
1732
+ let assignments;
1733
+ if (state.plan && task.approved) {
1734
+ assignments = normalizeAssignments(state.plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1735
+ assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1736
+ this.publish("log", { level: "info", msg: "Onaylanmis operator plani zorunlu rol zinciri korunarak devam ettiriliyor." }, task.id);
1737
+ } else {
1738
+ // Bilgi sorusu kestirmesi: operator, delege edilecek bir is olmadan yaniti dogrudan
1739
+ // verdiginde (or. "kac beceri var") tek turda tamamla. Boylece motorun otoriter verisi
1740
+ // bir alt agente aktarilirken kaybolmaz/carpitilmaz ve zorunlu assignment semasi bu
1741
+ // tur sorularda gorevi bosuna oldurmez. ANCAK gorev acikca bir yapim/degisiklik isiyse
1742
+ // kestirmeye izin verilmez: operator (cogunlukla proje hafizasindaki gecmis teslimati
1743
+ // "zaten yapildi" sanip) is uretmeden kapatmaya calisir. Once yeniden planlama isteriz;
1744
+ // israr ederse gorevi sahte basari yerine net bir hatayla dururuz.
1745
+ const requiresWork = taskRequiresDelegation(task.prompt);
1746
+ const shortcutRetries = requiresWork ? Math.max(1, cfg.operator?.protocolRetries ?? 1) : 0;
1747
+ let plan, shortcutAttempt = 0, planCorrection = "";
1748
+ while (true) {
1749
+ try {
1750
+ plan = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "plan") + planCorrection, cfg, "operator-plan", "Operator plani");
1751
+ } catch (error) {
1752
+ // Sohbet kurtarma: gorev delegasyon gerektirmiyorsa (selamlasma, kisa soru) ve operator
1753
+ // protokol nesnesi yerine duz metinle cevap verdiyse, kullaniciyi "gecerli JSON
1754
+ // dondurmedi" hatasiyla karsilamak yanlistir istenen sey zaten o metindi. Yalnizca
1755
+ // is gerektirmeyen gorevlerde ve elde gercek bir metin varken devreye girer.
1756
+ const prose = conversationalAnswer(error);
1757
+ if (requiresWork || !prose) throw error;
1758
+ this.publish("log", { level: "warn", msg: "Operator protokol nesnesi dondurmedi; gorev is gerektirmedigi icin duz metin cevabi teslimat sayildi." }, task.id);
1759
+ task.teamState = state;
1760
+ return this.complete(task, state, "done", prose, { verification: "Operatorun dogrudan yaniti (protokol nesnesi yerine duz metin)." });
1761
+ }
1762
+ const isShortcut = plan && String(plan.status).toLowerCase() === "complete" && !Array.isArray(plan.assignments);
1763
+ if (!isShortcut) break;
1764
+ if (!requiresWork) {
1765
+ if (!String(plan.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1766
+ this.publish("log", { level: "info", msg: "Operator gorevi delegasyona gerek gormeden dogrudan yanitladi." }, task.id);
1767
+ task.teamState = state;
1768
+ return this.complete(task, state, "done", String(plan.final), plan);
1769
+ }
1770
+ if (shortcutAttempt++ >= shortcutRetries) {
1771
+ throw new Error("Operator bu yapim gorevini delege etmeden kapatmaya calisti. Proje hafizasindaki gecmis teslimatlar gorevi tamamlanmis saymaz; en az bir uygulama delegasyonu gerekir.");
1772
+ }
1773
+ this.publish("log", { level: "warn", msg: "Operator yapim gorevini delegasyonsuz kapatmaya calisti; hafiza gecmisi kanit sayilmaz, yeniden planlama isteniyor." }, task.id);
1774
+ 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.";
1775
+ }
1776
+ assignments = normalizeAssignments(plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1777
+ assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1778
+ state.plan = { summary: String(plan.summary || ""), completionCriteria: Array.isArray(plan.completionCriteria) ? plan.completionCriteria.map(String) : [], assignments };
1779
+ state.criteria = state.plan.completionCriteria;
1780
+ state.operatorDecisions.push({ round: 0, ...state.plan });
1781
+ task.teamState = state;
1782
+ task.planPreview = JSON.stringify(state.plan, null, 2);
1783
+ store.saveTask("pending", task);
1784
+ if (cfg.approvalMode === "ask" && this.isRisky(task.planPreview, cfg)) {
1785
+ task.status = "awaiting-approval";
1786
+ task.planHash = store.hashText(task.planPreview);
1787
+ store.moveTask("pending", "approval", task);
1788
+ this.publish("log", { level: "warn", msg: "Riskli operator plani insan onayina alindi." }, task.id);
1789
+ this.busy = false;
1790
+ this.current = null;
1791
+ this.emit("queue");
1792
+ this.emit("status", this.status());
1793
+ this.stopLiveDiff();
1794
+ this._textBefore = null;
1795
+ return;
1796
+ }
1797
+ }
1798
+
1799
+ const maxRounds = Math.max(1, cfg.operator?.maxRounds || 6);
1800
+ const maxRecoveryRounds = Math.max(0, cfg.operator?.maxInfrastructureRecoveryRounds ?? 2);
1801
+ let recoveryRounds = 0;
1802
+ while (this.running && state.round < maxRounds + recoveryRounds) {
1803
+ state.round++;
1804
+ await this.runAssignments(task, cfg, assignments, state);
1805
+ const infrastructureFailures = assignments.filter((assignment) => {
1806
+ const result = state.results[assignment.id];
1807
+ return result?.status === "failed" && RECOVERABLE_CLI_ERRORS.has(result.error?.code);
1808
+ });
1809
+ if (infrastructureFailures.length && recoveryRounds < maxRecoveryRounds) {
1810
+ recoveryRounds++;
1811
+ this.publish("log", { level: "warn", msg: `CLI altyapi hatasi tur butcesinden dusulmedi; ${maxRecoveryRounds - recoveryRounds} ek kurtarma turu kaldi.` }, task.id);
1812
+ }
1813
+ // Uygulama agenti kalmadi guvenligi: Gorev acikca dosya olusturma/degistirme gerektiriyor
1814
+ // fakat katalogda 'implement' yapabilen kullanilabilir hicbir agent kalmadiysa (or. tek
1815
+ // executor opencode sessizlik nedeniyle karantinaya alindi) ve simdiye kadar tamamlanmis bir
1816
+ // uygulama isi da yoksa, HICBIR ek tur dosya uretemez. Operatoru planlayici/gap-analizi
1817
+ // dongusunde bosuna dondurup tur ve cagri butcesini yakmak yerine net, eyleme donuk bir
1818
+ // hatayla dur (kullanicinin gordugu "circle seklinde uzayip gitme" sorununu bu keser).
1819
+ if (taskRequiresDelegation(task.prompt)
1820
+ && !this.hasUsableImplementAgent(cfg, operatorCli)
1821
+ && !Object.values(state.results).some((result) => result.kind === "implement" && result.status === "completed")) {
1822
+ // Implement agenti yalnizca SURELI karantinadaysa gorevi oldurmek yerine bekleriz.
1823
+ // Kalici kisitlarda (kota bitti, CLI yok) null doner.
1824
+ const waitMs = this.msUntilImplementAgentReturns(cfg, operatorCli);
1825
+ let recovered = false;
1826
+ if (waitMs !== null) {
1827
+ this.publish("log", { level: "warn", msg: `Kullanilabilir implement agenti gecici olarak yok; karantina bitene kadar ${Math.ceil(waitMs / 1000)} sn bekleniyor.` }, task.id);
1828
+ await this.sleepRetry(waitMs + 250);
1829
+ // Bekleme sirasinda motor durdurulduysa gorevi "implement agenti yok" hatasiyla
1830
+ // basarisiz isaretlemek yaniltici olur; dongu kosulu zaten temiz cikisi saglar.
1831
+ if (!this.running) break;
1832
+ recovered = this.hasUsableImplementAgent(cfg, operatorCli);
1833
+ if (recovered) {
1834
+ cfg.runtimeUnavailableAgents = this.quarantinedAgents();
1835
+ this.publish("log", { level: "info", msg: "Implement agenti karantinadan dondu; gorev surduruluyor." }, task.id);
1836
+ }
1837
+ }
1838
+ const dead = this.quarantinedAgents();
1839
+ if (!recovered) throw new Error(
1840
+ "Bu gorev dosya olusturma/degistirme gerektiriyor fakat kullanilabilir bir uygulama (implement) agenti yok" +
1841
+ (dead.length ? ` (${dead.join(", ")} bu oturumda kullanim disi kaldi)` : "") +
1842
+ ". 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."
1843
+ );
1844
+ }
1845
+ if (task.executionMode === "fast" && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")) {
1846
+ const reports = assignments.map((assignment) => state.results[assignment.id].result).join("\n\n");
1847
+ this.publish("log", { level: "info", msg: "FAST mod: uzman teslimati basarili; ikinci operator degerlendirme cagrisi atlandi." }, task.id);
1848
+ return this.complete(task, state, "done", reports, {});
1849
+ }
1850
+ // PASS hizli yolu: turdaki tum delegasyonlar tamamlandi ve turun kendi bagimsiz
1851
+ // denetimi PASS verdiyse operatorun ikinci degerlendirme cagrisi bilgi eklemez,
1852
+ // yalnizca dakikalar kaybettirir. Bayat bir PASS'in yeni turu kapatmamasi icin
1853
+ // karar en guncel denetim olmalidir. operator.passFastPath=false ile kapatilabilir.
1854
+ const roundReview = assignments
1855
+ .map((assignment) => state.results[assignment.id])
1856
+ .filter((result) => result?.kind === "review" && result.status === "completed" && result.verdict)
1857
+ .pop();
1858
+ if (cfg.operator?.passFastPath !== false
1859
+ && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")
1860
+ && roundReview?.verdict === "PASS"
1861
+ && roundReview === this.latestReview(state)) {
1862
+ this.publish("log", { level: "ok", msg: `Denetim ${roundReview.id} PASS verdi; operator degerlendirme cagrisi atlandi ve teslimat tamamlandi.` }, task.id);
1863
+ const completedWork = Object.values(state.results).filter((result) => result.status === "completed");
1864
+ const final = `Teslimat bagimsiz denetimden gecti (${roundReview.id} VERDICT: PASS).\n` +
1865
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1866
+ return this.complete(task, state, "done", final, { verification: `${roundReview.agent} → VERDICT: PASS` });
1867
+ }
1868
+ const decision = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "review"), cfg, "operator-review", "Operator karari");
1869
+ state.operatorDecisions.push({ round: state.round, ...decision });
1870
+ task.teamState = state;
1871
+ store.saveTask("pending", task);
1872
+ if (String(decision.status).toLowerCase() === "complete") {
1873
+ if (!String(decision.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1874
+ return this.complete(task, state, "done", String(decision.final), decision);
1875
+ }
1876
+ if (String(decision.status).toLowerCase() !== "continue") throw new Error("Operator status alani continue veya complete olmali.");
1877
+ assignments = normalizeAssignments(decision.assignments, cfg, operatorCli, new Set(state.usedIds), task.prompt);
1878
+ // Inceleme dongusu valisi: bagimsiz denetim PASS verdiyse ve operator yalnizca yeni
1879
+ // inceleme turlari acmak istiyorsa dongu burada kesilir. Sonsuz review ping-pong'u
1880
+ // hem tur butcesini tuketiyor hem de basarili teslimati kullaniciya geciktiriyordu.
1881
+ const lastReview = this.latestReview(state);
1882
+ if (lastReview?.verdict === "PASS" && assignments.every((assignment) => assignment.kind === "review")) {
1883
+ this.publish("log", { level: "warn", msg: `Denetim ${lastReview.id} PASS verdi; yalnizca yeni inceleme iceren tur acilmadi ve teslimat tamamlandi.` }, task.id);
1884
+ const final = `Teslimat bagimsiz denetimden gecti (${lastReview.id} VERDICT: PASS). ` +
1885
+ `Operatorun ek inceleme talebi tur butcesini korumak icin motor tarafindan sonlandirildi.` +
1886
+ (String(decision.reason || "").trim() ? `\nOperator notu: ${clip(decision.reason, 600)}` : "");
1887
+ return this.complete(task, state, "done", final, { verification: `${lastReview.agent} → VERDICT: PASS` });
1888
+ }
1889
+ }
1890
+ if (!this.running) throw new Error("Motor durduruldu; gorev tamamlanmadan kesildi.");
1891
+ return this.finishExhausted(task, state, recoveryRounds);
1892
+ }
1893
+
1894
+ // Tur butcesi doldugunda tamamlanmis isi cope atmak yerine uyarili kismi teslimat yapar.
1895
+ // Kullanicinin gordugu sonuc "saatlerce bekledim ve hata aldim" degil, "teslimat hazir,
1896
+ // su riskler acik kaldi" olmalidir. Hic tamamlanan is yoksa eski davranis korunur.
1897
+ finishExhausted(task, state, recoveryRounds) {
1898
+ const completedWork = Object.values(state.results || {}).filter((result) => result.status === "completed");
1899
+ if (!completedWork.length) {
1900
+ throw new Error(`Operator ${state.round} tur sonunda gorevi tamamlayamadi${recoveryRounds ? ` (${recoveryRounds} CLI kurtarma turu kullanildi)` : ""}.`);
1901
+ }
1902
+ const lastReview = this.latestReview(state);
1903
+ const lastDecision = [...(state.operatorDecisions || [])].reverse().find((decision) => String(decision.status).toLowerCase() === "continue");
1904
+ const warnings = [
1905
+ `Tur butcesi doldu (${state.round} tur); operator complete karari veremeden teslimat kapatildi.`,
1906
+ ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} VERDICT: ${lastReview.verdict}.`] : []),
1907
+ ...(lastDecision && String(lastDecision.reason || "").trim() ? [`Operatorun acik biraktigi konu: ${clip(lastDecision.reason, 400)}`] : []),
1908
+ ];
1909
+ this.publish("log", { level: "warn", msg: `Tur butcesi doldu; tamamlanan is kismi teslimat olarak kapatiliyor (${completedWork.length} tamamlanmis delegasyon).` }, task.id);
1910
+ const final = `KISMI TESLIMAT: Tur butcesi doldugu icin gorev, tamamlanan isle kapatildi.\n` +
1911
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n") +
1912
+ `\nAcik kalan konular teslimat uyarilarinda listelendi.`;
1913
+ return this.complete(task, state, "done", final, {
1914
+ warnings,
1915
+ verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1916
+ });
1917
+ }
1918
+
1919
+ // Gorev beklenmedik bir hatayla kesildiginde (protokol hatasi, butce asimi vb.) somut is
1920
+ // uretilmisse gorevi failed yerine uyarili kismi teslimatla kapatir. Basarili olamazsa
1921
+ // false doner ve normal hata akisi calisir.
1922
+ async salvage(task, error) {
1923
+ try {
1924
+ if (task.kind === "operator-chat") return false;
1925
+ if (!this._cwd || !this._snapBefore) return false;
1926
+ const state = task.teamState;
1927
+ const completedWork = Object.values(state?.results || {}).filter((result) => result.status === "completed");
1928
+ const changes = diffSnapshots(this._snapBefore, snapshotDir(this._cwd));
1929
+ const hasFileChanges = Boolean(changes.created.length || changes.modified.length || changes.deleted.length);
1930
+ const hasImplementation = completedWork.some((result) => result.kind === "implement");
1931
+ // Kurtarma kosulu: YA tamamlanmis bir uygulama delegasyonu var (ekip uretti) YA DA calisma
1932
+ // klasoru fiilen degisti. Ikincisi, operatorun kendisi standart JSON protokolune uymadan
1933
+ // isi dogrudan yaptigi durumu da kapsar (or. OpenCode operator rolunde JSON plan yerine
1934
+ // dosyalari kendisi degistirip duz metin doner). Hicbir is yoksa normal hata akisina birak.
1935
+ if (!hasFileChanges && !hasImplementation) return false;
1936
+ const failure = classifyCliError(error);
1937
+ const rescueState = state || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1938
+ const operatorDirect = !completedWork.length && hasFileChanges;
1939
+ this.publish("log", { level: "warn", msg: `Gorev hatayla kesildi (${failure.summary}); ${operatorDirect ? "operatorun dogrudan yaptigi degisiklikler" : "tamamlanan is"} kismi teslimat olarak korunuyor.` }, task.id);
1940
+ const lastReview = this.latestReview(rescueState);
1941
+ const changedFiles = [...changes.created, ...changes.modified, ...changes.deleted];
1942
+ const final = operatorDirect
1943
+ ? `KISMI TESLIMAT: Operator (${task.operatorCli || "?"}) standart delegasyon protokolune uymadan isi dogrudan uyguladi; degisiklikler korundu.\n` +
1944
+ changedFiles.slice(0, 40).map((file) => `- ${file}`).join("\n")
1945
+ : `KISMI TESLIMAT: Gorev bir altyapi/protokol hatasiyla kesildi fakat tamamlanan is korundu.\n` +
1946
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1947
+ await this.complete(task, rescueState, "done", final, {
1948
+ warnings: [
1949
+ `Gorev su hatayla kesildi: ${failure.summary}`,
1950
+ ...(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.`] : []),
1951
+ ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} → VERDICT: ${lastReview.verdict}.`] : []),
1952
+ ],
1953
+ verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1954
+ });
1955
+ return true;
1956
+ } catch {
1957
+ return false;
1958
+ }
1959
+ }
1960
+
1961
+ async runChatTask(task, cfg) {
1962
+ const parentFound = store.findTask(task.parentTaskId);
1963
+ if (!parentFound) throw new Error("Sohbetin bagli oldugu tamamlanmis gorev bulunamadi.");
1964
+ const parent = parentFound.task;
1965
+ let operatorCli = parent.operatorCli;
1966
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) operatorCli = cfg.operator?.cli;
1967
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Sohbet icin operator CLI bulunamadi.");
1968
+ this.busy = true;
1969
+ this.current = { id: task.id, stage: "operator-chat", agent: operatorCli };
1970
+ this._cwd = canonicalDir(path.resolve(store.WORK_BASE, parent.targetDir || cfg.workingDir || "."));
1971
+ this._taskChars = String(task.prompt || "").length;
1972
+ this._attachmentRefs = [];
1973
+ this.publish("log", { level: "task", msg: `OPERATOR SOHBETI ${parent.id}: ${task.prompt}` }, task.id);
1974
+ this.emit("status", this.status());
1975
+
1976
+ const role = store.readRole("operator-chat.md");
1977
+ const resultContext = Object.values(parent.teamState?.results || {}).map((result) => ({
1978
+ id: result.id, agent: result.agent, status: result.status, result: clip(result.result, 1800),
1979
+ }));
1980
+ const prompt = `${role}\n\n---\n## Tamamlanmis gorev hakkinda takip sohbeti\n` +
1981
+ `Kullanici daha once tamamladiginiz gorev hakkinda soru soruyor. Yeni dosya degistirme, arac kullanma veya delegasyon yapma. ` +
1982
+ `Asagidaki kayitlara dayanarak dogrudan, kisa ve acik cevap ver. Bilmiyorsan bunu belirt.\n` +
1983
+ `## 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` +
1984
+ `## Takim raporlari\n${clip(JSON.stringify(resultContext, null, 2), 12000)}\n` +
1985
+ `## Onceki sohbet\n${clip(JSON.stringify(parent.conversation || [], null, 2), 8000)}\n## Yeni soru\n${task.prompt}`;
1986
+ const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-chat" });
1987
+ const entry = { question: task.prompt, answer: response.text, at: new Date().toISOString(), taskId: task.id };
1988
+ parent.conversation ||= [];
1989
+ parent.conversation.push(entry);
1990
+ store.saveTask(parentFound.state, parent);
1991
+ task.status = "done";
1992
+ task.finishedAt = new Date().toISOString();
1993
+ task.final = response.text;
1994
+ task.summary = clip(response.text, 1000);
1995
+ store.moveTask("pending", "done", task);
1996
+ this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: parent.id, answer: response.text, status: "done", operator: operatorCli }, task.id);
1997
+ this.publish("log", { level: "ok", msg: `Operator soruyu yanitladi: ${parent.id}` }, task.id);
1998
+ this.emit("queue");
1999
+ }
2000
+
2001
+ async complete(task, state, status, finalText, decision = {}) {
2002
+ const changes = diffSnapshots(this._snapBefore || new Map(), snapshotDir(this._cwd));
2003
+ // Son periyodik taramadan sonra yapilan degisiklikleri de replay kaydina ve dashboard'a
2004
+ // aktar; ardindan run --once yolunda da timer'in acik kalmamasini garanti et.
2005
+ this.publishFileChanges(task.id);
2006
+ this.stopLiveDiff();
2007
+ task.status = status;
2008
+ task.finishedAt = new Date().toISOString();
2009
+ const files = [
2010
+ ...changes.created.map((file) => ({ path: file, action: "created" })),
2011
+ ...changes.modified.map((file) => ({ path: file, action: "modified" })),
2012
+ ...changes.deleted.map((file) => ({ path: file, action: "deleted" })),
2013
+ ];
2014
+ const concise = clip(String(finalText).replace(/\n{3,}/g, "\n\n"), 700);
2015
+ let verification = String(decision.verification || "").trim();
2016
+ if (!verification) {
2017
+ for (const result of Object.values(state.results)) {
2018
+ const match = String(result.result || "").match(/DO(?:Ğ|G\u0306|G)RULAMA:\s*([^\n`]+)/i);
2019
+ if (match) { verification = match[1].trim(); break; }
2020
+ }
2021
+ }
2022
+ task.summary = concise;
2023
+ task.final = finalText;
2024
+ task.changes = changes;
2025
+ task.usage = this.usageSummary();
2026
+ const warnings = (Array.isArray(decision.warnings) ? decision.warnings : []).map(String).filter(Boolean);
2027
+ task.delivery = {
2028
+ summary: concise,
2029
+ files,
2030
+ location: this._cwd,
2031
+ verification,
2032
+ mode: task.executionMode,
2033
+ rounds: state.round,
2034
+ agents: [...new Set(Object.values(state.results).map((result) => result.agent))],
2035
+ ...(warnings.length ? { warnings } : {}),
2036
+ };
2037
+ task.teamState = state;
2038
+ const found = store.findTask(task.id);
2039
+ store.moveTask(found?.state || "pending", status, task);
2040
+ store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
2041
+ 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);
2042
+ this.publish("log", { level: "ok", msg: `GOREV tamamlandi: ${task.id}` }, task.id);
2043
+ this.notifyOutcome(task, status, { summary: task.summary, rounds: state.round, fileCount: files.length, verification: task.delivery.verification });
2044
+ this.emit("queue");
2045
+ this._textBefore = null;
2046
+ // Proje profilini guncelle (sonuc zaten kullaniciya iletildi; motor mesgul kaldigindan
2047
+ // sonraki gorevle es zamanli operator cagrisi olmaz). Basarisizsa gorevi ASLA etkilemez.
2048
+ await this.reviseProjectContext(task, status, files);
2049
+ }
2050
+
2051
+ // Gorev bitiminde bu calisma klasorunun .crewctl/CONTEXT.md proje profilini operatore REVIZE
2052
+ // ettirir (append degil): mimari/yapi/konvansiyon/komutlar guncel kalir, bayatlamaz. Yalnizca
2053
+ // basarili ve DOSYA DEGISTIREN gorevlerde calisir (bilgi sorulari profili degistirmez).
2054
+ async reviseProjectContext(task, status, files) {
2055
+ try {
2056
+ const cfg = this.cfg();
2057
+ if (cfg.projectContext === false) return;
2058
+ if (status !== "done" || !this._cwd) return;
2059
+ if (!Array.isArray(files) || !files.length) return;
2060
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
2061
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) return;
2062
+ const budget = cfg.projectContextCharBudget || 6000;
2063
+ const current = store.readProjectContext(this._cwd);
2064
+ const changed = files.map((f) => `${f.action}: ${f.path}`).join("\n");
2065
+ const prompt =
2066
+ `Sen bir projenin YASAYAN bilgi tabanini (proje profili) guncelliyorsun. Amac: sonraki gorevlerde ` +
2067
+ `ajan tum kodu bastan taramadan projeyi anlasin.\n` +
2068
+ `Asagida MEVCUT profil ve SON tamamlanan gorevin ozeti + degisen dosyalar var. Guncel, oz ve DOGRU ` +
2069
+ `bir proje profili YAZ (Markdown). Kurallar:\n` +
2070
+ `- Degisen gerceklere gore REVIZE et; silinen/tasinan/degisen seyleri guncelle, bayat bilgi birakma.\n` +
2071
+ `- Dayanikli bilgiyi tut: amac, mimari, klasor/dosya yapisi, giris noktalari, onemli moduller, ` +
2072
+ `konvansiyonlar, kurulum/calistirma/test komutlari, dikkat noktalari.\n` +
2073
+ `- Task-by-task changelog YAZMA; tek bir GUNCEL DURUM profili olsun.\n` +
2074
+ `- En fazla ~${budget} karakter. YALNIZCA profil metnini dondur; aciklama/selamlama/JSON/kod-bloğu isareti YAZMA.\n\n` +
2075
+ `## Mevcut profil\n${current || "(henuz yok — sifirdan olustur)"}\n\n` +
2076
+ `## Son tamamlanan gorev\n${task.prompt}\n\n## Sonuc ozeti\n${task.summary || ""}\n\n## Degisen dosyalar\n${changed}`;
2077
+ const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-context" });
2078
+ const profile = stripCodeFence(String(response?.text || "")).trim();
2079
+ if (profile && store.writeProjectContext(this._cwd, clip(profile, budget * 2))) {
2080
+ this.publish("log", { level: "info", msg: "Proje profili (.crewctl/CONTEXT.md) guncellendi." }, task.id);
2081
+ }
2082
+ } catch (error) {
2083
+ this.publish("log", { level: "warn", msg: `Proje profili guncellenemedi: ${error.message}` }, task.id);
2084
+ }
2085
+ }
2086
+
2087
+ // Dis bildirim icin tek cikis noktasi: server.js bu event'i dinleyip (varsa) webhook'a POST atar.
2088
+ // Motoru HTTP'den bagimsiz tutar; bildirim basarisiz olsa bile gorev akisini etkilemez.
2089
+ notifyOutcome(task, status, extra = {}) {
2090
+ try {
2091
+ this.emit("notify", {
2092
+ status,
2093
+ id: task.id,
2094
+ prompt: task.prompt,
2095
+ operator: task.operatorCli || null,
2096
+ dir: this._cwd || task.targetDir || null,
2097
+ at: new Date().toISOString(),
2098
+ ...extra,
2099
+ });
2100
+ } catch {}
2101
+ }
2102
+ }
2103
+
2104
+ module.exports = new Engine();
2105
+ // Testlerin dis davranisla birlikte kritik saf kurallari da dogrudan dogrulayabilmesi icin.
2106
+ module.exports._internals = {
2107
+ normalizeAssignments, ensureBalancedRoleChain, extractVerdict, clipMiddle, snapshotDir, diffSnapshots,
2108
+ captureTextSnapshot, buildLineDiff, describeFileDiff, isSensitiveDiffPath, taskRequiresDelegation,
2109
+ parseJson, conversationalAnswer, classifyCliError, RECOVERABLE_CLI_ERRORS, QUARANTINE_CLI_ERRORS,
2110
+ TRANSIENT_CLI_ERRORS, QUARANTINE_COOLDOWN_SECONDS, compatibleAgentsForKind,
2111
+ normalizeCliOutput, addUsage, usageTotal, claudeSandboxSettings,
2112
+ };