@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 +124 -0
- package/bin/cli.js +77 -0
- package/package.json +35 -0
- package/src/init.js +365 -0
- package/src/server.js +208 -0
- package/template/.agent-loop/README.md +75 -0
- package/template/.agent-loop/feedback.md +395 -0
- package/template/.agent-loop/glossary.md +113 -0
- package/template/.agent-loop/patterns/api-standards.md +132 -0
- package/template/.agent-loop/patterns/code-patterns.md +300 -0
- package/template/.agent-loop/patterns/refactoring-workflow.md +114 -0
- package/template/.agent-loop/patterns/testing-guide.md +258 -0
- package/template/.agent-loop/patterns.md +256 -0
- package/template/.agent-loop/reasoning-kernel.md +521 -0
- package/template/.agent-loop/validate-env.md +332 -0
- package/template/.agent-loop/validate-n.md +321 -0
- package/template/AGENTS.md +157 -0
- package/template/README.md +144 -0
- package/template/bootstrap-prompt.md +37 -0
- package/template/protocol-summary.md +54 -0
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
|
+
}
|