@oleksandr.rudnychenko/sync_loop 0.2.2 → 0.2.4
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 +246 -124
- package/bin/cli.js +131 -77
- package/package.json +3 -4
- package/src/init.js +569 -365
- package/src/server.js +289 -208
- package/{template → src/template}/README.md +3 -3
- /package/{template → src/template}/.agent-loop/README.md +0 -0
- /package/{template → src/template}/.agent-loop/feedback.md +0 -0
- /package/{template → src/template}/.agent-loop/glossary.md +0 -0
- /package/{template → src/template}/.agent-loop/patterns/api-standards.md +0 -0
- /package/{template → src/template}/.agent-loop/patterns/code-patterns.md +0 -0
- /package/{template → src/template}/.agent-loop/patterns/refactoring-workflow.md +0 -0
- /package/{template → src/template}/.agent-loop/patterns/testing-guide.md +0 -0
- /package/{template → src/template}/.agent-loop/patterns.md +0 -0
- /package/{template → src/template}/.agent-loop/reasoning-kernel.md +0 -0
- /package/{template → src/template}/.agent-loop/validate-env.md +0 -0
- /package/{template → src/template}/.agent-loop/validate-n.md +0 -0
- /package/{template → src/template}/AGENTS.md +0 -0
- /package/{template → src/template}/bootstrap-prompt.md +0 -0
- /package/{template → src/template}/protocol-summary.md +0 -0
package/src/init.js
CHANGED
|
@@ -1,365 +1,569 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return relativePath;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join, dirname, resolve, posix } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const TEMPLATE_DIR = join(__dirname, "template");
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
function readTemplate(relativePath) {
|
|
20
|
+
return readFileSync(join(TEMPLATE_DIR, relativePath), "utf-8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toPosixPath(value) {
|
|
24
|
+
return value.replace(/\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pathDir(value) {
|
|
28
|
+
const dir = posix.dirname(value);
|
|
29
|
+
return dir === "." ? "" : dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitHash(link) {
|
|
33
|
+
const idx = link.indexOf("#");
|
|
34
|
+
if (idx === -1) return { pathPart: link, hash: "" };
|
|
35
|
+
return {
|
|
36
|
+
pathPart: link.slice(0, idx),
|
|
37
|
+
hash: link.slice(idx),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isExternalLink(link) {
|
|
42
|
+
return /^([a-z]+:|#)/i.test(link);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rewriteMarkdownLinks(content, transform) {
|
|
46
|
+
let inFence = false;
|
|
47
|
+
return content
|
|
48
|
+
.split("\n")
|
|
49
|
+
.map((line) => {
|
|
50
|
+
if (line.trimStart().startsWith("```")) {
|
|
51
|
+
inFence = !inFence;
|
|
52
|
+
return line;
|
|
53
|
+
}
|
|
54
|
+
if (inFence) return line;
|
|
55
|
+
return line.replace(/\]\(([^)]+)\)/g, (_match, linkPath) => `](${transform(linkPath)})`);
|
|
56
|
+
})
|
|
57
|
+
.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJsonSafe(path) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function detectPackageManager(dirPath) {
|
|
69
|
+
if (existsSync(join(dirPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
70
|
+
if (existsSync(join(dirPath, "yarn.lock"))) return "yarn";
|
|
71
|
+
if (existsSync(join(dirPath, "package-lock.json"))) return "npm";
|
|
72
|
+
if (existsSync(join(dirPath, "uv.lock"))) return "uv";
|
|
73
|
+
return "npm";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runCommandPrefix(packageManager) {
|
|
77
|
+
if (packageManager === "npm") return "npm run";
|
|
78
|
+
if (packageManager === "pnpm") return "pnpm";
|
|
79
|
+
if (packageManager === "yarn") return "yarn";
|
|
80
|
+
return `${packageManager} run`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function detectNodeFrameworks(pkg) {
|
|
84
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
85
|
+
const frameworks = [];
|
|
86
|
+
const known = [
|
|
87
|
+
["next", "Next.js"],
|
|
88
|
+
["react", "React"],
|
|
89
|
+
["vite", "Vite"],
|
|
90
|
+
["vue", "Vue"],
|
|
91
|
+
["svelte", "Svelte"],
|
|
92
|
+
["tailwindcss", "TailwindCSS"],
|
|
93
|
+
["express", "Express"],
|
|
94
|
+
["fastify", "Fastify"],
|
|
95
|
+
["@nestjs/core", "NestJS"],
|
|
96
|
+
["@modelcontextprotocol/sdk", "MCP SDK"],
|
|
97
|
+
["@tanstack/react-query", "TanStack Query"],
|
|
98
|
+
["zustand", "Zustand"],
|
|
99
|
+
];
|
|
100
|
+
for (const [dep, name] of known) {
|
|
101
|
+
if (deps[dep]) frameworks.push(name);
|
|
102
|
+
}
|
|
103
|
+
return frameworks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function detectNodeStack(projectPath, relativePath = "") {
|
|
107
|
+
const stackRoot = join(projectPath, relativePath);
|
|
108
|
+
const pkgPath = join(stackRoot, "package.json");
|
|
109
|
+
if (!existsSync(pkgPath)) return null;
|
|
110
|
+
|
|
111
|
+
const pkg = readJsonSafe(pkgPath) ?? {};
|
|
112
|
+
const scripts = pkg.scripts ?? {};
|
|
113
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
114
|
+
const packageManager = detectPackageManager(stackRoot);
|
|
115
|
+
const runPrefix = runCommandPrefix(packageManager);
|
|
116
|
+
const frameworks = detectNodeFrameworks(pkg);
|
|
117
|
+
const usesTypeScript = Boolean(deps.typescript || existsSync(join(stackRoot, "tsconfig.json")));
|
|
118
|
+
const languages = usesTypeScript ? ["TypeScript", "JavaScript"] : ["JavaScript"];
|
|
119
|
+
|
|
120
|
+
const stackName = relativePath ? posix.basename(toPosixPath(relativePath)) : "app";
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
name: stackName,
|
|
124
|
+
languages,
|
|
125
|
+
frameworks: frameworks.length > 0 ? frameworks : ["Node.js"],
|
|
126
|
+
testRunner: scripts.test ? `${runPrefix} test` : undefined,
|
|
127
|
+
typeChecker: scripts.typecheck ? `${runPrefix} typecheck` : (usesTypeScript ? "tsc --noEmit" : undefined),
|
|
128
|
+
linter: scripts.lint ? `${runPrefix} lint` : undefined,
|
|
129
|
+
packageManager,
|
|
130
|
+
path: relativePath || undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function detectPythonStack(projectPath, relativePath = "") {
|
|
135
|
+
const stackRoot = join(projectPath, relativePath);
|
|
136
|
+
const pyprojectPath = join(stackRoot, "pyproject.toml");
|
|
137
|
+
const requirementsPath = join(stackRoot, "requirements.txt");
|
|
138
|
+
if (!existsSync(pyprojectPath) && !existsSync(requirementsPath)) return null;
|
|
139
|
+
|
|
140
|
+
const pyproject = existsSync(pyprojectPath) ? readFileSync(pyprojectPath, "utf-8").toLowerCase() : "";
|
|
141
|
+
const reqs = existsSync(requirementsPath) ? readFileSync(requirementsPath, "utf-8").toLowerCase() : "";
|
|
142
|
+
const merged = `${pyproject}\n${reqs}`;
|
|
143
|
+
|
|
144
|
+
const frameworks = [];
|
|
145
|
+
if (merged.includes("fastapi")) frameworks.push("FastAPI");
|
|
146
|
+
if (merged.includes("django")) frameworks.push("Django");
|
|
147
|
+
if (merged.includes("flask")) frameworks.push("Flask");
|
|
148
|
+
if (merged.includes("langgraph")) frameworks.push("LangGraph");
|
|
149
|
+
if (merged.includes("pydantic")) frameworks.push("Pydantic");
|
|
150
|
+
|
|
151
|
+
const stackName = relativePath ? posix.basename(toPosixPath(relativePath)) : "app";
|
|
152
|
+
const hasPyright = merged.includes("pyright");
|
|
153
|
+
const hasMypy = merged.includes("mypy");
|
|
154
|
+
const hasRuff = merged.includes("ruff");
|
|
155
|
+
const hasPytest = merged.includes("pytest");
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: stackName,
|
|
159
|
+
languages: ["Python"],
|
|
160
|
+
frameworks: frameworks.length > 0 ? frameworks : ["Python"],
|
|
161
|
+
testRunner: hasPytest ? "pytest" : undefined,
|
|
162
|
+
typeChecker: hasPyright ? "pyright" : (hasMypy ? "mypy" : undefined),
|
|
163
|
+
linter: hasRuff ? "ruff check ." : undefined,
|
|
164
|
+
path: relativePath || undefined,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function detectStacks(projectPath) {
|
|
169
|
+
const root = resolve(projectPath);
|
|
170
|
+
const candidates = [""];
|
|
171
|
+
|
|
172
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
173
|
+
if (!entry.isDirectory()) continue;
|
|
174
|
+
if (entry.name.startsWith(".")) continue;
|
|
175
|
+
if (entry.name === "node_modules") continue;
|
|
176
|
+
candidates.push(entry.name);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const stacks = [];
|
|
180
|
+
for (const rel of candidates) {
|
|
181
|
+
const nodeStack = detectNodeStack(root, rel);
|
|
182
|
+
if (nodeStack) stacks.push(nodeStack);
|
|
183
|
+
const pythonStack = detectPythonStack(root, rel);
|
|
184
|
+
if (pythonStack) stacks.push(pythonStack);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const deduped = [];
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
for (const stack of stacks) {
|
|
190
|
+
const key = `${stack.name}:${stack.path ?? ""}:${stack.languages.join(",")}`;
|
|
191
|
+
if (seen.has(key)) continue;
|
|
192
|
+
seen.add(key);
|
|
193
|
+
deduped.push(stack);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (deduped.length === 0) {
|
|
197
|
+
return [{
|
|
198
|
+
name: "app",
|
|
199
|
+
languages: ["Unknown"],
|
|
200
|
+
frameworks: ["Unknown"],
|
|
201
|
+
}];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return deduped;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function writeOutput(projectPath, relativePath, content, options = {}) {
|
|
208
|
+
const { dryRun = false, overwrite = true } = options;
|
|
209
|
+
const fullPath = join(projectPath, relativePath);
|
|
210
|
+
const alreadyExists = existsSync(fullPath);
|
|
211
|
+
|
|
212
|
+
if (alreadyExists && !overwrite) {
|
|
213
|
+
return { path: relativePath, status: "skipped" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!dryRun) {
|
|
217
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
218
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (dryRun) {
|
|
222
|
+
return { path: relativePath, status: alreadyExists ? "would-overwrite" : "would-create" };
|
|
223
|
+
}
|
|
224
|
+
return { path: relativePath, status: alreadyExists ? "overwritten" : "created" };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatWriteResult(result) {
|
|
228
|
+
if (result.status === "skipped") return `${result.path} (skipped: exists)`;
|
|
229
|
+
if (result.status.startsWith("would-")) return `${result.path} (dry-run)`;
|
|
230
|
+
return result.path;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function yamlFrontmatter(fields) {
|
|
234
|
+
const lines = ["---"];
|
|
235
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
236
|
+
if (Array.isArray(value)) {
|
|
237
|
+
lines.push(`${key}:`);
|
|
238
|
+
for (const item of value) {
|
|
239
|
+
lines.push(` - "${item}"`);
|
|
240
|
+
}
|
|
241
|
+
} else if (typeof value === "boolean") {
|
|
242
|
+
lines.push(`${key}: ${value}`);
|
|
243
|
+
} else {
|
|
244
|
+
lines.push(`${key}: "${value}"`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
lines.push("---");
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Source file list (maps to src/template/.agent-loop/)
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
const SOURCE_FILES = [
|
|
256
|
+
{ id: "reasoning-kernel", path: ".agent-loop/reasoning-kernel.md" },
|
|
257
|
+
{ id: "feedback", path: ".agent-loop/feedback.md" },
|
|
258
|
+
{ id: "validate-env", path: ".agent-loop/validate-env.md" },
|
|
259
|
+
{ id: "validate-n", path: ".agent-loop/validate-n.md" },
|
|
260
|
+
{ id: "patterns", path: ".agent-loop/patterns.md" },
|
|
261
|
+
{ id: "glossary", path: ".agent-loop/glossary.md" },
|
|
262
|
+
{ id: "code-patterns", path: ".agent-loop/patterns/code-patterns.md" },
|
|
263
|
+
{ id: "testing-guide", path: ".agent-loop/patterns/testing-guide.md" },
|
|
264
|
+
{ id: "refactoring-workflow", path: ".agent-loop/patterns/refactoring-workflow.md" },
|
|
265
|
+
{ id: "api-standards", path: ".agent-loop/patterns/api-standards.md" },
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Platform configs
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
const COPILOT = {
|
|
273
|
+
"reasoning-kernel": { target: ".github/instructions/reasoning-kernel.instructions.md", fm: { name: "SyncLoop: Reasoning Kernel", description: "7-stage agent reasoning loop with context clearage", applyTo: "**/*" } },
|
|
274
|
+
"feedback": { target: ".github/instructions/feedback.instructions.md", fm: { name: "SyncLoop: Feedback Loop", description: "Failure diagnosis, patch protocol, branch pruning", applyTo: "**/*" } },
|
|
275
|
+
"validate-env": { target: ".github/instructions/validate-env.instructions.md", fm: { name: "SyncLoop: Validate Environment", description: "NFR gates: types, tests, layers, complexity", applyTo: "**/*" } },
|
|
276
|
+
"validate-n": { target: ".github/instructions/validate-n.instructions.md", fm: { name: "SyncLoop: Validate Neighbors", description: "Shape, boundary, bridge checks", applyTo: "**/*" } },
|
|
277
|
+
"patterns": { target: ".github/instructions/patterns.instructions.md", fm: { name: "SyncLoop: Pattern Registry", description: "Pattern routing and learned patterns", applyTo: "**/*" } },
|
|
278
|
+
"glossary": { target: ".github/instructions/glossary.instructions.md", fm: { name: "SyncLoop: Glossary", description: "Canonical terminology", applyTo: "**/*" } },
|
|
279
|
+
"code-patterns": { target: ".github/instructions/code-patterns.instructions.md", fm: { name: "SyncLoop: Code Patterns", description: "P1-P11 implementation patterns", applyTo: "{src,app,lib}/**/*.{ts,py,js,jsx,tsx}" } },
|
|
280
|
+
"testing-guide": { target: ".github/instructions/testing-guide.instructions.md", fm: { name: "SyncLoop: Testing Guide", description: "Test patterns and strategies", applyTo: "{tests,test,__tests__}/**/*" } },
|
|
281
|
+
"refactoring-workflow": { target: ".github/instructions/refactoring-workflow.instructions.md", fm: { name: "SyncLoop: Refactoring Workflow", description: "4-phase refactoring checklist", applyTo: "**/*" } },
|
|
282
|
+
"api-standards": { target: ".github/instructions/api-standards.instructions.md", fm: { name: "SyncLoop: API Standards", description: "Boundary contracts and API conventions", applyTo: "{routes,routers,controllers,api}/**/*" } },
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const CURSOR = {
|
|
286
|
+
"reasoning-kernel": { target: ".cursor/rules/01-reasoning-kernel.md", fm: { description: "7-stage agent reasoning loop with context clearage and transitions", alwaysApply: true } },
|
|
287
|
+
"feedback": { target: ".cursor/rules/02-feedback.md", fm: { description: "Failure diagnosis, patch protocol, micro-loop, branch pruning", alwaysApply: true } },
|
|
288
|
+
"validate-env": { target: ".cursor/rules/03-validate-env.md", fm: { description: "Stage 1 NFR gates: types, tests, layers, complexity, debug hygiene", alwaysApply: true } },
|
|
289
|
+
"validate-n": { target: ".cursor/rules/04-validate-n.md", fm: { description: "Stage 2 checks: shapes, boundaries, bridges", alwaysApply: true } },
|
|
290
|
+
"patterns": { target: ".cursor/rules/05-patterns.md", fm: { description: "Pattern routing index and learned patterns", alwaysApply: true } },
|
|
291
|
+
"glossary": { target: ".cursor/rules/06-glossary.md", fm: { description: "Canonical domain terminology and naming rules", alwaysApply: true } },
|
|
292
|
+
"code-patterns": { target: ".cursor/rules/07-code-patterns.md", fm: { description: "P1-P11 implementation patterns for layered code", globs: "{src,app,lib}/**/*.{ts,py,js,jsx,tsx}" } },
|
|
293
|
+
"testing-guide": { target: ".cursor/rules/08-testing-guide.md", fm: { description: "Test patterns, fixtures, mocks, strategies", globs: "{tests,test,__tests__}/**/*" } },
|
|
294
|
+
"refactoring-workflow": { target: ".cursor/rules/09-refactoring-workflow.md", fm: { description: "4-phase refactoring checklist for safe restructuring", alwaysApply: false } },
|
|
295
|
+
"api-standards": { target: ".cursor/rules/10-api-standards.md", fm: { description: "Boundary contracts, typed models, error envelopes", globs: "{routes,routers,controllers,api}/**/*" } },
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const CLAUDE = {
|
|
299
|
+
"reasoning-kernel": { target: ".claude/rules/reasoning-kernel.md", fm: { paths: ["**/*"] } },
|
|
300
|
+
"feedback": { target: ".claude/rules/feedback.md", fm: { paths: ["**/*"] } },
|
|
301
|
+
"validate-env": { target: ".claude/rules/validate-env.md", fm: { paths: ["**/*"] } },
|
|
302
|
+
"validate-n": { target: ".claude/rules/validate-n.md", fm: { paths: ["**/*"] } },
|
|
303
|
+
"patterns": { target: ".claude/rules/patterns.md", fm: { paths: ["**/*"] } },
|
|
304
|
+
"glossary": { target: ".claude/rules/glossary.md", fm: { paths: ["**/*"] } },
|
|
305
|
+
"code-patterns": { target: ".claude/rules/code-patterns.md", fm: { paths: ["src/**", "app/**", "lib/**"] } },
|
|
306
|
+
"testing-guide": { target: ".claude/rules/testing-guide.md", fm: { paths: ["tests/**", "test/**", "__tests__/**"] } },
|
|
307
|
+
"refactoring-workflow": { target: ".claude/rules/refactoring-workflow.md", fm: { paths: ["**/*"] } },
|
|
308
|
+
"api-standards": { target: ".claude/rules/api-standards.md", fm: { paths: ["**/routes/**", "**/api/**", "**/controllers/**"] } },
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const PLATFORM_CONFIGS = { copilot: COPILOT, cursor: CURSOR, claude: CLAUDE };
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Link rewriting
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
const CANONICAL_TO_ID = {};
|
|
318
|
+
for (const source of SOURCE_FILES) {
|
|
319
|
+
CANONICAL_TO_ID[source.path.replace(".agent-loop/", "")] = source.id;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function buildTargetMap(platform) {
|
|
323
|
+
const config = PLATFORM_CONFIGS[platform];
|
|
324
|
+
const map = {};
|
|
325
|
+
for (const [id, entry] of Object.entries(config)) {
|
|
326
|
+
map[id] = toPosixPath(entry.target);
|
|
327
|
+
}
|
|
328
|
+
return map;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function resolveCanonicalLink(linkPath, sourceId) {
|
|
332
|
+
const source = SOURCE_FILES.find((item) => item.id === sourceId);
|
|
333
|
+
if (!source) return linkPath;
|
|
334
|
+
|
|
335
|
+
const sourceRelPath = source.path.replace(".agent-loop/", "");
|
|
336
|
+
const sourceDir = pathDir(sourceRelPath);
|
|
337
|
+
const normalized = posix.normalize(posix.join(sourceDir || ".", linkPath));
|
|
338
|
+
return normalized.replace(/^\.\//, "");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function rewriteSpecLinks(content, sourceId, platform) {
|
|
342
|
+
const targetMap = buildTargetMap(platform);
|
|
343
|
+
const currentTargetPath = targetMap[sourceId];
|
|
344
|
+
const currentDir = pathDir(currentTargetPath);
|
|
345
|
+
|
|
346
|
+
return rewriteMarkdownLinks(content, (linkPath) => {
|
|
347
|
+
if (isExternalLink(linkPath)) return linkPath;
|
|
348
|
+
|
|
349
|
+
const { pathPart, hash } = splitHash(linkPath);
|
|
350
|
+
if (!pathPart) return linkPath;
|
|
351
|
+
|
|
352
|
+
const canonical = resolveCanonicalLink(pathPart, sourceId);
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
pathPart === "../AGENTS.md" ||
|
|
356
|
+
canonical === "../AGENTS.md" ||
|
|
357
|
+
canonical === "AGENTS.md"
|
|
358
|
+
) {
|
|
359
|
+
const rel = posix.relative(currentDir || ".", "AGENTS.md") || "AGENTS.md";
|
|
360
|
+
return `${rel}${hash}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
pathPart === "../README.md" ||
|
|
365
|
+
canonical === "../README.md"
|
|
366
|
+
) {
|
|
367
|
+
const rel = posix.relative(currentDir || ".", "README.md") || "README.md";
|
|
368
|
+
return `${rel}${hash}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (pathPart === "patterns/" || pathPart === "patterns") {
|
|
372
|
+
const targetPath = targetMap.patterns;
|
|
373
|
+
if (!targetPath) return linkPath;
|
|
374
|
+
const rel = posix.relative(currentDir || ".", targetPath) || posix.basename(targetPath);
|
|
375
|
+
return `${rel}${hash}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const docId = CANONICAL_TO_ID[canonical];
|
|
379
|
+
if (!docId) return linkPath;
|
|
380
|
+
|
|
381
|
+
const targetPath = targetMap[docId];
|
|
382
|
+
if (!targetPath) return linkPath;
|
|
383
|
+
|
|
384
|
+
const rel = posix.relative(currentDir || ".", targetPath) || posix.basename(targetPath);
|
|
385
|
+
return `${rel}${hash}`;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function rewriteAgentsLinks(content, platform) {
|
|
390
|
+
if (platform === "all") return content;
|
|
391
|
+
|
|
392
|
+
const config = PLATFORM_CONFIGS[platform];
|
|
393
|
+
const pathMap = {};
|
|
394
|
+
for (const source of SOURCE_FILES) {
|
|
395
|
+
const entry = config[source.id];
|
|
396
|
+
if (entry) pathMap[source.path] = entry.target;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const platformDir = Object.values(config)[0].target.split("/").slice(0, -1).join("/");
|
|
400
|
+
let result = content;
|
|
401
|
+
|
|
402
|
+
result = result.replace(/\]\(\.agent-loop\/([^)]+)\)/g, (_match, relPath) => {
|
|
403
|
+
const fullPath = `.agent-loop/${relPath}`;
|
|
404
|
+
if (pathMap[fullPath]) return `](${pathMap[fullPath]})`;
|
|
405
|
+
if (relPath === "patterns/" || relPath === "patterns") return `](${platformDir}/)`;
|
|
406
|
+
return `](${fullPath})`;
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
result = result.replace(/`\.agent-loop\/([^`]+)`/g, (_match, relPath) => {
|
|
410
|
+
const fullPath = `.agent-loop/${relPath}`;
|
|
411
|
+
if (pathMap[fullPath]) return `\`${pathMap[fullPath]}\``;
|
|
412
|
+
if (relPath.startsWith("patterns/") || relPath === "patterns") return `\`${platformDir}/\``;
|
|
413
|
+
return `\`${fullPath}\``;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
result = result.replace(
|
|
417
|
+
/Routes into `\.agent-loop\/`/,
|
|
418
|
+
`Routes into \`${platformDir}/\``,
|
|
419
|
+
);
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Environment placeholder replacement
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
function applyStacks(content, stacks) {
|
|
428
|
+
if (!stacks || stacks.length === 0) return content;
|
|
429
|
+
|
|
430
|
+
const testRunners = stacks.map((stack) => stack.testRunner).filter(Boolean);
|
|
431
|
+
const typeCheckers = stacks.map((stack) => stack.typeChecker).filter(Boolean);
|
|
432
|
+
const linters = stacks.map((stack) => stack.linter).filter(Boolean);
|
|
433
|
+
const packageManagers = [...new Set(stacks.map((stack) => stack.packageManager).filter(Boolean))];
|
|
434
|
+
|
|
435
|
+
const stackRows = stacks
|
|
436
|
+
.map((stack) => `| ${stack.name}${stack.path ? ` (\`${stack.path}\`)` : ""} | ${stack.languages.join(", ")} | ${stack.frameworks.join(", ")} |`)
|
|
437
|
+
.join("\n");
|
|
438
|
+
const stackTable = `| Stack | Languages | Frameworks |\n|-------|-----------|------------|\n${stackRows}`;
|
|
439
|
+
|
|
440
|
+
const replacements = {
|
|
441
|
+
"{typecheck command}": typeCheckers.join(" && ") || "{typecheck command}",
|
|
442
|
+
"{lint command}": linters.join(" && ") || "{lint command}",
|
|
443
|
+
"{test command}": testRunners.join(" && ") || "{test command}",
|
|
444
|
+
"{targeted test command}": testRunners[0] ? `${testRunners[0]} {path}` : "{targeted test command}",
|
|
445
|
+
"{install command}": packageManagers.map((pm) => `${pm} install`).join(" && ") || "{install command}",
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
let result = content;
|
|
449
|
+
|
|
450
|
+
const legacyTableRegex = /\| Layer \| Stack \|\r?\n\|[-|]+\|\r?\n\| Backend \|[^\r\n]*\|\r?\n\| Frontend \|[^\r\n]*\|\r?\n\| Infra \|[^\r\n]*\|/;
|
|
451
|
+
const generatedTableRegex = /\| Stack \| Languages \| Frameworks \|\r?\n\|[-|]+\|(?:\r?\n\|[^\r\n]*\|)+/;
|
|
452
|
+
result = result.replace(legacyTableRegex, stackTable);
|
|
453
|
+
result = result.replace(generatedTableRegex, stackTable);
|
|
454
|
+
|
|
455
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
456
|
+
result = result.replaceAll(placeholder, value);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
// Platform file generation
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
function generatePlatformFiles(projectPath, platform, stacks, options) {
|
|
467
|
+
const config = PLATFORM_CONFIGS[platform];
|
|
468
|
+
const results = [];
|
|
469
|
+
|
|
470
|
+
for (const source of SOURCE_FILES) {
|
|
471
|
+
const entry = config[source.id];
|
|
472
|
+
if (!entry) continue;
|
|
473
|
+
|
|
474
|
+
let content = applyStacks(readTemplate(source.path), stacks);
|
|
475
|
+
content = rewriteSpecLinks(content, source.id, platform);
|
|
476
|
+
const frontmatter = yamlFrontmatter(entry.fm);
|
|
477
|
+
const writeResult = writeOutput(projectPath, entry.target, `${frontmatter}\n\n${content}`, options);
|
|
478
|
+
results.push(` ${formatWriteResult(writeResult)}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const platformDir = Object.values(config)[0].target.split("/").slice(0, -1).join("/");
|
|
482
|
+
const summary = readTemplate("protocol-summary.md")
|
|
483
|
+
.replace(/`\.agent-loop\/`/, `\`${platformDir}/\``);
|
|
484
|
+
|
|
485
|
+
if (platform === "copilot") {
|
|
486
|
+
const writeResult = writeOutput(projectPath, ".github/copilot-instructions.md", summary, options);
|
|
487
|
+
results.push(` ${formatWriteResult(writeResult)}`);
|
|
488
|
+
} else if (platform === "cursor") {
|
|
489
|
+
const frontmatter = yamlFrontmatter({ description: "SyncLoop protocol summary and guardrails", alwaysApply: true });
|
|
490
|
+
const writeResult = writeOutput(projectPath, ".cursor/rules/00-protocol.md", `${frontmatter}\n\n${summary}`, options);
|
|
491
|
+
results.push(` ${formatWriteResult(writeResult)}`);
|
|
492
|
+
} else if (platform === "claude") {
|
|
493
|
+
const writeResult = writeOutput(projectPath, "CLAUDE.md", summary, options);
|
|
494
|
+
results.push(` ${formatWriteResult(writeResult)}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return results;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Public API
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
export function init(projectPath, target = "all", stacks = [], options = {}) {
|
|
505
|
+
const root = resolve(projectPath);
|
|
506
|
+
const effectiveStacks = stacks?.length ? stacks : detectStacks(root);
|
|
507
|
+
const mergedOptions = {
|
|
508
|
+
dryRun: Boolean(options.dryRun),
|
|
509
|
+
overwrite: options.overwrite ?? true,
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const validTargets = ["copilot", "cursor", "claude", "all"];
|
|
513
|
+
if (!validTargets.includes(target)) {
|
|
514
|
+
throw new Error(`Unknown target "${target}". Use one of: ${validTargets.join(", ")}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const results = [];
|
|
518
|
+
|
|
519
|
+
if (target === "all") {
|
|
520
|
+
const src = join(TEMPLATE_DIR, ".agent-loop");
|
|
521
|
+
const dest = join(root, ".agent-loop");
|
|
522
|
+
|
|
523
|
+
if (!mergedOptions.dryRun) {
|
|
524
|
+
cpSync(src, dest, {
|
|
525
|
+
recursive: true,
|
|
526
|
+
force: mergedOptions.overwrite,
|
|
527
|
+
errorOnExist: false,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (effectiveStacks.length > 0) {
|
|
531
|
+
for (const source of SOURCE_FILES) {
|
|
532
|
+
const destFile = join(root, source.path);
|
|
533
|
+
if (!mergedOptions.overwrite && existsSync(destFile)) continue;
|
|
534
|
+
try {
|
|
535
|
+
const current = readFileSync(destFile, "utf-8");
|
|
536
|
+
const updated = applyStacks(current, effectiveStacks);
|
|
537
|
+
if (updated !== current) writeFileSync(destFile, updated, "utf-8");
|
|
538
|
+
} catch {
|
|
539
|
+
// Ignore files that are missing after copy.
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
results.push(`.agent-loop/ (canonical source${mergedOptions.dryRun ? ", dry-run" : ""})`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const agentsContent = rewriteAgentsLinks(
|
|
549
|
+
applyStacks(readTemplate("AGENTS.md"), effectiveStacks),
|
|
550
|
+
target === "all" ? "all" : target,
|
|
551
|
+
);
|
|
552
|
+
const agentsResult = writeOutput(root, "AGENTS.md", agentsContent, mergedOptions);
|
|
553
|
+
results.push(`AGENTS.md (cross-platform entrypoint: ${formatWriteResult(agentsResult)})`);
|
|
554
|
+
|
|
555
|
+
const targets = target === "all" ? ["copilot", "cursor", "claude"] : [target];
|
|
556
|
+
for (const currentTarget of targets) {
|
|
557
|
+
results.push(`\n[${currentTarget}]`);
|
|
558
|
+
results.push(...generatePlatformFiles(root, currentTarget, effectiveStacks, mergedOptions));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
projectPath: root,
|
|
563
|
+
target,
|
|
564
|
+
dryRun: mergedOptions.dryRun,
|
|
565
|
+
overwrite: mergedOptions.overwrite,
|
|
566
|
+
stacks: effectiveStacks,
|
|
567
|
+
results,
|
|
568
|
+
};
|
|
569
|
+
}
|