@ghl-ai/aw 0.1.39-beta.8 → 0.1.39

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/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.39-beta.8",
3
+ "version": "0.1.39",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
- "bin": "bin.js",
6
+ "bin": {
7
+ "aw": "bin.js"
8
+ },
7
9
  "files": [
8
10
  "bin.js",
9
11
  "cli.mjs",
10
12
  "commands/",
11
13
  "config.mjs",
14
+ "codex.mjs",
12
15
  "constants.mjs",
13
16
  "fmt.mjs",
14
17
  "git.mjs",
@@ -23,8 +26,11 @@
23
26
  "slack-sim/",
24
27
  "file-tree.mjs",
25
28
  "apply.mjs",
29
+ "hook-manifest.mjs",
26
30
  "update.mjs",
27
31
  "hooks.mjs",
32
+ "hooks/",
33
+ "startup.mjs",
28
34
  "ecc.mjs",
29
35
  "render-rules.mjs",
30
36
  "telemetry.mjs"
package/paths.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import { existsSync, statSync, lstatSync, readlinkSync } from 'node:fs';
4
4
  import { join, resolve, relative, basename, dirname } from 'node:path';
5
5
 
6
- const VALID_TYPES = new Set(['agents', 'skills', 'commands', 'evals']);
6
+ const VALID_TYPES = new Set(['agents', 'skills', 'commands', 'evals', 'references']);
7
7
 
8
8
  // IDE dirs that may contain symlinks into .aw_registry/
9
9
  const IDE_PREFIXES = ['.claude/', '.cursor/', '.codex/', '.agents/'];
package/registry.mjs CHANGED
@@ -4,7 +4,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
4
4
  import { join, relative } from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
 
7
- const TYPE_DIRS = new Set(['agents', 'skills', 'commands', 'evals']);
7
+ const TYPE_DIRS = new Set(['agents', 'skills', 'commands', 'evals', 'references']);
8
8
  const SKIP_DIRS = new Set(['docs']);
9
9
 
10
10
  export function sha256(content) {
package/render-rules.mjs CHANGED
@@ -1,12 +1,17 @@
1
- // render-rules.mjs — Render .aw_rules into IDE-specific output files.
1
+ // render-rules.mjs — Render runtime AW rules into IDE-specific output files.
2
2
 
3
3
  import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
4
- import { join } from 'node:path';
4
+ import { dirname, join, relative } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import * as fmt from './fmt.mjs';
7
- import { RULES_SOURCE_DIR } from './constants.mjs';
8
-
9
- const GENERATED_HEADER = '<!-- Generated by aw do not edit manually -->\n\n';
7
+ import { RULES_RUNTIME_DIR } from './constants.mjs';
8
+
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 */
@@ -160,6 +165,33 @@ function scopeToLabel(scope) {
160
165
  return scope.replaceAll('/', ' ');
161
166
  }
162
167
 
168
+ function toPosixPath(filePath) {
169
+ return filePath.replaceAll('\\', '/');
170
+ }
171
+
172
+ function relativeRuleLink(outputDir, targetPath) {
173
+ return toPosixPath(relative(outputDir, targetPath));
174
+ }
175
+
176
+ export function resolveRulesSourceDir(cwd, options = {}) {
177
+ const HOME = options.homeDir || homedir();
178
+ const candidates = [
179
+ join(cwd, RULES_RUNTIME_DIR),
180
+ ];
181
+
182
+ if (cwd !== HOME) {
183
+ candidates.push(
184
+ join(HOME, RULES_RUNTIME_DIR),
185
+ );
186
+ }
187
+
188
+ return candidates.find(existsSync) || null;
189
+ }
190
+
191
+ function rulesRootDir(rulesDir) {
192
+ return dirname(dirname(rulesDir));
193
+ }
194
+
163
195
  function pruneStaleGeneratedRules(outputDir, expectedFilenames) {
164
196
  if (!existsSync(outputDir)) return;
165
197
 
@@ -169,7 +201,8 @@ function pruneStaleGeneratedRules(outputDir, expectedFilenames) {
169
201
 
170
202
  const fullPath = join(outputDir, entry.name);
171
203
  const content = readOrNull(fullPath);
172
- 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;
173
206
 
174
207
  try {
175
208
  unlinkSync(fullPath);
@@ -267,19 +300,101 @@ function renderCursorRules(cwd, rulesDir, options = {}) {
267
300
  const refs = readdirSync(referencesDir).filter(f => f.endsWith('.md')).sort();
268
301
  if (refs.length > 0) {
269
302
  refSection = '\n## References\n\n' +
270
- refs.map(f => `- [${f.replace('.md', '')}](.aw_registry/.aw_rules/platform/${scope}/references/${f})`).join('\n') +
303
+ refs.map((f) => {
304
+ const targetPath = join(rulesDir, 'platform', scope, 'references', f);
305
+ const refPath = relativeRuleLink(cursorRulesDir, targetPath);
306
+ return `- [${f.replace('.md', '')}](${refPath})`;
307
+ }).join('\n') +
271
308
  '\n';
272
309
  }
273
310
  }
274
311
 
275
- 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;
276
315
  writeFileSync(join(cursorRulesDir, `${scopeToFilename(scope)}.mdc`), content);
277
316
  count++;
278
317
  }
279
318
 
319
+ // Generate the AW routing rule — this is the hard gate that forces
320
+ // route selection → stage skill read → follow behavior → respond.
321
+ const routingRule = generateCursorAwRoutingRule();
322
+ writeFileSync(join(cursorRulesDir, 'common-aw-routing.mdc'), routingRule);
323
+ count++;
324
+
280
325
  return count;
281
326
  }
282
327
 
328
+ function generateCursorAwRoutingRule() {
329
+ // Frontmatter MUST be at byte 0 for Cursor's alwaysApply/globs detection.
330
+ return `---
331
+ description: "AW global routing: select route, READ stage skill, then respond"
332
+ alwaysApply: true
333
+ ---
334
+ ${GENERATED_MARKER}
335
+
336
+ # AW Global Routing
337
+
338
+ ## Hard Gate (MUST — do not skip)
339
+
340
+ For every non-trivial request, execute these steps in order before any substantive response:
341
+
342
+ 1. **Load the router** — Read the using-aw-skills SKILL.md from the skills directory.
343
+
344
+ 2. **Select route** — using the decision tree below, pick the smallest correct AW route.
345
+
346
+ 3. **Read the stage skill** — you MUST Read the matching skill file before responding:
347
+ - /aw-plan → Read aw-plan/SKILL.md
348
+ - /aw-build → Read aw-build/SKILL.md
349
+ - /aw-investigate → Read aw-investigate/SKILL.md
350
+ - /aw-test → Read aw-test/SKILL.md
351
+ - /aw-review → Read aw-review/SKILL.md
352
+ - /aw-deploy → Read aw-deploy/SKILL.md
353
+ - /aw-ship → Read aw-ship/SKILL.md
354
+
355
+ 4. **Follow the skill's behavior** — produce the artifacts the skill defines, not general-knowledge answers.
356
+
357
+ 5. **Then respond** — only after steps 1-4.
358
+
359
+ Stating a route without Reading the skill file is NOT compliance.
360
+
361
+ ## Route Decision Tree
362
+
363
+ \`\`\`text
364
+ Does an approved plan/spec already exist for this exact work?
365
+ ├── NO → Is the request about a bug, alert, or unclear failure?
366
+ │ ├── YES → /aw-investigate
367
+ │ └── NO → /aw-plan ← DEFAULT for anything new
368
+ └── YES → Is the work implemented and needs testing/review?
369
+ ├── YES → /aw-test or /aw-review
370
+ └── NO → Is this a deploy or release action?
371
+ ├── YES → /aw-deploy or /aw-ship
372
+ └── NO → /aw-build
373
+ \`\`\`
374
+
375
+ **Plan-first rule**: New endpoints, services, schemas, features, architecture changes, and integrations ALL go through /aw-plan first. /aw-build requires an approved plan or a mechanical change.
376
+
377
+ ## Default Assumptions
378
+
379
+ - /aw-plan → write file artifacts under .aw_docs/features/<slug>/ unless user says "chat only".
380
+ - /aw-build → code changes with tests.
381
+ - /aw-investigate → reproduction + root cause evidence.
382
+
383
+ ## Org Rules
384
+
385
+ After the stage skill is loaded, also read the relevant org rules:
386
+
387
+ - Always read: universal/AGENTS.md and security/AGENTS.md
388
+ - Then pick the smallest correct domain rule (api-design, backend, data, frontend, infra, mobile, sdet).
389
+ - If repo-local instructions conflict with org-level AW rules, follow org-level.
390
+
391
+ ## Compatibility
392
+
393
+ - /aw-execute → resolve to /aw-build
394
+ - /aw-verify → resolve to /aw-test or /aw-review
395
+ `;
396
+ }
397
+
283
398
  /** Rule scope → Claude Code paths: frontmatter (only supported field for .claude/rules/) */
284
399
  const SCOPE_PATHS = {
285
400
  'api-design': ['src/**/*.controller.ts', 'src/**/dto/**/*.ts', 'src/**/*.client.ts'],
@@ -347,12 +462,17 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
347
462
  const refs = readdirSync(referencesDir).filter(f => f.endsWith('.md')).sort();
348
463
  if (refs.length > 0) {
349
464
  refSection = '\n## References\n\n' +
350
- refs.map(f => `- [${f.replace('.md', '')}](.aw_registry/.aw_rules/platform/${scope}/references/${f})`).join('\n') +
465
+ refs.map((f) => {
466
+ const targetPath = join(rulesDir, 'platform', scope, 'references', f);
467
+ const refPath = relativeRuleLink(claudeRulesDir, targetPath);
468
+ return `- [${f.replace('.md', '')}](${refPath})`;
469
+ }).join('\n') +
351
470
  '\n';
352
471
  }
353
472
  }
354
473
 
355
- 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;
356
476
  writeFileSync(join(claudeRulesDir, `${scopeToFilename(scope)}.md`), content);
357
477
  count++;
358
478
  }
@@ -361,44 +481,144 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
361
481
  }
362
482
 
363
483
  /**
364
- * Generate a rules section for CLAUDE.md from .aw_rules.
484
+ * Generate a rules section for CLAUDE.md from runtime AW rules.
365
485
  */
366
- 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 = {}) {
367
575
  const manifest = readManifest(rulesDir);
368
576
  if (!manifest) return '';
369
577
 
370
578
  const mustRules = manifest.rules.filter(r => r.severity === 'MUST');
371
- 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 '';
372
584
 
373
585
  const lines = [
374
586
  '## Platform Rules (MUST)',
375
587
  '',
376
- '> Auto-synced from `.aw_registry/.aw_rules/`. Full details in reference files.',
377
- '',
588
+ '> Rendered from platform `.aw/.aw_rules/`. Full details in reference files.',
378
589
  ];
379
590
 
380
- 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) {
381
600
  lines.push(`- [ ] **${rule.id}** — ${rule.description}`);
382
601
  }
383
602
 
384
603
  lines.push('');
385
- lines.push('See `.aw_registry/.aw_rules/rule-manifest.json` for all rules including SHOULD/MAY.');
604
+ lines.push('See `.aw/.aw_rules/rule-manifest.json` for all rules including SHOULD/MAY.');
386
605
  lines.push('');
387
606
 
388
607
  return lines.join('\n');
389
608
  }
390
609
 
391
610
  /**
392
- * Generate a rules section for AGENTS.md from the top-level AGENTS.md in .aw_rules.
611
+ * Generate a rules section for AGENTS.md from the top-level AGENTS.md in runtime AW rules.
393
612
  */
394
613
  export function generateAgentsMdRulesSection(rulesDir, options = {}) {
395
614
  const topLevelAgents = readOrNull(join(rulesDir, 'AGENTS.md'));
396
615
  if (!topLevelAgents) return '';
616
+ const outputDir = options.outputDir || rulesRootDir(rulesDir);
397
617
 
398
618
  const lines = [
399
619
  '## Platform Rules — Non-Negotiables',
400
620
  '',
401
- '> Auto-synced from `.aw_registry/.aw_rules/`.',
621
+ '> Rendered from platform `.aw/.aw_rules/`.',
402
622
  '',
403
623
  topLevelAgents.trim(),
404
624
  '',
@@ -407,14 +627,28 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
407
627
  // Reference table — tells all IDEs (especially Codex) where to read domain rules.
408
628
  // Codex can't auto-trigger by glob, but it CAN read these files when working in
409
629
  // the matching area. Keep AGENTS.md lean; full content stays in the source files.
410
- const scopes = listRuleScopes(rulesDir, {
630
+ const allScopes = listRuleScopes(rulesDir, {
411
631
  includeNestedScopes: stackOverlaysEnabled(options),
412
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));
413
640
  const domains = scopes.filter(({ scope }) => !scope.includes('/'));
414
641
  const overlays = scopes.filter(({ scope }) => scope.includes('/'));
415
642
  if (domains.length > 0) {
416
643
  lines.push('### Domain Rules');
417
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
+ }
418
652
  lines.push('When working in a specific domain, read the matching rules file:');
419
653
  lines.push('');
420
654
  lines.push('| Domain | Read when editing | Rules file |');
@@ -422,7 +656,8 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
422
656
 
423
657
  for (const { scope } of domains) {
424
658
  const hint = SCOPE_HINTS[scope] || 'Related files';
425
- lines.push(`| ${scope} | ${hint} | \`.aw_registry/.aw_rules/platform/${scope}/AGENTS.md\` |`);
659
+ const rulesFile = relativeRuleLink(outputDir, join(rulesDir, 'platform', scope, 'AGENTS.md'));
660
+ lines.push(`| ${scope} | ${hint} | \`${rulesFile}\` |`);
426
661
  }
427
662
  lines.push('');
428
663
  }
@@ -437,7 +672,8 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
437
672
 
438
673
  for (const { scope } of overlays) {
439
674
  const hint = SCOPE_HINTS[scope] || 'Stack-specific files';
440
- lines.push(`| ${scope} | ${hint} | \`.aw_registry/.aw_rules/platform/${scope}/AGENTS.md\` |`);
675
+ const rulesFile = relativeRuleLink(outputDir, join(rulesDir, 'platform', scope, 'AGENTS.md'));
676
+ lines.push(`| ${scope} | ${hint} | \`${rulesFile}\` |`);
441
677
  }
442
678
  lines.push('');
443
679
  }
@@ -447,18 +683,22 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
447
683
 
448
684
  /**
449
685
  * Main render function. Call after aw pull / aw sync.
450
- * Reads .aw_registry/.aw_rules/ and renders:
686
+ * Reads runtime .aw/.aw_rules/ and renders:
451
687
  * 1. .cursor/rules/<scope>.mdc — at cwd AND at $HOME (global)
452
688
  * 2. Returns sections for CLAUDE.md and AGENTS.md injection
453
689
  */
454
690
  export function renderRules(cwd, options = {}) {
455
- const rulesDir = join(cwd, '.aw_registry', RULES_SOURCE_DIR);
456
- if (!existsSync(rulesDir)) return { cursorCount: 0, claudeSection: '', agentsSection: '' };
691
+ const rulesDir = resolveRulesSourceDir(cwd, options);
692
+ if (!rulesDir) return { cursorCount: 0, claudeSection: '', agentsSection: '' };
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);
457
697
 
458
698
  const cursorCount = renderCursorRules(cwd, rulesDir, options);
459
699
  const claudeCount = renderClaudeRules(cwd, rulesDir, options);
460
- const claudeSection = generateClaudeMdRulesSection(rulesDir);
461
- const agentsSection = generateAgentsMdRulesSection(rulesDir, options);
700
+ const claudeSection = generateClaudeMdRulesSection(rulesDir, { applicableScopes });
701
+ const agentsSection = generateAgentsMdRulesSection(rulesDir, { ...options, outputDir: cwd, applicableScopes });
462
702
 
463
703
  // Also render to global dirs so rules apply everywhere
464
704
  const HOME = options.homeDir || homedir();