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

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,28 +195,77 @@ 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
+ * Read consumer config to decide whether to write managed sections into
199
+ * the repo's AGENTS.md / CLAUDE.md.
200
+ *
201
+ * Default behaviour: do NOT touch repo-local AGENTS.md/CLAUDE.md. The same
202
+ * AW Router Bridge + Platform Rules content is already injected into the
203
+ * GLOBAL files (~/.claude/CLAUDE.md and ~/.codex/AGENTS.md) by aw init,
204
+ * and Claude/Codex always read those. Modifying the repo file is invasive
205
+ * and bloats it with content that's already loaded globally.
206
+ *
207
+ * Repos that DO want managed sections in their repo file (e.g. for editors
208
+ * that don't read the global files, or to share AW context with collaborators
209
+ * who haven't run aw init) can opt in via .aw/config.json:
210
+ *
211
+ * { "writeRepoInstructionFiles": true }
212
+ *
213
+ * Existing repo files with managed sections are STRIPPED on next aw init
214
+ * (so users see the managed content go away — clean migration). Repos with
215
+ * the opt-in flag get them updated as before.
216
+ */
217
+ function shouldWriteRepoInstructionFiles(cwd) {
218
+ const configPath = join(cwd, '.aw', 'config.json');
219
+ if (!existsSync(configPath)) return false;
220
+ try {
221
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
222
+ return config.writeRepoInstructionFiles === true;
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Refresh rules sections in any existing AGENTS.md/CLAUDE.md at the repo
230
+ * root.
231
+ *
232
+ * By default, only STRIPS managed sections (we no longer want them in repo
233
+ * files — they're in global ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md).
234
+ *
235
+ * If `writeRepoInstructionFiles: true` is set in `.aw/config.json`, behaves
236
+ * as before: re-renders the AW Router Bridge + Platform Rules in the repo
237
+ * file too.
194
238
  */
195
239
  export function copyInstructions(cwd, tempDir, namespace) {
196
240
  const rulesSections = renderRules(cwd);
241
+ const writeManaged = shouldWriteRepoInstructionFiles(cwd);
197
242
  const createdFiles = [];
243
+
198
244
  for (const file of ['AGENTS.md', 'CLAUDE.md']) {
199
245
  const dest = join(cwd, file);
200
246
  if (existsSync(dest)) {
201
247
  const existing = readFileSync(dest, 'utf8');
202
- const updated = applyManagedInstructionSections(existing, file, rulesSections, { includeBridge: false });
248
+ const updated = writeManaged
249
+ ? applyManagedInstructionSections(existing, file, rulesSections, { includeBridge: false })
250
+ : applyManagedInstructionSections(existing, file, {}, { includeBridge: false });
203
251
 
204
252
  if (updated !== existing) {
205
253
  writeFileSync(dest, updated);
206
- fmt.logSuccess(`Updated ${file}`);
254
+ if (writeManaged) {
255
+ fmt.logSuccess(`Updated ${file}`);
256
+ } else {
257
+ fmt.logStep(`Stripped aw-managed sections from ${file} (now in ~/.claude/CLAUDE.md / ~/.codex/AGENTS.md)`);
258
+ }
207
259
  }
208
260
  continue;
209
261
  }
210
262
 
211
263
  if (file === 'CLAUDE.md') continue;
212
264
 
265
+ // Only CREATE a repo AGENTS.md if writeRepoInstructionFiles is opted in.
266
+ // Without the opt-in, repo files are user-owned and aw init never creates them.
267
+ if (!writeManaged) continue;
268
+
213
269
  if (tempDir) {
214
270
  const src = join(tempDir, '.aw_registry', file);
215
271
  if (existsSync(src)) {
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.16",
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();