@ghl-ai/aw 0.1.39-beta.15 → 0.1.39-beta.17

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/integrate.mjs CHANGED
@@ -129,8 +129,15 @@ function applyManagedInstructionSections(content, file, rulesSections = {}, opti
129
129
  return next ? `${next}\n` : '';
130
130
  }
131
131
 
132
- const appended = sections.join('\n\n').trim();
133
- return next ? `${next}\n\n${appended}\n` : `${appended}\n`;
132
+ // Marker tells users (and aw init) where the managed section starts.
133
+ // Everything BEFORE this marker is repo-owned and never touched by aw.
134
+ // Everything AFTER is managed by aw — re-rendered on every aw init.
135
+ const managedBoundary = '<!-- aw-managed: content below is regenerated by `aw init` — put your own content above this line -->';
136
+ const appended = [managedBoundary, ...sections].join('\n\n').trim();
137
+ // Strip any prior managedBoundary line from `next` so we don't accumulate them
138
+ // when re-running aw init.
139
+ const cleaned = next.split('\n').filter(line => line.trim() !== managedBoundary).join('\n').trimEnd();
140
+ return cleaned ? `${cleaned}\n\n${appended}\n` : `${appended}\n`;
134
141
  }
135
142
 
136
143
  /**
@@ -188,49 +195,34 @@ function findFiles(dir, typeName) {
188
195
  }
189
196
 
190
197
  /**
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.
198
+ * Refresh rules sections in any existing AGENTS.md/CLAUDE.md at the repo
199
+ * root.
200
+ *
201
+ * Repo instruction files are user-owned. aw init no longer creates or updates
202
+ * managed sections in repo-local AGENTS.md / CLAUDE.md.
203
+ *
204
+ * The only repo-file behavior left is cleanup: if a repo still contains old
205
+ * aw-managed sections from prior versions, strip those sections while leaving
206
+ * the user's own content intact.
194
207
  */
195
208
  export function copyInstructions(cwd, tempDir, namespace) {
196
209
  const rulesSections = renderRules(cwd);
197
210
  const createdFiles = [];
211
+
198
212
  for (const file of ['AGENTS.md', 'CLAUDE.md']) {
199
213
  const dest = join(cwd, file);
200
214
  if (existsSync(dest)) {
201
215
  const existing = readFileSync(dest, 'utf8');
202
- const updated = applyManagedInstructionSections(existing, file, rulesSections, { includeBridge: false });
216
+ const updated = applyManagedInstructionSections(existing, file, {}, { includeBridge: false });
203
217
 
204
218
  if (updated !== existing) {
205
219
  writeFileSync(dest, updated);
206
- fmt.logSuccess(`Updated ${file}`);
220
+ fmt.logStep(`Stripped aw-managed sections from ${file} (now in ~/.claude/CLAUDE.md / ~/.codex/AGENTS.md)`);
207
221
  }
208
222
  continue;
209
223
  }
210
224
 
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
- }
225
+ // Never create repo instruction files anymore.
234
226
  }
235
227
  return createdFiles;
236
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.39-beta.15",
3
+ "version": "0.1.39-beta.17",
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
@@ -322,62 +322,9 @@ function renderCursorRules(cwd, rulesDir, options = {}) {
322
322
  writeFileSync(join(cursorRulesDir, 'common-aw-routing.mdc'), routingRule);
323
323
  count++;
324
324
 
325
- // Generate the prompt-response-format rule — machine-parseable trace
326
- // of what skills/rules/MCPs were used during a response.
327
- const traceRule = generateCursorPromptTraceRule();
328
- writeFileSync(join(cursorRulesDir, 'common-prompt-trace.mdc'), traceRule);
329
- count++;
330
-
331
325
  return count;
332
326
  }
333
327
 
334
- function generateCursorPromptTraceRule() {
335
- // Frontmatter MUST be at byte 0. Machine-parseable JSON block at end of response.
336
- return `---
337
- description: "Append a machine-parseable AW trace at the end of every response"
338
- alwaysApply: true
339
- ---
340
- ${GENERATED_MARKER}
341
-
342
- # AW Prompt Trace
343
-
344
- At the end of every substantive response, append a JSON-fenced block named \`aw-trace\` with the skills, rules, and MCPs that were loaded or applied during the response.
345
-
346
- ## Format
347
-
348
- \`\`\`aw-trace
349
- {
350
- "route": "/aw-plan",
351
- "skills_used": ["using-aw-skills", "aw-plan", "incremental-implementation"],
352
- "rules_applied": ["common-aw-routing", "universal", "security", "backend"],
353
- "mcps_used": ["context7", "exa-search"],
354
- "artifacts_written": [".aw_docs/features/<slug>/spec.md"]
355
- }
356
- \`\`\`
357
-
358
- ## Field semantics
359
-
360
- | Field | Type | Required | Notes |
361
- |---|---|---|---|
362
- | \`route\` | string | yes | Selected AW route (e.g. \`/aw-plan\`, \`/aw-build\`) |
363
- | \`skills_used\` | string[] | yes | Skill names actually Read during the response. \`[]\` if none |
364
- | \`rules_applied\` | string[] | yes | Rule names from \`~/.cursor/rules/*.mdc\` or \`~/.aw_rules/\` that informed the response |
365
- | \`mcps_used\` | string[] | yes | MCP server names invoked. \`[]\` if none |
366
- | \`artifacts_written\` | string[] | optional | Files created or modified |
367
-
368
- ## Rules
369
-
370
- - Always include the trace block — even for trivial responses (use \`[]\` for empty arrays).
371
- - Skill and rule names must match the on-disk basenames (no \`.mdc\` / \`.md\` extension).
372
- - The trace goes AFTER the substantive response, never before.
373
- - Do not include skills/rules that were merely "available" — only those that actually influenced this response.
374
-
375
- ## Why
376
-
377
- This makes routing observable. Tools, audits, and compliance checks can parse the trace JSON to verify that the right AW routing happened (route selected → stage skill loaded → relevant rules applied) without scraping prose.
378
- `;
379
- }
380
-
381
328
  function generateCursorAwRoutingRule() {
382
329
  // Frontmatter MUST be at byte 0 for Cursor's alwaysApply/globs detection.
383
330
  return `---
@@ -536,21 +483,120 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
536
483
  /**
537
484
  * Generate a rules section for CLAUDE.md from runtime AW rules.
538
485
  */
539
- 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 = {}) {
540
575
  const manifest = readManifest(rulesDir);
541
576
  if (!manifest) return '';
542
577
 
543
578
  const mustRules = manifest.rules.filter(r => r.severity === 'MUST');
544
- 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 '';
545
584
 
546
585
  const lines = [
547
586
  '## Platform Rules (MUST)',
548
587
  '',
549
588
  '> Rendered from platform `.aw/.aw_rules/`. Full details in reference files.',
550
- '',
551
589
  ];
552
590
 
553
- 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) {
554
600
  lines.push(`- [ ] **${rule.id}** — ${rule.description}`);
555
601
  }
556
602
 
@@ -581,14 +627,28 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
581
627
  // Reference table — tells all IDEs (especially Codex) where to read domain rules.
582
628
  // Codex can't auto-trigger by glob, but it CAN read these files when working in
583
629
  // the matching area. Keep AGENTS.md lean; full content stays in the source files.
584
- const scopes = listRuleScopes(rulesDir, {
630
+ const allScopes = listRuleScopes(rulesDir, {
585
631
  includeNestedScopes: stackOverlaysEnabled(options),
586
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));
587
640
  const domains = scopes.filter(({ scope }) => !scope.includes('/'));
588
641
  const overlays = scopes.filter(({ scope }) => scope.includes('/'));
589
642
  if (domains.length > 0) {
590
643
  lines.push('### Domain Rules');
591
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
+ }
592
652
  lines.push('When working in a specific domain, read the matching rules file:');
593
653
  lines.push('');
594
654
  lines.push('| Domain | Read when editing | Rules file |');
@@ -631,10 +691,14 @@ export function renderRules(cwd, options = {}) {
631
691
  const rulesDir = resolveRulesSourceDir(cwd, options);
632
692
  if (!rulesDir) return { cursorCount: 0, claudeSection: '', agentsSection: '' };
633
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
+
634
698
  const cursorCount = renderCursorRules(cwd, rulesDir, options);
635
699
  const claudeCount = renderClaudeRules(cwd, rulesDir, options);
636
- const claudeSection = generateClaudeMdRulesSection(rulesDir);
637
- const agentsSection = generateAgentsMdRulesSection(rulesDir, { ...options, outputDir: cwd });
700
+ const claudeSection = generateClaudeMdRulesSection(rulesDir, { applicableScopes });
701
+ const agentsSection = generateAgentsMdRulesSection(rulesDir, { ...options, outputDir: cwd, applicableScopes });
638
702
 
639
703
  // Also render to global dirs so rules apply everywhere
640
704
  const HOME = options.homeDir || homedir();