syntropic 0.3.0 → 0.3.2
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 +95 -0
- package/bin/syntropic.js +2 -0
- package/commands/add.js +195 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# syntropic
|
|
2
|
+
|
|
3
|
+
A development methodology for AI-assisted coding. Works with **Claude Code**, **Cursor**, **Windsurf**, and **GitHub Copilot**.
|
|
4
|
+
|
|
5
|
+
One command gives your AI coding tools a disciplined development pipeline — so they research before they plan, plan before they build, and review before they ship.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx syntropic init my-project
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add to an existing project:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd my-project
|
|
17
|
+
npx syntropic init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You'll be asked which AI tools you use. The CLI generates the right instruction files for each:
|
|
21
|
+
|
|
22
|
+
| Tool | File Generated |
|
|
23
|
+
|------|---------------|
|
|
24
|
+
| Claude Code | `CLAUDE.md` + `.claude/commands/` agents |
|
|
25
|
+
| Cursor | `.cursorrules` |
|
|
26
|
+
| Windsurf | `.windsurfrules` |
|
|
27
|
+
| GitHub Copilot | `.github/copilot-instructions.md` |
|
|
28
|
+
|
|
29
|
+
## What You Get
|
|
30
|
+
|
|
31
|
+
**Evergreen Rules (EG1-EG9)** — a portable set of development principles:
|
|
32
|
+
|
|
33
|
+
- **EG1: Pre-Flight Loop** — build must pass, no localhost refs, verify after deploy
|
|
34
|
+
- **EG2: Ship and Iterate** — bias to action, only pause for destructive/expensive/irreversible
|
|
35
|
+
- **EG3: Hypothesis-Driven Debugging** — state hypothesis, test without code changes, only code if confirmed
|
|
36
|
+
- **EG7: Implementation Pipeline** — scale process to task size (Full / Lightweight / Minimum cycle)
|
|
37
|
+
- **EG8: Live Site Testing** — test page first, promote to production with approval
|
|
38
|
+
- **EG9: Session Continuity** — re-read rules on reset, verify state before proceeding
|
|
39
|
+
|
|
40
|
+
**Generic Agents** (Claude Code) — dev, qa, research, plan, devops, security
|
|
41
|
+
|
|
42
|
+
**Health Check** — daily GitHub Action with auto-remediation (npm audit fix PRs)
|
|
43
|
+
|
|
44
|
+
**PRISM Methodology** — track efficiency: estimate cost vs. value before complex tasks, learn what works
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Scaffold a new project (interactive)
|
|
50
|
+
npx syntropic init my-project
|
|
51
|
+
|
|
52
|
+
# Non-interactive with flags
|
|
53
|
+
npx syntropic init my-project --tools claude,cursor --name "My App" --domain myapp.com --test-url /staging --prod-url /
|
|
54
|
+
|
|
55
|
+
# Add a tool to an existing project
|
|
56
|
+
npx syntropic add cursor
|
|
57
|
+
npx syntropic add windsurf copilot
|
|
58
|
+
|
|
59
|
+
# Run a local health check (EG1 pre-flight)
|
|
60
|
+
npx syntropic health
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Existing Projects
|
|
64
|
+
|
|
65
|
+
Running `syntropic init` in a project that already has a `CLAUDE.md` or `.cursorrules` will **append** the methodology to your existing file — it never overwrites. A `<!-- syntropic -->` marker prevents duplicate rules on re-runs.
|
|
66
|
+
|
|
67
|
+
## The Methodology
|
|
68
|
+
|
|
69
|
+
The core idea: AI coding tools are powerful but undisciplined. Without rules, they jump straight to code, skip investigation, retry failed approaches, and introduce regressions.
|
|
70
|
+
|
|
71
|
+
Syntropic gives them a process:
|
|
72
|
+
|
|
73
|
+
1. **Research** — understand the problem before proposing solutions
|
|
74
|
+
2. **Plan** — break work into sequenced steps, identify risks
|
|
75
|
+
3. **Build** — implement with minimal changes, match existing patterns
|
|
76
|
+
4. **Review** — check security, quality, and correctness
|
|
77
|
+
5. **Verify** — test on a live URL, not just locally
|
|
78
|
+
|
|
79
|
+
Scale the process to the task. A typo fix doesn't need research. A new feature does.
|
|
80
|
+
|
|
81
|
+
## Philosophy
|
|
82
|
+
|
|
83
|
+
- **Ship and iterate** — momentum over perfection
|
|
84
|
+
- **Hypothesis-driven** — test before you code
|
|
85
|
+
- **Outcome-aligned** — if the user succeeds, do we succeed?
|
|
86
|
+
- **No workarounds** — no bypasses, no "fix later", no shortcuts without approval
|
|
87
|
+
|
|
88
|
+
## Links
|
|
89
|
+
|
|
90
|
+
- [Website](https://www.syntropicworks.com)
|
|
91
|
+
- [GitHub](https://github.com/petercholford-ship-it/syntropicworks)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
package/bin/syntropic.js
CHANGED
|
@@ -15,6 +15,7 @@ const command = args[0];
|
|
|
15
15
|
|
|
16
16
|
const COMMANDS = {
|
|
17
17
|
init: () => require('../commands/init'),
|
|
18
|
+
add: () => require('../commands/add'),
|
|
18
19
|
health: () => require('../commands/health'),
|
|
19
20
|
};
|
|
20
21
|
|
|
@@ -32,6 +33,7 @@ if (!command || args.includes('--help') || args.includes('-h')) {
|
|
|
32
33
|
|
|
33
34
|
Usage:
|
|
34
35
|
syntropic init [project-name] Scaffold a new project with the Syntropic pipeline
|
|
36
|
+
syntropic add <tool> [tool...] Add support for another AI tool (cursor, windsurf, copilot, claude)
|
|
35
37
|
syntropic health Run a local health check
|
|
36
38
|
syntropic --version Show version
|
|
37
39
|
syntropic --help Show this help
|
package/commands/add.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syntropic add <tool> [tool...]
|
|
3
|
+
*
|
|
4
|
+
* Adds Syntropic pipeline support for additional AI coding tools
|
|
5
|
+
* to an existing project. Creates new instruction files or appends
|
|
6
|
+
* rules to existing ones.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* syntropic add cursor
|
|
10
|
+
* syntropic add cursor windsurf copilot
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
|
|
17
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
18
|
+
|
|
19
|
+
const TOOLS = {
|
|
20
|
+
claude: { label: 'Claude Code', file: 'CLAUDE.md', hasAgents: true },
|
|
21
|
+
cursor: { label: 'Cursor', file: '.cursorrules', hasAgents: false },
|
|
22
|
+
windsurf: { label: 'Windsurf', file: '.windsurfrules', hasAgents: false },
|
|
23
|
+
copilot: { label: 'GitHub Copilot', file: '.github/copilot-instructions.md', hasAgents: false },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const TEMPLATE_MAP = {
|
|
27
|
+
claude: ['CLAUDE.md', 'claude-append.template'],
|
|
28
|
+
cursor: ['cursorrules.template', 'tool-append.template'],
|
|
29
|
+
windsurf: ['windsurfrules.template', 'tool-append.template'],
|
|
30
|
+
copilot: ['copilot-instructions.template', 'tool-append.template'],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function ask(question) {
|
|
34
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
rl.question(question, (answer) => {
|
|
37
|
+
rl.close();
|
|
38
|
+
resolve(answer.trim());
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasSyntropicMarker(filePath) {
|
|
44
|
+
if (!fs.existsSync(filePath)) return false;
|
|
45
|
+
return fs.readFileSync(filePath, 'utf8').includes('<!-- syntropic -->');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderTemplate(templateFile, replacements) {
|
|
49
|
+
const templatePath = path.join(TEMPLATES_DIR, templateFile);
|
|
50
|
+
if (!fs.existsSync(templatePath)) return null;
|
|
51
|
+
let content = fs.readFileSync(templatePath, 'utf8');
|
|
52
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
53
|
+
content = content.replace(new RegExp(key, 'g'), value);
|
|
54
|
+
}
|
|
55
|
+
return content;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeOrAppend(fullTemplate, appendTemplate, destPath, replacements) {
|
|
59
|
+
const relPath = path.relative(process.cwd(), destPath);
|
|
60
|
+
const destDir = path.dirname(destPath);
|
|
61
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(destPath)) {
|
|
64
|
+
const content = renderTemplate(fullTemplate, replacements);
|
|
65
|
+
if (content) fs.writeFileSync(destPath, content, 'utf8');
|
|
66
|
+
console.log(` create ${relPath}`);
|
|
67
|
+
return 'created';
|
|
68
|
+
} else if (hasSyntropicMarker(destPath)) {
|
|
69
|
+
console.log(` skip ${relPath} (Syntropic rules already present)`);
|
|
70
|
+
return 'skipped';
|
|
71
|
+
} else {
|
|
72
|
+
const appendContent = renderTemplate(appendTemplate, replacements);
|
|
73
|
+
if (appendContent) fs.appendFileSync(destPath, appendContent, 'utf8');
|
|
74
|
+
console.log(` append ${relPath} (added Syntropic pipeline rules)`);
|
|
75
|
+
return 'appended';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detectExistingConfig(targetDir) {
|
|
80
|
+
// Try to read project config from existing Syntropic instruction files
|
|
81
|
+
for (const tool of Object.values(TOOLS)) {
|
|
82
|
+
const filePath = path.join(targetDir, tool.file);
|
|
83
|
+
if (fs.existsSync(filePath)) {
|
|
84
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
85
|
+
const config = {};
|
|
86
|
+
|
|
87
|
+
// Extract test URL
|
|
88
|
+
const testMatch = content.match(/Test page:\s*(\S+)/);
|
|
89
|
+
if (testMatch) config.testUrl = testMatch[1];
|
|
90
|
+
|
|
91
|
+
// Extract prod URL
|
|
92
|
+
const prodMatch = content.match(/Production:\s*(\S+)/);
|
|
93
|
+
if (prodMatch) config.prodUrl = prodMatch[1];
|
|
94
|
+
|
|
95
|
+
// Extract domain
|
|
96
|
+
const domainMatch = content.match(/Production domain:\*?\*?\s*(\S+)/);
|
|
97
|
+
if (domainMatch) config.prodDomain = domainMatch[1];
|
|
98
|
+
|
|
99
|
+
// Extract project name
|
|
100
|
+
const nameMatch = content.match(/## Project:\s*(.+)/);
|
|
101
|
+
if (nameMatch) config.projectName = nameMatch[1].trim();
|
|
102
|
+
|
|
103
|
+
if (Object.keys(config).length > 0) return config;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function run(args) {
|
|
110
|
+
const validTools = Object.keys(TOOLS);
|
|
111
|
+
const requestedTools = args.map(a => a.toLowerCase()).filter(a => validTools.includes(a));
|
|
112
|
+
|
|
113
|
+
if (requestedTools.length === 0) {
|
|
114
|
+
console.log(`
|
|
115
|
+
syntropic add — Add AI tool support to an existing project
|
|
116
|
+
|
|
117
|
+
Usage:
|
|
118
|
+
syntropic add cursor Add Cursor support
|
|
119
|
+
syntropic add windsurf copilot Add multiple tools
|
|
120
|
+
syntropic add claude Add Claude Code support
|
|
121
|
+
|
|
122
|
+
Available tools: ${validTools.join(', ')}
|
|
123
|
+
`);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const targetDir = process.cwd();
|
|
128
|
+
console.log(`\n syntropic add — Adding ${requestedTools.map(t => TOOLS[t].label).join(', ')}\n`);
|
|
129
|
+
|
|
130
|
+
// Try to detect project config from existing instruction files
|
|
131
|
+
const existing = detectExistingConfig(targetDir);
|
|
132
|
+
const isInteractive = process.stdin.isTTY !== false;
|
|
133
|
+
|
|
134
|
+
const dirName = path.basename(targetDir);
|
|
135
|
+
const projectName = existing?.projectName || (isInteractive ? await ask(` Project name (${dirName}): `) : '') || dirName;
|
|
136
|
+
const testUrl = existing?.testUrl || (isInteractive ? await ask(' Test page URL path (e.g. /test): ') : '') || '/test';
|
|
137
|
+
const prodUrl = existing?.prodUrl || (isInteractive ? await ask(' Production URL path (e.g. /): ') : '') || '/';
|
|
138
|
+
const prodDomain = existing?.prodDomain || (isInteractive ? await ask(' Production domain (e.g. example.com): ') : '') || 'example.com';
|
|
139
|
+
|
|
140
|
+
if (existing) {
|
|
141
|
+
console.log(` Detected config: ${projectName} (${prodDomain})\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const replacements = {
|
|
145
|
+
'\\{\\{PROJECT_NAME\\}\\}': projectName,
|
|
146
|
+
'\\{\\{TEST_URL\\}\\}': testUrl,
|
|
147
|
+
'\\{\\{PROD_URL\\}\\}': prodUrl,
|
|
148
|
+
'\\{\\{PROD_DOMAIN\\}\\}': prodDomain,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const tool of requestedTools) {
|
|
152
|
+
const [fullTpl, appendTpl] = TEMPLATE_MAP[tool];
|
|
153
|
+
const destPath = path.join(targetDir, TOOLS[tool].file);
|
|
154
|
+
writeOrAppend(fullTpl, appendTpl, destPath, replacements);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If adding Claude, also copy agents
|
|
158
|
+
if (requestedTools.includes('claude')) {
|
|
159
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
160
|
+
const templateClaudeDir = path.join(TEMPLATES_DIR, '.claude');
|
|
161
|
+
if (fs.existsSync(templateClaudeDir)) {
|
|
162
|
+
const { copyDir } = require('./init');
|
|
163
|
+
if (typeof copyDir === 'function') {
|
|
164
|
+
copyDir(templateClaudeDir, claudeDir);
|
|
165
|
+
} else {
|
|
166
|
+
// Inline copy for agents
|
|
167
|
+
const commandsDir = path.join(templateClaudeDir, 'commands');
|
|
168
|
+
const destCommandsDir = path.join(claudeDir, 'commands');
|
|
169
|
+
if (fs.existsSync(commandsDir)) {
|
|
170
|
+
if (!fs.existsSync(destCommandsDir)) fs.mkdirSync(destCommandsDir, { recursive: true });
|
|
171
|
+
for (const file of fs.readdirSync(commandsDir)) {
|
|
172
|
+
const dest = path.join(destCommandsDir, file);
|
|
173
|
+
if (!fs.existsSync(dest)) {
|
|
174
|
+
fs.copyFileSync(path.join(commandsDir, file), dest);
|
|
175
|
+
console.log(` create ${path.relative(process.cwd(), dest)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Copy EVERGREEN_RULES.md
|
|
180
|
+
const egSrc = path.join(templateClaudeDir, 'EVERGREEN_RULES.md');
|
|
181
|
+
const egDest = path.join(claudeDir, 'EVERGREEN_RULES.md');
|
|
182
|
+
if (fs.existsSync(egSrc) && !fs.existsSync(egDest)) {
|
|
183
|
+
fs.copyFileSync(egSrc, egDest);
|
|
184
|
+
console.log(` create ${path.relative(process.cwd(), egDest)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`
|
|
191
|
+
Done! Added: ${requestedTools.map(t => TOOLS[t].label).join(', ')}
|
|
192
|
+
`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = run;
|