@bonesofspring/ai-rules 0.2.0 → 0.2.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 +10 -18
- package/bin/cli.js +73 -258
- package/package.json +3 -4
- package/presets/claude/ios-swift/CLAUDE.md +27 -0
- package/presets/claude/ios-swift/README.md +29 -0
- package/presets/claude/ios-swift/commands/README.md +3 -0
- package/presets/claude/ios-swift/rules/README.md +44 -0
- package/presets/claude/ios-swift/rules/api-and-data/README.md +9 -0
- package/presets/claude/ios-swift/rules/api-and-data/networking.md +49 -0
- package/presets/claude/ios-swift/rules/architecture/README.md +10 -0
- package/presets/claude/ios-swift/rules/architecture/boundaries.md +33 -0
- package/presets/claude/ios-swift/rules/architecture/feature-delivery.md +60 -0
- package/presets/claude/ios-swift/rules/stack/README.md +10 -0
- package/presets/claude/ios-swift/rules/stack/ios-app-core.md +42 -0
- package/presets/claude/ios-swift/rules/stack/swift-conventions.md +51 -0
- package/presets/claude/ios-swift/rules/testing/README.md +10 -0
- package/presets/claude/ios-swift/rules/testing/ui.md +41 -0
- package/presets/claude/ios-swift/rules/testing/unit.md +41 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/README.md +11 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/code-quality.md +38 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/code-review.md +41 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/post-change-build.md +31 -0
- package/presets/claude/ios-swift/rules/ui-and-accessibility/README.md +10 -0
- package/presets/claude/ios-swift/rules/ui-and-accessibility/swiftui.md +61 -0
- package/presets/claude/ios-swift/rules/ui-and-accessibility/viewmodels.md +43 -0
- package/presets/claude/next/CLAUDE.md +5 -29
- package/presets/claude/next/README.md +0 -10
- package/presets/claude/next/agents/README.md +0 -125
- package/presets/claude/next/commands/README.md +2 -10
- package/presets/claude/next/hooks/README.md +2 -5
- package/presets/claude/next/rules/README.md +11 -25
- package/presets/claude/next/rules/api-and-data/README.md +1 -7
- package/presets/claude/next/rules/architecture/README.md +2 -9
- package/presets/claude/next/rules/stack/README.md +1 -9
- package/presets/claude/next/rules/testing/README.md +1 -9
- package/presets/claude/next/rules/tooling-and-review/README.md +1 -14
- package/presets/claude/next/rules/ui-and-accessibility/README.md +1 -8
- package/presets/claude/next/skills/README.md +1 -11
- package/presets/cursor/ios-swift/README.md +17 -0
- package/presets/cursor/ios-swift/commands/README.md +3 -0
- package/presets/cursor/ios-swift/rules/README.md +33 -0
- package/presets/cursor/ios-swift/rules/architecture-boundaries.mdc +34 -0
- package/presets/cursor/ios-swift/rules/code-quality-and-refactoring.mdc +39 -0
- package/presets/cursor/ios-swift/rules/code-review-mr.mdc +42 -0
- package/presets/cursor/ios-swift/rules/feature-delivery-workflow.mdc +61 -0
- package/presets/cursor/ios-swift/rules/ios-app-core.mdc +43 -0
- package/presets/cursor/ios-swift/rules/networking-services.mdc +49 -0
- package/presets/cursor/ios-swift/rules/post-change-build.mdc +32 -0
- package/presets/cursor/ios-swift/rules/state-and-viewmodels.mdc +43 -0
- package/presets/cursor/ios-swift/rules/swift-conventions.mdc +51 -0
- package/presets/cursor/ios-swift/rules/swiftui-ui.mdc +61 -0
- package/presets/cursor/ios-swift/rules/tests-ui.mdc +41 -0
- package/presets/cursor/ios-swift/rules/tests-unit.mdc +41 -0
- package/presets/cursor/next/commands/README.md +1 -49
- package/presets/cursor/next/rules/README.md +0 -47
- package/presets/cursor/next/rules/api-services.mdc +10 -12
- package/presets/cursor/next/rules/architecture-boundaries.mdc +12 -31
- package/presets/cursor/next/rules/code-quality-and-refactoring.mdc +4 -5
- package/presets/cursor/next/rules/code-review-mr.mdc +7 -14
- package/presets/cursor/next/rules/next-app-core.mdc +61 -18
- package/presets/cursor/next/rules/no-props-spread.mdc +4 -27
- package/presets/cursor/next/rules/playwright-agents.mdc +1 -2
- package/presets/cursor/next/rules/react-ui.mdc +3 -32
- package/presets/cursor/next/rules/store-rtk.mdc +6 -13
- package/presets/cursor/next/rules/tests-unit.mdc +10 -30
- package/CHANGELOG.md +0 -22
- package/presets/claude/next/agents/accessibility-reviewer.md +0 -65
- package/presets/claude/next/agents/api-contract-reviewer.md +0 -69
- package/presets/claude/next/agents/build-verifier.md +0 -64
- package/presets/claude/next/agents/ci-investigator.md +0 -62
- package/presets/claude/next/agents/code-reviewer.md +0 -62
- package/presets/claude/next/agents/debugger.md +0 -63
- package/presets/claude/next/agents/feature-developer.md +0 -27
- package/presets/claude/next/agents/migration-specialist.md +0 -69
- package/presets/claude/next/agents/performance-auditor.md +0 -68
- package/presets/claude/next/agents/qa-tester.md +0 -56
- package/presets/claude/next/agents/security-reviewer.md +0 -64
- package/presets/claude/next/agents/solution-architect.md +0 -70
- package/presets/claude/next/agents/task-analyst.md +0 -109
- package/presets/claude/next/agents/task-router.md +0 -70
- package/presets/claude/next/agents/tech-writer.md +0 -60
- package/presets/claude/next/agents/unit-test-generator.md +0 -37
- package/presets/claude/next/agents/unit-test-healer.md +0 -38
- package/presets/claude/next/agents/unit-test-planner.md +0 -62
- package/presets/claude/next/commands/feature-continue.md +0 -51
- package/presets/claude/next/commands/feature-start.md +0 -30
- package/presets/claude/next/commands/task-continue.md +0 -49
- package/presets/claude/next/commands/task.md +0 -50
- package/presets/claude/next/commands/technical-retro.md +0 -58
- package/presets/claude/next/hooks/chain-team-phases.sh +0 -342
- package/presets/claude/next/hooks/guard-shell-command.sh +0 -77
- package/presets/claude/next/rules/api-and-data/api-services.md +0 -57
- package/presets/claude/next/rules/api-and-data/http-client.md +0 -40
- package/presets/claude/next/rules/api-and-data/store-rtk.md +0 -65
- package/presets/claude/next/rules/architecture/api-public-imports.md +0 -8
- package/presets/claude/next/rules/architecture/architecture-boundaries.md +0 -72
- package/presets/claude/next/rules/architecture/feature-delivery-workflow.md +0 -79
- package/presets/claude/next/rules/architecture/layer-barrel-exports.md +0 -58
- package/presets/claude/next/rules/architecture/public-imports.md +0 -46
- package/presets/claude/next/rules/architecture/reference-features.md +0 -37
- package/presets/claude/next/rules/architecture/types-public-imports.md +0 -8
- package/presets/claude/next/rules/stack/arrow-functions.md +0 -45
- package/presets/claude/next/rules/stack/navigation-router.md +0 -61
- package/presets/claude/next/rules/stack/next-app-core.md +0 -33
- package/presets/claude/next/rules/stack/next-app-router.md +0 -36
- package/presets/claude/next/rules/stack/no-type-assertion.md +0 -59
- package/presets/claude/next/rules/stack/types-jsdoc.md +0 -37
- package/presets/claude/next/rules/testing/playwright-agents.md +0 -74
- package/presets/claude/next/rules/testing/tests-e2e-structure.md +0 -52
- package/presets/claude/next/rules/testing/tests-unit.md +0 -66
- package/presets/claude/next/rules/tooling-and-review/agent-team-intake.md +0 -15
- package/presets/claude/next/rules/tooling-and-review/agent-team-orchestrator.md +0 -134
- package/presets/claude/next/rules/tooling-and-review/code-quality.md +0 -42
- package/presets/claude/next/rules/tooling-and-review/code-review-mr.md +0 -67
- package/presets/claude/next/rules/tooling-and-review/package-manager.md +0 -11
- package/presets/claude/next/rules/tooling-and-review/post-change-lint.md +0 -33
- package/presets/claude/next/rules/ui-and-accessibility/component-styles.md +0 -56
- package/presets/claude/next/rules/ui-and-accessibility/css-property-order.md +0 -14
- package/presets/claude/next/rules/ui-and-accessibility/no-props-spread.md +0 -57
- package/presets/claude/next/rules/ui-and-accessibility/react-a11y-coding.md +0 -37
- package/presets/claude/next/rules/ui-and-accessibility/react-ui.md +0 -90
- package/presets/claude/next/skills/ci-investigation/SKILL.md +0 -36
- package/presets/claude/next/skills/code-review/SKILL.md +0 -26
- package/presets/claude/next/skills/debug-investigation/SKILL.md +0 -28
- package/presets/claude/next/skills/feature-delivery/SKILL.md +0 -15
- package/presets/claude/next/skills/playwright-e2e/SKILL.md +0 -31
- package/presets/claude/next/skills/technical-retro/SKILL.md +0 -40
- package/presets/claude/next/skills/unit-testing/SKILL.md +0 -32
- package/presets/claude/next/team/README.md +0 -46
- package/presets/claude/next/team/tasks/.gitkeep +0 -1
- package/presets/cursor/next/AGENTS.md +0 -43
- package/presets/cursor/next/BUGBOT.md +0 -14
- package/presets/cursor/next/agents/README.md +0 -158
- package/presets/cursor/next/agents/accessibility-reviewer.md +0 -67
- package/presets/cursor/next/agents/api-contract-reviewer.md +0 -71
- package/presets/cursor/next/agents/build-verifier.md +0 -66
- package/presets/cursor/next/agents/ci-investigator.md +0 -64
- package/presets/cursor/next/agents/code-reviewer.md +0 -64
- package/presets/cursor/next/agents/debugger.md +0 -64
- package/presets/cursor/next/agents/feature-developer.md +0 -27
- package/presets/cursor/next/agents/migration-specialist.md +0 -71
- package/presets/cursor/next/agents/performance-auditor.md +0 -70
- package/presets/cursor/next/agents/playwright-test-generator.md +0 -26
- package/presets/cursor/next/agents/playwright-test-healer.md +0 -26
- package/presets/cursor/next/agents/playwright-test-planner.md +0 -28
- package/presets/cursor/next/agents/qa-tester.md +0 -57
- package/presets/cursor/next/agents/security-reviewer.md +0 -66
- package/presets/cursor/next/agents/solution-architect.md +0 -71
- package/presets/cursor/next/agents/task-analyst.md +0 -112
- package/presets/cursor/next/agents/task-router.md +0 -71
- package/presets/cursor/next/agents/tech-writer.md +0 -62
- package/presets/cursor/next/agents/unit-test-generator.md +0 -39
- package/presets/cursor/next/agents/unit-test-healer.md +0 -40
- package/presets/cursor/next/agents/unit-test-planner.md +0 -64
- package/presets/cursor/next/commands/feature-continue.md +0 -19
- package/presets/cursor/next/commands/feature-start.md +0 -33
- package/presets/cursor/next/commands/task-continue.md +0 -49
- package/presets/cursor/next/commands/task.md +0 -50
- package/presets/cursor/next/commands/technical-retro.md +0 -81
- package/presets/cursor/next/hooks/README.md +0 -8
- package/presets/cursor/next/hooks/chain-team-phases.sh +0 -380
- package/presets/cursor/next/hooks/guard-shell-command.sh +0 -77
- package/presets/cursor/next/hooks.json +0 -17
- package/presets/cursor/next/rules/agent-team-intake.mdc +0 -14
- package/presets/cursor/next/rules/agent-team-orchestrator.mdc +0 -139
- package/presets/cursor/next/rules/api-public-imports.mdc +0 -8
- package/presets/cursor/next/rules/arrow-functions.mdc +0 -46
- package/presets/cursor/next/rules/css-property-order-stylelint.mdc +0 -16
- package/presets/cursor/next/rules/feature-delivery-workflow.mdc +0 -76
- package/presets/cursor/next/rules/http-client.mdc +0 -42
- package/presets/cursor/next/rules/layer-barrel-exports.mdc +0 -59
- package/presets/cursor/next/rules/navigation-router-stack.mdc +0 -62
- package/presets/cursor/next/rules/next-app-router.mdc +0 -36
- package/presets/cursor/next/rules/no-cross-component-styles-import.mdc +0 -58
- package/presets/cursor/next/rules/no-type-assertion-as-import-export.mdc +0 -60
- package/presets/cursor/next/rules/package-manager.mdc +0 -16
- package/presets/cursor/next/rules/post-change-lint.mdc +0 -38
- package/presets/cursor/next/rules/public-imports.mdc +0 -48
- package/presets/cursor/next/rules/react-a11y-coding.mdc +0 -37
- package/presets/cursor/next/rules/reference-features.mdc +0 -39
- package/presets/cursor/next/rules/technical-retro.mdc +0 -12
- package/presets/cursor/next/rules/types-jsdoc.mdc +0 -42
- package/presets/cursor/next/rules/types-public-imports.mdc +0 -8
- package/presets/cursor/next/skills/README.md +0 -15
- package/presets/cursor/next/skills/ci-investigation/SKILL.md +0 -36
- package/presets/cursor/next/skills/code-review/SKILL.md +0 -26
- package/presets/cursor/next/skills/debug-investigation/SKILL.md +0 -28
- package/presets/cursor/next/skills/feature-delivery/SKILL.md +0 -15
- package/presets/cursor/next/skills/playwright-e2e/SKILL.md +0 -31
- package/presets/cursor/next/skills/technical-retro/SKILL.md +0 -40
- package/presets/cursor/next/skills/unit-testing/SKILL.md +0 -32
- package/presets/cursor/next/team/README.md +0 -104
- package/presets/cursor/next/team/tasks/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bonesofspring/ai-rules
|
|
2
2
|
|
|
3
|
-
Presets for Cursor and Claude Code: rules, commands, plus
|
|
3
|
+
Presets for Cursor and Claude Code: rules, commands, plus for Claude `CLAUDE.md` and `skills` / `agents` / `hooks` when the preset includes them. Install from npm into a project so you don’t have to copy files out of the repo by hand.
|
|
4
4
|
|
|
5
5
|
Node 18+.
|
|
6
6
|
|
|
@@ -13,18 +13,9 @@ npx @bonesofspring/ai-rules clean cursor
|
|
|
13
13
|
|
|
14
14
|
Global: `npm i -g @bonesofspring/ai-rules`, then `ai-rules ...`.
|
|
15
15
|
|
|
16
|
-
`init` merges into the
|
|
16
|
+
`init` merges into the current directory: dirs are created, matching files are **overwritten**. `--preset` and the subcommand can be in either order, e.g. `--preset=next init cursor`.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
npx @bonesofspring/ai-rules init cursor --preset next
|
|
20
|
-
npx @bonesofspring/ai-rules init cursor next
|
|
21
|
-
npx @bonesofspring/ai-rules init cursor # preset next by default
|
|
22
|
-
npx @bonesofspring/ai-rules cursor init --preset next
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
If `.cursor` does not appear, check the command output for errors and confirm you run init from the **target project root** (where the app lives), not from `node_modules`. Success ends with: `Done. .cursor installed at <absolute-path>`.
|
|
26
|
-
|
|
27
|
-
`clean cursor` / `clean claude` removes what this CLI installs: `rules`, `commands`, optional `agents`, `hooks`, `skills`, `team`, Cursor `hooks.json` / `BUGBOT.md`, and Claude `CLAUDE.md` when present. Other files under `.cursor/` or `.claude/` are left intact.
|
|
18
|
+
`clean cursor` / `clean claude` removes what this CLI installs: for Cursor, the entire `.cursor` directory; for Claude, `.claude/rules`, `.claude/commands`, `skills`, `agents`, `hooks`, and `CLAUDE.md` when present. Anything else under `.claude` is left alone.
|
|
28
19
|
|
|
29
20
|
Where things go on `init`:
|
|
30
21
|
|
|
@@ -32,13 +23,14 @@ Where things go on `init`:
|
|
|
32
23
|
|---|--------|--------|
|
|
33
24
|
| rules | `.cursor/rules` | `.claude/rules` |
|
|
34
25
|
| commands | `.cursor/commands` | `.claude/commands` |
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| team artifacts template | `.cursor/team` | `.claude/team` |
|
|
39
|
-
| other | root `BUGBOT.md` → `.cursor/BUGBOT.md` | root `CLAUDE.md` → `.claude/CLAUDE.md` |
|
|
26
|
+
| other | — | root `CLAUDE.md` → `.claude/CLAUDE.md`; folders `skills`, `agents`, `hooks` → under `.claude/` with the same names |
|
|
27
|
+
|
|
28
|
+
The package ships presets including **`next`** (Next.js stack) and **`ios-swift`** (iOS apps on Swift / SwiftUI). Other preset names: see `presets/` in the [repo](https://github.com/bonesofspring/ai-rules). Name is a single path segment, no `/` or `..`.
|
|
40
29
|
|
|
41
|
-
|
|
30
|
+
```bash
|
|
31
|
+
npx @bonesofspring/ai-rules init cursor --preset ios-swift
|
|
32
|
+
npx @bonesofspring/ai-rules init claude --preset ios-swift
|
|
33
|
+
```
|
|
42
34
|
|
|
43
35
|
Help: `ai-rules --help`.
|
|
44
36
|
|
package/bin/cli.js
CHANGED
|
@@ -5,86 +5,51 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
+
const packageRoot = path.join(__dirname, '..');
|
|
9
|
+
|
|
8
10
|
/** Single path segment: letters, digits, dot, underscore, hyphen; no separators or "..". */
|
|
9
11
|
const PRESET_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
10
|
-
const DEFAULT_PRESET = 'next';
|
|
11
|
-
const TOOLS = new Set(['cursor', 'claude']);
|
|
12
12
|
|
|
13
13
|
const PRESETS = {
|
|
14
14
|
cursor: {
|
|
15
15
|
rulesDir: '.cursor/rules',
|
|
16
16
|
commandsDir: '.cursor/commands',
|
|
17
17
|
relBase: ['presets', 'cursor'],
|
|
18
|
-
optionalCursorDirs: ['agents', 'hooks', 'skills', 'team'],
|
|
19
|
-
hooksJsonSrc: 'hooks.json',
|
|
20
|
-
toolRootFiles: ['BUGBOT.md', 'AGENTS.md'],
|
|
21
18
|
},
|
|
22
19
|
claude: {
|
|
23
20
|
rulesDir: '.claude/rules',
|
|
24
21
|
commandsDir: '.claude/commands',
|
|
25
22
|
relBase: ['presets', 'claude'],
|
|
26
|
-
|
|
23
|
+
/** Optional dirs under preset root → under `.claude/` (recursive copy). */
|
|
24
|
+
optionalClaudeDirs: ['skills', 'agents', 'hooks'],
|
|
25
|
+
/** Copied from preset root to `.claude/CLAUDE.md` when present. */
|
|
27
26
|
claudeMdSrc: 'CLAUDE.md',
|
|
28
27
|
},
|
|
29
28
|
};
|
|
30
29
|
|
|
31
|
-
function getPackageRoot() {
|
|
32
|
-
try {
|
|
33
|
-
return path.dirname(require.resolve('@bonesofspring/ai-rules/package.json'));
|
|
34
|
-
} catch {
|
|
35
|
-
return path.join(__dirname, '..');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
30
|
function printHelp() {
|
|
40
31
|
console.log(`@bonesofspring/ai-rules
|
|
41
32
|
|
|
42
33
|
Usage:
|
|
43
|
-
ai-rules init <cursor|claude>
|
|
44
|
-
ai-rules
|
|
45
|
-
ai-rules clean <cursor|claude> [--cwd <dir>]
|
|
46
|
-
|
|
47
|
-
Options:
|
|
48
|
-
--preset <name> Preset name (default: ${DEFAULT_PRESET})
|
|
49
|
-
--cwd <dir> Target project directory (default: current working directory)
|
|
34
|
+
ai-rules init <cursor|claude> --preset <name> Copy preset rules and commands into the current directory
|
|
35
|
+
ai-rules clean <cursor|claude> Remove preset target rules and commands directories
|
|
50
36
|
|
|
51
37
|
Notes:
|
|
52
|
-
init merges into
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Command forms: "init cursor --preset next", "init cursor next", "--preset next init cursor", "cursor init --preset next".
|
|
38
|
+
init merges into existing target folders; same-named files are overwritten.
|
|
39
|
+
clean cursor removes the entire .cursor directory (if present).
|
|
40
|
+
Flags may appear before or after the subcommand (e.g. --preset next init cursor).
|
|
56
41
|
|
|
57
42
|
Examples:
|
|
58
43
|
npx @bonesofspring/ai-rules init cursor --preset next
|
|
59
|
-
npx @bonesofspring/ai-rules init
|
|
60
|
-
npx @bonesofspring/ai-rules
|
|
61
|
-
npx @bonesofspring/ai-rules clean cursor
|
|
44
|
+
npx @bonesofspring/ai-rules init claude --preset ios-swift
|
|
45
|
+
npx @bonesofspring/ai-rules clean claude
|
|
62
46
|
`);
|
|
63
47
|
}
|
|
64
48
|
|
|
65
49
|
function parseArgs(argv) {
|
|
66
|
-
const args = { _: []
|
|
50
|
+
const args = { _: [] };
|
|
67
51
|
for (let i = 0; i < argv.length; i++) {
|
|
68
52
|
const a = argv[i];
|
|
69
|
-
if (a === '--cwd') {
|
|
70
|
-
const value = argv[i + 1];
|
|
71
|
-
if (value === undefined || value.startsWith('-')) {
|
|
72
|
-
args.cwdError = 'Missing value for --cwd';
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
args.cwd = value;
|
|
76
|
-
i++;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (a.startsWith('--cwd=')) {
|
|
80
|
-
const value = a.slice('--cwd='.length);
|
|
81
|
-
if (value === '') {
|
|
82
|
-
args.cwdError = 'Missing value after --cwd=';
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
args.cwd = value;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
53
|
if (a === '--preset') {
|
|
89
54
|
const value = argv[i + 1];
|
|
90
55
|
if (value === undefined || value.startsWith('-')) {
|
|
@@ -105,53 +70,17 @@ function parseArgs(argv) {
|
|
|
105
70
|
args.preset = value;
|
|
106
71
|
continue;
|
|
107
72
|
}
|
|
108
|
-
if (a === '-h' || a === '--help') {
|
|
109
|
-
args.help = true;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
73
|
args._.push(a);
|
|
113
74
|
}
|
|
114
75
|
return args;
|
|
115
76
|
}
|
|
116
77
|
|
|
117
|
-
/**
|
|
118
|
-
* Supports: init cursor, cursor init, init cursor next (positional preset).
|
|
119
|
-
*/
|
|
120
|
-
function normalizeInitClean(parsed) {
|
|
121
|
-
const positional = [...parsed._];
|
|
122
|
-
let command;
|
|
123
|
-
let tool;
|
|
124
|
-
let presetFromPositional;
|
|
125
|
-
|
|
126
|
-
if (positional.length >= 2 && TOOLS.has(positional[0]) && positional[1] === 'init') {
|
|
127
|
-
command = 'init';
|
|
128
|
-
tool = positional[0];
|
|
129
|
-
presetFromPositional = positional[2];
|
|
130
|
-
} else if (positional.length >= 2 && positional[0] === 'init' && TOOLS.has(positional[1])) {
|
|
131
|
-
command = 'init';
|
|
132
|
-
tool = positional[1];
|
|
133
|
-
presetFromPositional = positional[2];
|
|
134
|
-
} else if (positional.length >= 2 && positional[0] === 'clean' && TOOLS.has(positional[1])) {
|
|
135
|
-
command = 'clean';
|
|
136
|
-
tool = positional[1];
|
|
137
|
-
} else if (positional.length >= 1) {
|
|
138
|
-
command = positional[0];
|
|
139
|
-
tool = positional[1];
|
|
140
|
-
if (command === 'init' && positional[2] && PRESET_NAME_RE.test(positional[2])) {
|
|
141
|
-
presetFromPositional = positional[2];
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const preset = parsed.preset || presetFromPositional;
|
|
146
|
-
return { command, tool, preset };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
78
|
/**
|
|
150
79
|
* @returns {string | null} Error message or null if valid.
|
|
151
80
|
*/
|
|
152
81
|
function validatePresetName(preset) {
|
|
153
82
|
if (preset === undefined || preset === '') {
|
|
154
|
-
return 'Missing
|
|
83
|
+
return 'Missing --preset <name>';
|
|
155
84
|
}
|
|
156
85
|
if (!PRESET_NAME_RE.test(preset) || preset.includes('..')) {
|
|
157
86
|
return (
|
|
@@ -162,6 +91,9 @@ function validatePresetName(preset) {
|
|
|
162
91
|
return null;
|
|
163
92
|
}
|
|
164
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Ensures resolved preset dir stays under the tool presets root (defense in depth).
|
|
96
|
+
*/
|
|
165
97
|
function assertPresetDirInsideRoot(presetsRoot, presetBase) {
|
|
166
98
|
const root = path.resolve(presetsRoot);
|
|
167
99
|
const resolved = path.resolve(presetBase);
|
|
@@ -185,17 +117,10 @@ function copyDir(src, dest) {
|
|
|
185
117
|
copyDir(from, to);
|
|
186
118
|
} else {
|
|
187
119
|
fs.copyFileSync(from, to);
|
|
188
|
-
if (e.name.endsWith('.sh')) {
|
|
189
|
-
fs.chmodSync(to, 0o755);
|
|
190
|
-
}
|
|
191
120
|
}
|
|
192
121
|
}
|
|
193
122
|
}
|
|
194
123
|
|
|
195
|
-
function getOptionalDirs(cfg) {
|
|
196
|
-
return cfg.optionalCursorDirs || cfg.optionalClaudeDirs || [];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
124
|
function removeDirIfExists(dir) {
|
|
200
125
|
if (fs.existsSync(dir)) {
|
|
201
126
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -208,157 +133,76 @@ function removeFileIfExists(filePath) {
|
|
|
208
133
|
}
|
|
209
134
|
}
|
|
210
135
|
|
|
211
|
-
function listPresetNames(presetsRoot) {
|
|
212
|
-
if (!fs.existsSync(presetsRoot)) {
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
return fs
|
|
216
|
-
.readdirSync(presetsRoot, { withFileTypes: true })
|
|
217
|
-
.filter((e) => e.isDirectory())
|
|
218
|
-
.map((e) => e.name)
|
|
219
|
-
.filter((name) => PRESET_NAME_RE.test(name));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
136
|
function presetHasAnySource(base, cfg) {
|
|
223
137
|
const rulesSrc = path.join(base, 'rules');
|
|
224
138
|
const commandsSrc = path.join(base, 'commands');
|
|
225
139
|
if (fs.existsSync(rulesSrc) || fs.existsSync(commandsSrc)) {
|
|
226
140
|
return true;
|
|
227
141
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
142
|
+
if (cfg.optionalClaudeDirs) {
|
|
143
|
+
for (const name of cfg.optionalClaudeDirs) {
|
|
144
|
+
if (fs.existsSync(path.join(base, name))) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
231
147
|
}
|
|
232
148
|
}
|
|
233
149
|
if (cfg.claudeMdSrc && fs.existsSync(path.join(base, cfg.claudeMdSrc))) {
|
|
234
150
|
return true;
|
|
235
151
|
}
|
|
236
|
-
if (cfg.hooksJsonSrc && fs.existsSync(path.join(base, cfg.hooksJsonSrc))) {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
for (const name of cfg.toolRootFiles || []) {
|
|
240
|
-
if (fs.existsSync(path.join(base, name))) {
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
152
|
return false;
|
|
245
153
|
}
|
|
246
154
|
|
|
247
|
-
function
|
|
248
|
-
const
|
|
249
|
-
if (!
|
|
250
|
-
console.error(`
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
if (!fs.statSync(cwd).isDirectory()) {
|
|
254
|
-
console.error(`Target path is not a directory: ${cwd}`);
|
|
155
|
+
function cmdInit(tool, preset) {
|
|
156
|
+
const cfg = PRESETS[tool];
|
|
157
|
+
if (!cfg) {
|
|
158
|
+
console.error(`Unknown tool: ${tool}. Use cursor or claude.`);
|
|
255
159
|
process.exit(1);
|
|
256
160
|
}
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function resolvePresetName(tool, preset, packageRoot) {
|
|
261
|
-
const cfg = PRESETS[tool];
|
|
262
|
-
const presetsRoot = path.join(packageRoot, ...cfg.relBase);
|
|
263
|
-
let name = preset || DEFAULT_PRESET;
|
|
264
|
-
|
|
265
|
-
const nameErr = validatePresetName(name);
|
|
161
|
+
const nameErr = validatePresetName(preset);
|
|
266
162
|
if (nameErr) {
|
|
267
163
|
console.error(nameErr);
|
|
268
164
|
process.exit(1);
|
|
269
165
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (!presetHasAnySource(base, cfg)) {
|
|
273
|
-
const available = listPresetNames(presetsRoot);
|
|
274
|
-
if (!preset && available.includes(DEFAULT_PRESET)) {
|
|
275
|
-
name = DEFAULT_PRESET;
|
|
276
|
-
base = path.join(presetsRoot, name);
|
|
277
|
-
} else {
|
|
278
|
-
console.error(`Preset "${name}" for ${tool} not found.`);
|
|
279
|
-
console.error(`Looked in: ${base}`);
|
|
280
|
-
console.error(`Package root: ${packageRoot}`);
|
|
281
|
-
if (available.length > 0) {
|
|
282
|
-
console.error(`Available presets: ${available.join(', ')}`);
|
|
283
|
-
} else {
|
|
284
|
-
console.error(`No presets directory at: ${presetsRoot}`);
|
|
285
|
-
console.error(
|
|
286
|
-
'The installed package may be incomplete. Reinstall @bonesofspring/ai-rules or run from the package source.',
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
166
|
+
const presetsRoot = path.join(packageRoot, ...cfg.relBase);
|
|
167
|
+
const base = path.join(presetsRoot, preset);
|
|
293
168
|
assertPresetDirInsideRoot(presetsRoot, base);
|
|
294
|
-
return { name, base, presetsRoot };
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function cmdInit(tool, preset, cwdOption) {
|
|
298
|
-
const cfg = PRESETS[tool];
|
|
299
|
-
if (!cfg) {
|
|
300
|
-
console.error(`Unknown tool: ${tool}. Use cursor or claude.`);
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const packageRoot = getPackageRoot();
|
|
305
|
-
const cwd = resolveTargetCwd(cwdOption);
|
|
306
|
-
const { name: presetName, base } = resolvePresetName(tool, preset, packageRoot);
|
|
307
169
|
|
|
308
170
|
const rulesSrc = path.join(base, 'rules');
|
|
309
171
|
const commandsSrc = path.join(base, 'commands');
|
|
310
|
-
const
|
|
311
|
-
const toolRootAbs = path.join(cwd, toolRoot);
|
|
312
|
-
|
|
313
|
-
fs.mkdirSync(toolRootAbs, { recursive: true });
|
|
172
|
+
const cwd = process.cwd();
|
|
314
173
|
|
|
315
|
-
|
|
174
|
+
if (!presetHasAnySource(base, cfg)) {
|
|
175
|
+
console.error(`Preset "${preset}" for ${tool} not found under ${base}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
316
178
|
|
|
317
179
|
try {
|
|
318
180
|
if (tool === 'claude' && cfg.claudeMdSrc) {
|
|
319
181
|
const mdFrom = path.join(base, cfg.claudeMdSrc);
|
|
320
182
|
if (fs.existsSync(mdFrom)) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
183
|
+
const claudeRoot = path.join(cwd, '.claude');
|
|
184
|
+
fs.mkdirSync(claudeRoot, { recursive: true });
|
|
185
|
+
fs.copyFileSync(mdFrom, path.join(claudeRoot, 'CLAUDE.md'));
|
|
186
|
+
console.log('Copied CLAUDE.md → .claude/CLAUDE.md');
|
|
324
187
|
}
|
|
325
188
|
}
|
|
326
189
|
|
|
327
190
|
if (fs.existsSync(rulesSrc)) {
|
|
328
191
|
copyDir(rulesSrc, path.join(cwd, cfg.rulesDir));
|
|
329
192
|
console.log(`Copied rules → ${cfg.rulesDir}`);
|
|
330
|
-
copiedSteps++;
|
|
331
193
|
}
|
|
332
194
|
if (fs.existsSync(commandsSrc)) {
|
|
333
195
|
copyDir(commandsSrc, path.join(cwd, cfg.commandsDir));
|
|
334
196
|
console.log(`Copied commands → ${cfg.commandsDir}`);
|
|
335
|
-
copiedSteps++;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
for (const name of getOptionalDirs(cfg)) {
|
|
339
|
-
const src = path.join(base, name);
|
|
340
|
-
if (fs.existsSync(src)) {
|
|
341
|
-
copyDir(src, path.join(toolRootAbs, name));
|
|
342
|
-
console.log(`Copied ${name}/ → ${toolRoot}/${name}/`);
|
|
343
|
-
copiedSteps++;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (tool === 'cursor' && cfg.hooksJsonSrc) {
|
|
348
|
-
const hooksFrom = path.join(base, cfg.hooksJsonSrc);
|
|
349
|
-
if (fs.existsSync(hooksFrom)) {
|
|
350
|
-
fs.copyFileSync(hooksFrom, path.join(toolRootAbs, 'hooks.json'));
|
|
351
|
-
console.log(`Copied hooks.json → ${toolRoot}/hooks.json`);
|
|
352
|
-
copiedSteps++;
|
|
353
|
-
}
|
|
354
197
|
}
|
|
355
198
|
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
fs.
|
|
360
|
-
|
|
361
|
-
|
|
199
|
+
if (tool === 'claude' && cfg.optionalClaudeDirs) {
|
|
200
|
+
for (const name of cfg.optionalClaudeDirs) {
|
|
201
|
+
const src = path.join(base, name);
|
|
202
|
+
if (fs.existsSync(src)) {
|
|
203
|
+
copyDir(src, path.join(cwd, '.claude', name));
|
|
204
|
+
console.log(`Copied ${name}/ → .claude/${name}/`);
|
|
205
|
+
}
|
|
362
206
|
}
|
|
363
207
|
}
|
|
364
208
|
} catch (err) {
|
|
@@ -366,95 +210,66 @@ function cmdInit(tool, preset, cwdOption) {
|
|
|
366
210
|
console.error(`init failed: ${msg}`);
|
|
367
211
|
process.exit(1);
|
|
368
212
|
}
|
|
369
|
-
|
|
370
|
-
if (copiedSteps === 0) {
|
|
371
|
-
console.error(`init failed: preset "${presetName}" had no files to copy from ${base}`);
|
|
372
|
-
process.exit(1);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (!fs.existsSync(toolRootAbs)) {
|
|
376
|
-
console.error(`init failed: ${toolRootAbs} was not created`);
|
|
377
|
-
process.exit(1);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
console.log(`Done. ${toolRoot} installed at ${toolRootAbs} (preset: ${presetName})`);
|
|
381
213
|
}
|
|
382
214
|
|
|
383
|
-
function cmdClean(tool
|
|
215
|
+
function cmdClean(tool) {
|
|
384
216
|
const cfg = PRESETS[tool];
|
|
385
217
|
if (!cfg) {
|
|
386
218
|
console.error(`Unknown tool: ${tool}. Use cursor or claude.`);
|
|
387
219
|
process.exit(1);
|
|
388
220
|
}
|
|
389
|
-
const cwd =
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
221
|
+
const cwd = process.cwd();
|
|
222
|
+
if (tool === 'cursor') {
|
|
223
|
+
removeDirIfExists(path.join(cwd, '.cursor'));
|
|
224
|
+
console.log('Removed .cursor (if present)');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
393
227
|
removeDirIfExists(path.join(cwd, cfg.rulesDir));
|
|
394
228
|
removeDirIfExists(path.join(cwd, cfg.commandsDir));
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
removeFileIfExists(path.join(cwd, toolRoot, 'hooks.json'));
|
|
409
|
-
removedParts.push(`${toolRoot}/hooks.json`);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
for (const name of cfg.toolRootFiles || []) {
|
|
413
|
-
removeFileIfExists(path.join(cwd, toolRoot, name));
|
|
414
|
-
removedParts.push(`${toolRoot}/${name}`);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
console.log(`Removed ${removedParts.join(', ')} (if present)`);
|
|
229
|
+
if (tool === 'claude' && cfg.optionalClaudeDirs) {
|
|
230
|
+
for (const name of cfg.optionalClaudeDirs) {
|
|
231
|
+
removeDirIfExists(path.join(cwd, '.claude', name));
|
|
232
|
+
}
|
|
233
|
+
removeFileIfExists(path.join(cwd, '.claude', 'CLAUDE.md'));
|
|
234
|
+
}
|
|
235
|
+
console.log(
|
|
236
|
+
`Removed ${cfg.rulesDir} and ${cfg.commandsDir}` +
|
|
237
|
+
(tool === 'claude' && cfg.optionalClaudeDirs
|
|
238
|
+
? `, optional .claude/{${cfg.optionalClaudeDirs.join(',')}}, and .claude/CLAUDE.md`
|
|
239
|
+
: '') +
|
|
240
|
+
' (if present)',
|
|
241
|
+
);
|
|
418
242
|
}
|
|
419
243
|
|
|
420
244
|
function main() {
|
|
421
245
|
const argv = process.argv.slice(2);
|
|
422
|
-
if (argv.length === 0) {
|
|
246
|
+
if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
|
|
423
247
|
printHelp();
|
|
424
248
|
process.exit(0);
|
|
425
249
|
}
|
|
426
250
|
|
|
427
251
|
const parsed = parseArgs(argv);
|
|
428
|
-
|
|
429
|
-
printHelp();
|
|
430
|
-
process.exit(0);
|
|
431
|
-
}
|
|
252
|
+
const [command, tool] = parsed._;
|
|
432
253
|
|
|
433
254
|
if (parsed.presetError) {
|
|
434
255
|
console.error(parsed.presetError);
|
|
435
256
|
process.exit(1);
|
|
436
257
|
}
|
|
437
|
-
if (parsed.cwdError) {
|
|
438
|
-
console.error(parsed.cwdError);
|
|
439
|
-
process.exit(1);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const { command, tool, preset } = normalizeInitClean(parsed);
|
|
443
258
|
|
|
444
259
|
if (command === 'init') {
|
|
445
|
-
if (!tool
|
|
446
|
-
console.error('Missing
|
|
260
|
+
if (!tool) {
|
|
261
|
+
console.error('Missing <cursor|claude> after init.');
|
|
447
262
|
process.exit(1);
|
|
448
263
|
}
|
|
449
|
-
cmdInit(tool,
|
|
264
|
+
cmdInit(tool, parsed.preset);
|
|
450
265
|
return;
|
|
451
266
|
}
|
|
452
267
|
if (command === 'clean') {
|
|
453
|
-
if (!tool
|
|
454
|
-
console.error('Missing
|
|
268
|
+
if (!tool) {
|
|
269
|
+
console.error('Missing <cursor|claude> after clean.');
|
|
455
270
|
process.exit(1);
|
|
456
271
|
}
|
|
457
|
-
cmdClean(tool
|
|
272
|
+
cmdClean(tool);
|
|
458
273
|
return;
|
|
459
274
|
}
|
|
460
275
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonesofspring/ai-rules",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Presets of Cursor and Claude rules/commands for Revy Ross personal use",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Revy Ross",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "
|
|
9
|
+
"url": "https://github.com/bonesofspring/ai-rules.git"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"cursor",
|
|
@@ -16,11 +16,10 @@
|
|
|
16
16
|
"bonesofspring"
|
|
17
17
|
],
|
|
18
18
|
"bin": {
|
|
19
|
-
"ai-rules": "bin/cli.js"
|
|
19
|
+
"ai-rules": "./bin/cli.js"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"README.md",
|
|
23
|
-
"CHANGELOG.md",
|
|
24
23
|
"bin",
|
|
25
24
|
"presets"
|
|
26
25
|
],
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# iOS Swift stack — project instructions
|
|
2
|
+
|
|
3
|
+
Краткая точка входа для Claude Code. Подробные правила — модульные topic `.md` под `rules/` (derived из Cursor SoT).
|
|
4
|
+
|
|
5
|
+
See @rules/README.md for the full Cursor → Claude mapping and sync checklist.
|
|
6
|
+
|
|
7
|
+
## Where to look
|
|
8
|
+
|
|
9
|
+
| Area | Folder |
|
|
10
|
+
|------|--------|
|
|
11
|
+
| Stack / conventions | @rules/stack/ |
|
|
12
|
+
| Architecture / feature delivery | @rules/architecture/ |
|
|
13
|
+
| Networking / data | @rules/api-and-data/ |
|
|
14
|
+
| SwiftUI / ViewModels | @rules/ui-and-accessibility/ |
|
|
15
|
+
| Tests | @rules/testing/ |
|
|
16
|
+
| Quality / review / build | @rules/tooling-and-review/ |
|
|
17
|
+
|
|
18
|
+
## Conventions
|
|
19
|
+
|
|
20
|
+
- Одна тема на файл; path scope — frontmatter `paths:` (из Cursor `globs:`).
|
|
21
|
+
- SoT: `presets/cursor/ios-swift/rules/*.mdc`. Change Cursor → update Claude twin same PR.
|
|
22
|
+
- Slash-команды — в `commands/`; skills — когда появятся в пресете.
|
|
23
|
+
|
|
24
|
+
## Architecture (postcard)
|
|
25
|
+
|
|
26
|
+
- **Presentation** → **Domain** ← **Data**; DI в `App/`.
|
|
27
|
+
- Views тонкие; ViewModels на `@MainActor`; DTO не покидают Data.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Preset `ios-swift` (Claude Code)
|
|
2
|
+
|
|
3
|
+
Правила для Swift, SwiftUI, слоистой архитектуры и тестов (XCTest, XCUITest).
|
|
4
|
+
|
|
5
|
+
Структура отличается от пресета Cursor: здесь повторяется типичный **layout проекта Claude Code** под `.claude/`, а не плоский список `.mdc` в `rules/`.
|
|
6
|
+
|
|
7
|
+
| В пресете | Куда копирует `ai-rules init claude --preset ios-swift` |
|
|
8
|
+
|-----------|---------------------------------------------------------|
|
|
9
|
+
| `CLAUDE.md` | `.claude/CLAUDE.md` (точка входа, грузится в начале сессии) |
|
|
10
|
+
| `rules/**` | `.claude/rules/**` (рекурсивно: подпапки по темам, см. [документацию](https://code.claude.com/docs/en/memory#organize-rules-with-clauderules)) |
|
|
11
|
+
| `commands/**` | `.claude/commands/**` |
|
|
12
|
+
| `skills/**` | `.claude/skills/**` (если папка есть) |
|
|
13
|
+
| `agents/**` | `.claude/agents/**` (если папка есть) |
|
|
14
|
+
| `hooks/**` | `.claude/hooks/**` (если папка есть) |
|
|
15
|
+
|
|
16
|
+
Файлы правил — обычный Markdown с опциональным YAML frontmatter (`paths:` для скоупа по путям), не формат `.mdc` Cursor.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @bonesofspring/ai-rules init claude --preset ios-swift
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Parity with Cursor
|
|
23
|
+
|
|
24
|
+
- **SoT:** `presets/cursor/ios-swift/rules/*.mdc`
|
|
25
|
+
- **Claude twins:** topic `.md` under `rules/**` (bodies derived from Cursor; frontmatter uses `paths:` instead of `globs:` / `alwaysApply`)
|
|
26
|
+
- **Mapping + sync checklist:** [`rules/README.md`](./rules/README.md)
|
|
27
|
+
- **Entry index:** [`CLAUDE.md`](./CLAUDE.md) (thin — no second long-form SoT)
|
|
28
|
+
|
|
29
|
+
When editing rules: change Cursor first, then update the Claude twin in the same PR.
|