@oleksandr.rudnychenko/sync_loop 0.2.1

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 ADDED
@@ -0,0 +1,124 @@
1
+ # SyncLoop
2
+
3
+ MCP server that gives AI coding agents a self-correcting reasoning protocol.
4
+
5
+ Works with **GitHub Copilot**, **Cursor**, and **Claude Code** — any client that supports [Model Context Protocol](https://modelcontextprotocol.io/).
6
+
7
+ ## Quick Start
8
+
9
+ Add to your MCP client configuration:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "syncloop": {
15
+ "command": "npx",
16
+ "args": ["-y", "syncloop"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ That's it. Your AI agent now has access to the full SyncLoop reasoning protocol.
23
+
24
+ ### Where to add MCP config
25
+
26
+ | Client | Config location |
27
+ |--------|----------------|
28
+ | **VS Code (Copilot)** | `.vscode/mcp.json` or Settings → MCP Servers |
29
+ | **Cursor** | Settings → MCP Servers |
30
+ | **Claude Desktop** | `claude_desktop_config.json` |
31
+ | **Claude Code** | `claude_code_config.json` or `--mcp-config` flag |
32
+
33
+ ## What it provides
34
+
35
+ ### Resources (protocol docs, served on-demand)
36
+
37
+ | Resource | Content |
38
+ |----------|---------|
39
+ | `reasoning-kernel` | Core 7-stage loop, transition map, context clearage |
40
+ | `feedback` | Failure diagnosis, patch protocol, branch pruning |
41
+ | `validate-env` | Stage 1 NFR gates (types, tests, layers, complexity) |
42
+ | `validate-n` | Stage 2 neighbor checks (shapes, boundaries, bridges) |
43
+ | `patterns` | Pattern routing index and learned patterns |
44
+ | `glossary` | Canonical terminology and naming rules |
45
+ | `code-patterns` | P1–P11 implementation patterns |
46
+ | `testing-guide` | Test pyramid, fixtures, mocks, strategies |
47
+ | `refactoring-workflow` | 4-phase safe refactoring checklist |
48
+ | `api-standards` | Boundary contracts and versioning |
49
+ | `protocol-summary` | Condensed protocol overview (~50 lines) |
50
+ | `agents-md` | AGENTS.md entrypoint template |
51
+ | `overview` | File index and framework overview |
52
+
53
+ ### Tools
54
+
55
+ | Tool | Description |
56
+ |------|-------------|
57
+ | `init` | Scaffold platform-specific files into your project (`.github/instructions/`, `.cursor/rules/`, `.claude/rules/`) |
58
+
59
+ ### Prompts
60
+
61
+ | Prompt | Description |
62
+ |--------|-------------|
63
+ | `bootstrap` | Wire SyncLoop to your project — scans codebase and fills in project-specific details |
64
+ | `protocol` | Condensed reasoning protocol for system-level injection |
65
+
66
+ ## Protocol Overview
67
+
68
+ Every agent turn follows a 7-stage loop:
69
+
70
+ ```
71
+ SENSE → GKP → DECIDE+ACT → CHALLENGE-TEST → UPDATE → LEARN → REPORT
72
+ ```
73
+
74
+ | Stage | Purpose |
75
+ |-------|---------|
76
+ | **SENSE** | Detect state, issues, context gaps |
77
+ | **GKP** | Gather knowledge, compress into constraints + risks |
78
+ | **DECIDE+ACT** | Select mode, plan, execute immediately |
79
+ | **CHALLENGE-TEST** | 2-stage validation (ENV gates + NEIGHBOR checks) |
80
+ | **UPDATE** | Commit state transitions |
81
+ | **LEARN** | Persist fixes and patterns |
82
+ | **REPORT** | Session summary (skip if trivial) |
83
+
84
+ Three operational modes:
85
+
86
+ | Mode | Trigger |
87
+ |------|---------|
88
+ | **INTACT-STABILIZE** | All gates pass → harden quality |
89
+ | **BROKEN-EXPAND** | Issues detected → fix root cause |
90
+ | **OVERDENSE-SPLIT** | Complexity high → decompose first |
91
+
92
+ ## Scaffolding (optional)
93
+
94
+ If you also want the protocol files in your project (for offline use or customization), use the `init` tool:
95
+
96
+ ```
97
+ Use the syncloop init tool to scaffold files for copilot/cursor/claude/all
98
+ ```
99
+
100
+ When an AI agent performs this step, it should ask first:
101
+ "Which SyncLoop target platform should I scaffold: `copilot`, `cursor`, `claude`, or `all`?"
102
+
103
+ This creates:
104
+
105
+ | Target | Files generated |
106
+ |--------|----------------|
107
+ | `copilot` | `.github/copilot-instructions.md` + `.github/instructions/*.instructions.md` |
108
+ | `cursor` | `.cursor/rules/*.md` with proper frontmatter |
109
+ | `claude` | `CLAUDE.md` + `.claude/rules/*.md` with `paths` frontmatter |
110
+ | `all` | All of the above |
111
+
112
+ Plus `AGENTS.md` (cross-platform entrypoint) and `.agent-loop/` (canonical source).
113
+
114
+ ## Bootstrap
115
+
116
+ After scaffolding, use the `bootstrap` prompt to wire SyncLoop to your actual project:
117
+
118
+ 1. Ask the agent to use the **bootstrap** prompt
119
+ 2. The agent scans your codebase structure and fills in project-specific details
120
+ 3. `AGENTS.md`, validation commands, patterns, and glossary get wired to real code
121
+
122
+ ## License
123
+
124
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ const args = process.argv.slice(2);
4
+ const command = args[0];
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Help
8
+ // ---------------------------------------------------------------------------
9
+ if (args.includes("--help") || args.includes("-h")) {
10
+ process.stdout.write(`
11
+ syncloop — MCP server + CLI for the SyncLoop agent reasoning protocol
12
+
13
+ Usage:
14
+ npx syncloop Start MCP server (stdio transport)
15
+ npx syncloop init [--target <platform>] Scaffold files into current project
16
+ npx syncloop --help Show this help
17
+
18
+ Init targets:
19
+ copilot .github/instructions/ + copilot-instructions.md
20
+ cursor .cursor/rules/ with frontmatter
21
+ claude CLAUDE.md + .claude/rules/
22
+ all All of the above (default)
23
+
24
+ MCP Configuration (add to your client settings):
25
+
26
+ {
27
+ "mcpServers": {
28
+ "syncloop": {
29
+ "command": "npx",
30
+ "args": ["-y", "syncloop"]
31
+ }
32
+ }
33
+ }
34
+
35
+ Resources: Protocol docs on-demand (reasoning kernel, validation, feedback, patterns)
36
+ Tools: init — scaffold platform-specific files
37
+ Prompts: bootstrap — wire SyncLoop to your project; protocol — reasoning loop
38
+
39
+ https://github.com/nicekid1/syncloop
40
+ `);
41
+ process.exit(0);
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // CLI: npx syncloop init
46
+ // ---------------------------------------------------------------------------
47
+ if (command === "init") {
48
+ const targetIdx = args.indexOf("--target");
49
+ const target = targetIdx !== -1 && args[targetIdx + 1] ? args[targetIdx + 1] : "all";
50
+ const validTargets = ["copilot", "cursor", "claude", "all"];
51
+
52
+ if (!validTargets.includes(target)) {
53
+ process.stderr.write(`Error: unknown target "${target}". Use one of: ${validTargets.join(", ")}\n`);
54
+ process.exit(1);
55
+ }
56
+
57
+ const { init } = await import("../src/init.js");
58
+
59
+ // Positional arg after flags = project path; default to cwd
60
+ const positional = args.slice(1).filter(a => !a.startsWith("--") && a !== target);
61
+ const projectPath = positional[0] || process.cwd();
62
+
63
+ try {
64
+ const results = init(projectPath, target);
65
+ process.stdout.write(`SyncLoop initialized for ${target}:\n\n${results.join("\n")}\n\nDone. Run the bootstrap prompt to wire to your project.\n`);
66
+ } catch (err) {
67
+ process.stderr.write(`Error: ${err.message}\n`);
68
+ process.exit(1);
69
+ }
70
+
71
+ process.exit(0);
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Default: start MCP server
76
+ // ---------------------------------------------------------------------------
77
+ import("../src/server.js");
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@oleksandr.rudnychenko/sync_loop",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "description": "MCP server for the SyncLoop agent reasoning protocol — works with Copilot, Cursor, Claude Code",
6
+ "bin": {
7
+ "sync-loop": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "template/"
13
+ ],
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.0.0",
16
+ "zod": "^3.23.0"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "ai",
21
+ "agent",
22
+ "copilot",
23
+ "cursor",
24
+ "claude",
25
+ "prompt-engineering",
26
+ "coding-agent",
27
+ "reasoning-loop",
28
+ "sync_loop"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "oleksandr.rudnychenko",
32
+ "engines": {
33
+ "node": ">=18"
34
+ }
35
+ }
package/src/init.js ADDED
@@ -0,0 +1,365 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, cpSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const TEMPLATE_DIR = join(__dirname, "..", "template");
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Helpers
10
+ // ---------------------------------------------------------------------------
11
+
12
+ function readTemplate(relativePath) {
13
+ return readFileSync(join(TEMPLATE_DIR, relativePath), "utf-8");
14
+ }
15
+
16
+ function writeOutput(projectPath, relativePath, content) {
17
+ const fullPath = join(projectPath, relativePath);
18
+ mkdirSync(dirname(fullPath), { recursive: true });
19
+ writeFileSync(fullPath, content, "utf-8");
20
+ return relativePath;
21
+ }
22
+
23
+ function yamlFrontmatter(fields) {
24
+ const lines = ["---"];
25
+ for (const [key, value] of Object.entries(fields)) {
26
+ if (Array.isArray(value)) {
27
+ lines.push(`${key}:`);
28
+ for (const item of value) {
29
+ lines.push(` - "${item}"`);
30
+ }
31
+ } else if (typeof value === "boolean") {
32
+ lines.push(`${key}: ${value}`);
33
+ } else {
34
+ lines.push(`${key}: "${value}"`);
35
+ }
36
+ }
37
+ lines.push("---");
38
+ return lines.join("\n");
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Source file list (maps to template/.agent-loop/)
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const SOURCE_FILES = [
46
+ { id: "reasoning-kernel", path: ".agent-loop/reasoning-kernel.md" },
47
+ { id: "feedback", path: ".agent-loop/feedback.md" },
48
+ { id: "validate-env", path: ".agent-loop/validate-env.md" },
49
+ { id: "validate-n", path: ".agent-loop/validate-n.md" },
50
+ { id: "patterns", path: ".agent-loop/patterns.md" },
51
+ { id: "glossary", path: ".agent-loop/glossary.md" },
52
+ { id: "code-patterns", path: ".agent-loop/patterns/code-patterns.md" },
53
+ { id: "testing-guide", path: ".agent-loop/patterns/testing-guide.md" },
54
+ { id: "refactoring-workflow", path: ".agent-loop/patterns/refactoring-workflow.md" },
55
+ { id: "api-standards", path: ".agent-loop/patterns/api-standards.md" },
56
+ ];
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Platform configs — source ID → { target path, YAML frontmatter }
60
+ // ---------------------------------------------------------------------------
61
+
62
+ const COPILOT = {
63
+ "reasoning-kernel": { target: ".github/instructions/reasoning-kernel.instructions.md", fm: { name: "SyncLoop: Reasoning Kernel", description: "7-stage agent reasoning loop with context clearage", applyTo: "**/*" } },
64
+ "feedback": { target: ".github/instructions/feedback.instructions.md", fm: { name: "SyncLoop: Feedback Loop", description: "Failure diagnosis, patch protocol, branch pruning", applyTo: "**/*" } },
65
+ "validate-env": { target: ".github/instructions/validate-env.instructions.md", fm: { name: "SyncLoop: Validate Environment", description: "NFR gates: types, tests, layers, complexity", applyTo: "**/*" } },
66
+ "validate-n": { target: ".github/instructions/validate-n.instructions.md", fm: { name: "SyncLoop: Validate Neighbors", description: "Shape, boundary, bridge checks", applyTo: "**/*" } },
67
+ "patterns": { target: ".github/instructions/patterns.instructions.md", fm: { name: "SyncLoop: Pattern Registry", description: "Pattern routing and learned patterns", applyTo: "**/*" } },
68
+ "glossary": { target: ".github/instructions/glossary.instructions.md", fm: { name: "SyncLoop: Glossary", description: "Canonical terminology", applyTo: "**/*" } },
69
+ "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}" } },
70
+ "testing-guide": { target: ".github/instructions/testing-guide.instructions.md", fm: { name: "SyncLoop: Testing Guide", description: "Test patterns and strategies", applyTo: "{tests,test,__tests__}/**/*" } },
71
+ "refactoring-workflow":{ target: ".github/instructions/refactoring-workflow.instructions.md", fm: { name: "SyncLoop: Refactoring Workflow", description: "4-phase refactoring checklist", applyTo: "**/*" } },
72
+ "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}/**/*" } },
73
+ };
74
+
75
+ const CURSOR = {
76
+ "reasoning-kernel": { target: ".cursor/rules/01-reasoning-kernel.md", fm: { description: "7-stage agent reasoning loop with context clearage and transitions", alwaysApply: true } },
77
+ "feedback": { target: ".cursor/rules/02-feedback.md", fm: { description: "Failure diagnosis, patch protocol, micro-loop, branch pruning", alwaysApply: true } },
78
+ "validate-env": { target: ".cursor/rules/03-validate-env.md", fm: { description: "Stage 1 NFR gates: types, tests, layers, complexity, debug hygiene", alwaysApply: true } },
79
+ "validate-n": { target: ".cursor/rules/04-validate-n.md", fm: { description: "Stage 2 checks: shapes, boundaries, bridges", alwaysApply: true } },
80
+ "patterns": { target: ".cursor/rules/05-patterns.md", fm: { description: "Pattern routing index and learned patterns", alwaysApply: true } },
81
+ "glossary": { target: ".cursor/rules/06-glossary.md", fm: { description: "Canonical domain terminology and naming rules", alwaysApply: true } },
82
+ "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}" } },
83
+ "testing-guide": { target: ".cursor/rules/08-testing-guide.md", fm: { description: "Test patterns, fixtures, mocks, strategies", globs: "{tests,test,__tests__}/**/*" } },
84
+ "refactoring-workflow":{ target: ".cursor/rules/09-refactoring-workflow.md", fm: { description: "4-phase refactoring checklist for safe restructuring", alwaysApply: false } },
85
+ "api-standards": { target: ".cursor/rules/10-api-standards.md", fm: { description: "Boundary contracts, typed models, error envelopes", globs: "{routes,routers,controllers,api}/**/*" } },
86
+ };
87
+
88
+ const CLAUDE = {
89
+ "reasoning-kernel": { target: ".claude/rules/reasoning-kernel.md", fm: { paths: ["**/*"] } },
90
+ "feedback": { target: ".claude/rules/feedback.md", fm: { paths: ["**/*"] } },
91
+ "validate-env": { target: ".claude/rules/validate-env.md", fm: { paths: ["**/*"] } },
92
+ "validate-n": { target: ".claude/rules/validate-n.md", fm: { paths: ["**/*"] } },
93
+ "patterns": { target: ".claude/rules/patterns.md", fm: { paths: ["**/*"] } },
94
+ "glossary": { target: ".claude/rules/glossary.md", fm: { paths: ["**/*"] } },
95
+ "code-patterns": { target: ".claude/rules/code-patterns.md", fm: { paths: ["src/**", "app/**", "lib/**"] } },
96
+ "testing-guide": { target: ".claude/rules/testing-guide.md", fm: { paths: ["tests/**", "test/**", "__tests__/**"] } },
97
+ "refactoring-workflow":{ target: ".claude/rules/refactoring-workflow.md", fm: { paths: ["**/*"] } },
98
+ "api-standards": { target: ".claude/rules/api-standards.md", fm: { paths: ["**/routes/**", "**/api/**", "**/controllers/**"] } },
99
+ };
100
+
101
+ const PLATFORM_CONFIGS = { copilot: COPILOT, cursor: CURSOR, claude: CLAUDE };
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Link rewriting — rewrite .agent-loop/ refs to platform-specific paths
105
+ // ---------------------------------------------------------------------------
106
+
107
+ // Map from canonical .agent-loop/ filename to source ID
108
+ const CANONICAL_TO_ID = {};
109
+ for (const s of SOURCE_FILES) {
110
+ // ".agent-loop/reasoning-kernel.md" → "reasoning-kernel"
111
+ CANONICAL_TO_ID[s.path.replace(".agent-loop/", "")] = s.id;
112
+ }
113
+
114
+ /**
115
+ * Build a lookup: source-id → target filename (basename only, since platform
116
+ * files live in one flat directory per platform).
117
+ */
118
+ function buildTargetMap(platform) {
119
+ const config = PLATFORM_CONFIGS[platform];
120
+ const map = {};
121
+ for (const [id, entry] of Object.entries(config)) {
122
+ // e.g. ".github/instructions/feedback.instructions.md" → "feedback.instructions.md"
123
+ map[id] = entry.target.split("/").pop();
124
+ }
125
+ return map;
126
+ }
127
+
128
+ /**
129
+ * Rewrite internal cross-references inside a spec file.
130
+ *
131
+ * Template files use relative paths like:
132
+ * [reasoning-kernel.md](reasoning-kernel.md)
133
+ * [patterns/code-patterns.md](patterns/code-patterns.md)
134
+ * [../AGENTS.md](../AGENTS.md)
135
+ *
136
+ * For a platform target these must point to sibling files in the same directory.
137
+ */
138
+ function rewriteSpecLinks(content, sourceId, platform) {
139
+ const targetMap = buildTargetMap(platform);
140
+ const sourceFile = SOURCE_FILES.find(s => s.id === sourceId);
141
+ // Is this file in a subdirectory (patterns/)?
142
+ const isNested = sourceFile?.path.includes("patterns/") && sourceId !== "patterns";
143
+
144
+ return content.replace(/\]\(([^)]+\.md)\)/g, (_match, linkPath) => {
145
+ // Normalize: resolve relative paths from the source file's perspective
146
+ let canonical = linkPath;
147
+
148
+ if (isNested) {
149
+ // Files in patterns/ use "../" to reach parent-level siblings
150
+ if (canonical.startsWith("../")) {
151
+ canonical = canonical.replace(/^\.\.\//, "");
152
+ } else {
153
+ // Sibling reference within patterns/ → prefix with "patterns/"
154
+ canonical = `patterns/${canonical}`;
155
+ }
156
+ }
157
+
158
+ // Handle ../AGENTS.md → just skip (AGENTS.md is at root, not in platform dir)
159
+ if (canonical === "AGENTS.md" || canonical === "../AGENTS.md") {
160
+ return "](../AGENTS.md)";
161
+ }
162
+
163
+ // Handle ../README.md (agent-loop readme — not relevant for platform files)
164
+ if (canonical === "README.md" || canonical === "../README.md") {
165
+ return `](${linkPath})`;
166
+ }
167
+
168
+ // Look up the target filename for this canonical path
169
+ const id = CANONICAL_TO_ID[canonical];
170
+ if (id && targetMap[id]) {
171
+ return `](${targetMap[id]})`;
172
+ }
173
+
174
+ // No match — keep original
175
+ return `](${linkPath})`;
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Rewrite AGENTS.md links from .agent-loop/ paths to platform-specific paths.
181
+ *
182
+ * For target="all" links stay as .agent-loop/ (canonical source exists).
183
+ * For single platform, links point into that platform's directory.
184
+ */
185
+ function rewriteAgentsLinks(content, platform) {
186
+ if (platform === "all") return content;
187
+
188
+ const config = PLATFORM_CONFIGS[platform];
189
+
190
+ // Build map: ".agent-loop/reasoning-kernel.md" → platform target path
191
+ const pathMap = {};
192
+ for (const source of SOURCE_FILES) {
193
+ const entry = config[source.id];
194
+ if (entry) {
195
+ pathMap[source.path] = entry.target;
196
+ }
197
+ }
198
+
199
+ // Also map the patterns/ directory reference
200
+ const platformDir = Object.values(config)[0].target.split("/").slice(0, -1).join("/");
201
+
202
+ let result = content;
203
+
204
+ // Rewrite markdown links: [text](.agent-loop/xxx.md) → [text](platform/xxx.md)
205
+ result = result.replace(/\]\(\.agent-loop\/([^)]+)\)/g, (_match, relPath) => {
206
+ const fullPath = `.agent-loop/${relPath}`;
207
+ if (pathMap[fullPath]) {
208
+ return `](${pathMap[fullPath]})`;
209
+ }
210
+ // Directory reference like .agent-loop/patterns/
211
+ if (relPath === "patterns/" || relPath === "patterns") {
212
+ return `](${platformDir}/)`;
213
+ }
214
+ return `](${fullPath})`;
215
+ });
216
+
217
+ // Rewrite inline backtick references: `.agent-loop/patterns.md` → platform path
218
+ result = result.replace(/`\.agent-loop\/([^`]+)`/g, (_match, relPath) => {
219
+ const fullPath = `.agent-loop/${relPath}`;
220
+ if (pathMap[fullPath]) {
221
+ return `\`${pathMap[fullPath]}\``;
222
+ }
223
+ if (relPath.startsWith("patterns/") || relPath === "patterns") {
224
+ return `\`${platformDir}/\``;
225
+ }
226
+ return `\`${fullPath}\``;
227
+ });
228
+
229
+ // Rewrite the intro line
230
+ result = result.replace(
231
+ /Routes into `\.agent-loop\/`/,
232
+ `Routes into \`${platformDir}/\``,
233
+ );
234
+
235
+ return result;
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Environment placeholder replacement
240
+ // ---------------------------------------------------------------------------
241
+
242
+ function applyStacks(content, stacks) {
243
+ if (!stacks || stacks.length === 0) return content;
244
+
245
+ // Aggregate across all stacks
246
+ const allLanguages = [...new Set(stacks.flatMap(s => s.languages))];
247
+ const allFrameworks = [...new Set(stacks.flatMap(s => s.frameworks))];
248
+ const testRunners = stacks.map(s => s.testRunner).filter(Boolean);
249
+ const typeCheckers = stacks.map(s => s.typeChecker).filter(Boolean);
250
+ const linters = stacks.map(s => s.linter).filter(Boolean);
251
+ const packageManagers = [...new Set(stacks.map(s => s.packageManager).filter(Boolean))];
252
+
253
+ // Build stack table for AGENTS.md
254
+ const stackRows = stacks.map(s =>
255
+ `| ${s.name}${s.path ? ` (\`${s.path}\`)` : ""} | ${s.languages.join(", ")} | ${s.frameworks.join(", ")} |`
256
+ ).join("\n");
257
+ const stackTable = `| Stack | Languages | Frameworks |\n|-------|-----------|------------|\n${stackRows}`;
258
+
259
+ const replacements = {
260
+ "{typecheck command}": typeCheckers.join(" && ") || "{typecheck command}",
261
+ "{lint command}": linters.join(" && ") || "{lint command}",
262
+ "{test command}": testRunners.join(" && ") || "{test command}",
263
+ "{targeted test command}": testRunners[0] ? `${testRunners[0]} {path}` : "{targeted test command}",
264
+ "{install command}": packageManagers.map(pm => `${pm} install`).join(" && ") || "{install command}",
265
+ };
266
+
267
+ let result = content;
268
+
269
+ // Replace the entire Layer/Stack table section (handles \r\n line endings)
270
+ result = result.replace(
271
+ /\| Layer \| Stack \|\r?\n\|[-|]+\|\r?\n\| Backend \|[^\r\n]*\|\r?\n\| Frontend \|[^\r\n]*\|\r?\n\| Infra \|[^\r\n]*\|/,
272
+ stackTable,
273
+ );
274
+
275
+ for (const [placeholder, value] of Object.entries(replacements)) {
276
+ result = result.replaceAll(placeholder, value);
277
+ }
278
+ return result;
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Platform file generation
283
+ // ---------------------------------------------------------------------------
284
+
285
+ function generatePlatformFiles(projectPath, platform, stacks) {
286
+ const config = PLATFORM_CONFIGS[platform];
287
+ const results = [];
288
+
289
+ // Generate per-doc files with frontmatter + rewritten links
290
+ for (const source of SOURCE_FILES) {
291
+ const entry = config[source.id];
292
+ if (!entry) continue;
293
+
294
+ let content = applyStacks(readTemplate(source.path), stacks);
295
+ content = rewriteSpecLinks(content, source.id, platform);
296
+ const frontmatter = yamlFrontmatter(entry.fm);
297
+ writeOutput(projectPath, entry.target, `${frontmatter}\n\n${content}`);
298
+ results.push(` ${entry.target}`);
299
+ }
300
+
301
+ // Generate condensed entrypoint per platform
302
+ const platformDir = Object.values(config)[0].target.split("/").slice(0, -1).join("/");
303
+ const summary = readTemplate("protocol-summary.md")
304
+ .replace(/`\.agent-loop\/`/, `\`${platformDir}/\``);
305
+
306
+ if (platform === "copilot") {
307
+ writeOutput(projectPath, ".github/copilot-instructions.md", summary);
308
+ results.push(" .github/copilot-instructions.md");
309
+ } else if (platform === "cursor") {
310
+ const fm = yamlFrontmatter({ description: "SyncLoop protocol summary and guardrails", alwaysApply: true });
311
+ writeOutput(projectPath, ".cursor/rules/00-protocol.md", `${fm}\n\n${summary}`);
312
+ results.push(" .cursor/rules/00-protocol.md");
313
+ } else if (platform === "claude") {
314
+ writeOutput(projectPath, "CLAUDE.md", summary);
315
+ results.push(" CLAUDE.md");
316
+ }
317
+
318
+ return results;
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // Public API
323
+ // ---------------------------------------------------------------------------
324
+
325
+ export function init(projectPath, target, stacks) {
326
+ const results = [];
327
+
328
+ // 1. Copy .agent-loop/ canonical source — only for "all" (multi-platform needs the shared source)
329
+ if (target === "all") {
330
+ const agentLoopSrc = join(TEMPLATE_DIR, ".agent-loop");
331
+ const agentLoopDest = join(projectPath, ".agent-loop");
332
+ cpSync(agentLoopSrc, agentLoopDest, { recursive: true });
333
+
334
+ // Pre-fill stack placeholders in canonical source files
335
+ if (stacks && stacks.length > 0) {
336
+ for (const source of SOURCE_FILES) {
337
+ const destFile = join(projectPath, source.path);
338
+ try {
339
+ const content = readFileSync(destFile, "utf-8");
340
+ const updated = applyStacks(content, stacks);
341
+ if (updated !== content) writeFileSync(destFile, updated, "utf-8");
342
+ } catch { /* skip if file doesn't exist */ }
343
+ }
344
+ }
345
+ results.push(".agent-loop/ (canonical source)");
346
+ }
347
+
348
+ // AGENTS.md — always generated as root entrypoint
349
+ // For single-platform targets, rewrite .agent-loop/ links to platform paths
350
+ const effectivePlatform = target === "all" ? "all" : target;
351
+ let agentsMd = readTemplate("AGENTS.md");
352
+ if (stacks?.length) agentsMd = applyStacks(agentsMd, stacks);
353
+ agentsMd = rewriteAgentsLinks(agentsMd, effectivePlatform);
354
+ writeOutput(projectPath, "AGENTS.md", agentsMd);
355
+ results.push("AGENTS.md (cross-platform entrypoint)");
356
+
357
+ // 2. Generate platform-specific files
358
+ const targets = target === "all" ? ["copilot", "cursor", "claude"] : [target];
359
+ for (const t of targets) {
360
+ results.push(`\n[${t}]`);
361
+ results.push(...generatePlatformFiles(projectPath, t, stacks));
362
+ }
363
+
364
+ return results;
365
+ }