@omerrgocmen/crewctl 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +182 -0
  2. package/orchestrator/LICENSE +21 -0
  3. package/orchestrator/README.md +452 -0
  4. package/orchestrator/config.default.json +65 -0
  5. package/orchestrator/roles/executor.md +49 -0
  6. package/orchestrator/roles/operator-chat.md +25 -0
  7. package/orchestrator/roles/operator.md +75 -0
  8. package/orchestrator/roles/planner.md +53 -0
  9. package/orchestrator/roles/reviewer.md +54 -0
  10. package/orchestrator/skills/acceptance-criteria.md +18 -0
  11. package/orchestrator/skills/accessibility-audit.md +16 -0
  12. package/orchestrator/skills/accessible-forms.md +16 -0
  13. package/orchestrator/skills/api-design.md +27 -0
  14. package/orchestrator/skills/api-documentation.md +16 -0
  15. package/orchestrator/skills/architecture-decision-record.md +18 -0
  16. package/orchestrator/skills/authentication-design.md +16 -0
  17. package/orchestrator/skills/authorization-review.md +16 -0
  18. package/orchestrator/skills/backward-compatibility.md +17 -0
  19. package/orchestrator/skills/changelog-writing.md +16 -0
  20. package/orchestrator/skills/ci-pipeline-design.md +16 -0
  21. package/orchestrator/skills/cli-design.md +16 -0
  22. package/orchestrator/skills/code-review.md +27 -0
  23. package/orchestrator/skills/configuration-management.md +16 -0
  24. package/orchestrator/skills/container-review.md +16 -0
  25. package/orchestrator/skills/contract-testing.md +16 -0
  26. package/orchestrator/skills/dashboard-design.md +16 -0
  27. package/orchestrator/skills/database-migration.md +16 -0
  28. package/orchestrator/skills/database-schema-design.md +16 -0
  29. package/orchestrator/skills/debugging.md +26 -0
  30. package/orchestrator/skills/dependency-review.md +16 -0
  31. package/orchestrator/skills/design-review.md +29 -0
  32. package/orchestrator/skills/design-system.md +16 -0
  33. package/orchestrator/skills/docs-api-reference.md +16 -0
  34. package/orchestrator/skills/docs-troubleshooting.md +16 -0
  35. package/orchestrator/skills/docs-tutorial.md +16 -0
  36. package/orchestrator/skills/docs-writing.md +25 -0
  37. package/orchestrator/skills/empty-error-loading-states.md +16 -0
  38. package/orchestrator/skills/end-to-end-testing.md +16 -0
  39. package/orchestrator/skills/error-handling.md +16 -0
  40. package/orchestrator/skills/frontend-design.md +28 -0
  41. package/orchestrator/skills/git-commit-writing.md +16 -0
  42. package/orchestrator/skills/graphql-design.md +16 -0
  43. package/orchestrator/skills/incident-runbook.md +18 -0
  44. package/orchestrator/skills/input-validation.md +16 -0
  45. package/orchestrator/skills/integration-testing.md +16 -0
  46. package/orchestrator/skills/interaction-design.md +16 -0
  47. package/orchestrator/skills/landing-page-design.md +16 -0
  48. package/orchestrator/skills/observability-design.md +16 -0
  49. package/orchestrator/skills/openapi-contract.md +16 -0
  50. package/orchestrator/skills/performance-profiling.md +16 -0
  51. package/orchestrator/skills/privacy-review.md +16 -0
  52. package/orchestrator/skills/property-based-testing.md +16 -0
  53. package/orchestrator/skills/pull-request-writing.md +16 -0
  54. package/orchestrator/skills/refactoring.md +16 -0
  55. package/orchestrator/skills/release-readiness.md +16 -0
  56. package/orchestrator/skills/responsive-design.md +16 -0
  57. package/orchestrator/skills/secrets-management.md +16 -0
  58. package/orchestrator/skills/secure-file-upload.md +16 -0
  59. package/orchestrator/skills/security-review.md +26 -0
  60. package/orchestrator/skills/semantic-versioning.md +17 -0
  61. package/orchestrator/skills/seo-on-page.md +16 -0
  62. package/orchestrator/skills/seo-structured-data.md +16 -0
  63. package/orchestrator/skills/seo-technical-audit.md +16 -0
  64. package/orchestrator/skills/sql-query-review.md +16 -0
  65. package/orchestrator/skills/supply-chain-security.md +16 -0
  66. package/orchestrator/skills/test-strategy.md +16 -0
  67. package/orchestrator/skills/threat-modeling.md +16 -0
  68. package/orchestrator/skills/unit-testing.md +16 -0
  69. package/orchestrator/skills/write-tests.md +28 -0
  70. package/orchestrator/src/checkpoints.js +187 -0
  71. package/orchestrator/src/cli-registry.js +579 -0
  72. package/orchestrator/src/cli.js +135 -0
  73. package/orchestrator/src/doctor.js +64 -0
  74. package/orchestrator/src/engine.js +1364 -0
  75. package/orchestrator/src/schedule.js +126 -0
  76. package/orchestrator/src/server.js +706 -0
  77. package/orchestrator/src/skill-registry.js +272 -0
  78. package/orchestrator/src/store.js +364 -0
  79. package/orchestrator/web/OrbitControls.js +1417 -0
  80. package/orchestrator/web/app.css +116 -0
  81. package/orchestrator/web/app.js +70 -0
  82. package/orchestrator/web/board.html +141 -0
  83. package/orchestrator/web/code.html +208 -0
  84. package/orchestrator/web/flow.html +736 -0
  85. package/orchestrator/web/index.html +539 -0
  86. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -0
  87. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -0
  88. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -0
  89. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -0
  90. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -0
  91. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -0
  92. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -0
  93. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -0
  94. package/orchestrator/web/three.module.min.js +6 -0
  95. package/package.json +51 -0
@@ -0,0 +1,1364 @@
1
+ const { spawn } = require("child_process");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { EventEmitter } = require("events");
5
+ const store = require("./store");
6
+ const cliRegistry = require("./cli-registry");
7
+ const skillRegistry = require("./skill-registry");
8
+ const checkpoints = require("./checkpoints");
9
+
10
+ const isWin = process.platform === "win32";
11
+
12
+ function snapshotDir(dir) {
13
+ const map = new Map();
14
+ const ignored = (rel) => {
15
+ const r = rel.replace(/\\/g, "/");
16
+ return r.includes("node_modules") || r.includes(".git/") || r.startsWith(".git") ||
17
+ r.startsWith("orchestrator/queue") || r.startsWith("orchestrator/state") ||
18
+ r.startsWith("orchestrator/memory") || r.startsWith("orchestrator/node_modules");
19
+ };
20
+ let count = 0;
21
+ const walk = (abs, rel) => {
22
+ if (count > 12000) return;
23
+ let entries;
24
+ try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
25
+ for (const entry of entries) {
26
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
27
+ if (ignored(childRel)) continue;
28
+ const childAbs = path.join(abs, entry.name);
29
+ if (entry.isDirectory()) walk(childAbs, childRel);
30
+ else {
31
+ try {
32
+ const s = fs.statSync(childAbs);
33
+ map.set(childRel, `${s.mtimeMs}:${s.size}`);
34
+ count++;
35
+ } catch {}
36
+ }
37
+ }
38
+ };
39
+ walk(dir, "");
40
+ return map;
41
+ }
42
+
43
+ function diffSnapshots(before, after) {
44
+ const created = [], modified = [], deleted = [];
45
+ for (const [file, stamp] of after) {
46
+ if (!before.has(file)) created.push(file);
47
+ else if (before.get(file) !== stamp) modified.push(file);
48
+ }
49
+ for (const file of before.keys()) if (!after.has(file)) deleted.push(file);
50
+ return { created: created.sort(), modified: modified.sort(), deleted: deleted.sort() };
51
+ }
52
+
53
+ const DIFF_MAX_FILE_BYTES = 256 * 1024;
54
+ const DIFF_MAX_BASELINE_BYTES = 24 * 1024 * 1024;
55
+ const DIFF_MAX_BASELINE_FILES = 2000;
56
+ const DIFF_MAX_MATRIX_CELLS = 250000;
57
+ const DIFF_CONTEXT_LINES = 3;
58
+ const DIFF_MAX_RENDERED_LINES = 240;
59
+ const DIFF_MAX_EVENT_LINES = 1000;
60
+ const DIFF_MAX_SOURCE_LINES = 5000;
61
+
62
+ function isSensitiveDiffPath(file) {
63
+ const normalized = String(file || "").replace(/\\/g, "/").toLowerCase();
64
+ const base = path.posix.basename(normalized);
65
+ return /^\.env(?:\.|$)/.test(base) || [".npmrc", ".pypirc", ".netrc"].includes(base) ||
66
+ /(^|[._-])(secret|secrets|credential|credentials)([._-]|$)/.test(base) ||
67
+ /\.(pem|key|p12|pfx)$/.test(base) || /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/.test(base);
68
+ }
69
+
70
+ function readDiffText(file, maxBytes = DIFF_MAX_FILE_BYTES) {
71
+ try {
72
+ const stat = fs.statSync(file);
73
+ if (!stat.isFile()) return { ok: false, status: "unavailable" };
74
+ if (stat.size > maxBytes) return { ok: false, status: "too-large", size: stat.size };
75
+ const buffer = fs.readFileSync(file);
76
+ if (buffer.includes(0)) return { ok: false, status: "binary", size: stat.size };
77
+ let controls = 0;
78
+ for (const byte of buffer) {
79
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) controls++;
80
+ }
81
+ if (buffer.length && controls / buffer.length > 0.02) return { ok: false, status: "binary", size: stat.size };
82
+ return { ok: true, text: buffer.toString("utf8"), size: stat.size };
83
+ } catch {
84
+ return { ok: false, status: "unavailable" };
85
+ }
86
+ }
87
+
88
+ // Gorev basindaki metin iceriklerini sinirli bir butceyle saklar. Bu taban, Git deposu
89
+ // olmayan klasorlerde de modified/deleted dosyalar icin satir diff'i uretebilmemizi saglar.
90
+ function captureTextSnapshot(dir, snapshot, options = {}) {
91
+ const maxBytes = Math.max(1024, Number(options.maxBytes) || DIFF_MAX_BASELINE_BYTES);
92
+ const maxFiles = Math.max(1, Number(options.maxFiles) || DIFF_MAX_BASELINE_FILES);
93
+ const maxFileBytes = Math.max(1024, Number(options.maxFileBytes) || DIFF_MAX_FILE_BYTES);
94
+ const result = new Map();
95
+ let bytes = 0, files = 0;
96
+ for (const file of [...(snapshot?.keys?.() || [])].sort()) {
97
+ if (isSensitiveDiffPath(file)) {
98
+ result.set(file, { ok: false, status: "redacted" });
99
+ continue;
100
+ }
101
+ if (files >= maxFiles || bytes >= maxBytes) {
102
+ result.set(file, { ok: false, status: "baseline-limit" });
103
+ continue;
104
+ }
105
+ const value = readDiffText(path.join(dir, file), maxFileBytes);
106
+ if (value.ok && bytes + value.size > maxBytes) {
107
+ result.set(file, { ok: false, status: "baseline-limit" });
108
+ continue;
109
+ }
110
+ result.set(file, value);
111
+ if (value.ok) { bytes += value.size; files++; }
112
+ }
113
+ return result;
114
+ }
115
+
116
+ function splitDiffLines(text) {
117
+ const value = String(text || "").replace(/\r\n/g, "\n");
118
+ if (!value) return [];
119
+ const lines = value.split("\n");
120
+ if (lines[lines.length - 1] === "") lines.pop();
121
+ return lines;
122
+ }
123
+
124
+ function lineOperations(beforeText, afterText) {
125
+ const before = splitDiffLines(beforeText), after = splitDiffLines(afterText);
126
+ const cells = before.length * after.length;
127
+ let approximate = false;
128
+ const operations = [];
129
+ if (cells <= DIFF_MAX_MATRIX_CELLS) {
130
+ const width = after.length + 1;
131
+ const table = new Uint32Array((before.length + 1) * width);
132
+ for (let i = before.length - 1; i >= 0; i--) {
133
+ for (let j = after.length - 1; j >= 0; j--) {
134
+ table[i * width + j] = before[i] === after[j]
135
+ ? table[(i + 1) * width + j + 1] + 1
136
+ : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
137
+ }
138
+ }
139
+ let i = 0, j = 0;
140
+ while (i < before.length || j < after.length) {
141
+ if (i < before.length && j < after.length && before[i] === after[j]) {
142
+ operations.push({ type: "context", text: before[i++] }); j++;
143
+ } else if (i < before.length && (j >= after.length || table[(i + 1) * width + j] >= table[i * width + j + 1])) {
144
+ operations.push({ type: "delete", text: before[i++] });
145
+ } else {
146
+ operations.push({ type: "add", text: after[j++] });
147
+ }
148
+ }
149
+ } else {
150
+ // Cok buyuk dosyalarda karesel matris kurma. Ortak bas/sonu koruyup degisen orta
151
+ // bolumu tek hunk olarak goster; UI bu sonucu yaklasik olarak etiketler.
152
+ approximate = true;
153
+ let prefix = 0;
154
+ while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) prefix++;
155
+ let suffix = 0;
156
+ while (suffix < before.length - prefix && suffix < after.length - prefix &&
157
+ before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
158
+ for (let i = 0; i < prefix; i++) operations.push({ type: "context", text: before[i] });
159
+ for (let i = prefix; i < before.length - suffix; i++) operations.push({ type: "delete", text: before[i] });
160
+ for (let i = prefix; i < after.length - suffix; i++) operations.push({ type: "add", text: after[i] });
161
+ for (let i = suffix; i > 0; i--) operations.push({ type: "context", text: before[before.length - i] });
162
+ }
163
+ return { operations, approximate };
164
+ }
165
+
166
+ function buildLineDiff(beforeText, afterText, options = {}) {
167
+ const context = Math.max(0, Number(options.context) || DIFF_CONTEXT_LINES);
168
+ const maxLines = Math.max(20, Number(options.maxLines) || DIFF_MAX_RENDERED_LINES);
169
+ const { operations, approximate } = lineOperations(beforeText, afterText);
170
+ let oldNo = 1, newNo = 1;
171
+ const annotated = operations.map((operation) => {
172
+ const line = { ...operation, oldNumber: null, newNumber: null };
173
+ if (operation.type !== "add") line.oldNumber = oldNo++;
174
+ if (operation.type !== "delete") line.newNumber = newNo++;
175
+ return line;
176
+ });
177
+ const additions = annotated.filter((line) => line.type === "add").length;
178
+ const deletions = annotated.filter((line) => line.type === "delete").length;
179
+ const ranges = [];
180
+ for (let i = 0; i < annotated.length; i++) {
181
+ if (annotated[i].type === "context") continue;
182
+ const start = Math.max(0, i - context), end = Math.min(annotated.length, i + context + 1);
183
+ const last = ranges[ranges.length - 1];
184
+ if (last && start <= last.end) last.end = Math.max(last.end, end);
185
+ else ranges.push({ start, end });
186
+ }
187
+ const hunks = [];
188
+ let rendered = 0, truncated = false;
189
+ for (const range of ranges) {
190
+ if (rendered >= maxLines) { truncated = true; break; }
191
+ const oldStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "add").length;
192
+ const newStart = 1 + annotated.slice(0, range.start).filter((line) => line.type !== "delete").length;
193
+ const available = Math.max(0, maxLines - rendered);
194
+ const selected = annotated.slice(range.start, Math.min(range.end, range.start + available));
195
+ if (selected.length < range.end - range.start) truncated = true;
196
+ hunks.push({
197
+ oldStart,
198
+ oldLines: selected.filter((line) => line.type !== "add").length,
199
+ newStart,
200
+ newLines: selected.filter((line) => line.type !== "delete").length,
201
+ lines: selected.map((line) => ({ ...line, text: String(line.text).slice(0, 1000) })),
202
+ });
203
+ rendered += selected.length;
204
+ }
205
+ return { additions, deletions, hunks, truncated, approximate };
206
+ }
207
+
208
+ function describeFileDiff(root, file, action, baseline, options = {}) {
209
+ const base = action === "created" ? { ok: true, text: "" } : baseline?.get(file);
210
+ const current = action === "deleted" ? { ok: true, text: "" } :
211
+ (isSensitiveDiffPath(file) ? { ok: false, status: "redacted" } : readDiffText(path.join(root, file)));
212
+ const unavailable = !base?.ok ? base : (!current?.ok ? current : null);
213
+ if (unavailable) return { path: file, action, additions: null, deletions: null, previewStatus: unavailable.status || "unavailable", hunks: [] };
214
+ const lineCount = (text) => text ? (String(text).match(/\n/g) || []).length + 1 : 0;
215
+ if (lineCount(base.text) > DIFF_MAX_SOURCE_LINES || lineCount(current.text) > DIFF_MAX_SOURCE_LINES) {
216
+ return { path: file, action, additions: null, deletions: null, previewStatus: "too-many-lines", hunks: [] };
217
+ }
218
+ return { path: file, action, ...buildLineDiff(base.text, current.text, options) };
219
+ }
220
+
221
+ function clip(value, limit = 12000) {
222
+ const text = String(value || "");
223
+ return text.length <= limit ? text : `${text.slice(0, limit)}\n...[kesildi]`;
224
+ }
225
+
226
+ // Bas ve sonu koruyarak kirpar. Denetci raporlarinda VERDICT satiri metnin SONUNDadir;
227
+ // yalnizca bastan kirpmak operatorun karari gormemesine ve gereksiz yeniden inceleme
228
+ // turlarina yol acar.
229
+ function clipMiddle(value, limit = 12000) {
230
+ const text = String(value || "");
231
+ if (text.length <= limit) return text;
232
+ const head = Math.ceil(limit * 0.6);
233
+ const tail = Math.max(1, limit - head);
234
+ return `${text.slice(0, head)}\n...[orta kisim kesildi]...\n${text.slice(text.length - tail)}`;
235
+ }
236
+
237
+ function extractVerdict(text) {
238
+ const matches = String(text || "").match(/VERDICT:\s*(PASS|FAIL)/gi);
239
+ if (!matches || !matches.length) return null;
240
+ return /PASS/i.test(matches[matches.length - 1]) ? "PASS" : "FAIL";
241
+ }
242
+
243
+ function classifyCliError(error) {
244
+ const raw = String(error?.message || error || "Bilinmeyen CLI hatasi");
245
+ if (/requires a newer version|upgrade to the latest|model metadata.*not found|unsupported.*model|model.*unsupported/i.test(raw)) {
246
+ return { code: "VERSION_INCOMPATIBLE", summary: "CLI sürümü seçilen modeli desteklemiyor.", action: "CLI aracını güncelleyin veya desteklenen bir model seçin.", raw: clip(raw, 5000) };
247
+ }
248
+ if (/sessiz kaldi|cikti uretmedi|CLI_STALLED/i.test(raw)) {
249
+ return { code: "CLI_STALLED", summary: "CLI uzun süre yeni çıktı üretmeyince otomatik durduruldu.", action: "Önceki ilerleme kayıtları korundu; operatör bu oturumda farklı bir agent kullanacak.", raw: clip(raw, 5000) };
250
+ }
251
+ if (/API key not valid|API_KEY_INVALID/i.test(raw)) {
252
+ return { code: "AUTH_INVALID", summary: "API anahtari gecersiz veya kullanilamiyor.", action: "Bu agentin kimlik bilgilerini duzeltin ya da baska bir agent kullanin.", raw: clip(raw, 5000) };
253
+ }
254
+ if (/unauthorized|unauthenticated|authentication|login required|not logged in/i.test(raw)) {
255
+ return { code: "AUTH_REQUIRED", summary: "CLI oturumu veya kimlik dogrulamasi gerekli.", action: "CLI'yi terminalde oturum acarak dogrulayin.", raw: clip(raw, 5000) };
256
+ }
257
+ if (/rate.?limit|too many requests|quota/i.test(raw)) {
258
+ return { code: "RATE_LIMIT", summary: "Saglayici kota veya hiz sinirina takildi.", action: "Daha sonra deneyin veya alternatif agent kullanin.", raw: clip(raw, 5000) };
259
+ }
260
+ if (/ConnectionRefused|Unable to connect|provider hatasi|provider error|ECONNREFUSED/i.test(raw)) {
261
+ return { code: "PROVIDER_UNAVAILABLE", summary: "Secilen model saglayicisina baglanilamadi.", action: "OpenCode icin baska bir model secin veya saglayici baglantisini duzeltin.", raw: clip(raw, 5000) };
262
+ }
263
+ if (/not recognized as an internal|ENOENT|not found/i.test(raw)) {
264
+ return { code: "CLI_NOT_FOUND", summary: "CLI komutu bulunamadi veya baslatilamadi.", action: "Agent komutunu ve PATH ayarini kontrol edin.", raw: clip(raw, 5000) };
265
+ }
266
+ if (/timeout|timed out|zaman asim|zaman aşım/i.test(raw)) {
267
+ return { code: "TIMEOUT", summary: "CLI zaman asimina ugradi.", action: "Timeout degerini artirin veya gorevi daha kucuk parcalara bolun.", raw: clip(raw, 5000) };
268
+ }
269
+ return { code: "CLI_FAILED", summary: clip(raw.split(/\r?\n/)[0], 500), action: "Teknik ayrintiyi inceleyin veya alternatif agent kullanin.", raw: clip(raw, 5000) };
270
+ }
271
+
272
+ const RECOVERABLE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "RATE_LIMIT", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "TIMEOUT", "CLI_STALLED", "CLI_FAILED", "VERSION_INCOMPATIBLE"]);
273
+ const QUARANTINE_CLI_ERRORS = new Set(["AUTH_INVALID", "AUTH_REQUIRED", "PROVIDER_UNAVAILABLE", "CLI_NOT_FOUND", "CLI_STALLED", "VERSION_INCOMPATIBLE"]);
274
+
275
+ function resolveExecutionMode(task) {
276
+ if (["fast", "balanced", "deep"].includes(task.executionMode)) return task.executionMode;
277
+ const prompt = String(task.prompt || "");
278
+ const complex = /(mimari|migration|refactor|guvenlik|güvenlik|deploy|production|veritabani|veritabanı|authentication|entegrasyon|çoklu|multi|kapsamli|kapsamlı)/i.test(prompt);
279
+ // Not: "oyun/sayfa" gibi sifirdan build isleri BASIT sayilmaz — bunlar plan+uygula+incele
280
+ // gerektiren gercek gorevlerdir; fast moda dusurulup ekip kullanilmadan gecilmemeli.
281
+ // Not: "dosya" gibi genis kelimeler fast'a dusurmez; yorumdaki ilkeyle celisiyordu
282
+ // (sifirdan build/dosya olusturma gercek bir gorevdir). Yalnizca acik "kucuk/hizli" sinyalleri.
283
+ const simple = prompt.length < 350 && /(basit|ufak|küçük|kucuk|hizli|hızlı|simple|small|quick)/i.test(prompt);
284
+ return simple && !complex ? "fast" : "balanced";
285
+ }
286
+
287
+ function applyExecutionPolicy(base, mode) {
288
+ const cfg = { ...base, operator: { ...(base.operator || {}) } };
289
+ if (mode === "fast") {
290
+ cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 2);
291
+ cfg.operator.maxDelegationsPerRound = 1;
292
+ cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 2500);
293
+ cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 10000);
294
+ } else if (mode === "balanced") {
295
+ // Saglikli bir balanced gorev tek turda biter (uygulama + zincirli inceleme);
296
+ // 3 tur, iki duzeltme/yeniden dogrulama turuna yer birakir.
297
+ cfg.operator.maxRounds = Math.min(cfg.operator.maxRounds || 6, 3);
298
+ cfg.operator.maxDelegationsPerRound = Math.min(cfg.operator.maxDelegationsPerRound || 8, 3);
299
+ cfg.memoryCharBudget = Math.min(cfg.memoryCharBudget || 8000, 6000);
300
+ cfg.teamContextCharBudget = Math.min(cfg.teamContextCharBudget || 30000, 24000);
301
+ }
302
+ return cfg;
303
+ }
304
+
305
+ function normalizeCliOutput(agent, stdout) {
306
+ if (agent.adapter !== "opencode" || !(agent.args || []).includes("json")) return { text: String(stdout || "").trim(), error: "" };
307
+ const texts = [], errors = [];
308
+ for (const line of String(stdout || "").split(/\r?\n/)) {
309
+ if (!line.trim()) continue;
310
+ try {
311
+ const event = JSON.parse(line);
312
+ if (event.type === "text" && event.part?.text) texts.push(String(event.part.text));
313
+ if (event.type === "error") errors.push(String(event.error?.message || event.error || "OpenCode model/provider hatasi"));
314
+ } catch {}
315
+ }
316
+ return { text: texts.join("\n").trim(), error: errors.join("\n").trim() };
317
+ }
318
+
319
+ function parseJson(text, label) {
320
+ const candidates = [];
321
+ const fenced = String(text).match(/```(?:json)?\s*([\s\S]*?)```/i);
322
+ if (fenced) candidates.push(fenced[1]);
323
+ candidates.push(String(text).trim());
324
+ const first = String(text).indexOf("{");
325
+ const last = String(text).lastIndexOf("}");
326
+ if (first >= 0 && last > first) candidates.push(String(text).slice(first, last + 1));
327
+ for (const candidate of candidates) {
328
+ try { return JSON.parse(candidate); } catch {}
329
+ }
330
+ throw new Error(`${label} gecerli JSON dondurmedi.`);
331
+ }
332
+
333
+ function capabilitiesFor(agent) {
334
+ // Rol dosyasi bir uzmanlik dayatiyorsa yetenekler DOGRUDAN rolden turer. Aksi halde
335
+ // config'teki eski/genis capabilities listesi (or. planner'da "implementation") katalogda
336
+ // allowedKinds ile celisir ve operatore yaniltici sinyal verirdi. Rol yoksa config'e duseriz.
337
+ const role = String(agent.roleFile || "").toLowerCase();
338
+ if (role.includes("executor")) return new Set(["implementation", "debugging", "testing"]);
339
+ if (role.includes("review")) return new Set(["review", "testing", "analysis"]);
340
+ if (role.includes("planner")) return new Set(["planning", "analysis"]);
341
+ if (role.includes("operator")) return new Set(["planning", "delegation"]);
342
+ return new Set(Array.isArray(agent.capabilities) ? agent.capabilities.map((x) => String(x).toLowerCase()) : []);
343
+ }
344
+
345
+ function inferAssignmentKind(raw, instruction) {
346
+ const explicit = String(raw.kind || "").toLowerCase();
347
+ if (["implement", "review", "research", "plan"].includes(explicit)) return explicit;
348
+ 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";
349
+ if (/\b(incele|denetle|dogrula|doğrula|review|audit|verify|test et)\b/i.test(instruction)) return "review";
350
+ if (/\b(arastir|araştır|research|web)\b/i.test(instruction)) return "research";
351
+ if (/\b(planla|plan|tasarla|design)\b/i.test(instruction)) return "plan";
352
+ return "implement";
353
+ }
354
+
355
+ // Gorev metni gercek bir yapim/degisiklik isi mi? Operator, plan evresinde "bilgi sorusu
356
+ // kestirmesi" ile (delegasyon acmadan complete) yaniti dogrudan verebilir. Ama prompt acikca
357
+ // bir uygulama/olusturma istiyorsa bu kestirme kullaniciya HIC is uretmeden sahte basari doner
358
+ // (or. proje hafizasindaki gecmis teslimati "zaten yapildi" sanmak). Bu durumda kestirmeyi
359
+ // engelleyip yeniden planlama isteriz. inferAssignmentKind'in implement fiil kumesiyle uyumlu.
360
+ function taskRequiresDelegation(prompt) {
361
+ 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 || ""));
362
+ }
363
+
364
+ function roleAllowedKinds(agent) {
365
+ const role = path.basename(String(agent?.roleFile || "")).toLowerCase();
366
+ if (role.includes("executor")) return ["implement"];
367
+ if (role.includes("review")) return ["review"];
368
+ if (role.includes("planner")) return ["plan"];
369
+ return null;
370
+ }
371
+
372
+ function supportsKind(agent, kind) {
373
+ const roleKinds = roleAllowedKinds(agent);
374
+ if (roleKinds) return roleKinds.includes(kind);
375
+ const caps = capabilitiesFor(agent);
376
+ const wanted = {
377
+ implement: ["implementation", "coding", "development", "debugging"],
378
+ review: ["review", "testing", "security", "analysis"],
379
+ research: ["research", "web", "analysis"],
380
+ plan: ["planning", "analysis", "architecture"],
381
+ }[kind] || [];
382
+ return wanted.some((cap) => caps.has(cap));
383
+ }
384
+
385
+ function agentUsable(agent) {
386
+ return agent?.health?.status ? agent.health.status === "ready" : true;
387
+ }
388
+
389
+ function normalizeAssignments(value, cfg, operatorName, usedIds, taskText = "") {
390
+ if (!Array.isArray(value)) throw new Error("Operator assignments dizisi dondurmedi.");
391
+ const max = Math.max(1, cfg.operator?.maxDelegationsPerRound || 8);
392
+ // Operator daha once kullanilan bir kimligi yinelerse gorevi oldurmek yerine kimligi
393
+ // otomatik yeniden adlandiririz; saatlerce uretilmis teslimat bir ID cakismasi yuzunden
394
+ // basarisiz sayilmamalidir. Ayni turdaki dependsOn referanslari da yeni ada tasinir.
395
+ const renames = new Map();
396
+ return value.slice(0, max).map((raw, index) => {
397
+ let id = String(raw.id || `task-${Date.now()}-${index + 1}`).replace(/[^a-zA-Z0-9_.-]/g, "-");
398
+ let renamedFrom;
399
+ if (usedIds.has(id)) {
400
+ let n = 2;
401
+ while (usedIds.has(`${id}-r${n}`)) n++;
402
+ renamedFrom = id;
403
+ renames.set(id, `${id}-r${n}`);
404
+ id = `${id}-r${n}`;
405
+ }
406
+ usedIds.add(id);
407
+ const requestedAgent = String(raw.agent || "");
408
+ if (!cfg.agents[requestedAgent]) throw new Error(`Operator tanimsiz agent secti: ${requestedAgent}`);
409
+ if (cfg.agents[requestedAgent].enabled === false) throw new Error(`Operator devre disi agent secti: ${requestedAgent}`);
410
+ if (!agentUsable(cfg.agents[requestedAgent])) throw new Error(`Operator kullanilamaz agent secti: ${requestedAgent} (${cfg.agents[requestedAgent].health?.label || "saglik testi basarisiz"})`);
411
+ if (requestedAgent === operatorName) throw new Error("Operator kendisine uzman gorevi atayamaz.");
412
+ const instruction = String(raw.instruction || raw.task || "").trim();
413
+ if (!instruction) throw new Error(`${id} delegasyonunda instruction eksik.`);
414
+ const kind = inferAssignmentKind(raw, instruction);
415
+ // Beceriler kullanici-kapilidir: operator yalnizca cfg.skills.enabled icindekileri iliştirebilir.
416
+ // Tanimsiz/etkin olmayan beceri sessizce dusurulur; asla gorevi olduren bir hata degildir.
417
+ const hasExplicitSkills = Object.prototype.hasOwnProperty.call(raw, "skills");
418
+ const requestedSkills = Array.isArray(raw.skills)
419
+ ? raw.skills
420
+ : (!hasExplicitSkills && cfg.skills?.autoMatch !== false
421
+ ? skillRegistry.suggest(cfg, `${taskText}\n${instruction}`, kind)
422
+ : []);
423
+ const skills = skillRegistry.resolveForAssignment(requestedSkills, cfg).map((skill) => skill.name);
424
+ let agent = requestedAgent;
425
+ const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
426
+ if (unavailable.has(agent) || !supportsKind(cfg.agents[agent], kind)) {
427
+ const compatible = Object.entries(cfg.agents).find(([name, candidate]) => name !== operatorName && candidate.enabled !== false && agentUsable(candidate) && !unavailable.has(name) && supportsKind(candidate, kind));
428
+ if (compatible) agent = compatible[0];
429
+ else if (unavailable.has(agent)) throw new Error(`Operator bu oturumda kullanilamaz agent secti: ${requestedAgent}`);
430
+ }
431
+ return {
432
+ id, agent, kind, instruction, skills,
433
+ renamedFrom,
434
+ requestedAgent: agent === requestedAgent ? undefined : requestedAgent,
435
+ routingReason: agent === requestedAgent ? undefined : `${requestedAgent} ${unavailable.has(requestedAgent) ? "bu oturumda kullanilamaz" : `${kind} yetenegine sahip degil`}; ${agent} secildi.`,
436
+ dependsOn: Array.isArray(raw.dependsOn) ? raw.dependsOn.map((dep) => renames.get(String(dep)) || String(dep)) : [],
437
+ };
438
+ });
439
+ }
440
+
441
+ function compatibleAgentForKind(cfg, operatorName, kind) {
442
+ const unavailable = new Set(cfg.runtimeUnavailableAgents || []);
443
+ return Object.entries(cfg.agents || {}).find(([name, agent]) =>
444
+ name !== operatorName && agent.enabled !== false && agentUsable(agent) &&
445
+ !unavailable.has(name) && supportsKind(agent, kind)
446
+ )?.[0] || "";
447
+ }
448
+
449
+ function nextAssignmentId(base, usedIds) {
450
+ let id = base;
451
+ let suffix = 2;
452
+ while (usedIds.has(id)) id = `${base}-${suffix++}`;
453
+ usedIds.add(id);
454
+ return id;
455
+ }
456
+
457
+ // Balanced bir uygulama gorevinde katalogda standart roller varsa ilk turu
458
+ // PLAN -> IMPLEMENT -> REVIEW olarak garanti eder. Operatorun bir rolu yanlislikla
459
+ // atlamasi, kullanicinin etkinlestirdigi uzmani sessizce devre disi birakamaz.
460
+ function ensureBalancedRoleChain(assignments, cfg, operatorName, task, usedIds) {
461
+ if (task.executionMode !== "balanced" || !assignments.some((item) => item.kind === "implement")) return assignments;
462
+ const chained = assignments.map((item) => ({ ...item, dependsOn: [...item.dependsOn] }));
463
+ let plan = chained.find((item) => item.kind === "plan");
464
+ if (!plan) {
465
+ const planner = compatibleAgentForKind(cfg, operatorName, "plan");
466
+ if (planner) {
467
+ 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.";
468
+ plan = {
469
+ id: nextAssignmentId("balanced-plan", usedIds),
470
+ agent: planner,
471
+ kind: "plan",
472
+ instruction,
473
+ dependsOn: [],
474
+ skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "plan"),
475
+ routingReason: "Balanced rol zinciri: kullanilabilir planner ilk tura otomatik eklendi.",
476
+ };
477
+ chained.unshift(plan);
478
+ }
479
+ }
480
+ const implementations = chained.filter((item) => item.kind === "implement");
481
+ if (plan) {
482
+ for (const implementation of implementations) {
483
+ if (!implementation.dependsOn.includes(plan.id)) implementation.dependsOn.push(plan.id);
484
+ }
485
+ }
486
+ let reviews = chained.filter((item) => item.kind === "review");
487
+ if (!reviews.length) {
488
+ const reviewer = compatibleAgentForKind(cfg, operatorName, "review");
489
+ if (reviewer) {
490
+ 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.";
491
+ const review = {
492
+ id: nextAssignmentId("balanced-review", usedIds),
493
+ agent: reviewer,
494
+ kind: "review",
495
+ instruction,
496
+ dependsOn: implementations.map((item) => item.id),
497
+ skills: cfg.skills?.autoMatch === false ? [] : skillRegistry.suggest(cfg, `${task.prompt || ""}\n${instruction}`, "review"),
498
+ routingReason: "Balanced rol zinciri: kullanilabilir reviewer ilk tura otomatik eklendi.",
499
+ };
500
+ chained.push(review);
501
+ reviews = [review];
502
+ }
503
+ }
504
+ for (const review of reviews) {
505
+ // Operator belirli bir uygulamayi incelemeye bagladiysa bu kapsami genisletme;
506
+ // ayni turdaki bagimsiz/yardimci bir implement hatasi ana incelemeyi bloke etmemeli.
507
+ if (!review.dependsOn.length) {
508
+ for (const implementation of implementations) review.dependsOn.push(implementation.id);
509
+ }
510
+ }
511
+ const rank = { plan: 0, research: 1, implement: 2, review: 3 };
512
+ return chained.map((item, index) => ({ item, index }))
513
+ .sort((a, b) => (rank[a.item.kind] ?? 2) - (rank[b.item.kind] ?? 2) || a.index - b.index)
514
+ .map(({ item }) => item);
515
+ }
516
+
517
+ class Engine extends EventEmitter {
518
+ constructor() {
519
+ super();
520
+ this.running = false;
521
+ this.busy = false;
522
+ this.current = null;
523
+ this.activeChild = null;
524
+ this.callSequence = 0;
525
+ // Kesin kimlik/kurulum hatasi veren agent'lari ayni motor oturumunda tekrar secme.
526
+ // Kalici config'i degistirmeyiz; kullanici kimlik bilgisini duzeltip motoru yeniden
527
+ // baslattiginda agent tekrar denenebilir.
528
+ this.unhealthyAgents = new Map();
529
+ // Canli kodlama akisi: gorev calisirken calisma klasorunu periyodik tarayan zamanlayici
530
+ // ve son yayinlanan degisiklik imzasi (ayni durum tekrar tekrar yayinlanmasin diye).
531
+ this._liveTimer = null;
532
+ this._lastFileChangeKey = "";
533
+ this._textBefore = null;
534
+ }
535
+
536
+ cfg() { return store.loadConfig(); }
537
+
538
+ status() {
539
+ const cfg = this.cfg();
540
+ return {
541
+ running: this.running,
542
+ busy: this.busy,
543
+ current: this.current,
544
+ mode: cfg.approvalMode,
545
+ defaultOperator: cfg.operator?.cli || "",
546
+ callsToday: store.getCallCount(),
547
+ budget: cfg.dailyCallBudget,
548
+ };
549
+ }
550
+
551
+ publish(type, data, taskId = this.current?.id) {
552
+ const event = { at: new Date().toISOString(), taskId: taskId || null, ...data, type };
553
+ if (taskId) store.appendRunEvent(taskId, event);
554
+ this.emit(type, event);
555
+ return event;
556
+ }
557
+
558
+ // CANLI KODLAMA: gorev calisirken calisma klasorunu _snapBefore'a gore tarar ve olusan/
559
+ // degisen/silinen dosyalari "filechange" olayiyla yayinlar; complete()'teki nihai diff'in
560
+ // canli on-izlemesidir. Yalnizca durum degistiginde olay basar (ayni diff tekrarlanmaz).
561
+ // Tamamen additive ve salt-okunurdur; hata verirse gorevi etkilemeden sessizce atlar.
562
+ publishFileChanges(taskId = this.current?.id) {
563
+ if (!taskId || !this._cwd || !this._snapBefore) return null;
564
+ let changes, currentSnapshot;
565
+ try {
566
+ currentSnapshot = snapshotDir(this._cwd);
567
+ changes = diffSnapshots(this._snapBefore, currentSnapshot);
568
+ }
569
+ catch { return null; }
570
+ // Yalnizca dosya adlarini degil guncel stamp'i da imzaya kat. Ayni dosya gorev
571
+ // sirasinda ikinci kez degistiginde yeni satir diff'i mutlaka yayinlanmalidir.
572
+ const stampKey = [...changes.created, ...changes.modified]
573
+ .map((file) => `${file}:${currentSnapshot.get(file) || ""}`).join("|");
574
+ const key = `${stampKey}#${changes.deleted.join("|")}`;
575
+ if (key === this._lastFileChangeKey) return null;
576
+ const previouslyHadChanges = this._lastFileChangeKey !== "" && this._lastFileChangeKey !== "#";
577
+ const changedFiles = [
578
+ ...changes.created.map((file) => ({ file, action: "created" })),
579
+ ...changes.modified.map((file) => ({ file, action: "modified" })),
580
+ ...changes.deleted.map((file) => ({ file, action: "deleted" })),
581
+ ];
582
+ let remainingLines = DIFF_MAX_EVENT_LINES;
583
+ const files = changedFiles.map(({ file, action }) => {
584
+ if (remainingLines <= 0) return { path: file, action, additions: null, deletions: null, previewStatus: "event-limit", hunks: [] };
585
+ const described = describeFileDiff(this._cwd, file, action, this._textBefore, { maxLines: Math.min(DIFF_MAX_RENDERED_LINES, remainingLines) });
586
+ remainingLines -= (described.hunks || []).reduce((sum, hunk) => sum + (hunk.lines || []).length, 0);
587
+ return described;
588
+ });
589
+ // Bos degisim setini (or. baslangictaki taban veya tum degisikliklerin geri alinmasi)
590
+ // imzasini hatirla. Gorev basinda gurultu uretme; ancak daha once gorunen tum degisiklikler
591
+ // geri alindiysa bos olayi yayinla ki dashboard bayat diff'i temizlesin.
592
+ this._lastFileChangeKey = key;
593
+ if (!files.length && !previouslyHadChanges) return null;
594
+ return this.publish("filechange", {
595
+ files,
596
+ counts: { created: changes.created.length, modified: changes.modified.length, deleted: changes.deleted.length },
597
+ lineCounts: {
598
+ additions: files.reduce((sum, file) => sum + (Number(file.additions) || 0), 0),
599
+ deletions: files.reduce((sum, file) => sum + (Number(file.deletions) || 0), 0),
600
+ unavailable: files.filter((file) => file.additions == null || file.deletions == null).length,
601
+ },
602
+ }, taskId);
603
+ }
604
+
605
+ startLiveDiff(task) {
606
+ this.stopLiveDiff();
607
+ this._lastFileChangeKey = "";
608
+ // liveDiff kesin olarak false yapilmadikca aciktir (mevcut davranisi degistirmeyen additive
609
+ // ozellik). Tarama araligi config'ten ayarlanabilir; en dusuk 500ms ile sinirli.
610
+ const cfg = this.cfg();
611
+ if (cfg.liveDiff === false) { this._textBefore = null; return; }
612
+ this._textBefore = captureTextSnapshot(this._cwd, this._snapBefore);
613
+ const intervalMs = Math.max(500, Number(cfg.liveDiffIntervalMs) || 2500);
614
+ this._liveTimer = setInterval(() => this.publishFileChanges(task.id), intervalMs);
615
+ // unref: bekleyen bir tarama zamanlayicisi surecin kapanmasini asla engellemesin.
616
+ if (this._liveTimer && typeof this._liveTimer.unref === "function") this._liveTimer.unref();
617
+ }
618
+
619
+ stopLiveDiff() {
620
+ if (this._liveTimer) { clearInterval(this._liveTimer); this._liveTimer = null; }
621
+ }
622
+
623
+ setMode(mode) {
624
+ const cfg = this.cfg();
625
+ cfg.approvalMode = mode === "auto" ? "auto" : "ask";
626
+ store.saveConfig(cfg);
627
+ this.emit("status", this.status());
628
+ }
629
+
630
+ start() {
631
+ if (this.running) return;
632
+ if (!this.cfg().autonomousConsentAcceptedAt) {
633
+ const error = new Error("Otonom CLI calistirma kosullari henuz kabul edilmedi. Dashboard'daki ilk kullanim uyarisini onaylayin.");
634
+ error.code = "AUTONOMOUS_CONSENT_REQUIRED";
635
+ throw error;
636
+ }
637
+ this.running = true;
638
+ this.publish("log", { level: "info", msg: `Motor basladi. Mod=${this.cfg().approvalMode}` }, null);
639
+ this.emit("status", this.status());
640
+ this.loop();
641
+ }
642
+
643
+ stop() {
644
+ this.running = false;
645
+ this.wake();
646
+ this.stopLiveDiff();
647
+ if (this.activeChild) {
648
+ try { this.activeChild.kill(); } catch {}
649
+ }
650
+ this.publish("log", { level: "warn", msg: "Motor durduruluyor; aktif CLI islemi sonlandirildi." });
651
+ this.emit("status", this.status());
652
+ }
653
+
654
+ wake() {
655
+ if (this._wakeResolve) {
656
+ clearTimeout(this._wakeTimer);
657
+ const resolve = this._wakeResolve;
658
+ this._wakeResolve = null;
659
+ resolve();
660
+ }
661
+ }
662
+
663
+ sleepWake(ms) {
664
+ return new Promise((resolve) => {
665
+ this._wakeResolve = resolve;
666
+ this._wakeTimer = setTimeout(() => { this._wakeResolve = null; resolve(); }, ms);
667
+ });
668
+ }
669
+
670
+ async loop() {
671
+ while (this.running) {
672
+ const task = store.nextPending();
673
+ if (!task) {
674
+ await this.sleepWake((this.cfg().pollSeconds || 15) * 1000);
675
+ continue;
676
+ }
677
+ try {
678
+ await this.runTask(task);
679
+ } catch (error) {
680
+ if (!this.salvage(task, error)) {
681
+ const found = store.findTask(task.id);
682
+ task.status = "failed";
683
+ task.error = error.message;
684
+ task.finishedAt = new Date().toISOString();
685
+ if (found) store.moveTask(found.state, "failed", task);
686
+ this.publish("log", { level: "error", msg: `HATA: ${error.message}` }, task.id);
687
+ if (task.kind === "operator-chat") {
688
+ const failure = classifyCliError(error);
689
+ this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: task.parentTaskId, status: "failed", error: failure.summary }, task.id);
690
+ }
691
+ this.emit("queue");
692
+ }
693
+ } finally {
694
+ this.stopLiveDiff();
695
+ this._textBefore = null;
696
+ this.busy = false;
697
+ this.current = null;
698
+ this.activeChild = null;
699
+ this.emit("status", this.status());
700
+ }
701
+ }
702
+ }
703
+
704
+ // Uzman agent'i (operatorun altindaki ekip) cfg.agents'ten cozup calistirir.
705
+ invokeAgent(agentName, prompt, cfg, meta = {}) {
706
+ const configuredAgent = cfg.agents[agentName];
707
+ const agent = configuredAgent && cliRegistry.effectiveAgent(configuredAgent, cfg);
708
+ if (!agent) return Promise.reject(new Error(`Agent tanimsiz: ${agentName}`));
709
+ return this.runCli(agentName, agent, prompt, cfg, meta);
710
+ }
711
+
712
+ // Operatoru, uzman agent'lardan BAGIMSIZ olarak dogrudan CLI'dan (claude/codex/gemini/opencode)
713
+ // calistirir. operator.md rolunu bu CLI giyer.
714
+ invokeOperator(cli, prompt, cfg, meta = {}) {
715
+ const agent = cliRegistry.operatorSpec(cli, cfg);
716
+ if (!agent) return Promise.reject(new Error(`Operator CLI tanimsiz veya kurulu degil: ${cli}`));
717
+ return this.runCli(cli, agent, prompt, cfg, meta);
718
+ }
719
+
720
+ runCli(displayName, agent, prompt, cfg, meta = {}) {
721
+ return new Promise((resolve, reject) => {
722
+ const count = store.bumpCallCount();
723
+ if (count > (cfg.dailyCallBudget || Number.MAX_SAFE_INTEGER)) {
724
+ return reject(new Error(`Gunluk cagri butcesi asildi (${cfg.dailyCallBudget}).`));
725
+ }
726
+
727
+ const callId = `${this.current?.id || "run"}-${++this.callSequence}`;
728
+ let promptFile = "";
729
+ let useStdin = true;
730
+ const rawArgs = (agent.args || []).map((arg) => {
731
+ if (String(arg).includes("{PROMPT}")) {
732
+ useStdin = false;
733
+ return String(arg).replaceAll("{PROMPT}", prompt);
734
+ }
735
+ if (String(arg).includes("{PROMPT_FILE}")) {
736
+ useStdin = false;
737
+ if (!promptFile) {
738
+ const promptDir = path.join(store.ROOT, "state", "prompts");
739
+ fs.mkdirSync(promptDir, { recursive: true });
740
+ promptFile = path.join(promptDir, `${callId}.md`);
741
+ fs.writeFileSync(promptFile, prompt, "utf8");
742
+ }
743
+ return String(arg).replaceAll("{PROMPT_FILE}", promptFile);
744
+ }
745
+ return String(arg);
746
+ });
747
+ const command = cliRegistry.buildCommand(agent.cmd, rawArgs);
748
+ const file = command.file;
749
+ const args = command.args;
750
+ const cwd = this._cwd || path.resolve(store.WORK_BASE, cfg.workingDir || ".");
751
+ const started = Date.now();
752
+ let stdout = "", stderr = "", settled = false, timedOut = false, silenceTimedOut = false;
753
+ let timer, silenceTimer, progressTimer;
754
+ const base = { callId, agent: displayName, stage: meta.stage || "agent", assignmentId: meta.assignmentId || null };
755
+
756
+ this.current = { id: this.current?.id, stage: base.stage, agent: displayName, callId };
757
+ this.emit("status", this.status());
758
+ this.publish("activity", { ...base, kind: "process.started", cmd: agent.cmd, args: rawArgs, cwd });
759
+
760
+ let child;
761
+ try { child = spawn(file, args, { cwd, env: cliRegistry.agentEnvironment(agent), windowsHide: true, shell: command.shell, windowsVerbatimArguments: !!command.verbatim }); }
762
+ catch (error) { return reject(error); }
763
+ this.activeChild = child;
764
+ child.stdout.setEncoding("utf8");
765
+ child.stderr.setEncoding("utf8");
766
+
767
+ const timeoutMs = Math.max(10, agent.timeoutSeconds || cfg.agentTimeoutSeconds || 900) * 1000;
768
+ const silenceTimeoutMs = Math.max(1, agent.silenceTimeoutSeconds || cfg.cliSilenceTimeoutSeconds || 300) * 1000;
769
+ let lastOutputAt = Date.now();
770
+ const terminateTree = () => {
771
+ if (isWin && child.pid) {
772
+ try { spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" }); } catch {}
773
+ } else {
774
+ try { child.kill("SIGKILL"); } catch {}
775
+ }
776
+ };
777
+ const armSilenceTimer = () => {
778
+ clearTimeout(silenceTimer);
779
+ silenceTimer = setTimeout(() => {
780
+ if (settled) return;
781
+ silenceTimedOut = true;
782
+ this.publish("activity", { ...base, kind: "process.silence-timeout", silenceTimeoutMs, elapsedMs: Date.now() - started });
783
+ terminateTree();
784
+ }, silenceTimeoutMs);
785
+ };
786
+ armSilenceTimer();
787
+ progressTimer = setInterval(() => {
788
+ if (settled) return;
789
+ this.publish("activity", { ...base, kind: "process.progress", elapsedMs: Date.now() - started, silentMs: Date.now() - lastOutputAt, silenceTimeoutMs });
790
+ }, 15000);
791
+ timer = setTimeout(() => {
792
+ if (settled) return;
793
+ timedOut = true;
794
+ this.publish("activity", { ...base, kind: "process.timeout", timeoutMs });
795
+ terminateTree();
796
+ }, timeoutMs);
797
+
798
+ child.stdout.on("data", (data) => {
799
+ const text = String(data);
800
+ stdout += text;
801
+ lastOutputAt = Date.now();
802
+ armSilenceTimer();
803
+ this.publish("activity", { ...base, kind: "stdout", text });
804
+ });
805
+ child.stderr.on("data", (data) => {
806
+ const text = String(data);
807
+ stderr += text;
808
+ lastOutputAt = Date.now();
809
+ armSilenceTimer();
810
+ this.publish("activity", { ...base, kind: "stderr", text });
811
+ });
812
+ child.on("error", (error) => {
813
+ if (settled) return;
814
+ settled = true;
815
+ clearTimeout(timer);
816
+ clearTimeout(silenceTimer);
817
+ clearInterval(progressTimer);
818
+ this.activeChild = null;
819
+ if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
820
+ this.publish("activity", { ...base, kind: "process.failed", error: error.message, durationMs: Date.now() - started });
821
+ reject(error);
822
+ });
823
+ child.on("close", (code, signal) => {
824
+ if (settled) return;
825
+ settled = true;
826
+ clearTimeout(timer);
827
+ clearTimeout(silenceTimer);
828
+ clearInterval(progressTimer);
829
+ this.activeChild = null;
830
+ if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
831
+ const durationMs = Date.now() - started;
832
+ this.publish("activity", { ...base, kind: "process.finished", exitCode: code, signal, durationMs, reason: silenceTimedOut ? "silence-timeout" : (timedOut ? "timeout" : null) });
833
+ if (silenceTimedOut) return reject(new Error(`CLI_STALLED: ${agent.cmd} ${Math.round(silenceTimeoutMs / 1000)} saniye boyunca cikti uretmedi ve otomatik durduruldu.`));
834
+ if (timedOut) return reject(new Error(`${agent.cmd} ${Math.round(timeoutMs / 1000)} saniyede zaman asimina ugradi${signal ? ` (signal=${signal})` : ""}. ${clip(stdout || stderr, 500)}`));
835
+ if (code !== 0) return reject(new Error(`${agent.cmd} cikis kodu ${code ?? "yok"}${signal ? ` (signal=${signal})` : ""}. ${clip(stderr || stdout, 500)}`));
836
+ const normalized = normalizeCliOutput(agent, stdout);
837
+ if (normalized.error) return reject(new Error(`OpenCode model/provider hatasi: ${clip(normalized.error, 500)}`));
838
+ if (!normalized.text) return reject(new Error(`${agent.cmd} kullanilabilir cikti dondurmedi.${stderr ? ` ${clip(stderr, 300)}` : ""}`));
839
+ resolve({ text: normalized.text, stderr: stderr.trim(), exitCode: code, durationMs, callId });
840
+ });
841
+
842
+ // Prompt argumanla/dosyayla verilse bile stdin MUTLAKA kapatilmali. OpenCode gibi CLI'lar
843
+ // stdin TTY degilse borudan mesaj okur; EOF gelmezse model cagrisina hic gecmeden bloke olur.
844
+ child.stdin.on("error", () => {});
845
+ child.stdin.end(useStdin ? prompt : "");
846
+ });
847
+ }
848
+
849
+ agentCatalog(cfg, operatorName) {
850
+ return Object.entries(cfg.agents)
851
+ .filter(([name, agent]) => name !== operatorName && agent.enabled !== false && agentUsable(agent) && !this.unhealthyAgents.has(name))
852
+ .map(([name, a]) => ({
853
+ name,
854
+ description: a.description || "",
855
+ capabilities: [...capabilitiesFor(a)],
856
+ allowedKinds: roleAllowedKinds(a) || ["implement", "review", "research", "plan"].filter((kind) => supportsKind(a, kind)),
857
+ roleFile: a.roleFile || "",
858
+ costTier: a.costTier || "standard",
859
+ }));
860
+ }
861
+
862
+ async invokeOperatorJson(operatorCli, prompt, cfg, stage, label) {
863
+ const retries = Math.max(0, cfg.operator?.protocolRetries ?? 1);
864
+ let lastError;
865
+ let correction = "";
866
+ for (let attempt = 0; attempt <= retries; attempt++) {
867
+ try {
868
+ const response = await this.invokeOperator(operatorCli, prompt + correction, cfg, { stage });
869
+ return parseJson(response.text, label);
870
+ } catch (error) {
871
+ lastError = error;
872
+ // Yalnizca gercek JSON protokol hatasi yeniden denenir. CLI'nin sessiz kalmasi,
873
+ // auth/timeout veya proses hatasi ayni operatoru tekrar calistirmamalidir.
874
+ if (!/gecerli JSON dondurmedi/i.test(String(error?.message || ""))) throw error;
875
+ if (attempt >= retries) break;
876
+ correction = `\n\nONCEKI CEVAP PROTOKOLE UYMADI: ${error.message}\nAciklama veya Markdown eklemeden yalnizca istenen JSON nesnesini yeniden dondur.`;
877
+ this.publish("log", { level: "warn", msg: `${label} protokol hatasi; operator yeniden deneniyor (${attempt + 2}/${retries + 1}).` });
878
+ }
879
+ }
880
+ throw lastError;
881
+ }
882
+
883
+ // Insertion sirasi tamamlanma sirasidir; en son biten denetim en gecerli karardir.
884
+ latestReview(state) {
885
+ return Object.values(state.results || {}).reverse()
886
+ .find((result) => result.kind === "review" && result.status === "completed" && result.verdict) || null;
887
+ }
888
+
889
+ // Operatorun karar evresine verilen takim kaydi. Ham state dokumu yerine sonuc odakli
890
+ // bir ozet uretir; her sonucu kendi icinde ortadan kirpar ki en yeni denetim karari
891
+ // (metnin sonundaki VERDICT satiri) butce asiminda kaybolmasin.
892
+ teamDigest(state, cfg) {
893
+ const results = Object.values(state.results || {});
894
+ const budget = cfg.teamContextCharBudget || 30000;
895
+ const perResult = Math.max(900, Math.floor(budget / Math.max(1, results.length)));
896
+ const digest = {
897
+ round: state.round,
898
+ completionCriteria: state.criteria,
899
+ results: results.map((result) => ({
900
+ id: result.id, agent: result.agent, kind: result.kind, status: result.status,
901
+ ...(result.verdict ? { verdict: result.verdict } : {}),
902
+ result: clipMiddle(result.result, perResult),
903
+ })),
904
+ };
905
+ return clipMiddle(JSON.stringify(digest, null, 2), budget);
906
+ }
907
+
908
+ operatorPrompt(task, cfg, memory, state, phase) {
909
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
910
+ // Operatör rolü daima operator.md'dir; operatör bir CLI'dir, uzman agent'lar onun altında çalışır.
911
+ const roleFile = cfg.operator?.roleFile || "roles/operator.md";
912
+ const role = store.readRole(path.basename(roleFile));
913
+ const catalog = this.agentCatalog(cfg, operatorCli);
914
+ const skillContext = `${task.prompt}\n${phase === "review" ? Object.values(state.results || {}).map((result) => result.instruction || "").join("\n") : ""}`;
915
+ const skillDiscovery = skillRegistry.discover(cfg, skillContext);
916
+ const skillCatalog = skillDiscovery.catalog;
917
+ const skillStats = skillDiscovery.stats;
918
+ const enabledSkillNames = skillRegistry.enabledSkills(cfg).map((skill) => skill.name);
919
+ // Beceri envanteri motor tarafindan uretilen OTORITER veridir. Operatore TAM etkin listeyi
920
+ // (sayi + tum adlar) DAIMA veririz; boylece "kac beceri var" gibi sorular icin operatorun
921
+ // kendi CLI'sinin dahili becerilerini veya bir alt agent'in iddiasini kullanmasi gerekmez.
922
+ // Eslesen kisa liste yalnizca DELEGASYONA iliştirme icindir, envanterin tamami degildir.
923
+ const skillSection = skillStats.enabled
924
+ ? `## Beceriler (OTORITER kaynak)\nSistemde tam olarak ${skillStats.enabled} etkin beceri var: ${enabledSkillNames.join(", ")}.\n` +
925
+ `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` +
926
+ (skillCatalog.length
927
+ ? `Bu goreve en ilgili ${skillCatalog.length} beceri (delegasyonun skills alanina yalnizca bunlardan uygun olanlari ekle):\n${JSON.stringify(skillCatalog, null, 2)}\n`
928
+ : `Bu goreve gore taramada eslesme cikmadi; bu delegasyonlarda beceri iliştirme.\n`)
929
+ : "";
930
+ // Beceriler tamamen opsiyoneldir ve YALNIZCA kullanici etkinlestirdiginde katalogda gorunur.
931
+ // Operator, assignment'a yalnizca bu kataloktaki adlari "skills" dizisinde iliştirebilir.
932
+ const skillField = skillCatalog.length ? `, "skills":["beceri-adi"]` : "";
933
+ const skillProtocol = skillCatalog.length
934
+ ? ` BECERI: Kisa listedeki gercekten ilgili adlari skills dizisine ekle; ilgisiz veya liste disi ad kullanma, uygun yoksa alani bos birak.`
935
+ : "";
936
+ const protocol = phase === "plan"
937
+ ? `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). ` +
938
+ `Gorev yalnizca bir bilgi/soru ise ve yaniti sana verilen baglamdan (or. Beceriler bolumu, agent katalogu, proje hafizasi) dogrudan biliyorsan, HICBIR delegasyon acmadan: {"status":"complete", "final":"kisa ve net cevap", "verification":"cevabin dayandigi kaynak"}. Gercek is gerektiren gorevi bu kestirmeyle atlatma.${skillProtocol}`
939
+ : `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` +
940
+ `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.`;
941
+ const strategy = task.executionMode === "fast"
942
+ ? "HIZLI MOD: Kucuk bir is. Tek bir implementation agenti kullan. Ayri planlama veya review delegasyonu acma; uzman raporu hedefi karsiliyorsa ilk degerlendirmede tamamla."
943
+ : task.executionMode === "deep"
944
+ ? "DERIN MOD: Isi uygun uzmanliklar arasinda dagit — planlama, uygulama, test ve bagimsiz inceleme icin AYRI ve dogru uzmanlari kullan."
945
+ : "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.";
946
+ return `${role}\n\n---\n## Operator protokolu\n${protocol}\n` +
947
+ `Yalnizca katalogdaki agent adlarini kullan. Her assignment kind degeri secilen agentin allowedKinds listesinde olmali. Kendine gorev atama. Ayni delegasyon id'sini tekrar kullanma.\n` +
948
+ `Bir agent basarisiz veya kullanilamaz raporlandiysa ayni isi ona tekrar verme; katalogdaki alternatif bir uzmani sec.\n` +
949
+ `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` +
950
+ `## Calisma stratejisi\n${strategy}\n` +
951
+ `## Agent katalogu\n${JSON.stringify(catalog, null, 2)}\n` +
952
+ skillSection +
953
+ `## Kullanici gorevi\n${task.prompt}\n` +
954
+ `## Calisma klasoru\n${this._cwd}\n` +
955
+ `## Proje hafizasi (gecmis baglam — gecmis teslimatlar mevcut gorevi TAMAMLANMIS SAYDIRMAZ; kanit olarak degil ipucu olarak kullan)\n${clip(memory, cfg.memoryCharBudget || 8000)}\n` +
956
+ (phase === "review"
957
+ ? `## Son bagimsiz denetim\n${(() => { const review = this.latestReview(state); return review ? `${review.id} (${review.agent}) → VERDICT: ${review.verdict}` : "Henuz tamamlanmis denetim yok."; })()}\n` +
958
+ `## Takim calisma kaydi\n${this.teamDigest(state, cfg)}\n`
959
+ : "");
960
+ }
961
+
962
+ specialistPrompt(task, cfg, assignment, state) {
963
+ const agent = cfg.agents[assignment.agent];
964
+ const roleByKind = { implement: "executor.md", review: "reviewer.md", plan: "planner.md" };
965
+ const selectedRole = roleByKind[assignment.kind] || (agent.roleFile ? path.basename(agent.roleFile) : "executor.md");
966
+ const role = store.readRole(selectedRole);
967
+ const instructionByKind = {
968
+ implement: "Delegasyonu calisma klasorunde uygula, degisiklikleri dogrula ve rolundeki teslimat formatinda raporla.",
969
+ review: "Teslimati bagimsiz ve salt okunur olarak incele; gerekli kontrolleri calistir, dosyalari degistirme ve rolundeki karar formatinda raporla.",
970
+ plan: "Calisma klasorunu salt okunur olarak incele; hicbir dosyayi degistirmeden uygulanabilir plani rolundeki formatta raporla.",
971
+ research: "Delegasyon kapsaminda kanit topla; kaynaklari ve belirsizlikleri ayirarak kisa bir sonuc raporla.",
972
+ };
973
+ const completed = Object.values(state.results).map((r) => ({
974
+ id: r.id, agent: r.agent, instruction: r.instruction, ...(r.verdict ? { verdict: r.verdict } : {}), result: clipMiddle(r.result, 5000),
975
+ }));
976
+ // Progressive disclosure: operatorun bu delegasyona sectigi becerilerin tam govdesini YIGMAYIZ;
977
+ // yalnizca ad + kisa ozet + rehber dosya yolunu veririz. Uzman gercekten ihtiyac duyarsa dosyayi
978
+ // kendisi okur. Operator hicbir beceri secmediyse (veya beceriler kapali ise) blok bostur.
979
+ const skills = skillRegistry.resolveForAssignment(assignment.skills, cfg);
980
+ const skillRefs = skillRegistry.toPromptRefs(skills, cfg.skills?.referenceCharBudget || 1200);
981
+ return `${role}\n\n---\n## Takim agenti protokolu\n` +
982
+ `Operator sana asagidaki isi devretti. ${instructionByKind[assignment.kind] || instructionByKind.implement} ` +
983
+ `Planda olmayan riskli bir is gerekiyorsa yapma; BLOCKED olarak bildir. Yalnizca gercekten gozlemledigin veya dogruladigin sonuclari yaz.\n` +
984
+ (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` : "") +
985
+ `## Ana hedef\n${task.prompt}\n## Delegasyon\nID: ${assignment.id}\n${assignment.instruction}\n` +
986
+ `## Onceki tamamlanan takim isleri\n${clip(JSON.stringify(completed, null, 2), cfg.teamContextCharBudget || 30000)}`;
987
+ }
988
+
989
+ async runAssignments(task, cfg, assignments, state) {
990
+ const pending = assignments.slice();
991
+ while (pending.length) {
992
+ let index = pending.findIndex((a) => a.dependsOn.every((id) => state.results[id]?.status === "completed"));
993
+ if (index < 0) {
994
+ index = pending.findIndex((a) => a.dependsOn.some((id) => state.results[id] && state.results[id].status !== "completed"));
995
+ if (index >= 0) {
996
+ const blocked = pending.splice(index, 1)[0];
997
+ state.usedIds.push(blocked.id);
998
+ const failedDeps = blocked.dependsOn.filter((id) => state.results[id]?.status !== "completed");
999
+ state.results[blocked.id] = { ...blocked, status: "blocked", result: `Bagimli gorev basarisiz: ${failedDeps.join(", ")}` };
1000
+ 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() };
1001
+ state.messages.push(blockedMessage);
1002
+ this.publish("message", blockedMessage, task.id);
1003
+ continue;
1004
+ }
1005
+ throw new Error(`Delegasyon bagimliliklari cozumlenemedi: ${pending.map((a) => a.id).join(", ")}`);
1006
+ }
1007
+ const assignment = pending.splice(index, 1)[0];
1008
+ state.usedIds.push(assignment.id);
1009
+ state.messages.push({ from: task.operatorCli, to: assignment.agent, messageType: "delegation", assignmentId: assignment.id, body: assignment.instruction, at: new Date().toISOString() });
1010
+ this.publish("message", state.messages[state.messages.length - 1], task.id);
1011
+ if (assignment.routingReason) this.publish("log", { level: "info", msg: `Otomatik yonlendirme: ${assignment.routingReason}` }, task.id);
1012
+ if (assignment.renamedFrom) this.publish("log", { level: "warn", msg: `Operator delegasyon kimligini yineledi; ${assignment.renamedFrom} otomatik olarak ${assignment.id} yapildi.` }, task.id);
1013
+ this.publish("log", { level: "stage", msg: `DELEGE ${assignment.id} -> ${assignment.agent}` }, task.id);
1014
+ if (assignment.skills?.length) this.publish("log", { level: "info", msg: `Beceriler: ${assignment.skills.join(", ")}` }, task.id);
1015
+ try {
1016
+ const response = await this.invokeAgent(
1017
+ assignment.agent,
1018
+ this.specialistPrompt(task, cfg, assignment, state),
1019
+ cfg,
1020
+ { stage: "delegate", assignmentId: assignment.id }
1021
+ );
1022
+ const result = { ...assignment, status: "completed", result: response.text, durationMs: response.durationMs, callId: response.callId };
1023
+ if (assignment.kind === "review") {
1024
+ result.verdict = extractVerdict(response.text);
1025
+ this.publish("log", { level: "info", msg: `Denetim ${assignment.id}: VERDICT ${result.verdict || "BELIRSIZ"}` }, task.id);
1026
+ }
1027
+ state.results[assignment.id] = result;
1028
+ const message = { from: assignment.agent, to: task.operatorCli, messageType: "result", assignmentId: assignment.id, body: response.text, at: new Date().toISOString() };
1029
+ state.messages.push(message);
1030
+ this.publish("message", message, task.id);
1031
+ } catch (error) {
1032
+ const failure = classifyCliError(error);
1033
+ if (QUARANTINE_CLI_ERRORS.has(failure.code)) {
1034
+ this.unhealthyAgents.set(assignment.agent, { code: failure.code, at: new Date().toISOString() });
1035
+ cfg.runtimeUnavailableAgents = [...this.unhealthyAgents.keys()];
1036
+ }
1037
+ state.results[assignment.id] = { ...assignment, status: "failed", result: failure.summary, error: failure };
1038
+ const message = { from: assignment.agent, to: task.operatorCli, messageType: "failure", assignmentId: assignment.id, body: `${failure.summary}\n${failure.action}`, errorCode: failure.code, at: new Date().toISOString() };
1039
+ state.messages.push(message);
1040
+ this.publish("message", message, task.id);
1041
+ this.publish("log", { level: "warn", msg: `${assignment.agent} kullanilamadi [${failure.code}]. Operator alternatif agent sececek.` }, task.id);
1042
+ }
1043
+ task.teamState = state;
1044
+ store.saveTask("pending", task);
1045
+ }
1046
+ }
1047
+
1048
+ isRisky(text, cfg) {
1049
+ const lower = String(text).toLowerCase();
1050
+ return (cfg.riskyPatterns || []).some((pattern) => lower.includes(String(pattern).toLowerCase()));
1051
+ }
1052
+
1053
+ async runTask(task) {
1054
+ const baseCfg = this.cfg();
1055
+ if (!baseCfg.autonomousConsentAcceptedAt) throw new Error("Otonom CLI calistirma kosullari kabul edilmeden gorev calistirilamaz.");
1056
+ if (task.kind === "operator-chat") return this.runChatTask(task, baseCfg);
1057
+ task.executionMode = resolveExecutionMode(task);
1058
+ const cfg = applyExecutionPolicy(baseCfg, task.executionMode);
1059
+ const operatorCli = task.operatorCli || cfg.operator?.cli;
1060
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Gecerli bir operator CLI secilmedi (claude/codex/gemini/opencode).");
1061
+ task.operatorCli = operatorCli;
1062
+ this.busy = true;
1063
+ this.current = { id: task.id, stage: "operator", agent: operatorCli };
1064
+ this._cwd = path.resolve(store.WORK_BASE, task.targetDir || cfg.workingDir || ".");
1065
+ this._snapBefore = snapshotDir(this._cwd);
1066
+ this.startLiveDiff(task);
1067
+ this.publish("log", { level: "task", msg: `GOREV ${task.id}: ${task.prompt}` }, task.id);
1068
+ // Otomatik surumleme: gorev CALISMADAN ONCE calisma klasorunun bir surumunu al ki ajan
1069
+ // mevcut kodu bozarsa bu gorev oncesine tek tikla donulebilsin. Yalnizca ilk girus'te
1070
+ // (approval sonrasi devam da ayni checkpoint'i korur). Basarisizsa gorevi asla oldurmez.
1071
+ if (baseCfg.versioning !== false && !task.checkpointId) {
1072
+ const cp = checkpoints.createCheckpoint(this._cwd, { taskId: task.id, label: task.prompt, kind: "pre-task", retention: baseCfg.versioningRetention });
1073
+ if (cp.ok) {
1074
+ task.checkpointId = cp.id;
1075
+ store.saveTask("pending", task);
1076
+ this.publish("log", { level: "info", msg: `Surum alindi (${cp.fileCount} dosya · ${cp.backend}); bu gorev oncesine geri donulebilir.` }, task.id);
1077
+ } else if (cp.skipped) {
1078
+ this.publish("log", { level: "warn", msg: `Surum alinamadi: ${cp.reason}` }, task.id);
1079
+ }
1080
+ }
1081
+ this.publish("log", { level: "info", msg: `Calisma modu: ${task.executionMode.toUpperCase()} · en fazla ${cfg.operator.maxRounds} tur / ${cfg.operator.maxDelegationsPerRound} delegasyon` }, task.id);
1082
+ this.emit("status", this.status());
1083
+ this.emit("queue");
1084
+
1085
+ cfg.runtimeUnavailableAgents = [...this.unhealthyAgents.keys()];
1086
+ const memory = store.getMemory(cfg.memoryCharBudget);
1087
+ const state = task.teamState || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1088
+ const usedIds = new Set(state.usedIds || []);
1089
+
1090
+ let assignments;
1091
+ if (state.plan && task.approved) {
1092
+ assignments = normalizeAssignments(state.plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1093
+ assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1094
+ this.publish("log", { level: "info", msg: "Onaylanmis operator plani zorunlu rol zinciri korunarak devam ettiriliyor." }, task.id);
1095
+ } else {
1096
+ // Bilgi sorusu kestirmesi: operator, delege edilecek bir is olmadan yaniti dogrudan
1097
+ // verdiginde (or. "kac beceri var") tek turda tamamla. Boylece motorun otoriter verisi
1098
+ // bir alt agente aktarilirken kaybolmaz/carpitilmaz ve zorunlu assignment semasi bu
1099
+ // tur sorularda gorevi bosuna oldurmez. ANCAK gorev acikca bir yapim/degisiklik isiyse
1100
+ // kestirmeye izin verilmez: operator (cogunlukla proje hafizasindaki gecmis teslimati
1101
+ // "zaten yapildi" sanip) is uretmeden kapatmaya calisir. Once yeniden planlama isteriz;
1102
+ // israr ederse gorevi sahte basari yerine net bir hatayla dururuz.
1103
+ const requiresWork = taskRequiresDelegation(task.prompt);
1104
+ const shortcutRetries = requiresWork ? Math.max(1, cfg.operator?.protocolRetries ?? 1) : 0;
1105
+ let plan, shortcutAttempt = 0, planCorrection = "";
1106
+ while (true) {
1107
+ plan = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "plan") + planCorrection, cfg, "operator-plan", "Operator plani");
1108
+ const isShortcut = plan && String(plan.status).toLowerCase() === "complete" && !Array.isArray(plan.assignments);
1109
+ if (!isShortcut) break;
1110
+ if (!requiresWork) {
1111
+ if (!String(plan.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1112
+ this.publish("log", { level: "info", msg: "Operator gorevi delegasyona gerek gormeden dogrudan yanitladi." }, task.id);
1113
+ task.teamState = state;
1114
+ return this.complete(task, state, "done", String(plan.final), plan);
1115
+ }
1116
+ if (shortcutAttempt++ >= shortcutRetries) {
1117
+ throw new Error("Operator bu yapim gorevini delege etmeden kapatmaya calisti. Proje hafizasindaki gecmis teslimatlar gorevi tamamlanmis saymaz; en az bir uygulama delegasyonu gerekir.");
1118
+ }
1119
+ this.publish("log", { level: "warn", msg: "Operator yapim gorevini delegasyonsuz kapatmaya calisti; hafiza gecmisi kanit sayilmaz, yeniden planlama isteniyor." }, task.id);
1120
+ 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.";
1121
+ }
1122
+ assignments = normalizeAssignments(plan.assignments, cfg, operatorCli, usedIds, task.prompt);
1123
+ assignments = ensureBalancedRoleChain(assignments, cfg, operatorCli, task, usedIds);
1124
+ state.plan = { summary: String(plan.summary || ""), completionCriteria: Array.isArray(plan.completionCriteria) ? plan.completionCriteria.map(String) : [], assignments };
1125
+ state.criteria = state.plan.completionCriteria;
1126
+ state.operatorDecisions.push({ round: 0, ...state.plan });
1127
+ task.teamState = state;
1128
+ task.planPreview = JSON.stringify(state.plan, null, 2);
1129
+ store.saveTask("pending", task);
1130
+ if (cfg.approvalMode === "ask" && this.isRisky(task.planPreview, cfg)) {
1131
+ task.status = "awaiting-approval";
1132
+ task.planHash = store.hashText(task.planPreview);
1133
+ store.moveTask("pending", "approval", task);
1134
+ this.publish("log", { level: "warn", msg: "Riskli operator plani insan onayina alindi." }, task.id);
1135
+ this.busy = false;
1136
+ this.current = null;
1137
+ this.emit("queue");
1138
+ this.emit("status", this.status());
1139
+ this.stopLiveDiff();
1140
+ this._textBefore = null;
1141
+ return;
1142
+ }
1143
+ }
1144
+
1145
+ const maxRounds = Math.max(1, cfg.operator?.maxRounds || 6);
1146
+ const maxRecoveryRounds = Math.max(0, cfg.operator?.maxInfrastructureRecoveryRounds ?? 2);
1147
+ let recoveryRounds = 0;
1148
+ while (this.running && state.round < maxRounds + recoveryRounds) {
1149
+ state.round++;
1150
+ await this.runAssignments(task, cfg, assignments, state);
1151
+ const infrastructureFailures = assignments.filter((assignment) => {
1152
+ const result = state.results[assignment.id];
1153
+ return result?.status === "failed" && RECOVERABLE_CLI_ERRORS.has(result.error?.code);
1154
+ });
1155
+ if (infrastructureFailures.length && recoveryRounds < maxRecoveryRounds) {
1156
+ recoveryRounds++;
1157
+ this.publish("log", { level: "warn", msg: `CLI altyapi hatasi tur butcesinden dusulmedi; ${maxRecoveryRounds - recoveryRounds} ek kurtarma turu kaldi.` }, task.id);
1158
+ }
1159
+ if (task.executionMode === "fast" && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")) {
1160
+ const reports = assignments.map((assignment) => state.results[assignment.id].result).join("\n\n");
1161
+ this.publish("log", { level: "info", msg: "FAST mod: uzman teslimati basarili; ikinci operator degerlendirme cagrisi atlandi." }, task.id);
1162
+ return this.complete(task, state, "done", reports, {});
1163
+ }
1164
+ // PASS hizli yolu: turdaki tum delegasyonlar tamamlandi ve turun kendi bagimsiz
1165
+ // denetimi PASS verdiyse operatorun ikinci degerlendirme cagrisi bilgi eklemez,
1166
+ // yalnizca dakikalar kaybettirir. Bayat bir PASS'in yeni turu kapatmamasi icin
1167
+ // karar en guncel denetim olmalidir. operator.passFastPath=false ile kapatilabilir.
1168
+ const roundReview = assignments
1169
+ .map((assignment) => state.results[assignment.id])
1170
+ .filter((result) => result?.kind === "review" && result.status === "completed" && result.verdict)
1171
+ .pop();
1172
+ if (cfg.operator?.passFastPath !== false
1173
+ && assignments.every((assignment) => state.results[assignment.id]?.status === "completed")
1174
+ && roundReview?.verdict === "PASS"
1175
+ && roundReview === this.latestReview(state)) {
1176
+ this.publish("log", { level: "ok", msg: `Denetim ${roundReview.id} PASS verdi; operator degerlendirme cagrisi atlandi ve teslimat tamamlandi.` }, task.id);
1177
+ const completedWork = Object.values(state.results).filter((result) => result.status === "completed");
1178
+ const final = `Teslimat bagimsiz denetimden gecti (${roundReview.id} → VERDICT: PASS).\n` +
1179
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1180
+ return this.complete(task, state, "done", final, { verification: `${roundReview.agent} → VERDICT: PASS` });
1181
+ }
1182
+ const decision = await this.invokeOperatorJson(operatorCli, this.operatorPrompt(task, cfg, memory, state, "review"), cfg, "operator-review", "Operator karari");
1183
+ state.operatorDecisions.push({ round: state.round, ...decision });
1184
+ task.teamState = state;
1185
+ store.saveTask("pending", task);
1186
+ if (String(decision.status).toLowerCase() === "complete") {
1187
+ if (!String(decision.final || "").trim()) throw new Error("Operator complete dedi fakat final sonucu bos birakti.");
1188
+ return this.complete(task, state, "done", String(decision.final), decision);
1189
+ }
1190
+ if (String(decision.status).toLowerCase() !== "continue") throw new Error("Operator status alani continue veya complete olmali.");
1191
+ assignments = normalizeAssignments(decision.assignments, cfg, operatorCli, new Set(state.usedIds), task.prompt);
1192
+ // Inceleme dongusu valisi: bagimsiz denetim PASS verdiyse ve operator yalnizca yeni
1193
+ // inceleme turlari acmak istiyorsa dongu burada kesilir. Sonsuz review ping-pong'u
1194
+ // hem tur butcesini tuketiyor hem de basarili teslimati kullaniciya geciktiriyordu.
1195
+ const lastReview = this.latestReview(state);
1196
+ if (lastReview?.verdict === "PASS" && assignments.every((assignment) => assignment.kind === "review")) {
1197
+ this.publish("log", { level: "warn", msg: `Denetim ${lastReview.id} PASS verdi; yalnizca yeni inceleme iceren tur acilmadi ve teslimat tamamlandi.` }, task.id);
1198
+ const final = `Teslimat bagimsiz denetimden gecti (${lastReview.id} → VERDICT: PASS). ` +
1199
+ `Operatorun ek inceleme talebi tur butcesini korumak icin motor tarafindan sonlandirildi.` +
1200
+ (String(decision.reason || "").trim() ? `\nOperator notu: ${clip(decision.reason, 600)}` : "");
1201
+ return this.complete(task, state, "done", final, { verification: `${lastReview.agent} → VERDICT: PASS` });
1202
+ }
1203
+ }
1204
+ if (!this.running) throw new Error("Motor durduruldu; gorev tamamlanmadan kesildi.");
1205
+ return this.finishExhausted(task, state, recoveryRounds);
1206
+ }
1207
+
1208
+ // Tur butcesi doldugunda tamamlanmis isi cope atmak yerine uyarili kismi teslimat yapar.
1209
+ // Kullanicinin gordugu sonuc "saatlerce bekledim ve hata aldim" degil, "teslimat hazir,
1210
+ // su riskler acik kaldi" olmalidir. Hic tamamlanan is yoksa eski davranis korunur.
1211
+ finishExhausted(task, state, recoveryRounds) {
1212
+ const completedWork = Object.values(state.results || {}).filter((result) => result.status === "completed");
1213
+ if (!completedWork.length) {
1214
+ throw new Error(`Operator ${state.round} tur sonunda gorevi tamamlayamadi${recoveryRounds ? ` (${recoveryRounds} CLI kurtarma turu kullanildi)` : ""}.`);
1215
+ }
1216
+ const lastReview = this.latestReview(state);
1217
+ const lastDecision = [...(state.operatorDecisions || [])].reverse().find((decision) => String(decision.status).toLowerCase() === "continue");
1218
+ const warnings = [
1219
+ `Tur butcesi doldu (${state.round} tur); operator complete karari veremeden teslimat kapatildi.`,
1220
+ ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} → VERDICT: ${lastReview.verdict}.`] : []),
1221
+ ...(lastDecision && String(lastDecision.reason || "").trim() ? [`Operatorun acik biraktigi konu: ${clip(lastDecision.reason, 400)}`] : []),
1222
+ ];
1223
+ this.publish("log", { level: "warn", msg: `Tur butcesi doldu; tamamlanan is kismi teslimat olarak kapatiliyor (${completedWork.length} tamamlanmis delegasyon).` }, task.id);
1224
+ const final = `KISMI TESLIMAT: Tur butcesi doldugu icin gorev, tamamlanan isle kapatildi.\n` +
1225
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n") +
1226
+ `\nAcik kalan konular teslimat uyarilarinda listelendi.`;
1227
+ return this.complete(task, state, "done", final, {
1228
+ warnings,
1229
+ verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1230
+ });
1231
+ }
1232
+
1233
+ // Gorev beklenmedik bir hatayla kesildiginde (protokol hatasi, butce asimi vb.) somut is
1234
+ // uretilmisse gorevi failed yerine uyarili kismi teslimatla kapatir. Basarili olamazsa
1235
+ // false doner ve normal hata akisi calisir.
1236
+ salvage(task, error) {
1237
+ try {
1238
+ if (task.kind === "operator-chat") return false;
1239
+ if (!this._cwd || !this._snapBefore) return false;
1240
+ const state = task.teamState;
1241
+ const completedWork = Object.values(state?.results || {}).filter((result) => result.status === "completed");
1242
+ const changes = diffSnapshots(this._snapBefore, snapshotDir(this._cwd));
1243
+ const hasFileChanges = Boolean(changes.created.length || changes.modified.length || changes.deleted.length);
1244
+ const hasImplementation = completedWork.some((result) => result.kind === "implement");
1245
+ // Kurtarma kosulu: YA tamamlanmis bir uygulama delegasyonu var (ekip uretti) YA DA calisma
1246
+ // klasoru fiilen degisti. Ikincisi, operatorun kendisi standart JSON protokolune uymadan
1247
+ // isi dogrudan yaptigi durumu da kapsar (or. OpenCode operator rolunde JSON plan yerine
1248
+ // dosyalari kendisi degistirip duz metin doner). Hicbir is yoksa normal hata akisina birak.
1249
+ if (!hasFileChanges && !hasImplementation) return false;
1250
+ const failure = classifyCliError(error);
1251
+ const rescueState = state || { round: 0, plan: null, criteria: [], results: {}, messages: [], operatorDecisions: [], usedIds: [] };
1252
+ const operatorDirect = !completedWork.length && hasFileChanges;
1253
+ this.publish("log", { level: "warn", msg: `Gorev hatayla kesildi (${failure.summary}); ${operatorDirect ? "operatorun dogrudan yaptigi degisiklikler" : "tamamlanan is"} kismi teslimat olarak korunuyor.` }, task.id);
1254
+ const lastReview = this.latestReview(rescueState);
1255
+ const changedFiles = [...changes.created, ...changes.modified, ...changes.deleted];
1256
+ const final = operatorDirect
1257
+ ? `KISMI TESLIMAT: Operator (${task.operatorCli || "?"}) standart delegasyon protokolune uymadan isi dogrudan uyguladi; degisiklikler korundu.\n` +
1258
+ changedFiles.slice(0, 40).map((file) => `- ${file}`).join("\n")
1259
+ : `KISMI TESLIMAT: Gorev bir altyapi/protokol hatasiyla kesildi fakat tamamlanan is korundu.\n` +
1260
+ completedWork.map((result) => `- ${result.id} (${result.agent}${result.verdict ? ` · VERDICT: ${result.verdict}` : ""})`).join("\n");
1261
+ this.complete(task, rescueState, "done", final, {
1262
+ warnings: [
1263
+ `Gorev su hatayla kesildi: ${failure.summary}`,
1264
+ ...(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.`] : []),
1265
+ ...(lastReview ? [`Son bagimsiz denetim: ${lastReview.id} → VERDICT: ${lastReview.verdict}.`] : []),
1266
+ ],
1267
+ verification: lastReview ? `${lastReview.agent} → VERDICT: ${lastReview.verdict}` : "",
1268
+ });
1269
+ return true;
1270
+ } catch {
1271
+ return false;
1272
+ }
1273
+ }
1274
+
1275
+ async runChatTask(task, cfg) {
1276
+ const parentFound = store.findTask(task.parentTaskId);
1277
+ if (!parentFound) throw new Error("Sohbetin bagli oldugu tamamlanmis gorev bulunamadi.");
1278
+ const parent = parentFound.task;
1279
+ let operatorCli = parent.operatorCli;
1280
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) operatorCli = cfg.operator?.cli;
1281
+ if (!operatorCli || !cliRegistry.operatorSpec(operatorCli, cfg)) throw new Error("Sohbet icin operator CLI bulunamadi.");
1282
+ this.busy = true;
1283
+ this.current = { id: task.id, stage: "operator-chat", agent: operatorCli };
1284
+ this._cwd = path.resolve(store.WORK_BASE, parent.targetDir || cfg.workingDir || ".");
1285
+ this.publish("log", { level: "task", msg: `OPERATOR SOHBETI ${parent.id}: ${task.prompt}` }, task.id);
1286
+ this.emit("status", this.status());
1287
+
1288
+ const role = store.readRole("operator-chat.md");
1289
+ const resultContext = Object.values(parent.teamState?.results || {}).map((result) => ({
1290
+ id: result.id, agent: result.agent, status: result.status, result: clip(result.result, 1800),
1291
+ }));
1292
+ const prompt = `${role}\n\n---\n## Tamamlanmis gorev hakkinda takip sohbeti\n` +
1293
+ `Kullanici daha once tamamladiginiz gorev hakkinda soru soruyor. Yeni dosya degistirme, arac kullanma veya delegasyon yapma. ` +
1294
+ `Asagidaki kayitlara dayanarak dogrudan, kisa ve acik cevap ver. Bilmiyorsan bunu belirt.\n` +
1295
+ `## 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` +
1296
+ `## Takim raporlari\n${clip(JSON.stringify(resultContext, null, 2), 12000)}\n` +
1297
+ `## Onceki sohbet\n${clip(JSON.stringify(parent.conversation || [], null, 2), 8000)}\n## Yeni soru\n${task.prompt}`;
1298
+ const response = await this.invokeOperator(operatorCli, prompt, cfg, { stage: "operator-chat" });
1299
+ const entry = { question: task.prompt, answer: response.text, at: new Date().toISOString(), taskId: task.id };
1300
+ parent.conversation ||= [];
1301
+ parent.conversation.push(entry);
1302
+ store.saveTask(parentFound.state, parent);
1303
+ task.status = "done";
1304
+ task.finishedAt = new Date().toISOString();
1305
+ task.final = response.text;
1306
+ task.summary = clip(response.text, 1000);
1307
+ store.moveTask("pending", "done", task);
1308
+ this.publish("result", { id: task.id, kind: "operator-chat", parentTaskId: parent.id, answer: response.text, status: "done", operator: operatorCli }, task.id);
1309
+ this.publish("log", { level: "ok", msg: `Operator soruyu yanitladi: ${parent.id}` }, task.id);
1310
+ this.emit("queue");
1311
+ }
1312
+
1313
+ complete(task, state, status, finalText, decision = {}) {
1314
+ const changes = diffSnapshots(this._snapBefore || new Map(), snapshotDir(this._cwd));
1315
+ // Son periyodik taramadan sonra yapilan degisiklikleri de replay kaydina ve dashboard'a
1316
+ // aktar; ardindan run --once yolunda da timer'in acik kalmamasini garanti et.
1317
+ this.publishFileChanges(task.id);
1318
+ this.stopLiveDiff();
1319
+ task.status = status;
1320
+ task.finishedAt = new Date().toISOString();
1321
+ const files = [
1322
+ ...changes.created.map((file) => ({ path: file, action: "created" })),
1323
+ ...changes.modified.map((file) => ({ path: file, action: "modified" })),
1324
+ ...changes.deleted.map((file) => ({ path: file, action: "deleted" })),
1325
+ ];
1326
+ const concise = clip(String(finalText).replace(/\n{3,}/g, "\n\n"), 700);
1327
+ let verification = String(decision.verification || "").trim();
1328
+ if (!verification) {
1329
+ for (const result of Object.values(state.results)) {
1330
+ const match = String(result.result || "").match(/DO(?:Ğ|G\u0306|G)RULAMA:\s*([^\n`]+)/i);
1331
+ if (match) { verification = match[1].trim(); break; }
1332
+ }
1333
+ }
1334
+ task.summary = concise;
1335
+ task.final = finalText;
1336
+ task.changes = changes;
1337
+ const warnings = (Array.isArray(decision.warnings) ? decision.warnings : []).map(String).filter(Boolean);
1338
+ task.delivery = {
1339
+ summary: concise,
1340
+ files,
1341
+ location: this._cwd,
1342
+ verification,
1343
+ mode: task.executionMode,
1344
+ rounds: state.round,
1345
+ agents: [...new Set(Object.values(state.results).map((result) => result.agent))],
1346
+ ...(warnings.length ? { warnings } : {}),
1347
+ };
1348
+ task.teamState = state;
1349
+ const found = store.findTask(task.id);
1350
+ store.moveTask(found?.state || "pending", status, task);
1351
+ store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
1352
+ this.publish("result", { id: task.id, prompt: task.prompt, status, dir: this._cwd, changes, summary: task.summary, delivery: task.delivery, operator: task.operatorCli, rounds: state.round }, task.id);
1353
+ this.publish("log", { level: "ok", msg: `GOREV tamamlandi: ${task.id}` }, task.id);
1354
+ this.emit("queue");
1355
+ this._textBefore = null;
1356
+ }
1357
+ }
1358
+
1359
+ module.exports = new Engine();
1360
+ // Testlerin dis davranisla birlikte kritik saf kurallari da dogrudan dogrulayabilmesi icin.
1361
+ module.exports._internals = {
1362
+ normalizeAssignments, ensureBalancedRoleChain, extractVerdict, clipMiddle, snapshotDir, diffSnapshots,
1363
+ captureTextSnapshot, buildLineDiff, describeFileDiff, isSensitiveDiffPath, taskRequiresDelegation,
1364
+ };