@aksp/opencrew 1.0.0

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +24 -0
  3. package/README.md +118 -0
  4. package/bin/opencrew.js +8 -0
  5. package/package.json +57 -0
  6. package/src/cli.js +70 -0
  7. package/src/commands/init.js +97 -0
  8. package/src/commands/update.js +58 -0
  9. package/src/lib/fsx.js +55 -0
  10. package/src/lib/ides.js +120 -0
  11. package/src/lib/paths.js +8 -0
  12. package/src/lib/prompts.js +35 -0
  13. package/src/lib/ui.js +20 -0
  14. package/templates/.env.example +23 -0
  15. package/templates/.mcp.json +8 -0
  16. package/templates/AGENTS.md +105 -0
  17. package/templates/_opencrew/.opencrew-version +1 -0
  18. package/templates/_opencrew/_investigations/.gitkeep +0 -0
  19. package/templates/_opencrew/_memory/company.md +4 -0
  20. package/templates/_opencrew/_memory/preferences.md +9 -0
  21. package/templates/_opencrew/config/playwright.config.json +11 -0
  22. package/templates/_opencrew/core/architect.agent.yaml +110 -0
  23. package/templates/_opencrew/core/best-practices/_catalog.yaml +116 -0
  24. package/templates/_opencrew/core/best-practices/blog-post.md +151 -0
  25. package/templates/_opencrew/core/best-practices/blog-seo.md +146 -0
  26. package/templates/_opencrew/core/best-practices/copywriting.md +446 -0
  27. package/templates/_opencrew/core/best-practices/data-analysis.md +420 -0
  28. package/templates/_opencrew/core/best-practices/email-newsletter.md +136 -0
  29. package/templates/_opencrew/core/best-practices/email-sales.md +127 -0
  30. package/templates/_opencrew/core/best-practices/image-design.md +365 -0
  31. package/templates/_opencrew/core/best-practices/instagram-feed.md +252 -0
  32. package/templates/_opencrew/core/best-practices/instagram-reels.md +128 -0
  33. package/templates/_opencrew/core/best-practices/instagram-stories.md +123 -0
  34. package/templates/_opencrew/core/best-practices/linkedin-article.md +133 -0
  35. package/templates/_opencrew/core/best-practices/linkedin-post.md +138 -0
  36. package/templates/_opencrew/core/best-practices/researching.md +366 -0
  37. package/templates/_opencrew/core/best-practices/review.md +286 -0
  38. package/templates/_opencrew/core/best-practices/social-networks-publishing.md +311 -0
  39. package/templates/_opencrew/core/best-practices/strategist.md +361 -0
  40. package/templates/_opencrew/core/best-practices/technical-writing.md +382 -0
  41. package/templates/_opencrew/core/best-practices/twitter-post.md +122 -0
  42. package/templates/_opencrew/core/best-practices/twitter-thread.md +139 -0
  43. package/templates/_opencrew/core/best-practices/whatsapp-broadcast.md +124 -0
  44. package/templates/_opencrew/core/best-practices/youtube-script.md +139 -0
  45. package/templates/_opencrew/core/best-practices/youtube-shorts.md +129 -0
  46. package/templates/_opencrew/core/prompts/build.prompt.md +547 -0
  47. package/templates/_opencrew/core/prompts/design.prompt.md +469 -0
  48. package/templates/_opencrew/core/prompts/discovery.prompt.md +269 -0
  49. package/templates/_opencrew/core/prompts/sherlock-instagram.md +123 -0
  50. package/templates/_opencrew/core/prompts/sherlock-linkedin.md +73 -0
  51. package/templates/_opencrew/core/prompts/sherlock-shared.md +684 -0
  52. package/templates/_opencrew/core/prompts/sherlock-twitter.md +78 -0
  53. package/templates/_opencrew/core/prompts/sherlock-youtube.md +85 -0
  54. package/templates/_opencrew/core/runner.pipeline.md +611 -0
  55. package/templates/_opencrew/core/skills.engine.md +388 -0
  56. package/templates/_opencrew/logs/.gitkeep +0 -0
  57. package/templates/crews/.gitkeep +0 -0
  58. package/templates/gitignore +8 -0
  59. package/templates/skills/apify/SKILL.md +55 -0
  60. package/templates/skills/blotato/SKILL.md +63 -0
  61. package/templates/skills/canva/SKILL.md +60 -0
  62. package/templates/skills/image-ai-generator/SKILL.md +124 -0
  63. package/templates/skills/image-ai-generator/scripts/generate.py +175 -0
  64. package/templates/skills/image-creator/SKILL.md +155 -0
  65. package/templates/skills/image-fetcher/SKILL.md +91 -0
  66. package/templates/skills/instagram-publisher/SKILL.md +119 -0
  67. package/templates/skills/instagram-publisher/scripts/publish.js +165 -0
  68. package/templates/skills/opencrew-best-practice-creator/SKILL.md +192 -0
  69. package/templates/skills/opencrew-skill-creator/SKILL.md +420 -0
  70. package/templates/skills/opencrew-skill-creator/agents/analyzer.md +274 -0
  71. package/templates/skills/opencrew-skill-creator/agents/comparator.md +202 -0
  72. package/templates/skills/opencrew-skill-creator/agents/grader.md +223 -0
  73. package/templates/skills/opencrew-skill-creator/assets/eval_review.html +146 -0
  74. package/templates/skills/opencrew-skill-creator/eval-viewer/generate_review.py +471 -0
  75. package/templates/skills/opencrew-skill-creator/eval-viewer/viewer.html +1325 -0
  76. package/templates/skills/opencrew-skill-creator/references/schemas.md +430 -0
  77. package/templates/skills/opencrew-skill-creator/references/skill-format.md +235 -0
  78. package/templates/skills/opencrew-skill-creator/scripts/__init__.py +0 -0
  79. package/templates/skills/opencrew-skill-creator/scripts/aggregate_benchmark.py +401 -0
  80. package/templates/skills/opencrew-skill-creator/scripts/quick_validate.py +103 -0
  81. package/templates/skills/opencrew-skill-creator/scripts/run_eval.py +310 -0
  82. package/templates/skills/opencrew-skill-creator/scripts/utils.py +47 -0
  83. package/templates/skills/resend/SKILL.md +80 -0
  84. package/templates/skills/template-designer/SKILL.md +208 -0
  85. package/templates/skills/template-designer/base-templates/model-a.html +27 -0
  86. package/templates/skills/template-designer/base-templates/model-b.html +31 -0
  87. package/templates/skills/template-designer/base-templates/model-c.html +42 -0
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ // Instagram Carousel Publisher
3
+ // Usage: node --env-file=.env publish.js --images "slide1.jpg,slide2.jpg" --caption "..." [--dry-run]
4
+
5
+ import { readFileSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ // ── Argument parsing ──────────────────────────────────────────
10
+
11
+ export function parseArgs(argv) {
12
+ const args = { images: [], caption: '', dryRun: false };
13
+ for (let i = 2; i < argv.length; i++) {
14
+ if (argv[i] === '--images') {
15
+ if (i + 1 < argv.length) args.images = argv[++i].split(',').map(s => s.trim());
16
+ }
17
+ else if (argv[i] === '--caption') {
18
+ if (i + 1 < argv.length) args.caption = argv[++i];
19
+ }
20
+ else if (argv[i] === '--dry-run') args.dryRun = true;
21
+ }
22
+ return args;
23
+ }
24
+
25
+ // ── Image upload (imgBB) ──────────────────────────────────────
26
+
27
+ export async function uploadToImgBB(imagePath, apiKey) {
28
+ const absolutePath = resolve(imagePath);
29
+ const fileBuffer = readFileSync(absolutePath);
30
+ const base64Image = fileBuffer.toString('base64');
31
+ const form = new FormData();
32
+ form.append('key', apiKey);
33
+ form.append('image', base64Image);
34
+ const res = await fetch('https://api.imgbb.com/1/upload', {
35
+ method: 'POST',
36
+ body: form,
37
+ });
38
+ if (!res.ok) throw new Error(`imgBB upload failed [${res.status}]: ${await res.text()}`);
39
+ const json = await res.json();
40
+ if (!json.success) throw new Error(`imgBB upload failed: ${JSON.stringify(json)}`);
41
+ return json.data.url;
42
+ }
43
+
44
+ // ── Instagram Graph API ───────────────────────────────────────
45
+
46
+ const IG_BASE = 'https://graph.facebook.com/v21.0';
47
+
48
+ export async function createChildContainer(userId, imageUrl, accessToken) {
49
+ const params = new URLSearchParams({
50
+ image_url: imageUrl,
51
+ is_carousel_item: 'true',
52
+ access_token: accessToken,
53
+ });
54
+ const res = await fetch(`${IG_BASE}/${userId}/media?${params}`, { method: 'POST' });
55
+ if (!res.ok) throw new Error(`createChildContainer failed [${res.status}]: ${await res.text()}`);
56
+ return (await res.json()).id;
57
+ }
58
+
59
+ export async function getContainerStatus(containerId, accessToken) {
60
+ const params = new URLSearchParams({ fields: 'status_code', access_token: accessToken });
61
+ const res = await fetch(`${IG_BASE}/${containerId}?${params}`);
62
+ if (!res.ok) throw new Error(`getContainerStatus failed [${res.status}]: ${await res.text()}`);
63
+ return (await res.json()).status_code;
64
+ }
65
+
66
+ export async function pollUntilFinished(containerId, accessToken, timeoutMs = 60_000, intervalMs = 3_000) {
67
+ const deadline = Date.now() + timeoutMs;
68
+ while (Date.now() < deadline) {
69
+ const status = await getContainerStatus(containerId, accessToken);
70
+ if (status === 'FINISHED') return;
71
+ if (status === 'ERROR') throw new Error(`Container ${containerId} entered ERROR state`);
72
+ await new Promise(r => setTimeout(r, intervalMs));
73
+ }
74
+ throw new Error(`Container ${containerId} timed out after ${timeoutMs}ms`);
75
+ }
76
+
77
+ export async function createCarouselContainer(userId, childIds, caption, accessToken) {
78
+ const params = new URLSearchParams({
79
+ media_type: 'CAROUSEL',
80
+ children: childIds.join(','),
81
+ caption,
82
+ access_token: accessToken,
83
+ });
84
+ const res = await fetch(`${IG_BASE}/${userId}/media?${params}`, { method: 'POST' });
85
+ if (!res.ok) throw new Error(`createCarouselContainer failed [${res.status}]: ${await res.text()}`);
86
+ return (await res.json()).id;
87
+ }
88
+
89
+ export async function publishMedia(userId, containerId, accessToken) {
90
+ const params = new URLSearchParams({ creation_id: containerId, access_token: accessToken });
91
+ const res = await fetch(`${IG_BASE}/${userId}/media_publish?${params}`, { method: 'POST' });
92
+ if (!res.ok) throw new Error(`publishMedia failed [${res.status}]: ${await res.text()}`);
93
+ return (await res.json()).id;
94
+ }
95
+
96
+ export async function getPermalink(mediaId, accessToken) {
97
+ const params = new URLSearchParams({ fields: 'permalink', access_token: accessToken });
98
+ const res = await fetch(`${IG_BASE}/${mediaId}?${params}`);
99
+ if (!res.ok) return null; // non-fatal — just skip the URL display
100
+ const json = await res.json();
101
+ return json.permalink ?? null;
102
+ }
103
+
104
+ // ── Main ──────────────────────────────────────────────────────
105
+
106
+ async function main() {
107
+ const { images, caption, dryRun } = parseArgs(process.argv);
108
+
109
+ if (!images.length) throw new Error('--images is required (e.g. --images "slide1.jpg,slide2.jpg")');
110
+ if (!caption) throw new Error('--caption is required');
111
+ if (images.length < 2 || images.length > 10) {
112
+ throw new Error(`Instagram carousels require 2–10 images (got ${images.length})`);
113
+ }
114
+ if (caption.length > 2200) {
115
+ throw new Error(`Caption exceeds Instagram's 2200-character limit (got ${caption.length})`);
116
+ }
117
+
118
+ const { INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_USER_ID, IMGBB_API_KEY } = process.env;
119
+ if (!INSTAGRAM_ACCESS_TOKEN) throw new Error('INSTAGRAM_ACCESS_TOKEN is not set in environment');
120
+ if (!INSTAGRAM_USER_ID) throw new Error('INSTAGRAM_USER_ID is not set in environment');
121
+ if (!IMGBB_API_KEY) throw new Error('IMGBB_API_KEY is not set in environment. Get one at https://api.imgbb.com/');
122
+
123
+ console.log(`📸 Uploading ${images.length} image(s) to imgBB...`);
124
+ const imageUrls = await Promise.all(images.map(p => uploadToImgBB(p, IMGBB_API_KEY)));
125
+ imageUrls.forEach((url, i) => console.log(` [${i + 1}] ${url}`));
126
+
127
+ console.log('\n📦 Creating Instagram media containers...');
128
+ const childIds = await Promise.all(
129
+ imageUrls.map(url => createChildContainer(INSTAGRAM_USER_ID, url, INSTAGRAM_ACCESS_TOKEN))
130
+ );
131
+ console.log(` Container IDs: ${childIds.join(', ')}`);
132
+
133
+ console.log('\n⏳ Waiting for containers to finish processing...');
134
+ await Promise.all(childIds.map(id => pollUntilFinished(id, INSTAGRAM_ACCESS_TOKEN)));
135
+ console.log(' All containers ready.');
136
+
137
+ console.log('\n🎠 Creating carousel container...');
138
+ const carouselId = await createCarouselContainer(
139
+ INSTAGRAM_USER_ID, childIds, caption, INSTAGRAM_ACCESS_TOKEN
140
+ );
141
+ await pollUntilFinished(carouselId, INSTAGRAM_ACCESS_TOKEN);
142
+ console.log(` Carousel container ID: ${carouselId}`);
143
+
144
+ if (dryRun) {
145
+ console.log('\n✅ DRY RUN complete — skipping final publish call.');
146
+ console.log(` Carousel container ready: ${carouselId}`);
147
+ return;
148
+ }
149
+
150
+ console.log('\n🚀 Publishing to Instagram...');
151
+ const postId = await publishMedia(INSTAGRAM_USER_ID, carouselId, INSTAGRAM_ACCESS_TOKEN);
152
+ const permalink = await getPermalink(postId, INSTAGRAM_ACCESS_TOKEN);
153
+ console.log(`\n✅ Published successfully!`);
154
+ console.log(` Post ID: ${postId}`);
155
+ if (permalink) console.log(` URL: ${permalink}`);
156
+ }
157
+
158
+ // Run only when executed directly (not when imported for tests)
159
+ const isMain = process.argv[1] === fileURLToPath(import.meta.url);
160
+ if (isMain) {
161
+ main().catch(err => {
162
+ console.error(`\n❌ ${err.message}`);
163
+ process.exit(1);
164
+ });
165
+ }
@@ -0,0 +1,192 @@
1
+ ---
2
+ name: "Best-Practice Creator"
3
+ description: >
4
+ Guides creation and maintenance of best-practice files for the opencrew best-practices library.
5
+ Handles format validation, cross-references, versioning, and catalog consistency.
6
+ description_pt-BR: >
7
+ Guia a criação e manutenção de arquivos de best-practice na biblioteca de best-practices do opencrew.
8
+ Cuida de validação de formato, referências cruzadas, versionamento e consistência do catálogo.
9
+ description_es: >
10
+ Guía la creación y mantenimiento de archivos de best-practice en la biblioteca de best-practices de opencrew.
11
+ Maneja validación de formato, referencias cruzadas, versionamiento y consistencia del catálogo.
12
+ type: prompt
13
+ version: "2.0.0"
14
+ ---
15
+
16
+ # Best-Practice Creator — Workflow
17
+
18
+ Use this workflow when creating a new best-practice file for the `_opencrew/core/best-practices/` library.
19
+
20
+ ## Pre-flight Checks
21
+
22
+ 1. **Scan existing best-practice files**: Read `_opencrew/core/best-practices/_catalog.yaml`. Extract `id`, `name`, `whenToUse`, `file` from each entry.
23
+ 2. **Check for overlap**: Verify the new best-practice file doesn't duplicate an existing entry's `whenToUse` scope. If there's overlap, clarify the differentiation before proceeding.
24
+ 3. **List available skills**: Read all `skills/*/SKILL.md` files. Extract `name`, `description`, `type` from each — these may inform the best-practice file's content.
25
+
26
+ ## Creation Checklist
27
+
28
+ For each new best-practice file, ensure ALL of the following:
29
+
30
+ ### Frontmatter (YAML)
31
+
32
+ - [ ] `id`: lowercase kebab-case (e.g., `copywriting`)
33
+ - [ ] `name`: Display name for catalog listing (e.g., `"Copywriting & Persuasive Writing"`)
34
+ - [ ] `whenToUse`: Multi-line with positive scope AND "NOT for: ..." negative scope referencing other best-practice IDs
35
+ - [ ] `version`: `"1.0.0"` for new best-practice files
36
+
37
+ ### Body (Markdown) — All sections mandatory
38
+
39
+ - [ ] **Core Principles**: 6+ numbered domain-specific decision rules, each with a bold title and detailed explanation
40
+ - [ ] **Techniques & Frameworks**: Concrete methods, models, or processes practitioners use in this discipline (e.g., diagnostic steps, framework selections, structural patterns)
41
+ - [ ] **Quality Criteria**: 4+ checkable criteria as `- [ ]` list that can be used to evaluate output
42
+ - [ ] **Output Examples**: 2+ complete examples, 15+ lines each, realistic NOT template-like
43
+ - [ ] **Anti-Patterns**: Never Do (4+) + Always Do (3+), each with explanation
44
+ - [ ] **Vocabulary Guidance**: Terms/phrases to Always Use (5+), Terms/phrases to Never Use (3+), Tone Rules (2+)
45
+
46
+ ### Quality Minimums
47
+
48
+ | Section | Minimum |
49
+ |---------|---------|
50
+ | Total file lines | 200+ |
51
+ | Core Principles | 6+ numbered rules |
52
+ | Techniques & Frameworks | 3+ concrete techniques |
53
+ | Vocabulary Always Use | 5+ terms |
54
+ | Vocabulary Never Use | 3+ terms |
55
+ | Output Examples | 2 complete, 15+ lines each |
56
+ | Anti-Patterns (Never Do) | 4+ |
57
+ | Anti-Patterns (Always Do) | 3+ |
58
+ | Quality Criteria | 4+ checkable items |
59
+
60
+ ## Post-Creation Steps
61
+
62
+ ### 1. Update existing best-practice files' `whenToUse`
63
+
64
+ For each existing best-practice file whose scope overlaps with the new one:
65
+ - Add a "NOT for: {overlapping-scope} → See {new-best-practice-id}" line to their `whenToUse`
66
+ - Bump their version (patch increment)
67
+
68
+ ### 2. Update `_catalog.yaml`
69
+
70
+ Add a new entry to `_opencrew/core/best-practices/_catalog.yaml` with:
71
+ - `id`: matching the frontmatter `id`
72
+ - `name`: matching the frontmatter `name`
73
+ - `whenToUse`: single-line summary of the scope (positive only, no "NOT for")
74
+ - `file`: `{id}.md`
75
+
76
+ Place it under the appropriate section comment (Discipline or Platform best practices).
77
+
78
+ ### 3. File placement
79
+
80
+ Save to `_opencrew/core/best-practices/{id}.md`.
81
+
82
+ ### 4. Validation
83
+
84
+ Re-read the created file and verify:
85
+ - [ ] All checklist items above are present
86
+ - [ ] YAML frontmatter parses correctly (no syntax errors)
87
+ - [ ] `whenToUse` references only existing best-practice IDs
88
+ - [ ] Output examples are realistic, not template placeholders
89
+ - [ ] File exceeds 200 lines
90
+ - [ ] Corresponding entry exists in `_catalog.yaml`
91
+
92
+ ---
93
+
94
+ # Best-Practice Updater — Workflow
95
+
96
+ Use this workflow when updating best-practice files in the `_opencrew/core/best-practices/` library.
97
+
98
+ ## Versioning Rules (Semver)
99
+
100
+ | Change Type | Version Bump | Examples |
101
+ |-------------|-------------|----------|
102
+ | **Patch** (x.x.X) | Fix typos, adjust wording, minor refinements | Fix anti-pattern phrasing, correct a vocabulary term |
103
+ | **Minor** (x.X.0) | Add new content, extend capabilities | Add new principle, new output example, new technique |
104
+ | **Major** (X.0.0) | Rewrite or restructure significantly | Rewrite core principles, fundamentally change scope |
105
+
106
+ Always update the `version` field in the YAML frontmatter after any change.
107
+
108
+ ## Update Scenarios
109
+
110
+ ### When a best-practice file is removed from the library
111
+
112
+ 1. Get the removed best-practice file's `id`
113
+ 2. Remove its entry from `_opencrew/core/best-practices/_catalog.yaml`
114
+ 3. Scan ALL remaining best-practice files in `_opencrew/core/best-practices/*.md`
115
+ 4. For each file, check if the removed ID is referenced in `whenToUse`
116
+ - Look for patterns: "NOT for: ... → See {removed-id}"
117
+ 5. If found, remove that "NOT for" line
118
+ 6. Bump the affected files' version (patch: x.x.X)
119
+
120
+ ### When a new best-practice file is added to the library
121
+
122
+ The Best-Practice Creator workflow (above) handles the initial `whenToUse` cross-references during creation. This section is only needed if cross-references were missed or need adjustment after the fact.
123
+
124
+ 1. Read the new best-practice file's `whenToUse` — identify its scope
125
+ 2. Scan existing best-practice files for overlapping scope
126
+ 3. Add "NOT for: {new-scope} → See {new-id}" where appropriate
127
+ 4. Bump affected files' version (patch)
128
+ 5. Ensure the new entry exists in `_catalog.yaml`
129
+
130
+ ### When updating a best-practice file's content
131
+
132
+ 1. Make the content changes
133
+ 2. Verify ALL mandatory sections still exist:
134
+ - [ ] Core Principles (6+ rules)
135
+ - [ ] Techniques & Frameworks (3+ techniques)
136
+ - [ ] Quality Criteria (4+ checkable items)
137
+ - [ ] Output Examples (2+ complete examples)
138
+ - [ ] Anti-Patterns (Never Do + Always Do)
139
+ - [ ] Vocabulary Guidance (Always Use, Never Use, Tone Rules)
140
+ 3. Bump version according to semver rules above
141
+ 4. If the `whenToUse` scope changed, update cross-references in other best-practice files and in `_catalog.yaml`
142
+
143
+ ### When updating a best-practice file's `whenToUse` scope
144
+
145
+ This is the most impactful change — it affects how the Architect selects best practices during crew creation.
146
+
147
+ 1. Document the old scope and new scope
148
+ 2. Update the best-practice file's `whenToUse` field
149
+ 3. Scan ALL other best-practice files' `whenToUse` for references to this ID
150
+ 4. Update cross-references to reflect the new scope
151
+ 5. Update the `whenToUse` summary in `_catalog.yaml`
152
+ 6. Bump version (minor if scope expanded, patch if scope narrowed)
153
+
154
+ ## Validation Checklist
155
+
156
+ After ANY update, verify:
157
+
158
+ - [ ] Version was bumped correctly (patch/minor/major per rules above)
159
+ - [ ] All mandatory sections still present and non-empty
160
+ - [ ] `whenToUse` cross-references are consistent across ALL best-practice files
161
+ - [ ] No broken cross-references to removed best-practice IDs
162
+ - [ ] Output examples are still realistic and complete
163
+ - [ ] File still exceeds 200 lines minimum
164
+ - [ ] `_catalog.yaml` entry is in sync with frontmatter (`id`, `name`, `whenToUse`)
165
+
166
+ ## Bulk Operations
167
+
168
+ ### Verify catalog consistency
169
+
170
+ ```
171
+ Read _opencrew/core/best-practices/_catalog.yaml
172
+ For each entry in catalog:
173
+ 1. Verify _opencrew/core/best-practices/{entry.file} exists
174
+ 2. Read the file's frontmatter
175
+ 3. Verify entry.id matches frontmatter id
176
+ 4. Verify entry.name matches frontmatter name
177
+ 5. Flag any mismatches
178
+
179
+ For each .md file in _opencrew/core/best-practices/ (excluding _catalog.yaml):
180
+ 1. Verify a corresponding entry exists in _catalog.yaml
181
+ 2. Flag any orphaned files with no catalog entry
182
+ ```
183
+
184
+ ### Verify cross-reference consistency
185
+
186
+ ```
187
+ For each best-practice file A in _opencrew/core/best-practices/*.md:
188
+ For each "NOT for: ... → See {id}" in A.whenToUse:
189
+ 1. Verify _opencrew/core/best-practices/{id}.md exists
190
+ 2. Verify {id}'s whenToUse covers the referenced scope
191
+ 3. Flag inconsistencies
192
+ ```