@ghl-ai/aw 0.1.38-beta.1 → 0.1.38-beta.11

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
@@ -4,8 +4,9 @@ import { readFileSync } from 'node:fs';
4
4
  import { join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import * as fmt from './fmt.mjs';
7
- import { chalk } from './fmt.mjs';
7
+ import { chalk, CancelError } from './fmt.mjs';
8
8
  import { checkForUpdate, notifyUpdate } from './update.mjs';
9
+ import { startSpan } from './telemetry.mjs';
9
10
 
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
@@ -24,6 +25,7 @@ const COMMANDS = {
24
25
  link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
25
26
  nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
26
27
  daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
28
+ telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
27
29
  };
28
30
 
29
31
  function parseArgs(argv) {
@@ -109,6 +111,13 @@ function printHelp() {
109
111
  cmd('aw daemon install --interval 30m', 'Set custom interval (e.g. 30m, 2h, 3600)'),
110
112
  cmd('aw daemon uninstall', 'Stop the background daemon'),
111
113
  cmd('aw daemon status', 'Check if daemon is running'),
114
+ cmd('aw slack-sim run <scenario>', 'Replay Slack-like scenarios against real runtime'),
115
+ cmd('aw slack-sim list-scenarios', 'List built-in Slack simulator scenarios'),
116
+
117
+ sec('Settings'),
118
+ cmd('aw telemetry status', 'Show telemetry status'),
119
+ cmd('aw telemetry disable', 'Opt out of anonymous analytics'),
120
+ cmd('aw telemetry enable', 'Re-enable analytics'),
112
121
 
113
122
  sec('Examples'),
114
123
  '',
@@ -125,7 +134,7 @@ function printHelp() {
125
134
  cmd('aw push .aw_registry/agents/<name>.md', 'Push a single agent'),
126
135
  cmd('aw push .aw_registry/skills/<name>/', 'Push a single skill folder'),
127
136
  cmd('aw push .aw_rules', 'Auto-redirects to aw push-rules'),
128
- cmd('aw push-rules', 'Pushes .aw_rules or .aw_registry/.aw_rules'),
137
+ cmd('aw push-rules', 'Pushes .aw_rules'),
129
138
  '',
130
139
  ` ${chalk.dim('# Remove content from workspace')}`,
131
140
  cmd('aw drop <team>', 'Stop syncing a namespace (removes all files)'),
@@ -158,9 +167,21 @@ export async function run(argv) {
158
167
  }
159
168
 
160
169
  if (command && COMMANDS[command]) {
170
+ const span = await startSpan(command, args);
171
+ span.notice();
161
172
  args._updateCheck = updateCheck;
162
- const handler = await COMMANDS[command]();
163
- await handler(args);
173
+ try {
174
+ const handler = await COMMANDS[command]();
175
+ await handler(args);
176
+ await span.end({ status: 'completed' });
177
+ } catch (err) {
178
+ if (err instanceof CancelError) {
179
+ await span.end({ status: 'cancelled', error_type: 'CancelError' });
180
+ process.exit(err.exitCode ?? 1);
181
+ }
182
+ await span.end({ status: 'failed', error_type: err.constructor.name });
183
+ throw err;
184
+ }
164
185
  notifyUpdate(await updateCheck);
165
186
  return;
166
187
  }
@@ -170,5 +191,5 @@ export async function run(argv) {
170
191
  process.exit(0);
171
192
  }
172
193
 
173
- fmt.cancel(`Unknown command: ${command}`);
194
+ fmt.cancelAndExit(`Unknown command: ${command}`);
174
195
  }
@@ -162,10 +162,12 @@ 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_rules'),
167
+ join(homeDir, '.aw_registry', '.aw_rules'),
168
+ join(homeDir, '.aw', '.aw_registry', '.aw_rules'),
169
+ ];
170
+ return candidates.find(existsSync) || null;
169
171
  }
170
172
 
171
173
  function findClaudePlugin(homeDir) {
@@ -471,7 +473,7 @@ function getProjectClaudeSessionStartStatus(cwd) {
471
473
  }
472
474
 
473
475
  function textHasRulesReference(text) {
474
- return text.includes('Platform Rules') && text.includes('.aw_registry/.aw_rules/platform/');
476
+ return text.includes('Platform Rules') && text.includes('.aw_rules/platform/');
475
477
  }
476
478
 
477
479
  function textHasManagedRouterBridge(text) {
@@ -484,9 +486,9 @@ function textHasManagedRouterBridge(text) {
484
486
  && text.includes('incremental-implementation');
485
487
  }
486
488
 
487
- function extractRegistryReferencePaths(text) {
489
+ function extractRuleReferencePaths(text) {
488
490
  const matches = [];
489
- const pattern = /(?:\]\(|`)([^)\n`]*\.aw_registry\/[^)\n`]+\.md)(?:\)|`)/g;
491
+ const pattern = /(?:\]\(|`)([^)\n`]*(?:\.aw_rules|\.aw_registry\/\.aw_rules)\/[^)\n`]+\.md)(?:\)|`)/g;
490
492
  for (const match of text.matchAll(pattern)) {
491
493
  if (match[1]) matches.push(match[1]);
492
494
  }
@@ -505,11 +507,11 @@ function listGeneratedRuleFiles(dirPath, extension) {
505
507
  }
506
508
  }
507
509
 
508
- function findBrokenRegistryReferences(filePaths) {
510
+ function findBrokenRuleReferences(filePaths) {
509
511
  const broken = [];
510
512
  for (const filePath of filePaths) {
511
513
  const content = readText(filePath);
512
- const refs = extractRegistryReferencePaths(content);
514
+ const refs = extractRuleReferencePaths(content);
513
515
  for (const ref of refs) {
514
516
  if (ref.includes('<domain>')) continue;
515
517
  const resolvedPath = join(dirname(filePath), ref);
@@ -565,8 +567,8 @@ function buildDoctorChecks(homeDir, cwd) {
565
567
  'aw-rules-source',
566
568
  'AW rules source',
567
569
  '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.',
570
+ 'No synced .aw_rules tree found under ~/.aw_rules',
571
+ 'Run `aw init` or `aw pull platform` to sync the AW rules source into ~/.aw_rules.',
570
572
  ));
571
573
  } else {
572
574
  const topLevelRulesAgents = join(rulesDir, 'AGENTS.md');
@@ -578,7 +580,7 @@ function buildDoctorChecks(homeDir, cwd) {
578
580
  'aw-rules-source',
579
581
  'AW rules source',
580
582
  'fail',
581
- 'Rules source is incomplete under ~/.aw_registry/.aw_rules',
583
+ 'Rules source is incomplete under ~/.aw_rules',
582
584
  'Run `aw pull platform` to refresh .aw_rules, then rerun `aw doctor`.',
583
585
  ),
584
586
  );
@@ -784,7 +786,7 @@ function buildDoctorChecks(homeDir, cwd) {
784
786
  );
785
787
 
786
788
  const claudeRuleFiles = listGeneratedRuleFiles(claudeRulesDir, '.md');
787
- const brokenClaudeRuleRefs = findBrokenRegistryReferences(claudeRuleFiles);
789
+ const brokenClaudeRuleRefs = findBrokenRuleReferences(claudeRuleFiles);
788
790
  checks.push(
789
791
  claudeRuleFiles.length === 0
790
792
  ? makeCheck(
@@ -795,13 +797,13 @@ function buildDoctorChecks(homeDir, cwd) {
795
797
  'Run `aw pull platform` or `aw init` to render Claude rules, then rerun `aw doctor`.',
796
798
  )
797
799
  : brokenClaudeRuleRefs.length === 0
798
- ? makeCheck('claude-rule-references', 'Claude rule references', 'pass', 'Claude generated rule references resolve to real files in ~/.aw_registry')
800
+ ? makeCheck('claude-rule-references', 'Claude rule references', 'pass', 'Claude generated rule references resolve to real files in ~/.aw_rules')
799
801
  : makeCheck(
800
802
  'claude-rule-references',
801
803
  'Claude rule references',
802
804
  'fail',
803
805
  `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.',
806
+ 'Refresh the rendered Claude rules so embedded reference links point at real files under ~/.aw_rules.',
805
807
  ),
806
808
  );
807
809
 
@@ -894,7 +896,7 @@ function buildDoctorChecks(homeDir, cwd) {
894
896
  );
895
897
 
896
898
  const codexAgentsText = readText(codexAgentsPath);
897
- const codexBrokenRuleRefs = existsSync(codexAgentsPath) ? findBrokenRegistryReferences([codexAgentsPath]) : [];
899
+ const codexBrokenRuleRefs = existsSync(codexAgentsPath) ? findBrokenRuleReferences([codexAgentsPath]) : [];
898
900
  checks.push(
899
901
  existsSync(codexAgentsPath) && textHasRulesReference(codexAgentsText)
900
902
  ? codexBrokenRuleRefs.length === 0
@@ -1004,7 +1006,7 @@ function buildDoctorChecks(homeDir, cwd) {
1004
1006
 
1005
1007
  const cursorRulesDir = join(homeDir, '.cursor', 'rules');
1006
1008
  const cursorRuleFiles = listGeneratedRuleFiles(cursorRulesDir, '.mdc');
1007
- const brokenCursorRuleRefs = findBrokenRegistryReferences(cursorRuleFiles);
1009
+ const brokenCursorRuleRefs = findBrokenRuleReferences(cursorRuleFiles);
1008
1010
  checks.push(
1009
1011
  cursorRuleFiles.length === 0
1010
1012
  ? makeCheck(
@@ -1015,13 +1017,13 @@ function buildDoctorChecks(homeDir, cwd) {
1015
1017
  'Run `aw pull platform` or `aw init` to render Cursor rules, then rerun `aw doctor`.',
1016
1018
  )
1017
1019
  : brokenCursorRuleRefs.length === 0
1018
- ? makeCheck('cursor-rule-references', 'Cursor rule references', 'pass', 'Cursor generated rule references resolve to real files in ~/.aw_registry')
1020
+ ? makeCheck('cursor-rule-references', 'Cursor rule references', 'pass', 'Cursor generated rule references resolve to real files in ~/.aw_rules')
1019
1021
  : makeCheck(
1020
1022
  'cursor-rule-references',
1021
1023
  'Cursor rule references',
1022
1024
  'fail',
1023
1025
  `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.',
1026
+ 'Refresh the rendered Cursor rules so embedded reference links point at real files under ~/.aw_rules.',
1025
1027
  ),
1026
1028
  );
1027
1029
 
package/commands/init.mjs CHANGED
@@ -26,6 +26,7 @@ import { linkWorkspace } from '../link.mjs';
26
26
  import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
27
27
  import { setupMcp } from '../mcp.mjs';
28
28
  import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
29
+ import { installLocalCommitHook } from '../hooks.mjs';
29
30
  import { autoUpdate, promptUpdate } from '../update.mjs';
30
31
  import { installGlobalHooks } from '../hooks.mjs';
31
32
  import { installAwEcc } from '../ecc.mjs';
@@ -58,6 +59,21 @@ const HOME = (() => { try { return realpathSync(_rawHome); } catch { return _raw
58
59
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
59
60
  const AW_HOME = join(HOME, '.aw');
60
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_SOURCE_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
+ } catch {
73
+ // best effort cleanup
74
+ }
75
+ }
76
+
61
77
  function syncInstructionsAndAwDocs(targetDir, namespace) {
62
78
  copyInstructions(targetDir, null, namespace);
63
79
  initAwDocs(targetDir);
@@ -100,7 +116,7 @@ function installIdeTasks() {
100
116
  {
101
117
  label: 'aw: sync registry',
102
118
  type: 'shell',
103
- command: 'aw init --silent',
119
+ command: 'AW_TRIGGER=ide:task aw init --silent',
104
120
  presentation: { reveal: 'silent', panel: 'shared', close: true },
105
121
  runOptions: { runOn: 'folderOpen' },
106
122
  problemMatcher: [],
@@ -221,7 +237,7 @@ export async function initCommand(args) {
221
237
  }
222
238
 
223
239
  if (choice === 'platform-only') {
224
- namespace = null; team = null; subTeam = null; folderName = null;
240
+ namespace = 'platform'; team = 'platform'; subTeam = null; folderName = null;
225
241
  }
226
242
  }
227
243
 
@@ -261,9 +277,11 @@ export async function initCommand(args) {
261
277
 
262
278
  ensureAwGitignore(AW_HOME);
263
279
  const freshCfg = config.load(GLOBAL_AW_DIR);
264
- if (existsSync(GLOBAL_AW_DIR)) {
265
- syncFileTree(join(AW_HOME, RULES_SOURCE_DIR), join(GLOBAL_AW_DIR, RULES_SOURCE_DIR));
280
+ syncRulesTargets(HOME);
281
+ if (cwd !== HOME) {
282
+ syncRulesTargets(cwd);
266
283
  }
284
+ removeLegacyRegistryRules();
267
285
 
268
286
  // Ensure project worktree sparse checkout matches the global clone.
269
287
  // Covers the case where a namespace was added from HOME (or another project)
@@ -310,6 +328,7 @@ export async function initCommand(args) {
310
328
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
311
329
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
312
330
  const commands = generateCommands(HOME, { silent: true });
331
+ if (cwd !== HOME) installLocalCommitHook(cwd);
313
332
 
314
333
  if (silent) {
315
334
  autoUpdate(await args._updateCheck);
@@ -397,14 +416,16 @@ export async function initCommand(args) {
397
416
  }
398
417
  }
399
418
 
400
- // Create sync config
401
- const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
419
+ // Create sync config — default to 'platform' when no namespace specified
420
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
402
421
  if (folderName) {
403
422
  config.addPattern(GLOBAL_AW_DIR, folderName);
404
423
  }
405
- if (existsSync(GLOBAL_AW_DIR)) {
406
- syncFileTree(join(AW_HOME, RULES_SOURCE_DIR), join(GLOBAL_AW_DIR, RULES_SOURCE_DIR));
424
+ syncRulesTargets(HOME);
425
+ if (cwd !== HOME) {
426
+ syncRulesTargets(cwd);
407
427
  }
428
+ removeLegacyRegistryRules();
408
429
 
409
430
  // Step 3: Setup tasks, MCP, hooks
410
431
  await installAwEcc(cwd, { silent });
@@ -445,6 +466,7 @@ export async function initCommand(args) {
445
466
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
446
467
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
447
468
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
469
+ if (cwd !== HOME) installLocalCommitHook(cwd);
448
470
  ideSpinner.message('Generating commands...');
449
471
  const commands = generateCommands(HOME, { silent: true });
450
472
  ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
@@ -11,6 +11,7 @@ import { linkWorkspace } from '../link.mjs';
11
11
  import { generateCommands } from '../integrate.mjs';
12
12
  import { removeWorkspaceHookDefaults } from '../codex.mjs';
13
13
  import { applyStoredStartupPreferences } from '../startup.mjs';
14
+ import { installLocalCommitHook } from '../hooks.mjs';
14
15
 
15
16
  const HOME = homedir();
16
17
  const AW_HOME = join(HOME, '.aw');
@@ -43,8 +44,10 @@ export function linkProjectCommand(args) {
43
44
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
44
45
  const commands = generateCommands(HOME, { silent: true });
45
46
  applyStoredStartupPreferences(HOME);
47
+ installLocalCommitHook(cwd);
46
48
  const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
47
49
  fmt.logSuccess(`Already linked — refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands${removedLegacyStartupFiles.length > 0 ? ` · removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}` : ''}`);
50
+
48
51
  return;
49
52
  }
50
53
 
@@ -55,6 +58,7 @@ export function linkProjectCommand(args) {
55
58
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
56
59
  const commands = generateCommands(HOME, { silent: true });
57
60
  applyStoredStartupPreferences(HOME);
61
+ installLocalCommitHook(cwd);
58
62
  const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
59
63
  fmt.logSuccess([
60
64
  `Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
package/commands/nuke.mjs CHANGED
@@ -148,19 +148,24 @@ async function removeProjectSymlinks() {
148
148
  }
149
149
 
150
150
  // Also remove legacy local .git/hooks/post-checkout installed by old aw versions
151
+ // and prepare-commit-msg hooks installed by installLocalCommitHook
151
152
  let hooksRemoved = 0;
152
- const { stdout: hookFiles } = await exec(
153
- `find "${HOME}" -maxdepth 5 -path "*/.git/hooks/post-checkout" -type f 2>/dev/null || true`,
154
- { encoding: 'utf8', timeout: 30000 }
155
- );
156
- for (const hookPath of hookFiles.trim().split('\n').filter(Boolean)) {
157
- try {
158
- const content = readFileSync(hookPath, 'utf8');
159
- if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
160
- unlinkSync(hookPath);
161
- hooksRemoved++;
162
- }
163
- } catch { /* best effort */ }
153
+ const hookNames = ['post-checkout', 'prepare-commit-msg'];
154
+ for (const hookName of hookNames) {
155
+ const { stdout: hookFiles } = await exec(
156
+ `find "${HOME}" -maxdepth 5 -path "*/.git/hooks/${hookName}" -type f 2>/dev/null || true`,
157
+ { encoding: 'utf8', timeout: 30000 }
158
+ );
159
+ for (const hookPath of hookFiles.trim().split('\n').filter(Boolean)) {
160
+ try {
161
+ const content = readFileSync(hookPath, 'utf8');
162
+ // Only remove hooks that AW installed — identified by our marker comment
163
+ if (content.includes('aw:') || content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
164
+ unlinkSync(hookPath);
165
+ hooksRemoved++;
166
+ }
167
+ } catch { /* best effort */ }
168
+ }
164
169
  }
165
170
 
166
171
  return { removed, hooksRemoved };
@@ -209,8 +214,8 @@ function removeIdeTasks() {
209
214
 
210
215
  export async function nukeCommand(args) {
211
216
  // Catch unhandled errors and surface them instead of letting clack show generic "Something went wrong"
212
- process.on('uncaughtException', (e) => { fmt.cancel(`Unexpected error: ${e.message}`); });
213
- process.on('unhandledRejection', (e) => { fmt.cancel(`Unexpected error: ${e?.message ?? e}`); });
217
+ process.on('uncaughtException', (e) => { fmt.cancelAndExit(`Unexpected error: ${e.message}`); });
218
+ process.on('unhandledRejection', (e) => { fmt.cancelAndExit(`Unexpected error: ${e?.message ?? e}`); });
214
219
 
215
220
  fmt.intro('aw nuke');
216
221
 
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';
@@ -38,13 +39,28 @@ const HOME = homedir();
38
39
  const AW_HOME = join(HOME, '.aw');
39
40
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
40
41
 
42
+ function syncRulesTargets(targetDir) {
43
+ const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
44
+ if (!existsSync(rulesSrc)) return false;
45
+ syncFileTree(rulesSrc, join(targetDir, RULES_SOURCE_DIR));
46
+ return true;
47
+ }
48
+
49
+ function removeLegacyRegistryRules() {
50
+ try {
51
+ rmSync(join(GLOBAL_AW_DIR, RULES_SOURCE_DIR), { recursive: true, force: true });
52
+ } catch {
53
+ // best effort cleanup
54
+ }
55
+ }
56
+
41
57
  export async function pullCommand(args) {
42
58
  const input = args._positional?.[0] || '';
43
59
  const cwd = process.cwd();
44
60
  const silent = args['--silent'] === true || args._silent === true;
45
61
 
46
62
  const log = {
47
- cancel: silent ? () => { process.exit(0); } : fmt.cancel,
63
+ cancel: silent ? (msg) => { throw new fmt.CancelError(msg || 'silent cancel', { exitCode: 0 }); } : fmt.cancel,
48
64
  logInfo: silent ? () => {} : fmt.logInfo,
49
65
  logSuccess: silent ? () => {} : fmt.logSuccess,
50
66
  logStep: silent ? () => {} : fmt.logStep,
@@ -179,13 +195,6 @@ export async function pullCommand(args) {
179
195
  log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
180
196
  }
181
197
 
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
198
  // Rebase project worktree branch onto origin/main — only for legacy git worktrees.
190
199
  // In the symlink model, <project>/.aw IS ~/.aw (same repo), so fetchAndMerge already
191
200
  // brought it up to date. Nothing to rebase.
@@ -244,6 +253,16 @@ export async function pullCommand(args) {
244
253
  }
245
254
  }
246
255
 
256
+ let rulesSynced = syncRulesTargets(HOME);
257
+ const workspaceRoot = localAw ? dirname(localAw) : (cwd !== HOME ? cwd : null);
258
+ if (workspaceRoot && workspaceRoot !== HOME) {
259
+ rulesSynced = syncRulesTargets(workspaceRoot) || rulesSynced;
260
+ }
261
+ removeLegacyRegistryRules();
262
+ if (rulesSynced && !silent) {
263
+ log.logSuccess('Synced .aw_rules');
264
+ }
265
+
247
266
  if (!args._skipIntegrate) {
248
267
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
249
268
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
5
5
 
6
6
  import * as fmt from '../fmt.mjs';
7
7
  import { chalk } from '../fmt.mjs';
8
- import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR } from '../constants.mjs';
8
+ import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR, AW_CO_AUTHOR } from '../constants.mjs';
9
9
  import { syncFileTree } from '../file-tree.mjs';
10
10
 
11
11
  function normalizeSlashes(path) {
@@ -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,13 +136,11 @@ 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
 
150
- execSync('git commit -m "registry: sync platform rules"', { cwd: tempDir, stdio: 'pipe' });
143
+ execSync(`git commit -m "registry: sync platform rules\n\n${AW_CO_AUTHOR}"`, { cwd: tempDir, stdio: 'pipe' });
151
144
  s2.stop('Rules sync prepared');
152
145
 
153
146
  const s3 = fmt.spinner();
@@ -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
 
package/commands/push.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  // commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
2
2
 
3
3
  import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
4
- import { join } from 'node:path';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { exec as execCb, execFile as execFileCb } from 'node:child_process';
6
7
  import { promisify } from 'node:util';
7
8
  import { homedir } from 'node:os';
@@ -10,7 +11,7 @@ const exec = promisify(execCb);
10
11
  const execFile = promisify(execFileCb);
11
12
  import * as fmt from '../fmt.mjs';
12
13
  import { chalk } from '../fmt.mjs';
13
- import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
14
+ import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR, AW_CO_AUTHOR } from '../constants.mjs';
14
15
  import { resolveInput } from '../paths.mjs';
15
16
  import { walkRegistryTree } from '../registry.mjs';
16
17
  import {
@@ -25,6 +26,9 @@ import {
25
26
  } from '../git.mjs';
26
27
  import { hasRulesChanges, isRulesPushInput, pushRulesCommand } from './push-rules.mjs';
27
28
 
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
31
+
28
32
  const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals', 'references'];
29
33
 
30
34
  // ── PR content generation ────────────────────────────────────────────
@@ -187,11 +191,14 @@ function generateCommitMsg(files) {
187
191
  const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)} removed`);
188
192
  const countParts = [...addedParts, ...deletedParts];
189
193
 
194
+ const version = VERSION;
195
+ const trailer = `\n\nGenerated-By: aw/${version}\n${AW_CO_AUTHOR}`;
196
+
190
197
  if (files.length === 1) {
191
198
  const f = files[0];
192
- return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}`;
199
+ return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}${trailer}`;
193
200
  }
194
- return `registry: sync ${files.length} files (${countParts.join(', ')})`;
201
+ return `registry: sync ${files.length} files (${countParts.join(', ')})${trailer}`;
195
202
  }
196
203
 
197
204
  // ── Batch file collection from folder ────────────────────────────────