@ghl-ai/aw 0.1.38-beta.9 → 0.1.38

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/cli.mjs CHANGED
@@ -134,7 +134,7 @@ function printHelp() {
134
134
  cmd('aw push .aw_registry/agents/<name>.md', 'Push a single agent'),
135
135
  cmd('aw push .aw_registry/skills/<name>/', 'Push a single skill folder'),
136
136
  cmd('aw push .aw_rules', 'Auto-redirects to aw push-rules'),
137
- cmd('aw push-rules', 'Pushes .aw_rules or .aw_registry/.aw_rules'),
137
+ cmd('aw push-rules', 'Pushes .aw_rules'),
138
138
  '',
139
139
  ` ${chalk.dim('# Remove content from workspace')}`,
140
140
  cmd('aw drop <team>', 'Stop syncing a namespace (removes all files)'),
@@ -162,10 +162,11 @@ function resolveAwRegistryDir(homeDir) {
162
162
  }
163
163
 
164
164
  function resolveRulesDir(homeDir) {
165
- const awRegistryDir = resolveAwRegistryDir(homeDir);
166
- if (!awRegistryDir) return null;
167
- const rulesDir = join(awRegistryDir, '.aw_rules');
168
- return existsSync(rulesDir) ? rulesDir : null;
165
+ const candidates = [
166
+ join(homeDir, '.aw', '.aw_rules'),
167
+ join(homeDir, '.aw', '.aw_registry', '.aw_rules'),
168
+ ];
169
+ return candidates.find(existsSync) || null;
169
170
  }
170
171
 
171
172
  function findClaudePlugin(homeDir) {
@@ -471,7 +472,7 @@ function getProjectClaudeSessionStartStatus(cwd) {
471
472
  }
472
473
 
473
474
  function textHasRulesReference(text) {
474
- return text.includes('Platform Rules') && text.includes('.aw_registry/.aw_rules/platform/');
475
+ return text.includes('Platform Rules') && text.includes('.aw/.aw_rules/platform/');
475
476
  }
476
477
 
477
478
  function textHasManagedRouterBridge(text) {
@@ -484,9 +485,9 @@ function textHasManagedRouterBridge(text) {
484
485
  && text.includes('incremental-implementation');
485
486
  }
486
487
 
487
- function extractRegistryReferencePaths(text) {
488
+ function extractRuleReferencePaths(text) {
488
489
  const matches = [];
489
- const pattern = /(?:\]\(|`)([^)\n`]*\.aw_registry\/[^)\n`]+\.md)(?:\)|`)/g;
490
+ const pattern = /(?:\]\(|`)([^)\n`]*(?:\.aw_rules|\.aw_registry\/\.aw_rules)\/[^)\n`]+\.md)(?:\)|`)/g;
490
491
  for (const match of text.matchAll(pattern)) {
491
492
  if (match[1]) matches.push(match[1]);
492
493
  }
@@ -505,11 +506,11 @@ function listGeneratedRuleFiles(dirPath, extension) {
505
506
  }
506
507
  }
507
508
 
508
- function findBrokenRegistryReferences(filePaths) {
509
+ function findBrokenRuleReferences(filePaths) {
509
510
  const broken = [];
510
511
  for (const filePath of filePaths) {
511
512
  const content = readText(filePath);
512
- const refs = extractRegistryReferencePaths(content);
513
+ const refs = extractRuleReferencePaths(content);
513
514
  for (const ref of refs) {
514
515
  if (ref.includes('<domain>')) continue;
515
516
  const resolvedPath = join(dirname(filePath), ref);
@@ -565,8 +566,8 @@ function buildDoctorChecks(homeDir, cwd) {
565
566
  'aw-rules-source',
566
567
  'AW rules source',
567
568
  'fail',
568
- 'No synced .aw_rules tree found under ~/.aw_registry',
569
- 'Run `aw init` or `aw pull platform` to sync the AW rules source into ~/.aw_registry.',
569
+ 'No synced .aw/.aw_rules tree found under ~/.aw/.aw_rules',
570
+ 'Run `aw init` or `aw pull platform` to sync the AW rules source into ~/.aw/.aw_rules.',
570
571
  ));
571
572
  } else {
572
573
  const topLevelRulesAgents = join(rulesDir, 'AGENTS.md');
@@ -578,8 +579,8 @@ function buildDoctorChecks(homeDir, cwd) {
578
579
  'aw-rules-source',
579
580
  'AW rules source',
580
581
  'fail',
581
- 'Rules source is incomplete under ~/.aw_registry/.aw_rules',
582
- 'Run `aw pull platform` to refresh .aw_rules, then rerun `aw doctor`.',
582
+ 'Rules source is incomplete under ~/.aw/.aw_rules',
583
+ 'Run `aw pull platform` to refresh .aw/.aw_rules, then rerun `aw doctor`.',
583
584
  ),
584
585
  );
585
586
  }
@@ -778,30 +779,30 @@ function buildDoctorChecks(homeDir, cwd) {
778
779
  'claude-rules',
779
780
  'Claude AW rules',
780
781
  rulesDir ? 'fail' : 'warn',
781
- rulesDir ? 'Claude platform rules are missing or not generated by AW' : 'Claude platform rules were not checked because .aw_rules is not synced',
782
+ rulesDir ? 'Claude platform rules are missing or not generated by AW' : 'Claude platform rules were not checked because .aw/.aw_rules is not synced',
782
783
  'Run `aw pull platform` or `aw init` to render Claude platform rules.',
783
784
  ),
784
785
  );
785
786
 
786
787
  const claudeRuleFiles = listGeneratedRuleFiles(claudeRulesDir, '.md');
787
- const brokenClaudeRuleRefs = findBrokenRegistryReferences(claudeRuleFiles);
788
+ const brokenClaudeRuleRefs = findBrokenRuleReferences(claudeRuleFiles);
788
789
  checks.push(
789
790
  claudeRuleFiles.length === 0
790
791
  ? makeCheck(
791
792
  'claude-rule-references',
792
793
  'Claude rule references',
793
794
  rulesDir ? 'fail' : 'warn',
794
- rulesDir ? 'Claude generated rule files are missing, so reference links could not be validated' : 'Claude rule references were not checked because .aw_rules is not synced',
795
+ rulesDir ? 'Claude generated rule files are missing, so reference links could not be validated' : 'Claude rule references were not checked because .aw/.aw_rules is not synced',
795
796
  'Run `aw pull platform` or `aw init` to render Claude rules, then rerun `aw doctor`.',
796
797
  )
797
798
  : brokenClaudeRuleRefs.length === 0
798
- ? makeCheck('claude-rule-references', 'Claude rule references', 'pass', 'Claude generated rule references resolve to real files in ~/.aw_registry')
799
+ ? makeCheck('claude-rule-references', 'Claude rule references', 'pass', 'Claude generated rule references resolve to real files in ~/.aw/.aw_rules')
799
800
  : makeCheck(
800
801
  'claude-rule-references',
801
802
  'Claude rule references',
802
803
  'fail',
803
804
  `Claude rule reference is broken in ${brokenClaudeRuleRefs[0].filePath.replace(`${homeDir}/`, '~/')}: ${brokenClaudeRuleRefs[0].ref}`,
804
- 'Refresh the rendered Claude rules so embedded reference links point at real files under ~/.aw_registry/.aw_rules.',
805
+ 'Refresh the rendered Claude rules so embedded reference links point at real files under ~/.aw/.aw_rules.',
805
806
  ),
806
807
  );
807
808
 
@@ -894,7 +895,7 @@ function buildDoctorChecks(homeDir, cwd) {
894
895
  );
895
896
 
896
897
  const codexAgentsText = readText(codexAgentsPath);
897
- const codexBrokenRuleRefs = existsSync(codexAgentsPath) ? findBrokenRegistryReferences([codexAgentsPath]) : [];
898
+ const codexBrokenRuleRefs = existsSync(codexAgentsPath) ? findBrokenRuleReferences([codexAgentsPath]) : [];
898
899
  checks.push(
899
900
  existsSync(codexAgentsPath) && textHasRulesReference(codexAgentsText)
900
901
  ? codexBrokenRuleRefs.length === 0
@@ -997,31 +998,31 @@ function buildDoctorChecks(homeDir, cwd) {
997
998
  'cursor-rules',
998
999
  'Cursor AW rules',
999
1000
  rulesDir ? 'fail' : 'warn',
1000
- rulesDir ? 'Cursor rules are missing or not generated by AW' : 'Cursor rules were not checked because .aw_rules is not synced',
1001
+ rulesDir ? 'Cursor rules are missing or not generated by AW' : 'Cursor rules were not checked because .aw/.aw_rules is not synced',
1001
1002
  'Run `aw pull platform` or `aw init` to render Cursor rule files.',
1002
1003
  ),
1003
1004
  );
1004
1005
 
1005
1006
  const cursorRulesDir = join(homeDir, '.cursor', 'rules');
1006
1007
  const cursorRuleFiles = listGeneratedRuleFiles(cursorRulesDir, '.mdc');
1007
- const brokenCursorRuleRefs = findBrokenRegistryReferences(cursorRuleFiles);
1008
+ const brokenCursorRuleRefs = findBrokenRuleReferences(cursorRuleFiles);
1008
1009
  checks.push(
1009
1010
  cursorRuleFiles.length === 0
1010
1011
  ? makeCheck(
1011
1012
  'cursor-rule-references',
1012
1013
  'Cursor rule references',
1013
1014
  rulesDir ? 'fail' : 'warn',
1014
- rulesDir ? 'Cursor generated rule files are missing, so reference links could not be validated' : 'Cursor rule references were not checked because .aw_rules is not synced',
1015
+ rulesDir ? 'Cursor generated rule files are missing, so reference links could not be validated' : 'Cursor rule references were not checked because .aw/.aw_rules is not synced',
1015
1016
  'Run `aw pull platform` or `aw init` to render Cursor rules, then rerun `aw doctor`.',
1016
1017
  )
1017
1018
  : brokenCursorRuleRefs.length === 0
1018
- ? makeCheck('cursor-rule-references', 'Cursor rule references', 'pass', 'Cursor generated rule references resolve to real files in ~/.aw_registry')
1019
+ ? makeCheck('cursor-rule-references', 'Cursor rule references', 'pass', 'Cursor generated rule references resolve to real files in ~/.aw/.aw_rules')
1019
1020
  : makeCheck(
1020
1021
  'cursor-rule-references',
1021
1022
  'Cursor rule references',
1022
1023
  'fail',
1023
1024
  `Cursor rule reference is broken in ${brokenCursorRuleRefs[0].filePath.replace(`${homeDir}/`, '~/')}: ${brokenCursorRuleRefs[0].ref}`,
1024
- 'Refresh the rendered Cursor rules so embedded reference links point at real files under ~/.aw_registry/.aw_rules.',
1025
+ 'Refresh the rendered Cursor rules so embedded reference links point at real files under ~/.aw/.aw_rules.',
1025
1026
  ),
1026
1027
  );
1027
1028
 
package/commands/init.mjs CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  syncWorktreeSparseCheckout,
45
45
  findNearestWorktree,
46
46
  } from '../git.mjs';
47
- import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, RULES_SOURCE_DIR } from '../constants.mjs';
47
+ import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, RULES_SOURCE_DIR, RULES_RUNTIME_DIR } from '../constants.mjs';
48
48
  import { syncFileTree } from '../file-tree.mjs';
49
49
 
50
50
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -59,6 +59,22 @@ const HOME = (() => { try { return realpathSync(_rawHome); } catch { return _raw
59
59
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
60
60
  const AW_HOME = join(HOME, '.aw');
61
61
 
62
+ function syncRulesTargets(targetDir) {
63
+ const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
64
+ if (!existsSync(rulesSrc)) return false;
65
+ syncFileTree(rulesSrc, join(targetDir, RULES_RUNTIME_DIR));
66
+ return true;
67
+ }
68
+
69
+ function removeLegacyRegistryRules() {
70
+ try {
71
+ rmSync(join(GLOBAL_AW_DIR, RULES_SOURCE_DIR), { recursive: true, force: true });
72
+ rmSync(join(HOME, RULES_SOURCE_DIR), { recursive: true, force: true });
73
+ } catch {
74
+ // best effort cleanup
75
+ }
76
+ }
77
+
62
78
  function syncInstructionsAndAwDocs(targetDir, namespace) {
63
79
  copyInstructions(targetDir, null, namespace);
64
80
  initAwDocs(targetDir);
@@ -262,9 +278,11 @@ export async function initCommand(args) {
262
278
 
263
279
  ensureAwGitignore(AW_HOME);
264
280
  const freshCfg = config.load(GLOBAL_AW_DIR);
265
- if (existsSync(GLOBAL_AW_DIR)) {
266
- syncFileTree(join(AW_HOME, RULES_SOURCE_DIR), join(GLOBAL_AW_DIR, RULES_SOURCE_DIR));
281
+ syncRulesTargets(HOME);
282
+ if (cwd !== HOME) {
283
+ syncRulesTargets(cwd);
267
284
  }
285
+ removeLegacyRegistryRules();
268
286
 
269
287
  // Ensure project worktree sparse checkout matches the global clone.
270
288
  // Covers the case where a namespace was added from HOME (or another project)
@@ -404,9 +422,11 @@ export async function initCommand(args) {
404
422
  if (folderName) {
405
423
  config.addPattern(GLOBAL_AW_DIR, folderName);
406
424
  }
407
- if (existsSync(GLOBAL_AW_DIR)) {
408
- syncFileTree(join(AW_HOME, RULES_SOURCE_DIR), join(GLOBAL_AW_DIR, RULES_SOURCE_DIR));
425
+ syncRulesTargets(HOME);
426
+ if (cwd !== HOME) {
427
+ syncRulesTargets(cwd);
409
428
  }
429
+ removeLegacyRegistryRules();
410
430
 
411
431
  // Step 3: Setup tasks, MCP, hooks
412
432
  await installAwEcc(cwd, { silent });
package/commands/pull.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  existsSync,
5
5
  lstatSync,
6
+ rmSync,
6
7
  } from 'node:fs';
7
8
  import { dirname, join, extname } from 'node:path';
8
9
  import { homedir } from 'node:os';
@@ -27,6 +28,7 @@ import {
27
28
  REGISTRY_URL,
28
29
  DOCS_SOURCE_DIR,
29
30
  RULES_SOURCE_DIR,
31
+ RULES_RUNTIME_DIR,
30
32
  } from '../constants.mjs';
31
33
  import { collectAllPaths, syncFileTree } from '../file-tree.mjs';
32
34
  import { linkWorkspace } from '../link.mjs';
@@ -38,6 +40,22 @@ const HOME = homedir();
38
40
  const AW_HOME = join(HOME, '.aw');
39
41
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
40
42
 
43
+ function syncRulesTargets(targetDir) {
44
+ const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
45
+ if (!existsSync(rulesSrc)) return false;
46
+ syncFileTree(rulesSrc, join(targetDir, RULES_RUNTIME_DIR));
47
+ return true;
48
+ }
49
+
50
+ function removeLegacyRegistryRules() {
51
+ try {
52
+ rmSync(join(GLOBAL_AW_DIR, RULES_SOURCE_DIR), { recursive: true, force: true });
53
+ rmSync(join(HOME, RULES_SOURCE_DIR), { recursive: true, force: true });
54
+ } catch {
55
+ // best effort cleanup
56
+ }
57
+ }
58
+
41
59
  export async function pullCommand(args) {
42
60
  const input = args._positional?.[0] || '';
43
61
  const cwd = process.cwd();
@@ -179,13 +197,6 @@ export async function pullCommand(args) {
179
197
  log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
180
198
  }
181
199
 
182
- const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
183
- if (existsSync(rulesSrc)) {
184
- const rulesDest = join(GLOBAL_AW_DIR, RULES_SOURCE_DIR);
185
- syncFileTree(rulesSrc, rulesDest);
186
- if (!silent) log.logSuccess('Synced .aw_rules');
187
- }
188
-
189
200
  // Rebase project worktree branch onto origin/main — only for legacy git worktrees.
190
201
  // In the symlink model, <project>/.aw IS ~/.aw (same repo), so fetchAndMerge already
191
202
  // brought it up to date. Nothing to rebase.
@@ -244,6 +255,16 @@ export async function pullCommand(args) {
244
255
  }
245
256
  }
246
257
 
258
+ let rulesSynced = syncRulesTargets(HOME);
259
+ const workspaceRoot = localAw ? dirname(localAw) : (cwd !== HOME ? cwd : null);
260
+ if (workspaceRoot && workspaceRoot !== HOME) {
261
+ rulesSynced = syncRulesTargets(workspaceRoot) || rulesSynced;
262
+ }
263
+ removeLegacyRegistryRules();
264
+ if (rulesSynced && !silent) {
265
+ log.logSuccess('Synced .aw_rules');
266
+ }
267
+
247
268
  if (!args._skipIntegrate) {
248
269
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
249
270
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
@@ -23,11 +23,9 @@ export function isRulesPushInput(input) {
23
23
 
24
24
  export function resolveRulesPushSource(input, cwd = process.cwd()) {
25
25
  const localRulesRoot = join(cwd, RULES_SOURCE_DIR);
26
- const syncedRulesRoot = join(cwd, '.aw_registry', RULES_SOURCE_DIR);
27
26
 
28
27
  if (!input) {
29
28
  if (existsSync(localRulesRoot)) return { sourceRoot: localRulesRoot, sourceType: 'local' };
30
- if (existsSync(syncedRulesRoot)) return { sourceRoot: syncedRulesRoot, sourceType: 'synced' };
31
29
  return null;
32
30
  }
33
31
 
@@ -39,22 +37,19 @@ export function resolveRulesPushSource(input, cwd = process.cwd()) {
39
37
  }
40
38
 
41
39
  if (normalizedInput === `.aw_registry/${RULES_SOURCE_DIR}` || normalizedInput.startsWith(`.aw_registry/${RULES_SOURCE_DIR}/`)) {
42
- if (!existsSync(syncedRulesRoot)) return null;
43
-
44
40
  const relativeRulesPath = normalizedInput.slice(`.aw_registry/${RULES_SOURCE_DIR}`.length).replace(/^\/+/, '');
45
41
  const localOverridePath = relativeRulesPath ? join(localRulesRoot, relativeRulesPath) : localRulesRoot;
46
42
  if (existsSync(localOverridePath)) {
47
43
  return { sourceRoot: localRulesRoot, sourceType: 'local' };
48
44
  }
49
-
50
- return { sourceRoot: syncedRulesRoot, sourceType: 'synced' };
45
+ return null;
51
46
  }
52
47
 
53
48
  return null;
54
49
  }
55
50
 
56
51
  export function hasRulesChanges(cwd = process.cwd()) {
57
- const candidateDirs = [RULES_SOURCE_DIR, `.aw_registry/${RULES_SOURCE_DIR}`]
52
+ const candidateDirs = [RULES_SOURCE_DIR]
58
53
  .filter(rel => existsSync(join(cwd, rel)));
59
54
 
60
55
  if (candidateDirs.length === 0) return false;
@@ -141,9 +136,7 @@ function pushRulesTree(sourceRoot, { repo, dryRun, cwd }) {
141
136
  fmt.cancel('Nothing to push — remote rules already match local content.');
142
137
  }
143
138
 
144
- const sourceType = normalize(sourceRoot).includes(normalize(join('.aw_registry', RULES_SOURCE_DIR)))
145
- ? 'synced'
146
- : 'local';
139
+ const sourceType = 'local';
147
140
  const prTitle = buildRulesPrTitle(sourceRoot, cwd);
148
141
  const prBody = buildRulesPrBody(sourceRoot, sourceType, cwd);
149
142
 
@@ -191,16 +184,12 @@ export function pushRulesCommand(args) {
191
184
  fmt.cancel([
192
185
  'Could not find a rules source to push.',
193
186
  '',
194
- ` Checked ${chalk.cyan('.aw_rules/')} and ${chalk.cyan('.aw_registry/.aw_rules/')}.`,
187
+ ` Checked ${chalk.cyan('.aw_rules/')}.`,
195
188
  '',
196
189
  ' Use `aw pull platform` first or create a local `.aw_rules/` authoring tree.',
197
190
  ].join('\n'));
198
191
  }
199
192
 
200
- if (resolved.sourceType === 'synced') {
201
- fmt.logWarn('Pushing from synced `.aw_registry/.aw_rules/`. Local `.aw_rules/` is safer for authoring.');
202
- }
203
-
204
193
  pushRulesTree(resolved.sourceRoot, { repo, dryRun, cwd });
205
194
  }
206
195
 
@@ -1,8 +1,5 @@
1
- // commands/telemetry.mjs — `aw telemetry [enable|disable|status|flush-queue]`
1
+ // commands/telemetry.mjs — `aw telemetry [enable|disable|status]`
2
2
 
3
- import { existsSync, readFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { homedir } from 'node:os';
6
3
  import { enableTelemetry, disableTelemetry, getStatus } from '../telemetry.mjs';
7
4
  import * as fmt from '../fmt.mjs';
8
5
  import { chalk } from '../fmt.mjs';
@@ -22,66 +19,13 @@ export async function telemetryCommand(args) {
22
19
  return;
23
20
  }
24
21
 
25
- if (sub === 'flush-queue') {
26
- await flushQueueCommand();
27
- return;
28
- }
29
-
30
22
  // status (default)
31
23
  const status = getStatus();
32
24
  fmt.intro('aw telemetry');
33
25
  fmt.logStep(`Status: ${status.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
34
26
  fmt.logStep(`Machine ID: ${chalk.dim(status.machine_id)}`);
35
27
  fmt.logStep(`Config: ${chalk.dim(status.config_path)}`);
36
-
37
- // Show queue depth if available
38
- const queueFile = join(homedir(), '.aw', 'telemetry', 'queue.jsonl');
39
- if (existsSync(queueFile)) {
40
- try {
41
- const lines = readFileSync(queueFile, 'utf8').trim().split('\n').filter(Boolean);
42
- fmt.logStep(`Queue: ${chalk.yellow(lines.length)} pending prompt(s)`);
43
- } catch { /* best effort */ }
44
- }
45
-
46
28
  fmt.logMessage('');
47
- fmt.logMessage(` ${chalk.dim('aw telemetry disable')} — opt out of anonymous analytics`);
48
- fmt.logMessage(` ${chalk.dim('aw telemetry enable')} — re-enable analytics`);
49
- fmt.logMessage(` ${chalk.dim('aw telemetry flush-queue')} — manually flush pending queue`);
50
- }
51
-
52
- async function flushQueueCommand() {
53
- const eccBase = join(homedir(), '.aw-ecc');
54
- const libPath = join(eccBase, 'scripts', 'hooks', 'capabilities', 'telemetry', 'telemetry-lib.js');
55
-
56
- if (!existsSync(libPath)) {
57
- fmt.logWarn('Telemetry library not found. Run aw init first.');
58
- return;
59
- }
60
-
61
- const {
62
- readQueue,
63
- flushQueueToApi,
64
- getNamespace,
65
- buildTelemetryHeaders,
66
- } = await import(libPath);
67
-
68
- const entries = readQueue();
69
- if (entries.length === 0) {
70
- fmt.logSuccess('Queue is empty — nothing to flush.');
71
- return;
72
- }
73
-
74
- fmt.logStep(`Flushing ${entries.length} pending prompt(s)...`);
75
-
76
- const namespace = getNamespace();
77
- const headers = buildTelemetryHeaders(namespace);
78
- const result = await flushQueueToApi(headers);
79
-
80
- if (result.flushed > 0) {
81
- fmt.logSuccess(`Flushed ${result.flushed} prompt(s) to API.`);
82
- } else if (result.failed) {
83
- fmt.logWarn('Flush failed — entries remain in queue for retry.');
84
- } else {
85
- fmt.logSuccess('Nothing to flush.');
86
- }
29
+ fmt.logMessage(` ${chalk.dim('aw telemetry disable')} — opt out of anonymous analytics`);
30
+ fmt.logMessage(` ${chalk.dim('aw telemetry enable')} — re-enable analytics`);
87
31
  }
package/constants.mjs CHANGED
@@ -25,8 +25,10 @@ export const DOCS_SOURCE_DIR = 'content';
25
25
  /** Persistent git clone root — ~/.aw/ */
26
26
  export const AW_HOME = join(homedir(), '.aw');
27
27
 
28
- /** Directory in platform-docs repo containing platform rules (pulled into .aw_registry/.aw_rules/) */
28
+ /** Directory in platform-docs repo containing platform rules (synced into root-level .aw_rules/) */
29
29
  export const RULES_SOURCE_DIR = '.aw_rules';
30
+ /** Runtime location exposed to harnesses and generated instructions */
31
+ export const RULES_RUNTIME_DIR = '.aw/.aw_rules';
30
32
  /** Telemetry endpoint — override with AW_TELEMETRY_URL env var */
31
33
  export const TELEMETRY_URL = process.env.AW_TELEMETRY_URL || 'https://services.leadconnectorhq.com/agentic-workspace/api/telemetry/events';
32
34
 
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.0.5";
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}`;
@@ -160,6 +160,32 @@ function cloneOrUpdate(tag, dest) {
160
160
  }
161
161
  }
162
162
 
163
+ /**
164
+ * Transform canonical /aw: references to Cursor-compatible /aw- in installed
165
+ * skill and rule files. Cursor namespaces commands via directory structure
166
+ * (commands/aw/plan.md → /aw-plan) rather than colons.
167
+ */
168
+ function transformCursorAwRefs(home) {
169
+ const dirs = [
170
+ join(home, ".cursor", "skills"),
171
+ join(home, ".cursor", "rules"),
172
+ ];
173
+ for (const dir of dirs) {
174
+ if (!existsSync(dir)) continue;
175
+ for (const entry of readdirSync(dir, { withFileTypes: true, recursive: true })) {
176
+ if (!entry.isFile()) continue;
177
+ const ext = entry.name.split(".").pop();
178
+ if (!["md", "mdc"].includes(ext)) continue;
179
+ const filePath = join(entry.parentPath || entry.path, entry.name);
180
+ try {
181
+ const content = readFileSync(filePath, "utf8");
182
+ if (!content.includes("/aw:")) continue;
183
+ writeFileSync(filePath, content.replace(/\/aw:/g, "/aw-"));
184
+ } catch { /* best effort */ }
185
+ }
186
+ }
187
+ }
188
+
163
189
  function installClaudePlugin(repoDir) {
164
190
  // Always remove first so we re-register from the correct repoDir.
165
191
  // Falling back to `marketplace update` is unsafe — it updates from the
@@ -241,9 +267,6 @@ export async function installAwEcc(
241
267
  try {
242
268
  cloneOrUpdate(AW_ECC_TAG, repoDir);
243
269
 
244
- // Ensure telemetry state directory exists (vendor-agnostic, shared across IDEs)
245
- mkdirSync(join(home, ".aw", "telemetry"), { recursive: true });
246
-
247
270
  // Claude Code: plugin install via marketplace CLI (proper agent dispatch)
248
271
  if (targets.includes("claude")) {
249
272
  try {
@@ -259,6 +282,15 @@ export async function installAwEcc(
259
282
  run("npm install --no-audit --no-fund --ignore-scripts --loglevel=error", {
260
283
  cwd: repoDir,
261
284
  });
285
+ // generate-aw-hooks.js produces hooks.json and hook script sources that
286
+ // install-apply.js needs. npm install uses --ignore-scripts so the
287
+ // prepare script (which normally runs this) is skipped — call explicitly.
288
+ const generateHooksScript = join(repoDir, "scripts", "generate-aw-hooks.js");
289
+ if (existsSync(generateHooksScript)) {
290
+ try {
291
+ run(`node "${generateHooksScript}"`, { cwd: repoDir });
292
+ } catch { /* best effort — older engine versions may not have this script */ }
293
+ }
262
294
  for (const target of fileCopyTargets) {
263
295
  try {
264
296
  const snapshot = snapshotProtectedConfigs(home, target);
@@ -279,6 +311,10 @@ export async function installAwEcc(
279
311
  // so they're accessible as /aw:tdd, /aw:plan — same as Claude Code plugin.
280
312
  if (target === "cursor") {
281
313
  namespaceCursorCommands(runCwd);
314
+ // Cursor commands use hyphens (commands/aw/plan.md -> /aw-plan)
315
+ // but source skill/rule files use canonical colons (/aw:plan).
316
+ // Transform /aw: -> /aw- in installed cursor skill/rule files.
317
+ transformCursorAwRefs(home);
282
318
  }
283
319
  // Run sync script for codex: generates ~/.codex/prompts/*.md and
284
320
  // merges AGENTS.md — Codex has no slash commands, so prompts are the
@@ -3,7 +3,6 @@ import { join } from 'node:path';
3
3
  import { getSupportedHarnessPhaseEntries } from '../hook-manifest.mjs';
4
4
  import {
5
5
  buildDelegatingPhaseScript,
6
- buildNodeSilentPhaseScript,
7
6
  buildRegistryDelegatingPhaseScript,
8
7
  buildReservedPhaseScript,
9
8
  } from './shared-phase-scripts.mjs';
@@ -111,9 +110,10 @@ const CODEX_HOME_PHASE_BLUEPRINTS = {
111
110
  scriptName: 'aw-stop.sh',
112
111
  scriptMarker: '# aw-managed: codex-global-stop',
113
112
  buildScriptContent() {
114
- return buildNodeSilentPhaseScript({
113
+ return buildReservedPhaseScript({
115
114
  marker: this.scriptMarker,
116
- targetPath: '$HOME/.aw-ecc/scripts/hooks/capabilities/telemetry/telemetry-stop.js',
115
+ phase: 'Stop',
116
+ harnessLabel: 'Codex home routing',
117
117
  });
118
118
  },
119
119
  buildEntry(command) {
@@ -54,23 +54,6 @@ exit 0
54
54
  `;
55
55
  }
56
56
 
57
- export function buildNodeSilentPhaseScript({
58
- marker,
59
- targetPath,
60
- }) {
61
- return `#!/usr/bin/env bash
62
- ${marker}
63
- set -euo pipefail
64
-
65
- TARGET="${targetPath}"
66
- if [[ -f "$TARGET" ]]; then
67
- node "$TARGET" > /dev/null
68
- fi
69
-
70
- exit 0
71
- `;
72
- }
73
-
74
57
  export function buildReservedPhaseScript({
75
58
  marker,
76
59
  phase,
package/integrate.mjs CHANGED
@@ -29,7 +29,7 @@ function generateAwRouterBridgeSection() {
29
29
  - Do not assume the previous skill stack is still active
30
30
 
31
31
  ### 2. Read Rules Before Acting (MANDATORY)
32
- - Read applicable AW rules from \`~/.aw/.aw_registry/.aw_rules/platform/<domain>/AGENTS.md\`
32
+ - Read applicable AW rules from \`~/.aw/.aw_rules/platform/<domain>/AGENTS.md\`
33
33
  - Also read \`universal\` and \`security\` rules whenever they apply
34
34
  - If repo-local instructions conflict with org-level sources of truth, follow org-level sources
35
35
 
@@ -260,12 +260,12 @@ export function syncHomeHarnessInstructions(homeDir = homedir()) {
260
260
  const codexRulesSection = rulesDir
261
261
  ? generateAgentsMdRulesSection(rulesDir, {
262
262
  outputDir: join(homeDir, '.codex'),
263
- }).replaceAll('../.aw/.aw_registry/', '../.aw_registry/')
263
+ })
264
264
  : '';
265
265
  const cursorRulesSection = rulesDir
266
266
  ? generateAgentsMdRulesSection(rulesDir, {
267
267
  outputDir: join(homeDir, '.cursor'),
268
- }).replaceAll('../.aw/.aw_registry/', '../.aw_registry/')
268
+ })
269
269
  : '';
270
270
  const claudeRulesSection = rulesDir ? generateClaudeMdRulesSection(rulesDir) : '';
271
271
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.38-beta.9",
3
+ "version": "0.1.38",
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
@@ -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
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 */
@@ -171,14 +176,12 @@ function relativeRuleLink(outputDir, targetPath) {
171
176
  export function resolveRulesSourceDir(cwd, options = {}) {
172
177
  const HOME = options.homeDir || homedir();
173
178
  const candidates = [
174
- join(cwd, '.aw_registry', RULES_SOURCE_DIR),
175
- join(cwd, '.aw', '.aw_registry', RULES_SOURCE_DIR),
179
+ join(cwd, RULES_RUNTIME_DIR),
176
180
  ];
177
181
 
178
182
  if (cwd !== HOME) {
179
183
  candidates.push(
180
- join(HOME, '.aw_registry', RULES_SOURCE_DIR),
181
- join(HOME, '.aw', '.aw_registry', RULES_SOURCE_DIR),
184
+ join(HOME, RULES_RUNTIME_DIR),
182
185
  );
183
186
  }
184
187
 
@@ -198,7 +201,8 @@ function pruneStaleGeneratedRules(outputDir, expectedFilenames) {
198
201
 
199
202
  const fullPath = join(outputDir, entry.name);
200
203
  const content = readOrNull(fullPath);
201
- 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;
202
206
 
203
207
  try {
204
208
  unlinkSync(fullPath);
@@ -305,14 +309,92 @@ function renderCursorRules(cwd, rulesDir, options = {}) {
305
309
  }
306
310
  }
307
311
 
308
- 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;
309
315
  writeFileSync(join(cursorRulesDir, `${scopeToFilename(scope)}.mdc`), content);
310
316
  count++;
311
317
  }
312
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
+
313
325
  return count;
314
326
  }
315
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
+
316
398
  /** Rule scope → Claude Code paths: frontmatter (only supported field for .claude/rules/) */
317
399
  const SCOPE_PATHS = {
318
400
  'api-design': ['src/**/*.controller.ts', 'src/**/dto/**/*.ts', 'src/**/*.client.ts'],
@@ -389,7 +471,8 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
389
471
  }
390
472
  }
391
473
 
392
- 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;
393
476
  writeFileSync(join(claudeRulesDir, `${scopeToFilename(scope)}.md`), content);
394
477
  count++;
395
478
  }
@@ -398,7 +481,7 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
398
481
  }
399
482
 
400
483
  /**
401
- * Generate a rules section for CLAUDE.md from .aw_rules.
484
+ * Generate a rules section for CLAUDE.md from runtime AW rules.
402
485
  */
403
486
  export function generateClaudeMdRulesSection(rulesDir) {
404
487
  const manifest = readManifest(rulesDir);
@@ -410,7 +493,7 @@ export function generateClaudeMdRulesSection(rulesDir) {
410
493
  const lines = [
411
494
  '## Platform Rules (MUST)',
412
495
  '',
413
- '> Auto-synced from `.aw_registry/.aw_rules/`. Full details in reference files.',
496
+ '> Rendered from platform `.aw/.aw_rules/`. Full details in reference files.',
414
497
  '',
415
498
  ];
416
499
 
@@ -419,14 +502,14 @@ export function generateClaudeMdRulesSection(rulesDir) {
419
502
  }
420
503
 
421
504
  lines.push('');
422
- lines.push('See `.aw_registry/.aw_rules/rule-manifest.json` for all rules including SHOULD/MAY.');
505
+ lines.push('See `.aw/.aw_rules/rule-manifest.json` for all rules including SHOULD/MAY.');
423
506
  lines.push('');
424
507
 
425
508
  return lines.join('\n');
426
509
  }
427
510
 
428
511
  /**
429
- * Generate a rules section for AGENTS.md from the top-level AGENTS.md in .aw_rules.
512
+ * Generate a rules section for AGENTS.md from the top-level AGENTS.md in runtime AW rules.
430
513
  */
431
514
  export function generateAgentsMdRulesSection(rulesDir, options = {}) {
432
515
  const topLevelAgents = readOrNull(join(rulesDir, 'AGENTS.md'));
@@ -436,7 +519,7 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
436
519
  const lines = [
437
520
  '## Platform Rules — Non-Negotiables',
438
521
  '',
439
- '> Auto-synced from `.aw_registry/.aw_rules/`.',
522
+ '> Rendered from platform `.aw/.aw_rules/`.',
440
523
  '',
441
524
  topLevelAgents.trim(),
442
525
  '',
@@ -487,7 +570,7 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
487
570
 
488
571
  /**
489
572
  * Main render function. Call after aw pull / aw sync.
490
- * Reads .aw_registry/.aw_rules/ and renders:
573
+ * Reads runtime .aw/.aw_rules/ and renders:
491
574
  * 1. .cursor/rules/<scope>.mdc — at cwd AND at $HOME (global)
492
575
  * 2. Returns sections for CLAUDE.md and AGENTS.md injection
493
576
  */