@ghl-ai/aw 0.1.39-beta.9 → 0.1.40-beta.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.
package/ecc.mjs CHANGED
@@ -10,7 +10,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
10
10
 
11
11
  const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
12
12
  const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
13
- export const AW_ECC_TAG = "v1.4.33";
13
+ export const AW_ECC_TAG = "v1.4.37";
14
14
 
15
15
  const MARKETPLACE_NAME = "aw-marketplace";
16
16
  const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
package/git.mjs CHANGED
@@ -9,6 +9,49 @@ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, RULES_SOURCE_DIR }
9
9
 
10
10
  const exec = promisify(execCb);
11
11
 
12
+ /**
13
+ * Env vars applied to every git command that touches the network.
14
+ * GIT_TERMINAL_PROMPT=0 prevents git from hanging when it would otherwise
15
+ * prompt for credentials (e.g. HTTPS URL with SSH-only auth configured).
16
+ * Instead, git exits immediately with a non-zero code that we can catch.
17
+ */
18
+ const GIT_NET_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
19
+
20
+ /**
21
+ * Convert an HTTPS GitHub URL to its SSH equivalent.
22
+ * e.g. https://github.com/Org/Repo.git → git@github.com:Org/Repo.git
23
+ */
24
+ function toSshUrl(httpsUrl) {
25
+ const m = httpsUrl.match(/^https:\/\/github\.com\/(.+)$/);
26
+ return m ? `git@github.com:${m[1]}` : httpsUrl;
27
+ }
28
+
29
+ /**
30
+ * Detect whether the user's git is configured to prefer SSH for github.com.
31
+ * Checks: 1) git insteadOf config 2) gh CLI auth status
32
+ * Returns true if SSH is preferred.
33
+ */
34
+ function prefersSsh() {
35
+ // Check git url."git@github.com:".insteadOf
36
+ try {
37
+ const out = execSync(
38
+ 'git config --global --get-regexp "url\\.git@github\\.com.*\\.insteadOf"',
39
+ { stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV },
40
+ ).trim();
41
+ if (out.includes('https://github.com')) return true;
42
+ } catch { /* not configured */ }
43
+
44
+ // Check gh auth — if protocol is ssh, prefer SSH
45
+ try {
46
+ const out = execSync('gh auth status 2>&1', {
47
+ stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV, timeout: 5000,
48
+ });
49
+ if (/git protocol:\s*ssh/i.test(out)) return true;
50
+ } catch { /* gh not installed or not authed */ }
51
+
52
+ return false;
53
+ }
54
+
12
55
  // ── Backward-compat: temp-dir sparse checkout (used by search.mjs) ────────────
13
56
 
14
57
  /**
@@ -18,12 +61,23 @@ const exec = promisify(execCb);
18
61
  export function sparseCheckout(repo, paths) {
19
62
  const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
20
63
 
21
- const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
22
- try {
23
- execSync(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`, {
24
- stdio: 'pipe',
25
- });
26
- } catch (e) {
64
+ const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
65
+ const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
66
+
67
+ let cloned = false;
68
+ for (const url of urls) {
69
+ try {
70
+ execSync(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, {
71
+ stdio: 'pipe', env: GIT_NET_ENV,
72
+ });
73
+ cloned = true;
74
+ break;
75
+ } catch {
76
+ // Clean up partial clone so next attempt can use the same tempDir
77
+ try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
78
+ }
79
+ }
80
+ if (!cloned) {
27
81
  throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
28
82
  }
29
83
 
@@ -47,10 +101,20 @@ export function sparseCheckout(repo, paths) {
47
101
  export async function sparseCheckoutAsync(repo, paths) {
48
102
  const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
49
103
 
50
- const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
51
- try {
52
- await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`);
53
- } catch (e) {
104
+ const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
105
+ const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
106
+
107
+ let cloned = false;
108
+ for (const url of urls) {
109
+ try {
110
+ await exec(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, { env: GIT_NET_ENV });
111
+ cloned = true;
112
+ break;
113
+ } catch {
114
+ try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
115
+ }
116
+ }
117
+ if (!cloned) {
54
118
  throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
55
119
  }
56
120
 
@@ -106,7 +170,9 @@ export function isValidClone(awHome, repoUrl) {
106
170
  if (!existsSync(join(awHome, '.git'))) return false;
107
171
  try {
108
172
  const remote = execSync('git remote get-url origin', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' }).trim();
109
- return remote === repoUrl || remote === repoUrl.replace(/\.git$/, '') + '.git' || remote.replace(/\.git$/, '') === repoUrl.replace(/\.git$/, '');
173
+ // Normalize both sides to bare repo path for comparison (handles HTTPS SSH)
174
+ const normalize = (u) => u.replace(/\.git$/, '').replace(/^git@github\.com:/, 'https://github.com/');
175
+ return normalize(remote) === normalize(repoUrl);
110
176
  } catch {
111
177
  return false;
112
178
  }
@@ -123,10 +189,26 @@ export async function initPersistentClone(repoUrl, awHome, sparsePaths) {
123
189
  try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
124
190
  }
125
191
 
126
- try {
127
- await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${awHome}"`);
128
- } catch (e) {
129
- throw new Error(`Failed to clone ${repoUrl}: ${e.message}`);
192
+ const urls = prefersSsh()
193
+ ? [toSshUrl(repoUrl), repoUrl]
194
+ : [repoUrl, toSshUrl(repoUrl)];
195
+
196
+ let cloned = false;
197
+ for (const url of urls) {
198
+ // Clean up any partial clone from a previous attempt
199
+ if (existsSync(awHome) && !isValidClone(awHome, url)) {
200
+ try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
201
+ }
202
+ try {
203
+ await exec(`git clone --filter=blob:none --no-checkout "${url}" "${awHome}"`, { env: GIT_NET_ENV });
204
+ cloned = true;
205
+ break;
206
+ } catch {
207
+ // try next URL
208
+ }
209
+ }
210
+ if (!cloned) {
211
+ throw new Error(`Failed to clone ${repoUrl}. Check your git credentials and repo access (HTTPS and SSH both failed).`);
130
212
  }
131
213
 
132
214
  try {
@@ -286,7 +368,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
286
368
 
287
369
  // ── 2. Fetch ──────────────────────────────────────────────────────────────
288
370
  try {
289
- await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`);
371
+ await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`, { env: GIT_NET_ENV });
290
372
  } catch (e) {
291
373
  throw new Error(`Failed to fetch from origin: ${e.message}`);
292
374
  }
@@ -315,7 +397,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
315
397
  // someone else pushed to the remote tracking branch since our last fetch.
316
398
  if (isPushBranch) {
317
399
  try {
318
- await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`);
400
+ await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`, { env: GIT_NET_ENV });
319
401
  } catch { /* non-blocking — divergence will be resolved on next aw push */ }
320
402
  }
321
403
  } catch {
@@ -442,7 +524,7 @@ export function updatePushBranch(awHome, pushBranchName) {
442
524
  }
443
525
 
444
526
  try {
445
- execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe' });
527
+ execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe', env: GIT_NET_ENV });
446
528
  } catch (e) {
447
529
  throw new Error(`Failed to push branch: ${e.message}`);
448
530
  }
@@ -485,7 +567,7 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
485
567
  }
486
568
 
487
569
  try {
488
- await exec(`git -C "${awHome}" push -u origin "${branchName}"`);
570
+ await exec(`git -C "${awHome}" push -u origin "${branchName}"`, { env: GIT_NET_ENV });
489
571
  } catch (e) {
490
572
  throw new Error(`Failed to push branch: ${e.message}`);
491
573
  }
package/integrate.mjs CHANGED
@@ -114,12 +114,41 @@ function shouldResetHomeInstructionFile(content, file) {
114
114
  return legacyMarkers.some(marker => content.includes(marker));
115
115
  }
116
116
 
117
+ function stripLegacyRepoInstructionContent(content, file) {
118
+ const legacyMarkers = file === 'CLAUDE.md'
119
+ ? [
120
+ '# CLAUDE.md — ',
121
+ '## Routing Rule (ABSOLUTE)',
122
+ 'This supplements the root `AGENTS.md` with Codex-specific guidance.',
123
+ '<!-- BEGIN ECC -->',
124
+ ]
125
+ : [
126
+ '# AGENTS.md — ',
127
+ '# ECC for Codex CLI',
128
+ '# AW SDLC Repo Instructions',
129
+ 'Use the repo-local AW SDLC files as the source of truth for routing and stage behavior.',
130
+ '## Agent System',
131
+ '<!-- BEGIN ECC -->',
132
+ ];
133
+
134
+ const startIndexes = legacyMarkers
135
+ .map(marker => content.indexOf(marker))
136
+ .filter(idx => idx !== -1);
137
+
138
+ if (startIndexes.length === 0) return content;
139
+
140
+ const startIdx = Math.min(...startIndexes);
141
+ const preserved = content.slice(0, startIdx).trimEnd();
142
+ return preserved ? `${preserved}\n` : '';
143
+ }
144
+
117
145
  function applyManagedInstructionSections(content, file, rulesSections = {}, options = {}) {
118
146
  const rulesHeader = file === 'CLAUDE.md' ? CLAUDE_RULES_HEADER : AGENTS_RULES_HEADER;
119
147
  const rulesSection = file === 'CLAUDE.md' ? rulesSections.claudeSection : rulesSections.agentsSection;
120
148
  const includeBridge = options.includeBridge !== false;
121
149
 
122
- let next = stripManagedBlock(content, AW_ROUTER_BRIDGE_START_MARKER, AW_ROUTER_BRIDGE_END_MARKER);
150
+ let next = stripLegacyRepoInstructionContent(content, file);
151
+ next = stripManagedBlock(next, AW_ROUTER_BRIDGE_START_MARKER, AW_ROUTER_BRIDGE_END_MARKER);
123
152
  next = stripManagedSection(next, AW_ROUTER_BRIDGE_HEADER, [rulesHeader]);
124
153
  next = stripManagedSection(next, rulesHeader);
125
154
  next = next.trimEnd();
@@ -129,8 +158,15 @@ function applyManagedInstructionSections(content, file, rulesSections = {}, opti
129
158
  return next ? `${next}\n` : '';
130
159
  }
131
160
 
132
- const appended = sections.join('\n\n').trim();
133
- return next ? `${next}\n\n${appended}\n` : `${appended}\n`;
161
+ // Marker tells users (and aw init) where the managed section starts.
162
+ // Everything BEFORE this marker is repo-owned and never touched by aw.
163
+ // Everything AFTER is managed by aw — re-rendered on every aw init.
164
+ const managedBoundary = '<!-- aw-managed: content below is regenerated by `aw init` — put your own content above this line -->';
165
+ const appended = [managedBoundary, ...sections].join('\n\n').trim();
166
+ // Strip any prior managedBoundary line from `next` so we don't accumulate them
167
+ // when re-running aw init.
168
+ const cleaned = next.split('\n').filter(line => line.trim() !== managedBoundary).join('\n').trimEnd();
169
+ return cleaned ? `${cleaned}\n\n${appended}\n` : `${appended}\n`;
134
170
  }
135
171
 
136
172
  /**
@@ -188,49 +224,34 @@ function findFiles(dir, typeName) {
188
224
  }
189
225
 
190
226
  /**
191
- * Copy AGENTS.md to project root and refresh rules sections in any existing
192
- * CLAUDE.md. New CLAUDE.md files are intentionally not generated because their
193
- * routing rules can hijack plugin command dispatch in some workspaces.
227
+ * Refresh rules sections in any existing AGENTS.md/CLAUDE.md at the repo
228
+ * root.
229
+ *
230
+ * Repo instruction files are user-owned. aw init no longer creates or updates
231
+ * managed sections in repo-local AGENTS.md / CLAUDE.md.
232
+ *
233
+ * The only repo-file behavior left is cleanup: if a repo still contains old
234
+ * aw-managed sections from prior versions, strip those sections while leaving
235
+ * the user's own content intact.
194
236
  */
195
237
  export function copyInstructions(cwd, tempDir, namespace) {
196
238
  const rulesSections = renderRules(cwd);
197
239
  const createdFiles = [];
240
+
198
241
  for (const file of ['AGENTS.md', 'CLAUDE.md']) {
199
242
  const dest = join(cwd, file);
200
243
  if (existsSync(dest)) {
201
244
  const existing = readFileSync(dest, 'utf8');
202
- const updated = applyManagedInstructionSections(existing, file, rulesSections, { includeBridge: false });
245
+ const updated = applyManagedInstructionSections(existing, file, {}, { includeBridge: false });
203
246
 
204
247
  if (updated !== existing) {
205
248
  writeFileSync(dest, updated);
206
- fmt.logSuccess(`Updated ${file}`);
249
+ fmt.logStep(`Stripped aw-managed sections from ${file} (now in ~/.claude/CLAUDE.md / ~/.codex/AGENTS.md)`);
207
250
  }
208
251
  continue;
209
252
  }
210
253
 
211
- if (file === 'CLAUDE.md') continue;
212
-
213
- if (tempDir) {
214
- const src = join(tempDir, '.aw_registry', file);
215
- if (existsSync(src)) {
216
- let content = readFileSync(src, 'utf8');
217
- if (namespace) {
218
- content = content.replace(/\{\{TEAM\}\}/g, namespace);
219
- }
220
- content = applyManagedInstructionSections(content, file, rulesSections, { includeBridge: false });
221
- writeFileSync(dest, content);
222
- fmt.logSuccess(`Created ${file}`);
223
- createdFiles.push(dest);
224
- continue;
225
- }
226
- }
227
-
228
- const content = generateAgentsMd(cwd, namespace, rulesSections);
229
- if (content) {
230
- writeFileSync(dest, applyManagedInstructionSections(content, file, rulesSections, { includeBridge: false }));
231
- fmt.logSuccess(`Created ${file}`);
232
- createdFiles.push(dest);
233
- }
254
+ // Never create repo instruction files anymore.
234
255
  }
235
256
  return createdFiles;
236
257
  }
@@ -286,7 +307,7 @@ function generateClaudeMd(cwd, namespace, rulesSections = {}) {
286
307
  const team = namespace || 'my-team';
287
308
  let base = `# CLAUDE.md — ${team}
288
309
 
289
- Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\` (shared knowledge), \`git-jenkins\` (CI/CD), \`grafana\` (observability)
310
+ Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\` (shared knowledge), \`jenkins_*\` (CI/CD via ghl-ai MCP), \`grafana\` (observability)
290
311
 
291
312
  ## Routing Rule (ABSOLUTE)
292
313
 
@@ -357,7 +378,7 @@ memory/search → Search shared team knowledge base
357
378
  memory/store → Push learnings to shared knowledge (eager sync after runs)
358
379
  memory/get → Fetch specific memory by ID
359
380
  grafana/* → External observability
360
- git-jenkins/* External CI/CD pipelines
381
+ jenkins_* → CI/CD pipelines (provided by ghl-ai MCP)
361
382
  stitch/* → External design generation
362
383
  \`\`\`
363
384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.39-beta.9",
3
+ "version": "0.1.40-beta.0",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/render-rules.mjs CHANGED
@@ -6,7 +6,12 @@ import { homedir } from 'node:os';
6
6
  import * as fmt from './fmt.mjs';
7
7
  import { RULES_RUNTIME_DIR } from './constants.mjs';
8
8
 
9
- const GENERATED_HEADER = '<!-- Generated by aw do not edit manually -->\n\n';
9
+ // The marker is placed AFTER the YAML frontmatter so Cursor/Markdown parsers
10
+ // see the frontmatter at byte 0. Putting an HTML comment before --- breaks
11
+ // Cursor's alwaysApply/globs detection.
12
+ const GENERATED_MARKER = '<!-- Generated by aw — do not edit manually -->';
13
+ // Legacy header (pre-pattern comment first) — kept for pruning old files.
14
+ const LEGACY_GENERATED_HEADER = '<!-- Generated by aw — do not edit manually -->\n\n';
10
15
  const STACK_OVERLAY_FLAG = 'AW_ENABLE_STACK_OVERLAY_RULES';
11
16
 
12
17
  /** Rule scope → Cursor .mdc glob patterns */
@@ -196,7 +201,8 @@ function pruneStaleGeneratedRules(outputDir, expectedFilenames) {
196
201
 
197
202
  const fullPath = join(outputDir, entry.name);
198
203
  const content = readOrNull(fullPath);
199
- if (!content?.startsWith(GENERATED_HEADER)) continue;
204
+ // Match both new pattern (marker after frontmatter) and legacy (marker at top).
205
+ if (!content || (!content.includes(GENERATED_MARKER) && !content.startsWith(LEGACY_GENERATED_HEADER))) continue;
200
206
 
201
207
  try {
202
208
  unlinkSync(fullPath);
@@ -303,7 +309,9 @@ function renderCursorRules(cwd, rulesDir, options = {}) {
303
309
  }
304
310
  }
305
311
 
306
- const content = GENERATED_HEADER + frontmatter.join('\n') + '\n\n' + agentsMd.trim() + '\n' + refSection;
312
+ // Frontmatter MUST be at byte 0 for Cursor's YAML parser.
313
+ // Generated marker goes on the line immediately after the closing ---.
314
+ const content = frontmatter.join('\n') + '\n' + GENERATED_MARKER + '\n\n' + agentsMd.trim() + '\n' + refSection;
307
315
  writeFileSync(join(cursorRulesDir, `${scopeToFilename(scope)}.mdc`), content);
308
316
  count++;
309
317
  }
@@ -318,10 +326,13 @@ function renderCursorRules(cwd, rulesDir, options = {}) {
318
326
  }
319
327
 
320
328
  function generateCursorAwRoutingRule() {
321
- return `${GENERATED_HEADER}---
329
+ // Frontmatter MUST be at byte 0 for Cursor's alwaysApply/globs detection.
330
+ return `---
322
331
  description: "AW global routing: select route, READ stage skill, then respond"
323
332
  alwaysApply: true
324
333
  ---
334
+ ${GENERATED_MARKER}
335
+
325
336
  # AW Global Routing
326
337
 
327
338
  ## Hard Gate (MUST — do not skip)
@@ -460,7 +471,8 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
460
471
  }
461
472
  }
462
473
 
463
- const content = GENERATED_HEADER + frontmatter + agentsMd.trim() + '\n' + refSection;
474
+ // Claude Code reads .md frontmatter similarly keep marker after frontmatter.
475
+ const content = frontmatter + GENERATED_MARKER + '\n\n' + agentsMd.trim() + '\n' + refSection;
464
476
  writeFileSync(join(claudeRulesDir, `${scopeToFilename(scope)}.md`), content);
465
477
  count++;
466
478
  }
@@ -471,21 +483,120 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
471
483
  /**
472
484
  * Generate a rules section for CLAUDE.md from runtime AW rules.
473
485
  */
474
- export function generateClaudeMdRulesSection(rulesDir) {
486
+ /**
487
+ * Resolve the final scope set for AGENTS.md/CLAUDE.md filtering.
488
+ * Precedence: explicit option > .aw/config.json awRuleScopes > auto-detect.
489
+ * Returns undefined if user explicitly disabled filtering (awRuleScopes: "all").
490
+ */
491
+ export function resolveApplicableScopes(cwd, options = {}) {
492
+ if (options.applicableScopes) return options.applicableScopes;
493
+
494
+ // Read .aw/config.json if present.
495
+ const configPath = join(cwd, '.aw', 'config.json');
496
+ if (existsSync(configPath)) {
497
+ try {
498
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
499
+ const setting = config.awRuleScopes;
500
+ if (setting === 'all') return undefined; // disable filtering
501
+ if (Array.isArray(setting)) return new Set(setting);
502
+ } catch { /* fall through to auto-detect */ }
503
+ }
504
+
505
+ return detectApplicableScopes(cwd);
506
+ }
507
+
508
+ /**
509
+ * Detect applicable rule scopes for a consumer repo based on filesystem signals.
510
+ * Returns a Set of scope names (e.g. 'frontend', 'backend', 'mobile').
511
+ * 'universal' and 'security' are ALWAYS returned (cross-cutting, apply everywhere).
512
+ */
513
+ export function detectApplicableScopes(cwd) {
514
+ const scopes = new Set(['universal', 'security']);
515
+
516
+ const has = (rel) => existsSync(join(cwd, rel));
517
+ const readPkg = () => {
518
+ try {
519
+ if (!has('package.json')) return null;
520
+ return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
521
+ } catch {
522
+ return null;
523
+ }
524
+ };
525
+
526
+ const pkg = readPkg();
527
+ if (pkg) {
528
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
529
+ // Frontend signals
530
+ if (deps.vue || deps.nuxt || deps['@vue/cli-service'] || deps.react || deps.next || deps.svelte) {
531
+ scopes.add('frontend');
532
+ }
533
+ // Backend signals
534
+ if (deps['@nestjs/core'] || deps.express || deps.fastify || deps.koa || deps['@platform-core/base-service']) {
535
+ scopes.add('backend');
536
+ scopes.add('api-design');
537
+ }
538
+ // Data signals
539
+ if (deps.mongoose || deps.typeorm || deps.prisma || deps.knex || deps['@platform-core/firestore']) {
540
+ scopes.add('data');
541
+ }
542
+ // SDET signals
543
+ if (deps.playwright || deps['@playwright/test'] || deps['@gohighlevel/sdet-platform-core']) {
544
+ scopes.add('sdet');
545
+ }
546
+ }
547
+
548
+ if (has('pubspec.yaml')) scopes.add('mobile'); // Flutter/Dart
549
+ if (has('pom.xml') || has('build.gradle') || has('build.gradle.kts')) scopes.add('backend');
550
+ if (has('go.mod')) scopes.add('backend');
551
+ if (has('Cargo.toml')) scopes.add('backend');
552
+ if (has('requirements.txt') || has('pyproject.toml')) scopes.add('backend');
553
+
554
+ // Infra signals
555
+ if (has('Dockerfile') || has('Jenkinsfile') || has('helm') || has('terraform')) {
556
+ scopes.add('infra');
557
+ }
558
+
559
+ // If we only found the defaults (universal + security) with no other signals,
560
+ // return undefined to preserve current behavior (show all rules). Filtering
561
+ // only kicks in when we positively detect a framework or stack.
562
+ if (scopes.size <= 2) return undefined;
563
+ return scopes;
564
+ }
565
+
566
+ function filterRulesByScopes(rules, applicableScopes) {
567
+ if (!applicableScopes || applicableScopes.size === 0) return rules;
568
+ return rules.filter(r => {
569
+ const scope = String(r.id || '').split('/')[0];
570
+ return applicableScopes.has(scope);
571
+ });
572
+ }
573
+
574
+ export function generateClaudeMdRulesSection(rulesDir, options = {}) {
475
575
  const manifest = readManifest(rulesDir);
476
576
  if (!manifest) return '';
477
577
 
478
578
  const mustRules = manifest.rules.filter(r => r.severity === 'MUST');
479
- if (mustRules.length === 0) return '';
579
+ const applicableScopes = options.applicableScopes;
580
+ const filteredRules = applicableScopes
581
+ ? filterRulesByScopes(mustRules, applicableScopes)
582
+ : mustRules;
583
+ if (filteredRules.length === 0) return '';
480
584
 
481
585
  const lines = [
482
586
  '## Platform Rules (MUST)',
483
587
  '',
484
588
  '> Rendered from platform `.aw/.aw_rules/`. Full details in reference files.',
485
- '',
486
589
  ];
487
590
 
488
- for (const rule of mustRules) {
591
+ if (applicableScopes) {
592
+ lines.push(
593
+ `> Filtered to scopes detected in this repo: \`${[...applicableScopes].sort().join('`, `')}\`. ` +
594
+ `To override, set \`awRuleScopes\` in \`.aw/config.json\`.`,
595
+ );
596
+ }
597
+ lines.push('');
598
+
599
+ for (const rule of filteredRules) {
489
600
  lines.push(`- [ ] **${rule.id}** — ${rule.description}`);
490
601
  }
491
602
 
@@ -516,14 +627,28 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
516
627
  // Reference table — tells all IDEs (especially Codex) where to read domain rules.
517
628
  // Codex can't auto-trigger by glob, but it CAN read these files when working in
518
629
  // the matching area. Keep AGENTS.md lean; full content stays in the source files.
519
- const scopes = listRuleScopes(rulesDir, {
630
+ const allScopes = listRuleScopes(rulesDir, {
520
631
  includeNestedScopes: stackOverlaysEnabled(options),
521
632
  });
633
+ // Filter domain scopes by what's relevant to this consumer repo.
634
+ // 'universal' and 'security' always apply; other domains require detection.
635
+ const applicableScopes = options.applicableScopes;
636
+ const isApplicable = (scope) => !applicableScopes
637
+ || applicableScopes.has(scope)
638
+ || applicableScopes.has(scope.split('/')[0]);
639
+ const scopes = allScopes.filter(({ scope }) => isApplicable(scope));
522
640
  const domains = scopes.filter(({ scope }) => !scope.includes('/'));
523
641
  const overlays = scopes.filter(({ scope }) => scope.includes('/'));
524
642
  if (domains.length > 0) {
525
643
  lines.push('### Domain Rules');
526
644
  lines.push('');
645
+ if (applicableScopes) {
646
+ lines.push(
647
+ `> Filtered to scopes detected in this repo: \`${[...applicableScopes].sort().join('`, `')}\`. ` +
648
+ `To override, set \`awRuleScopes\` (array or "all") in \`.aw/config.json\`.`,
649
+ );
650
+ lines.push('');
651
+ }
527
652
  lines.push('When working in a specific domain, read the matching rules file:');
528
653
  lines.push('');
529
654
  lines.push('| Domain | Read when editing | Rules file |');
@@ -566,10 +691,14 @@ export function renderRules(cwd, options = {}) {
566
691
  const rulesDir = resolveRulesSourceDir(cwd, options);
567
692
  if (!rulesDir) return { cursorCount: 0, claudeSection: '', agentsSection: '' };
568
693
 
694
+ // Resolve applicable scopes for AGENTS.md / CLAUDE.md MUST-rule list.
695
+ // Order: explicit option → .aw/config.json awRuleScopes → auto-detect.
696
+ const applicableScopes = resolveApplicableScopes(cwd, options);
697
+
569
698
  const cursorCount = renderCursorRules(cwd, rulesDir, options);
570
699
  const claudeCount = renderClaudeRules(cwd, rulesDir, options);
571
- const claudeSection = generateClaudeMdRulesSection(rulesDir);
572
- const agentsSection = generateAgentsMdRulesSection(rulesDir, { ...options, outputDir: cwd });
700
+ const claudeSection = generateClaudeMdRulesSection(rulesDir, { applicableScopes });
701
+ const agentsSection = generateAgentsMdRulesSection(rulesDir, { ...options, outputDir: cwd, applicableScopes });
573
702
 
574
703
  // Also render to global dirs so rules apply everywhere
575
704
  const HOME = options.homeDir || homedir();