@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.
- package/README.md +182 -0
- package/orchestrator/LICENSE +21 -0
- package/orchestrator/README.md +452 -0
- package/orchestrator/config.default.json +65 -0
- package/orchestrator/roles/executor.md +49 -0
- package/orchestrator/roles/operator-chat.md +25 -0
- package/orchestrator/roles/operator.md +75 -0
- package/orchestrator/roles/planner.md +53 -0
- package/orchestrator/roles/reviewer.md +54 -0
- package/orchestrator/skills/acceptance-criteria.md +18 -0
- package/orchestrator/skills/accessibility-audit.md +16 -0
- package/orchestrator/skills/accessible-forms.md +16 -0
- package/orchestrator/skills/api-design.md +27 -0
- package/orchestrator/skills/api-documentation.md +16 -0
- package/orchestrator/skills/architecture-decision-record.md +18 -0
- package/orchestrator/skills/authentication-design.md +16 -0
- package/orchestrator/skills/authorization-review.md +16 -0
- package/orchestrator/skills/backward-compatibility.md +17 -0
- package/orchestrator/skills/changelog-writing.md +16 -0
- package/orchestrator/skills/ci-pipeline-design.md +16 -0
- package/orchestrator/skills/cli-design.md +16 -0
- package/orchestrator/skills/code-review.md +27 -0
- package/orchestrator/skills/configuration-management.md +16 -0
- package/orchestrator/skills/container-review.md +16 -0
- package/orchestrator/skills/contract-testing.md +16 -0
- package/orchestrator/skills/dashboard-design.md +16 -0
- package/orchestrator/skills/database-migration.md +16 -0
- package/orchestrator/skills/database-schema-design.md +16 -0
- package/orchestrator/skills/debugging.md +26 -0
- package/orchestrator/skills/dependency-review.md +16 -0
- package/orchestrator/skills/design-review.md +29 -0
- package/orchestrator/skills/design-system.md +16 -0
- package/orchestrator/skills/docs-api-reference.md +16 -0
- package/orchestrator/skills/docs-troubleshooting.md +16 -0
- package/orchestrator/skills/docs-tutorial.md +16 -0
- package/orchestrator/skills/docs-writing.md +25 -0
- package/orchestrator/skills/empty-error-loading-states.md +16 -0
- package/orchestrator/skills/end-to-end-testing.md +16 -0
- package/orchestrator/skills/error-handling.md +16 -0
- package/orchestrator/skills/frontend-design.md +28 -0
- package/orchestrator/skills/git-commit-writing.md +16 -0
- package/orchestrator/skills/graphql-design.md +16 -0
- package/orchestrator/skills/incident-runbook.md +18 -0
- package/orchestrator/skills/input-validation.md +16 -0
- package/orchestrator/skills/integration-testing.md +16 -0
- package/orchestrator/skills/interaction-design.md +16 -0
- package/orchestrator/skills/landing-page-design.md +16 -0
- package/orchestrator/skills/observability-design.md +16 -0
- package/orchestrator/skills/openapi-contract.md +16 -0
- package/orchestrator/skills/performance-profiling.md +16 -0
- package/orchestrator/skills/privacy-review.md +16 -0
- package/orchestrator/skills/property-based-testing.md +16 -0
- package/orchestrator/skills/pull-request-writing.md +16 -0
- package/orchestrator/skills/refactoring.md +16 -0
- package/orchestrator/skills/release-readiness.md +16 -0
- package/orchestrator/skills/responsive-design.md +16 -0
- package/orchestrator/skills/secrets-management.md +16 -0
- package/orchestrator/skills/secure-file-upload.md +16 -0
- package/orchestrator/skills/security-review.md +26 -0
- package/orchestrator/skills/semantic-versioning.md +17 -0
- package/orchestrator/skills/seo-on-page.md +16 -0
- package/orchestrator/skills/seo-structured-data.md +16 -0
- package/orchestrator/skills/seo-technical-audit.md +16 -0
- package/orchestrator/skills/sql-query-review.md +16 -0
- package/orchestrator/skills/supply-chain-security.md +16 -0
- package/orchestrator/skills/test-strategy.md +16 -0
- package/orchestrator/skills/threat-modeling.md +16 -0
- package/orchestrator/skills/unit-testing.md +16 -0
- package/orchestrator/skills/write-tests.md +28 -0
- package/orchestrator/src/checkpoints.js +187 -0
- package/orchestrator/src/cli-registry.js +579 -0
- package/orchestrator/src/cli.js +135 -0
- package/orchestrator/src/doctor.js +64 -0
- package/orchestrator/src/engine.js +1364 -0
- package/orchestrator/src/schedule.js +126 -0
- package/orchestrator/src/server.js +706 -0
- package/orchestrator/src/skill-registry.js +272 -0
- package/orchestrator/src/store.js +364 -0
- package/orchestrator/web/OrbitControls.js +1417 -0
- package/orchestrator/web/app.css +116 -0
- package/orchestrator/web/app.js +70 -0
- package/orchestrator/web/board.html +141 -0
- package/orchestrator/web/code.html +208 -0
- package/orchestrator/web/flow.html +736 -0
- package/orchestrator/web/index.html +539 -0
- package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -0
- package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -0
- package/orchestrator/web/jsm/postprocessing/Pass.js +95 -0
- package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -0
- package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -0
- package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -0
- package/orchestrator/web/jsm/shaders/CopyShader.js +45 -0
- package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -0
- package/orchestrator/web/three.module.min.js +6 -0
- package/package.json +51 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// skill-registry.js — yeniden kullanilabilir "beceri" (skill) dosyalarini yukler, ayristirir
|
|
2
|
+
// ve operatore/uzmana verilecek bicime cevirir. Beceriler roles/*.md gibi Markdown'dir; farki
|
|
3
|
+
// frontmatter tasimalari ve KULLANICI TARAFINDAN ETKINLESTIRILENLERIN operatore katalog olarak
|
|
4
|
+
// sunulmasidir. Sifir bagimlilik: frontmatter'i kendi minimal ayristiricimizla okuruz.
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const store = require("./store");
|
|
8
|
+
|
|
9
|
+
const SKILLS_DIR = path.join(store.ROOT, "skills");
|
|
10
|
+
|
|
11
|
+
function ensureDir() {
|
|
12
|
+
try { fs.mkdirSync(SKILLS_DIR, { recursive: true }); } catch {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toList(value) {
|
|
16
|
+
if (Array.isArray(value)) return value.map(String).map((x) => x.trim()).filter(Boolean);
|
|
17
|
+
if (typeof value === "string" && value.trim()) return value.split(",").map((x) => x.trim()).filter(Boolean);
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// `--- ... ---` bloğundan basit `key: value` ciftlerini okur. Deger `[a, b]` veya virgullu
|
|
22
|
+
// liste ise diziye cevrilir. YAML'in tamamini desteklemeyiz; skill dosyalari icin bu yeterli.
|
|
23
|
+
function parseFrontmatter(text) {
|
|
24
|
+
const raw = String(text || "");
|
|
25
|
+
const match = raw.match(/^?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/);
|
|
26
|
+
if (!match) return { meta: {}, body: raw.trim() };
|
|
27
|
+
const meta = {};
|
|
28
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
29
|
+
const kv = line.match(/^([A-Za-z0-9_-]+)[ \t]*:[ \t]*(.*)$/);
|
|
30
|
+
if (!kv) continue;
|
|
31
|
+
const key = kv[1].trim();
|
|
32
|
+
let value = kv[2].trim();
|
|
33
|
+
if (/^\[.*\]$/.test(value)) {
|
|
34
|
+
value = value.slice(1, -1).split(",").map((x) => x.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
35
|
+
} else {
|
|
36
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
37
|
+
}
|
|
38
|
+
meta[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return { meta, body: match[2].trim() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fileNameFor(nameOrFile) {
|
|
44
|
+
const base = path.basename(String(nameOrFile || ""));
|
|
45
|
+
return base.toLowerCase().endsWith(".md") ? base : `${base}.md`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function slugFor(file) {
|
|
49
|
+
return path.basename(String(file)).replace(/\.md$/i, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function listSkillFiles() {
|
|
53
|
+
ensureDir();
|
|
54
|
+
try { return fs.readdirSync(SKILLS_DIR).filter((f) => f.toLowerCase().endsWith(".md")).sort(); }
|
|
55
|
+
catch { return []; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadSkill(nameOrFile) {
|
|
59
|
+
const file = fileNameFor(nameOrFile);
|
|
60
|
+
const p = path.join(SKILLS_DIR, file);
|
|
61
|
+
if (!fs.existsSync(p)) {
|
|
62
|
+
// Etkin liste dosya adi yerine frontmatter `name` slug'ini tutuyor olabilir.
|
|
63
|
+
const byName = listSkillFiles().map((f) => ({ f, s: parseFrontmatter(readRaw(f)) }))
|
|
64
|
+
.find(({ f, s }) => String(s.meta.name || slugFor(f)) === slugFor(nameOrFile));
|
|
65
|
+
if (!byName) return null;
|
|
66
|
+
return buildSkill(byName.f, byName.s);
|
|
67
|
+
}
|
|
68
|
+
return buildSkill(file, parseFrontmatter(fs.readFileSync(p, "utf8")));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildSkill(file, parsed) {
|
|
72
|
+
const { meta, body } = parsed;
|
|
73
|
+
return {
|
|
74
|
+
name: String(meta.name || slugFor(file)),
|
|
75
|
+
file,
|
|
76
|
+
description: String(meta.description || ""),
|
|
77
|
+
category: String(meta.category || "general"),
|
|
78
|
+
appliesTo: toList(meta.appliesTo),
|
|
79
|
+
match: toList(meta.match),
|
|
80
|
+
body,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readRaw(nameOrFile) {
|
|
85
|
+
const p = path.join(SKILLS_DIR, fileNameFor(nameOrFile));
|
|
86
|
+
return fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function allSkills() {
|
|
90
|
+
return listSkillFiles().map((f) => buildSkill(f, parseFrontmatter(readRaw(f))));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function enabledNames(cfg) {
|
|
94
|
+
const list = cfg && cfg.skills && Array.isArray(cfg.skills.enabled) ? cfg.skills.enabled.map(String) : [];
|
|
95
|
+
return new Set(list);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function enabledSkills(cfg, installed = null) {
|
|
99
|
+
const enabled = enabledNames(cfg);
|
|
100
|
+
if (!enabled.size) return [];
|
|
101
|
+
const wildcard = enabled.has("*");
|
|
102
|
+
return (installed || allSkills()).filter((skill) => wildcard || enabled.has(skill.name));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeText(value) {
|
|
106
|
+
return String(value || "").toLocaleLowerCase("tr-TR").normalize("NFD")
|
|
107
|
+
.replace(/[\u0300-\u036f]/g, "").replace(/ı/g, "i").replace(/[^a-z0-9]+/g, " ").trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function scoreSkill(skill, context, kind = "") {
|
|
111
|
+
const haystack = ` ${normalizeText(context)} `;
|
|
112
|
+
if (!haystack.trim()) return 0;
|
|
113
|
+
const stopwords = new Set(["icin", "ile", "veya", "olan", "olarak", "yap", "yaz", "ekle", "kur", "kullan", "tasarla", "incele", "duzelt", "olustur", "guncelle", "planla", "create", "build", "write", "add", "use", "design", "review", "fix", "update", "implement"]);
|
|
114
|
+
const tokens = new Set(haystack.trim().split(/\s+/).filter((token) => token.length > 1 && !stopwords.has(token)));
|
|
115
|
+
let lexical = 0;
|
|
116
|
+
for (const rawPhrase of skill.match) {
|
|
117
|
+
const phrase = normalizeText(rawPhrase);
|
|
118
|
+
if (!phrase) continue;
|
|
119
|
+
if (!phrase.includes(" ") && stopwords.has(phrase)) continue;
|
|
120
|
+
if (haystack.includes(` ${phrase} `)) lexical += 18 + Math.min(8, phrase.split(" ").length * 2);
|
|
121
|
+
else if (phrase.split(" ").every((token) => tokens.has(token))) lexical += 10;
|
|
122
|
+
}
|
|
123
|
+
const nameParts = normalizeText(skill.name).split("-").filter((token) => token.length > 2);
|
|
124
|
+
lexical += nameParts.filter((token) => tokens.has(token)).length * 8;
|
|
125
|
+
const descriptionTokens = new Set(normalizeText(skill.description).split(" ").filter((token) => token.length > 3));
|
|
126
|
+
const descriptionHits = [...tokens].filter((token) => descriptionTokens.has(token));
|
|
127
|
+
if (descriptionHits.length > 1) lexical += descriptionHits.length * 2;
|
|
128
|
+
if (!lexical) return 0;
|
|
129
|
+
const kindFit = !kind || !skill.appliesTo.length || skill.appliesTo.includes(kind) ? 6 : -8;
|
|
130
|
+
return Math.max(1, lexical + kindFit);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ranked(cfg, context = "", kind = "", installed = null) {
|
|
134
|
+
return enabledSkills(cfg, installed).map((skill) => ({ skill, score: scoreSkill(skill, context, kind) }))
|
|
135
|
+
.filter(({ score }) => !context || score > 0)
|
|
136
|
+
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name, "en"));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function compact(skill) {
|
|
140
|
+
const description = skill.description.length > 180 ? `${skill.description.slice(0, 177)}...` : skill.description;
|
|
141
|
+
return { name: skill.name, description, appliesTo: skill.appliesTo };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Operatore tum govdeleri veya tum aciklamalari yuklemek yerine, gorev metnine gore en ilgili
|
|
145
|
+
// etkin becerileri verir. Limit ve karakter butcesi katalog buyuse bile prompt maliyetini sabitler.
|
|
146
|
+
function catalogFromRanked(cfg, rankedSkills) {
|
|
147
|
+
const limit = Math.max(1, Number(cfg?.skills?.catalogLimit) || 12);
|
|
148
|
+
const budget = Math.max(200, Number(cfg?.skills?.charBudget) || 2400);
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const { skill } of rankedSkills) {
|
|
151
|
+
if (out.length >= limit) break;
|
|
152
|
+
let item = compact(skill);
|
|
153
|
+
let candidate = [...out, item];
|
|
154
|
+
if (JSON.stringify(candidate).length > budget) {
|
|
155
|
+
if (out.length) break;
|
|
156
|
+
const withoutDescription = { ...item, description: "" };
|
|
157
|
+
const fixedCost = JSON.stringify([withoutDescription]).length;
|
|
158
|
+
if (fixedCost > budget) break;
|
|
159
|
+
const room = Math.max(0, budget - fixedCost - 3);
|
|
160
|
+
item = { ...item, description: room ? `${item.description.slice(0, room)}...` : "" };
|
|
161
|
+
candidate = [item];
|
|
162
|
+
}
|
|
163
|
+
out.push(item);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function discover(cfg, context = "", kind = "") {
|
|
169
|
+
const installed = allSkills();
|
|
170
|
+
const enabled = enabledSkills(cfg, installed);
|
|
171
|
+
const matches = ranked(cfg, context, kind, installed);
|
|
172
|
+
return {
|
|
173
|
+
catalog: catalogFromRanked(cfg, matches),
|
|
174
|
+
stats: { installed: installed.length, enabled: enabled.length, matched: matches.length },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function catalog(cfg, context = "", kind = "") {
|
|
179
|
+
return discover(cfg, context, kind).catalog;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function catalogStats(cfg, context = "", kind = "") {
|
|
183
|
+
return discover(cfg, context, kind).stats;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function suggest(cfg, context, kind = "") {
|
|
187
|
+
const limit = Math.max(1, Number(cfg?.skills?.maxSkillsPerAssignment) || 3);
|
|
188
|
+
return ranked(cfg, context, kind).slice(0, limit).map(({ skill }) => skill.name);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Bir delegasyona istenen beceri adlarini cozer; yalnizca hem var olan hem de kullanicinin
|
|
192
|
+
// etkinlestirdigi beceriler donuir. Boylece operator, katalog disinda bir beceri iliştiremez.
|
|
193
|
+
function resolveForAssignment(names, cfg) {
|
|
194
|
+
const enabled = enabledNames(cfg);
|
|
195
|
+
const wildcard = enabled.has("*");
|
|
196
|
+
const limit = Math.max(1, Number(cfg?.skills?.maxSkillsPerAssignment) || 3);
|
|
197
|
+
const out = [];
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
for (const name of toList(names)) {
|
|
200
|
+
if (out.length >= limit) break;
|
|
201
|
+
if (seen.has(name) || (!wildcard && !enabled.has(name))) continue;
|
|
202
|
+
const skill = loadSkill(name);
|
|
203
|
+
if (skill) { out.push(skill); seen.add(name); }
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Progressive disclosure: uzmana becerinin TAM govdesini yigmadan, ad + kisa ozet + rehber dosya
|
|
209
|
+
// yolunu verir. Uzman gercekten ihtiyac duyarsa dosyayi kendisi okur. Boylece prompt hafif kalir.
|
|
210
|
+
function toPromptRefs(skills, charBudget = 1200) {
|
|
211
|
+
if (!Array.isArray(skills) || !skills.length) return "";
|
|
212
|
+
const lines = [];
|
|
213
|
+
for (const skill of skills) {
|
|
214
|
+
const location = path.join(SKILLS_DIR, skill.file);
|
|
215
|
+
let line = `- ${skill.name}: ${skill.description || "(aciklama yok)"}\n Rehber: ${location}`;
|
|
216
|
+
if (charBudget && [...lines, line].join("\n").length > charBudget) {
|
|
217
|
+
if (lines.length) break;
|
|
218
|
+
line = `- ${skill.name}\n Rehber: ${location}`;
|
|
219
|
+
if (line.length > charBudget) break;
|
|
220
|
+
}
|
|
221
|
+
lines.push(line);
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function validateSkill(nameOrFile, content) {
|
|
227
|
+
const { meta, body } = parseFrontmatter(content);
|
|
228
|
+
const errors = [];
|
|
229
|
+
const name = String(meta.name || "");
|
|
230
|
+
const expected = slugFor(fileNameFor(nameOrFile));
|
|
231
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64) errors.push("name kucuk harfli kebab-case ve en fazla 64 karakter olmali");
|
|
232
|
+
if (name && name !== expected) errors.push("frontmatter name dosya adiyla ayni olmali");
|
|
233
|
+
const description = String(meta.description || "").trim();
|
|
234
|
+
if (!description || description.length > 1024) errors.push("description 1-1024 karakter olmali");
|
|
235
|
+
if (!body.trim()) errors.push("beceri govdesi bos olamaz");
|
|
236
|
+
const invalidKinds = toList(meta.appliesTo).filter((kind) => !["implement", "review", "plan", "research"].includes(kind));
|
|
237
|
+
if (invalidKinds.length) errors.push(`gecersiz appliesTo: ${invalidKinds.join(", ")}`);
|
|
238
|
+
return { ok: !errors.length, errors };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function writeSkill(nameOrFile, content) {
|
|
242
|
+
ensureDir();
|
|
243
|
+
const file = fileNameFor(nameOrFile);
|
|
244
|
+
fs.writeFileSync(path.join(SKILLS_DIR, file), String(content == null ? "" : content));
|
|
245
|
+
return file;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function deleteSkill(nameOrFile) {
|
|
249
|
+
const p = path.join(SKILLS_DIR, fileNameFor(nameOrFile));
|
|
250
|
+
if (fs.existsSync(p)) fs.rmSync(p);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
SKILLS_DIR,
|
|
255
|
+
parseFrontmatter,
|
|
256
|
+
listSkillFiles,
|
|
257
|
+
loadSkill,
|
|
258
|
+
allSkills,
|
|
259
|
+
enabledNames,
|
|
260
|
+
enabledSkills,
|
|
261
|
+
scoreSkill,
|
|
262
|
+
discover,
|
|
263
|
+
catalog,
|
|
264
|
+
catalogStats,
|
|
265
|
+
suggest,
|
|
266
|
+
resolveForAssignment,
|
|
267
|
+
toPromptRefs,
|
|
268
|
+
validateSkill,
|
|
269
|
+
readRaw,
|
|
270
|
+
writeSkill,
|
|
271
|
+
deleteSkill,
|
|
272
|
+
};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// store.js — dosya tabanli kuyruk, config, hafiza, butce (sifir bagimlilik)
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
const isWin = process.platform === "win32";
|
|
8
|
+
// ASSETS: paket dizini (salt-okunur varlıklar: web/, config.default.json, varsayilan roles/+skills/).
|
|
9
|
+
// __dirname/.. daima paketin gercek yeridir (npm global/npx onbellegi dahil).
|
|
10
|
+
const ASSETS = path.join(__dirname, "..");
|
|
11
|
+
// ROOT: yazilabilir VERI koku (config.json, queue/, state/, memory/, kullanicinin roles/+skills/).
|
|
12
|
+
// Oncelik: CLI_TEAM_ROOT (izole test/CI) > CREWCTL_HOME (kullanici override) > mod tespiti.
|
|
13
|
+
// Gelistirme kopyasinda test/ klasoru bulunur ve paket dizini yazilabilirdir; o zaman ROOT=ASSETS
|
|
14
|
+
// (mevcut npm start/npm test davranisi aynen korunur). Yayinlanan pakette test/ gonderilmez, bu
|
|
15
|
+
// yuzden kurulu modda veri ~/.crewctl'e yazilir (npm onbelleğine degil).
|
|
16
|
+
const isDevCheckout = fs.existsSync(path.join(ASSETS, "test"));
|
|
17
|
+
const ROOT = path.resolve(
|
|
18
|
+
process.env.CLI_TEAM_ROOT ||
|
|
19
|
+
process.env.CREWCTL_HOME ||
|
|
20
|
+
(isDevCheckout ? ASSETS : path.join(os.homedir(), ".crewctl"))
|
|
21
|
+
);
|
|
22
|
+
// WORK_BASE: agent'larin uzerinde calisacagi klasorun goreli cozumleme tabani. Kurulu modda
|
|
23
|
+
// kullanici komutu nerede calistirdiysa orasidir (cwd); mutlak yol secilirse zaten o kullanilir.
|
|
24
|
+
const WORK_BASE = process.cwd();
|
|
25
|
+
const Q = path.join(ROOT, "queue");
|
|
26
|
+
const STATES = ["pending", "done", "failed", "approval"];
|
|
27
|
+
const MEM = path.join(ROOT, "memory");
|
|
28
|
+
const STATE = path.join(ROOT, "state");
|
|
29
|
+
const EVENTS = path.join(STATE, "events");
|
|
30
|
+
const ROLES = path.join(ROOT, "roles");
|
|
31
|
+
const SKILLS = path.join(ROOT, "skills");
|
|
32
|
+
|
|
33
|
+
// Windows'ta rename/unlink; antivirus, arama dizinleyici veya kuyrugu ayni anda tarayan
|
|
34
|
+
// canli sunucu dosyada kisa sureli bir handle tuttugunda gecici olarak EPERM/EACCES/EBUSY
|
|
35
|
+
// verir. Tek seferlik cagri bu durumda tum gorevi coldururdu; kisa artan beklemelerle yeniden
|
|
36
|
+
// deneriz. Bu kod yolu tum saveTask/saveConfig/addTask cagrilarinin sicak yolu.
|
|
37
|
+
const TRANSIENT_FS_CODES = new Set(["EPERM", "EACCES", "EBUSY", "ENOENT", "EEXIST"]);
|
|
38
|
+
function sleepSync(ms) {
|
|
39
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch {}
|
|
40
|
+
}
|
|
41
|
+
function isTransientFsError(error) {
|
|
42
|
+
return isWin && error && TRANSIENT_FS_CODES.has(error.code);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sweepStaleTemp(dir, maxAgeMs = 5 * 60 * 1000) {
|
|
46
|
+
// Kalici olarak yeniden adlandirilamamis eski .tmp dosyalarini birak birikmesin diye temizle.
|
|
47
|
+
let entries;
|
|
48
|
+
try { entries = fs.readdirSync(dir); } catch { return; }
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const name of entries) {
|
|
51
|
+
if (!name.endsWith(".tmp")) continue;
|
|
52
|
+
const full = path.join(dir, name);
|
|
53
|
+
try {
|
|
54
|
+
if (now - fs.statSync(full).mtimeMs > maxAgeMs) fs.rmSync(full, { force: true });
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Kurulu modda (ROOT != ASSETS) veri kokundeki roles/ ve skills/ bos ise paketle gelen
|
|
60
|
+
// varsayilanlari BIR KEZ kopyala. Kullanicinin duzenlemeleri korunur (yalnizca bos dizine seed).
|
|
61
|
+
let seededAssets = false;
|
|
62
|
+
function seedDefaults() {
|
|
63
|
+
if (seededAssets) return;
|
|
64
|
+
seededAssets = true;
|
|
65
|
+
if (ROOT === ASSETS) return;
|
|
66
|
+
for (const [srcDir, dstDir] of [[path.join(ASSETS, "roles"), ROLES], [path.join(ASSETS, "skills"), SKILLS]]) {
|
|
67
|
+
try {
|
|
68
|
+
const existing = fs.existsSync(dstDir) ? fs.readdirSync(dstDir).filter((f) => f.toLowerCase().endsWith(".md")) : [];
|
|
69
|
+
if (existing.length) continue;
|
|
70
|
+
const defaults = fs.existsSync(srcDir) ? fs.readdirSync(srcDir).filter((f) => f.toLowerCase().endsWith(".md")) : [];
|
|
71
|
+
for (const f of defaults) fs.copyFileSync(path.join(srcDir, f), path.join(dstDir, f));
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ensureDirs, olay akisinda cok sik cagrilir; temizligi dakikada bir defayla sinirla ki
|
|
77
|
+
// her stdout parcasinda dizin taramasi yapmayalim.
|
|
78
|
+
let lastSweepAt = 0;
|
|
79
|
+
function ensureDirs() {
|
|
80
|
+
const dirs = [...STATES.map((s) => path.join(Q, s)), MEM, STATE, EVENTS, ROLES, SKILLS];
|
|
81
|
+
dirs.forEach((d) => fs.mkdirSync(d, { recursive: true }));
|
|
82
|
+
seedDefaults();
|
|
83
|
+
if (Date.now() - lastSweepAt > 60 * 1000) {
|
|
84
|
+
lastSweepAt = Date.now();
|
|
85
|
+
for (const s of STATES) sweepStaleTemp(path.join(Q, s));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Atomik yazma: once .tmp'ye yaz, sonra rename et. Rename ayni disk uzerinde atomiktir;
|
|
90
|
+
// boylece surec ortasinda cokme olsa bile yarim/bozuk JSON dosyasi kalmaz (her PC'de guvenli).
|
|
91
|
+
// Windows'ta rename gecici olarak kilitlenebildigi icin (bkz. TRANSIENT_FS_CODES) yeniden dener.
|
|
92
|
+
function atomicWrite(file, data) {
|
|
93
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
94
|
+
fs.writeFileSync(tmp, data);
|
|
95
|
+
let lastError;
|
|
96
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
97
|
+
try {
|
|
98
|
+
fs.renameSync(tmp, file);
|
|
99
|
+
return;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
lastError = error;
|
|
102
|
+
if (!isTransientFsError(error)) break;
|
|
103
|
+
sleepSync(20 * (attempt + 1));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Rename kalici olarak basarisiz. Gorevi tamamen kaybetmektense hedefe dogrudan (atomik
|
|
107
|
+
// olmayan) yazmayi son care olarak dene; sonra gecici dosyayi temizle.
|
|
108
|
+
try {
|
|
109
|
+
fs.writeFileSync(file, data);
|
|
110
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
111
|
+
return;
|
|
112
|
+
} catch {}
|
|
113
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
114
|
+
throw lastError;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- Config ----
|
|
118
|
+
// Minimal geri-donus varsayilani (config.default.json de yoksa kullanilir) — sistem her
|
|
119
|
+
// zaman calisir durumda acilsin diye.
|
|
120
|
+
const FALLBACK_CONFIG = {
|
|
121
|
+
approvalMode: "auto", workingDir: "..", dailyCallBudget: 150,
|
|
122
|
+
autonomousConsentAcceptedAt: null,
|
|
123
|
+
pollSeconds: 15, memoryCharBudget: 8000, teamContextCharBudget: 30000, agentTimeoutSeconds: 900,
|
|
124
|
+
cliSilenceTimeoutSeconds: 300,
|
|
125
|
+
discoveryIgnoredAdapters: [],
|
|
126
|
+
liveDiff: true, liveDiffIntervalMs: 2500,
|
|
127
|
+
versioning: true, versioningRetention: 20,
|
|
128
|
+
operator: { roleFile: "roles/operator.md", maxRounds: 6, maxDelegationsPerRound: 8, maxInfrastructureRecoveryRounds: 2, protocolRetries: 1 },
|
|
129
|
+
cliSettings: {
|
|
130
|
+
codex: { model: "", reasoningEffort: "medium", serviceTier: "fast" },
|
|
131
|
+
opencode: { model: "" },
|
|
132
|
+
},
|
|
133
|
+
skills: { enabled: [], autoMatch: true, catalogLimit: 12, maxSkillsPerAssignment: 3, charBudget: 2400, referenceCharBudget: 1200 },
|
|
134
|
+
agents: {}, riskyPatterns: [], schedules: [],
|
|
135
|
+
};
|
|
136
|
+
// Eski sema, model ayarlarini operator altina gomuyordu (operator.codexSettings ve
|
|
137
|
+
// operator.model). Yeni sema CLI bazlidir: cliSettings[adapter] o CLI'nin operator ve
|
|
138
|
+
// uzman kullanimlarinin tumune uygulanir. Saf ve idempotent olmali; loadConfig sicak yoldur.
|
|
139
|
+
function normalizeConfig(cfg) {
|
|
140
|
+
if (!cfg || typeof cfg !== "object") return cfg;
|
|
141
|
+
const current = cfg.cliSettings && typeof cfg.cliSettings === "object" ? cfg.cliSettings : {};
|
|
142
|
+
const legacyCodex = cfg.operator && typeof cfg.operator.codexSettings === "object" ? cfg.operator.codexSettings : {};
|
|
143
|
+
const legacyOpenCodeModel = typeof cfg.operator?.model === "string" ? cfg.operator.model : "";
|
|
144
|
+
const normalized = {
|
|
145
|
+
...cfg,
|
|
146
|
+
skills: { enabled: [], autoMatch: true, catalogLimit: 12, maxSkillsPerAssignment: 3, charBudget: 2400, referenceCharBudget: 1200, ...(cfg.skills || {}) },
|
|
147
|
+
schedules: Array.isArray(cfg.schedules) ? cfg.schedules : [],
|
|
148
|
+
cliSettings: {
|
|
149
|
+
codex: { model: "", reasoningEffort: "medium", serviceTier: "fast", ...legacyCodex, ...(current.codex || {}) },
|
|
150
|
+
opencode: { model: legacyOpenCodeModel, ...(current.opencode || {}) },
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
if (cfg.operator && typeof cfg.operator === "object") {
|
|
154
|
+
normalized.operator = { ...cfg.operator };
|
|
155
|
+
delete normalized.operator.codexSettings;
|
|
156
|
+
delete normalized.operator.model;
|
|
157
|
+
}
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
// config.json kisiye ozeldir (gitignore). Yoksa config.default.json sablonundan uretilir;
|
|
161
|
+
// boylece kullanici klonlayip `npm start` dedigi anda calisir ve kurulu CLI'lar keşifle eklenir.
|
|
162
|
+
function loadConfig() {
|
|
163
|
+
const file = path.join(ROOT, "config.json");
|
|
164
|
+
if (!fs.existsSync(file)) {
|
|
165
|
+
const template = path.join(ASSETS, "config.default.json");
|
|
166
|
+
const seed = fs.existsSync(template) ? fs.readFileSync(template, "utf8") : JSON.stringify(FALLBACK_CONFIG, null, 2);
|
|
167
|
+
atomicWrite(file, seed);
|
|
168
|
+
}
|
|
169
|
+
return normalizeConfig(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
170
|
+
}
|
|
171
|
+
function saveConfig(cfg) {
|
|
172
|
+
atomicWrite(path.join(ROOT, "config.json"), JSON.stringify(cfg, null, 2));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- Roller ----
|
|
176
|
+
function listRoles() {
|
|
177
|
+
if (!fs.existsSync(ROLES)) return [];
|
|
178
|
+
return fs.readdirSync(ROLES).filter((f) => f.endsWith(".md"));
|
|
179
|
+
}
|
|
180
|
+
function readRole(file) {
|
|
181
|
+
const p = path.join(ROLES, path.basename(file));
|
|
182
|
+
return fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
|
|
183
|
+
}
|
|
184
|
+
function writeRole(file, content) {
|
|
185
|
+
fs.writeFileSync(path.join(ROLES, path.basename(file)), content);
|
|
186
|
+
}
|
|
187
|
+
function deleteRole(file) {
|
|
188
|
+
const p = path.join(ROLES, path.basename(file));
|
|
189
|
+
if (fs.existsSync(p)) fs.rmSync(p);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- Gorevler ----
|
|
193
|
+
function taskPath(state, id) {
|
|
194
|
+
return path.join(Q, state, `${id}.json`);
|
|
195
|
+
}
|
|
196
|
+
function listTasks(state) {
|
|
197
|
+
const dir = path.join(Q, state);
|
|
198
|
+
if (!fs.existsSync(dir)) return [];
|
|
199
|
+
return fs
|
|
200
|
+
.readdirSync(dir)
|
|
201
|
+
.filter((f) => f.endsWith(".json"))
|
|
202
|
+
.map((f) => {
|
|
203
|
+
try {
|
|
204
|
+
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
211
|
+
}
|
|
212
|
+
function nextPending() {
|
|
213
|
+
return listTasks("pending")[0] || null;
|
|
214
|
+
}
|
|
215
|
+
function addTask(prompt, targetDir, operatorCli, executionMode) {
|
|
216
|
+
const id =
|
|
217
|
+
new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 15) +
|
|
218
|
+
"-" +
|
|
219
|
+
Math.floor(Math.random() * 9000 + 1000);
|
|
220
|
+
const task = { id, prompt, status: "pending", createdAt: new Date().toISOString() };
|
|
221
|
+
if (targetDir) task.targetDir = targetDir;
|
|
222
|
+
if (operatorCli) task.operatorCli = operatorCli;
|
|
223
|
+
if (executionMode) task.executionMode = executionMode;
|
|
224
|
+
atomicWrite(taskPath("pending", id), JSON.stringify(task, null, 2));
|
|
225
|
+
return task;
|
|
226
|
+
}
|
|
227
|
+
function addChatTask(parentTask, question) {
|
|
228
|
+
const task = addTask(question, parentTask.targetDir, parentTask.operatorCli, "chat");
|
|
229
|
+
task.kind = "operator-chat";
|
|
230
|
+
task.parentTaskId = parentTask.id;
|
|
231
|
+
saveTask("pending", task);
|
|
232
|
+
return task;
|
|
233
|
+
}
|
|
234
|
+
// Zamanlanmis bir gorevi kuyruga ekler. Normal gorevle ayni yolu kullanir; yalnizca kaynagini
|
|
235
|
+
// isaretlemek icin scheduleId iliştirir (Pano bu görevi "zamanlanmis" rozetiyle gosterebilsin).
|
|
236
|
+
function addScheduledTask(schedule) {
|
|
237
|
+
const task = addTask(schedule.prompt, schedule.targetDir, schedule.operatorCli, schedule.executionMode || "auto");
|
|
238
|
+
task.scheduleId = schedule.id;
|
|
239
|
+
saveTask("pending", task);
|
|
240
|
+
return task;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---- Kalici calisma olaylari ----
|
|
244
|
+
function appendRunEvent(taskId, event) {
|
|
245
|
+
ensureDirs();
|
|
246
|
+
fs.appendFileSync(path.join(EVENTS, `${path.basename(taskId)}.jsonl`), JSON.stringify(event) + "\n");
|
|
247
|
+
}
|
|
248
|
+
function listRunEvents(taskId, limit = 1000) {
|
|
249
|
+
const file = path.join(EVENTS, `${path.basename(taskId)}.jsonl`);
|
|
250
|
+
if (!fs.existsSync(file)) return [];
|
|
251
|
+
return fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).slice(-Math.max(1, limit)).map((line) => {
|
|
252
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
253
|
+
}).filter(Boolean);
|
|
254
|
+
}
|
|
255
|
+
function hashText(text) {
|
|
256
|
+
return crypto.createHash("sha256").update(String(text)).digest("hex");
|
|
257
|
+
}
|
|
258
|
+
function saveTask(state, task) {
|
|
259
|
+
atomicWrite(taskPath(state, task.id), JSON.stringify(task, null, 2));
|
|
260
|
+
}
|
|
261
|
+
function findTask(id) {
|
|
262
|
+
for (const s of STATES) {
|
|
263
|
+
const p = taskPath(s, id);
|
|
264
|
+
if (fs.existsSync(p)) return { state: s, task: JSON.parse(fs.readFileSync(p, "utf8")) };
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
function moveTask(fromState, toState, task) {
|
|
269
|
+
const src = taskPath(fromState, task.id);
|
|
270
|
+
// Hedefi once atomik olarak yaz. Surec iki islem arasinda cokse bile gorev kaybolmaz.
|
|
271
|
+
saveTask(toState, task);
|
|
272
|
+
if (fromState !== toState && fs.existsSync(src)) fs.rmSync(src);
|
|
273
|
+
}
|
|
274
|
+
function removeTask(state, id) {
|
|
275
|
+
const p = taskPath(state, id);
|
|
276
|
+
if (fs.existsSync(p)) fs.rmSync(p);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function approveTask(id) {
|
|
280
|
+
const found = findTask(id);
|
|
281
|
+
if (!found) throw new Error(`Gorev bulunamadi: ${id}`);
|
|
282
|
+
if (found.state !== "approval") throw new Error(`Gorev onay beklemiyor: ${id}`);
|
|
283
|
+
const task = found.task;
|
|
284
|
+
if (task.planHash && task.planPreview && task.planHash !== hashText(task.planPreview)) {
|
|
285
|
+
throw new Error("Plan onay beklerken degismis; yeniden planlanmasi gerekiyor.");
|
|
286
|
+
}
|
|
287
|
+
task.approved = true;
|
|
288
|
+
task.status = "pending";
|
|
289
|
+
moveTask(found.state, "pending", task);
|
|
290
|
+
return task;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function rejectTask(id) {
|
|
294
|
+
const found = findTask(id);
|
|
295
|
+
if (!found) throw new Error(`Gorev bulunamadi: ${id}`);
|
|
296
|
+
if (found.state !== "approval") throw new Error(`Gorev onay beklemiyor: ${id}`);
|
|
297
|
+
found.task.status = "rejected";
|
|
298
|
+
found.task.finishedAt = new Date().toISOString();
|
|
299
|
+
moveTask(found.state, "failed", found.task);
|
|
300
|
+
return found.task;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---- Hafiza ----
|
|
304
|
+
function getMemory(charBudget) {
|
|
305
|
+
if (!fs.existsSync(MEM)) return "(hafiza bos)";
|
|
306
|
+
const files = fs
|
|
307
|
+
.readdirSync(MEM)
|
|
308
|
+
.filter((f) => f.endsWith(".md"))
|
|
309
|
+
.map((f) => path.join(MEM, f));
|
|
310
|
+
if (!files.length) return "(hafiza bos)";
|
|
311
|
+
let text = files.map((f) => fs.readFileSync(f, "utf8")).join("\n\n");
|
|
312
|
+
if (charBudget && text.length > charBudget) text = text.slice(-charBudget);
|
|
313
|
+
return text;
|
|
314
|
+
}
|
|
315
|
+
function appendMemory(title, body) {
|
|
316
|
+
const stamp = new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
317
|
+
fs.appendFileSync(path.join(MEM, "log.md"), `\n## ${stamp} — ${title}\n${body}\n`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---- Gunluk cagri butcesi ----
|
|
321
|
+
function callFile() {
|
|
322
|
+
return path.join(STATE, `calls-${new Date().toISOString().slice(0, 10)}.txt`);
|
|
323
|
+
}
|
|
324
|
+
function getCallCount() {
|
|
325
|
+
const f = callFile();
|
|
326
|
+
return fs.existsSync(f) ? parseInt(fs.readFileSync(f, "utf8"), 10) || 0 : 0;
|
|
327
|
+
}
|
|
328
|
+
function bumpCallCount() {
|
|
329
|
+
const n = getCallCount() + 1;
|
|
330
|
+
fs.writeFileSync(callFile(), String(n));
|
|
331
|
+
return n;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
ROOT,
|
|
336
|
+
ASSETS,
|
|
337
|
+
WORK_BASE,
|
|
338
|
+
ensureDirs,
|
|
339
|
+
loadConfig,
|
|
340
|
+
normalizeConfig,
|
|
341
|
+
saveConfig,
|
|
342
|
+
listRoles,
|
|
343
|
+
readRole,
|
|
344
|
+
writeRole,
|
|
345
|
+
deleteRole,
|
|
346
|
+
listTasks,
|
|
347
|
+
nextPending,
|
|
348
|
+
addTask,
|
|
349
|
+
addChatTask,
|
|
350
|
+
addScheduledTask,
|
|
351
|
+
saveTask,
|
|
352
|
+
findTask,
|
|
353
|
+
moveTask,
|
|
354
|
+
removeTask,
|
|
355
|
+
approveTask,
|
|
356
|
+
rejectTask,
|
|
357
|
+
getMemory,
|
|
358
|
+
appendMemory,
|
|
359
|
+
getCallCount,
|
|
360
|
+
bumpCallCount,
|
|
361
|
+
appendRunEvent,
|
|
362
|
+
listRunEvents,
|
|
363
|
+
hashText,
|
|
364
|
+
};
|