@ghl-ai/aw 0.1.37-beta.67 → 0.1.37-beta.69

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.
Files changed (49) hide show
  1. package/bin.js +2 -2
  2. package/cli.mjs +25 -11
  3. package/commands/daemon.mjs +4 -4
  4. package/commands/init.mjs +15 -35
  5. package/commands/link-project.mjs +4 -11
  6. package/commands/nuke.mjs +23 -28
  7. package/commands/protocol.mjs +107 -0
  8. package/commands/pull.mjs +5 -84
  9. package/commands/push-rules.mjs +2 -2
  10. package/commands/push.mjs +15 -8
  11. package/commands/search.mjs +1 -1
  12. package/commands/slack-sim.mjs +128 -0
  13. package/commands/telemetry.mjs +395 -0
  14. package/config.mjs +1 -1
  15. package/constants.mjs +50 -0
  16. package/ecc.mjs +92 -89
  17. package/fmt.mjs +14 -0
  18. package/hooks/activity-tracker.js +61 -0
  19. package/hooks/capabilities/telemetry.mjs +880 -0
  20. package/hooks/manifest.mjs +25 -0
  21. package/hooks/shared/dispatch.mjs +83 -0
  22. package/hooks/shared/pre-compact.mjs +5 -0
  23. package/hooks/shared/session-end.mjs +5 -0
  24. package/hooks/shared/session-start.mjs +5 -0
  25. package/hooks/shared/stop.mjs +5 -0
  26. package/hooks/telemetry-stop.js +411 -0
  27. package/hooks.mjs +107 -6
  28. package/integrate.mjs +243 -172
  29. package/link.mjs +1 -36
  30. package/package.json +10 -10
  31. package/paths.mjs +1 -1
  32. package/registry.mjs +1 -1
  33. package/render-rules.mjs +8 -48
  34. package/slack-sim/fake-slack.mjs +200 -0
  35. package/slack-sim/http.mjs +170 -0
  36. package/slack-sim/in-process.mjs +263 -0
  37. package/slack-sim/render.mjs +42 -0
  38. package/slack-sim/scenario.mjs +64 -0
  39. package/slack-sim/scenarios/checkpoint-approve.json +21 -0
  40. package/slack-sim/scenarios/image-thread.json +27 -0
  41. package/slack-sim/scenarios/implementation-basic.json +18 -0
  42. package/slack-sim/scenarios/poll-webhook-race.json +18 -0
  43. package/slack-sim/scenarios/review-pr.json +14 -0
  44. package/telemetry.mjs +233 -0
  45. package/update.mjs +6 -1
  46. package/codex.mjs +0 -828
  47. package/commands/doctor.mjs +0 -927
  48. package/commands/startup.mjs +0 -87
  49. package/startup.mjs +0 -531
package/bin.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- // Standalone bin entrypoint for published npm package
3
- import('./cli.mjs').then(m => m.run(process.argv.slice(2)));
2
+ import { run } from './cli.mjs';
3
+ run(process.argv.slice(2));
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;
@@ -17,13 +18,11 @@ const COMMANDS = {
17
18
  'push-rules': () => import('./commands/push-rules.mjs').then(m => m.pushRulesCommand),
18
19
  drop: () => import('./commands/drop.mjs').then(m => m.dropCommand),
19
20
  status: () => import('./commands/status.mjs').then(m => m.statusCommand),
20
- doctor: () => import('./commands/doctor.mjs').then(m => m.doctorCommand),
21
- routing: () => import('./commands/startup.mjs').then(m => m.routingCommand),
22
- startup: () => import('./commands/startup.mjs').then(m => m.startupCommand),
23
21
  search: () => import('./commands/search.mjs').then(m => m.searchCommand),
24
22
  link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
25
23
  nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
26
24
  daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
25
+ telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
27
26
  };
28
27
 
29
28
  function parseArgs(argv) {
@@ -98,17 +97,20 @@ function printHelp() {
98
97
 
99
98
  sec('Manage'),
100
99
  cmd('aw status', 'Show synced paths, modified files & conflicts'),
101
- cmd('aw doctor', 'Run a health check for routing, MCP, plugin, and AW ECC surfaces'),
102
100
  cmd('aw link', 'Link current project as a git worktree (wires IDE symlinks)'),
103
- cmd('aw routing status', 'Show global AW session-routing mode for Claude/Cursor/Codex'),
104
- cmd('aw routing disable', 'Disable automatic AW session routing globally'),
105
- cmd('aw routing enable', 'Re-enable automatic AW session routing globally'),
106
101
  cmd('aw drop <path>', 'Stop syncing or delete local content'),
107
102
  cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
108
103
  cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
109
104
  cmd('aw daemon install --interval 30m', 'Set custom interval (e.g. 30m, 2h, 3600)'),
110
105
  cmd('aw daemon uninstall', 'Stop the background daemon'),
111
106
  cmd('aw daemon status', 'Check if daemon is running'),
107
+ cmd('aw slack-sim run <scenario>', 'Replay Slack-like scenarios against real runtime'),
108
+ cmd('aw slack-sim list-scenarios', 'List built-in Slack simulator scenarios'),
109
+
110
+ sec('Settings'),
111
+ cmd('aw telemetry status', 'Show telemetry status'),
112
+ cmd('aw telemetry disable', 'Opt out of anonymous analytics'),
113
+ cmd('aw telemetry enable', 'Re-enable analytics'),
112
114
 
113
115
  sec('Examples'),
114
116
  '',
@@ -158,9 +160,21 @@ export async function run(argv) {
158
160
  }
159
161
 
160
162
  if (command && COMMANDS[command]) {
163
+ const span = await startSpan(command, args);
164
+ span.notice();
161
165
  args._updateCheck = updateCheck;
162
- const handler = await COMMANDS[command]();
163
- await handler(args);
166
+ try {
167
+ const handler = await COMMANDS[command]();
168
+ await handler(args);
169
+ await span.end({ status: 'completed' });
170
+ } catch (err) {
171
+ if (err instanceof CancelError) {
172
+ await span.end({ status: 'cancelled', error_type: 'CancelError' });
173
+ process.exit(err.exitCode ?? 1);
174
+ }
175
+ await span.end({ status: 'failed', error_type: err.constructor.name });
176
+ throw err;
177
+ }
164
178
  notifyUpdate(await updateCheck);
165
179
  return;
166
180
  }
@@ -170,5 +184,5 @@ export async function run(argv) {
170
184
  process.exit(0);
171
185
  }
172
186
 
173
- fmt.cancel(`Unknown command: ${command}`);
187
+ fmt.cancelAndExit(`Unknown command: ${command}`);
174
188
  }
@@ -39,9 +39,9 @@ async function installLaunchd(intervalSeconds) {
39
39
 
40
40
  <key>ProgramArguments</key>
41
41
  <array>
42
- <string>${awBin}</string>
43
- <string>pull</string>
44
- <string>--silent</string>
42
+ <string>/bin/sh</string>
43
+ <string>-c</string>
44
+ <string>'${awBin}' pull --silent && '${awBin}' telemetry flush 2>/dev/null</string>
45
45
  </array>
46
46
 
47
47
  <key>StartInterval</key>
@@ -113,7 +113,7 @@ async function installCron(intervalSeconds) {
113
113
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
114
114
 
115
115
  const cronExpr = toCronExpression(intervalSeconds);
116
- const cronLine = `${cronExpr} ${awBin} pull --silent >> ${logDir}/pull.log 2>&1 # aw-daemon`;
116
+ const cronLine = `${cronExpr} /bin/sh -c '${awBin} pull --silent && ${awBin} telemetry flush 2>/dev/null' >> ${logDir}/pull.log 2>&1 # aw-daemon`;
117
117
 
118
118
  let current = '';
119
119
  try { current = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' }); } catch { /* empty */ }
package/commands/init.mjs CHANGED
@@ -23,13 +23,12 @@ import * as config from '../config.mjs';
23
23
  import * as fmt from '../fmt.mjs';
24
24
  import { chalk } from '../fmt.mjs';
25
25
  import { linkWorkspace } from '../link.mjs';
26
- import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
26
+ import { generateCommands, copyInstructions, initAwDocs, installIdeHooks } from '../integrate.mjs';
27
27
  import { setupMcp } from '../mcp.mjs';
28
- import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
28
+ import { installLocalCommitHook } from '../hooks.mjs';
29
29
  import { autoUpdate, promptUpdate } from '../update.mjs';
30
30
  import { installGlobalHooks } from '../hooks.mjs';
31
31
  import { installAwEcc } from '../ecc.mjs';
32
- import { removeWorkspaceHookDefaults } from '../codex.mjs';
33
32
  import {
34
33
  initPersistentClone,
35
34
  isValidClone,
@@ -58,19 +57,6 @@ const HOME = (() => { try { return realpathSync(_rawHome); } catch { return _raw
58
57
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
59
58
  const AW_HOME = join(HOME, '.aw');
60
59
 
61
- function syncInstructionsAndAwDocs(targetDir, namespace) {
62
- copyInstructions(targetDir, null, namespace);
63
- initAwDocs(targetDir);
64
- }
65
-
66
- function syncHomeAndProjectInstructions(cwd, namespace) {
67
- syncHomeHarnessInstructions(HOME);
68
- initAwDocs(HOME);
69
- if (cwd !== HOME) {
70
- syncInstructionsAndAwDocs(cwd, namespace);
71
- }
72
- }
73
-
74
60
  // ── Ensure ~/.aw/.gitignore has personal/local entries ───────────────────
75
61
 
76
62
  const AW_GITIGNORE_ENTRIES = [
@@ -100,7 +86,7 @@ function installIdeTasks() {
100
86
  {
101
87
  label: 'aw: sync registry',
102
88
  type: 'shell',
103
- command: 'aw init --silent',
89
+ command: 'AW_TRIGGER=ide:task aw init --silent',
104
90
  presentation: { reveal: 'silent', panel: 'shared', close: true },
105
91
  runOptions: { runOn: 'folderOpen' },
106
92
  problemMatcher: [],
@@ -221,7 +207,7 @@ export async function initCommand(args) {
221
207
  }
222
208
 
223
209
  if (choice === 'platform-only') {
224
- namespace = null; team = null; subTeam = null; folderName = null;
210
+ namespace = 'platform'; team = 'platform'; subTeam = null; folderName = null;
225
211
  }
226
212
  }
227
213
 
@@ -274,11 +260,11 @@ export async function initCommand(args) {
274
260
  }
275
261
 
276
262
  await installAwEcc(cwd, { silent });
277
- ensureAwRuntimeHook(HOME);
278
- syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
263
+ copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
264
+ initAwDocs(HOME);
279
265
  await setupMcp(HOME, freshCfg?.namespace || team, { silent });
280
- const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
281
266
  installGlobalHooks();
267
+ installIdeHooks();
282
268
 
283
269
  // Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
284
270
  if (cwd !== HOME) {
@@ -309,6 +295,7 @@ export async function initCommand(args) {
309
295
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
310
296
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
311
297
  const commands = generateCommands(HOME, { silent: true });
298
+ if (cwd !== HOME) installLocalCommitHook(cwd);
312
299
 
313
300
  if (silent) {
314
301
  autoUpdate(await args._updateCheck);
@@ -318,9 +305,6 @@ export async function initCommand(args) {
318
305
  '',
319
306
  ` ${chalk.green('✓')} Registry synced`,
320
307
  ` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
321
- removedLegacyStartupFiles.length > 0
322
- ? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
323
- : null,
324
308
  cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
325
309
  ].filter(Boolean).join('\n'));
326
310
  }
@@ -345,7 +329,7 @@ export async function initCommand(args) {
345
329
 
346
330
  if (!user) {
347
331
  try {
348
- user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
332
+ user = execSync('git config --global user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
349
333
  } catch { /* git not configured */ }
350
334
  }
351
335
 
@@ -396,8 +380,8 @@ export async function initCommand(args) {
396
380
  }
397
381
  }
398
382
 
399
- // Create sync config
400
- const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
383
+ // Create sync config — default to 'platform' when no namespace specified
384
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
401
385
  if (folderName) {
402
386
  config.addPattern(GLOBAL_AW_DIR, folderName);
403
387
  }
@@ -407,12 +391,11 @@ export async function initCommand(args) {
407
391
 
408
392
  // Step 3: Setup tasks, MCP, hooks
409
393
  await installAwEcc(cwd, { silent });
410
- ensureAwRuntimeHook(HOME);
411
- syncHomeAndProjectInstructions(cwd, team);
394
+ const instructionFiles = copyInstructions(HOME, null, team) || [];
395
+ initAwDocs(HOME);
412
396
  const mcpFiles = await setupMcp(HOME, team, { silent }) || [];
413
- applyStoredStartupPreferences(HOME);
414
- const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
415
397
  const hooksInstalled = installGlobalHooks();
398
+ installIdeHooks();
416
399
  installIdeTasks();
417
400
 
418
401
  // Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
@@ -444,6 +427,7 @@ export async function initCommand(args) {
444
427
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
445
428
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
446
429
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
430
+ if (cwd !== HOME) installLocalCommitHook(cwd);
447
431
  ideSpinner.message('Generating commands...');
448
432
  const commands = generateCommands(HOME, { silent: true });
449
433
  ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
@@ -457,10 +441,6 @@ export async function initCommand(args) {
457
441
  ` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
458
442
  ` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
459
443
  ` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
460
- cwd !== HOME ? ` ${chalk.green('✓')} Global startup managed from ~/.claude/, ~/.cursor/, ~/.codex/` : null,
461
- removedLegacyStartupFiles.length > 0
462
- ? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
463
- : null,
464
444
  hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
465
445
  ` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
466
446
  cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Linked in current project` : null,
@@ -9,8 +9,7 @@ import { addProjectWorktree, isWorktree, isValidClone } from '../git.mjs';
9
9
  import { REGISTRY_DIR, REGISTRY_URL } from '../constants.mjs';
10
10
  import { linkWorkspace } from '../link.mjs';
11
11
  import { generateCommands } from '../integrate.mjs';
12
- import { removeWorkspaceHookDefaults } from '../codex.mjs';
13
- import { applyStoredStartupPreferences } from '../startup.mjs';
12
+ import { installLocalCommitHook } from '../hooks.mjs';
14
13
 
15
14
  const HOME = homedir();
16
15
  const AW_HOME = join(HOME, '.aw');
@@ -42,9 +41,8 @@ export function linkProjectCommand(args) {
42
41
  const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
43
42
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
44
43
  const commands = generateCommands(HOME, { silent: true });
45
- applyStoredStartupPreferences(HOME);
46
- const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
47
- 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' : ''}` : ''}`);
44
+ installLocalCommitHook(cwd);
45
+ fmt.logSuccess(`Already linked refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`);
48
46
  return;
49
47
  }
50
48
 
@@ -54,18 +52,13 @@ export function linkProjectCommand(args) {
54
52
  const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
55
53
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
56
54
  const commands = generateCommands(HOME, { silent: true });
57
- applyStoredStartupPreferences(HOME);
58
- const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
55
+ installLocalCommitHook(cwd);
59
56
  fmt.logSuccess([
60
57
  `Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
61
58
  '',
62
59
  ` ${chalk.green('✓')} ${chalk.dim('.aw/')} git worktree (IDE git panel enabled)`,
63
60
  ` ${chalk.green('✓')} ${chalk.dim(`.aw/${REGISTRY_DIR}/`)} registry content`,
64
61
  ` ${chalk.green('✓')} ${chalk.dim('.claude/.cursor/.codex/')} IDE symlinks wired`,
65
- ` ${chalk.green('✓')} ${chalk.dim('startup mode')} global-first via ~/.claude/, ~/.cursor/, ~/.codex/`,
66
- removedLegacyStartupFiles.length > 0
67
- ? ` ${chalk.green('✓')} ${chalk.dim('legacy repo hooks')} removed ${removedLegacyStartupFiles.length} AW-managed file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
68
- : null,
69
62
  ].join('\n'));
70
63
  } catch (e) {
71
64
  fmt.cancel(`Failed to link project: ${e.message}`);
package/commands/nuke.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Safety guarantee: NEVER deletes files that AW didn't create.
4
4
 
5
- import { dirname, join } from 'node:path';
5
+ import { join } from 'node:path';
6
6
  import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync, readFileSync, readlinkSync, writeFileSync } from 'node:fs';
7
7
  import { homedir } from 'node:os';
8
8
  import { execSync, exec as execCb } from 'node:child_process';
@@ -15,14 +15,13 @@ import { removeGlobalHooks } from '../hooks.mjs';
15
15
  import { uninstallAwEcc } from '../ecc.mjs';
16
16
  import { removeMcpConfig } from '../mcp.mjs';
17
17
  import { listProjectWorktrees } from '../git.mjs';
18
- import { removeWorkspaceHookDefaults } from '../codex.mjs';
19
18
 
20
19
  const HOME = homedir();
21
20
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
22
21
  const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
23
22
 
24
23
  const IDE_DIRS = ['.claude', '.cursor', '.codex', '.agents'];
25
- const CONTENT_TYPES = ['agents', 'skills', 'commands', 'evals', 'references'];
24
+ const CONTENT_TYPES = ['agents', 'skills', 'commands', 'evals'];
26
25
 
27
26
  function loadManifest() {
28
27
  if (!existsSync(MANIFEST_PATH)) return null;
@@ -140,27 +139,28 @@ async function removeProjectSymlinks() {
140
139
  { encoding: 'utf8', timeout: 30000 }
141
140
  );
142
141
  for (const linkPath of registryLinks.trim().split('\n').filter(Boolean)) {
143
- try {
144
- removeWorkspaceHookDefaults(dirname(linkPath));
145
- unlinkSync(linkPath);
146
- removed++;
147
- } catch { /* best effort */ }
142
+ try { unlinkSync(linkPath); removed++; } catch { /* best effort */ }
148
143
  }
149
144
 
150
145
  // Also remove legacy local .git/hooks/post-checkout installed by old aw versions
146
+ // and prepare-commit-msg hooks installed by installLocalCommitHook
151
147
  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 */ }
148
+ const hookNames = ['post-checkout', 'prepare-commit-msg'];
149
+ for (const hookName of hookNames) {
150
+ const { stdout: hookFiles } = await exec(
151
+ `find "${HOME}" -maxdepth 5 -path "*/.git/hooks/${hookName}" -type f 2>/dev/null || true`,
152
+ { encoding: 'utf8', timeout: 30000 }
153
+ );
154
+ for (const hookPath of hookFiles.trim().split('\n').filter(Boolean)) {
155
+ try {
156
+ const content = readFileSync(hookPath, 'utf8');
157
+ // Only remove hooks that AW installed — identified by our marker comment
158
+ if (content.includes('aw:') || content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
159
+ unlinkSync(hookPath);
160
+ hooksRemoved++;
161
+ }
162
+ } catch { /* best effort */ }
163
+ }
164
164
  }
165
165
 
166
166
  return { removed, hooksRemoved };
@@ -209,20 +209,15 @@ function removeIdeTasks() {
209
209
 
210
210
  export async function nukeCommand(args) {
211
211
  // 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}`); });
212
+ process.on('uncaughtException', (e) => { fmt.cancelAndExit(`Unexpected error: ${e.message}`); });
213
+ process.on('unhandledRejection', (e) => { fmt.cancelAndExit(`Unexpected error: ${e?.message ?? e}`); });
214
214
 
215
215
  fmt.intro('aw nuke');
216
216
 
217
217
  // Remove stale local .aw_registry symlink if present (skip if cwd IS home — that's the global one)
218
218
  if (process.cwd() !== HOME) {
219
219
  const local = join(process.cwd(), '.aw_registry');
220
- try {
221
- if (lstatSync(local).isSymbolicLink()) {
222
- removeWorkspaceHookDefaults(process.cwd());
223
- unlinkSync(local);
224
- }
225
- } catch { /* fine */ }
220
+ try { if (lstatSync(local).isSymbolicLink()) unlinkSync(local); } catch { /* fine */ }
226
221
  }
227
222
 
228
223
  const manifest = loadManifest();
@@ -0,0 +1,107 @@
1
+ // commands/protocol.mjs — Write AW-PROTOCOL into machine-global AI tool files.
2
+ // Uses block-replacement markers so running multiple times is safe.
3
+
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { AW_PROTOCOL_START, AW_PROTOCOL_END } from '../constants.mjs';
8
+ import * as fmt from '../fmt.mjs';
9
+
10
+ const HOME = homedir();
11
+
12
+ /**
13
+ * Read AW-PROTOCOL.md from the global registry.
14
+ * Returns null if not found.
15
+ */
16
+ function readProtocol() {
17
+ const path = join(HOME, '.aw_registry', 'AW-PROTOCOL.md');
18
+ if (!existsSync(path)) return null;
19
+ return readFileSync(path, 'utf8');
20
+ }
21
+
22
+ /**
23
+ * Inject/replace the AW-PROTOCOL block in a target file.
24
+ * If file doesn't exist, creates it with just the block.
25
+ * If file exists, replaces content between markers or appends block.
26
+ * Returns true if file was written/updated.
27
+ */
28
+ function injectBlock(filePath, content, toolName) {
29
+ const block = `${AW_PROTOCOL_START}\n${content}\n${AW_PROTOCOL_END}`;
30
+
31
+ // Ensure parent directory exists
32
+ mkdirSync(dirname(filePath), { recursive: true });
33
+
34
+ if (!existsSync(filePath)) {
35
+ writeFileSync(filePath, block + '\n', 'utf8');
36
+ fmt.logStep(`Created ${toolName} global rules: ${filePath}`);
37
+ return true;
38
+ }
39
+
40
+ const existing = readFileSync(filePath, 'utf8');
41
+ const startIdx = existing.indexOf(AW_PROTOCOL_START);
42
+ const endIdx = existing.indexOf(AW_PROTOCOL_END);
43
+
44
+ if (startIdx !== -1 && endIdx !== -1) {
45
+ // Replace existing block
46
+ const before = existing.slice(0, startIdx);
47
+ const after = existing.slice(endIdx + AW_PROTOCOL_END.length);
48
+ const updated = before + block + after;
49
+ if (updated === existing) return false; // no change
50
+ writeFileSync(filePath, updated, 'utf8');
51
+ fmt.logStep(`Updated ${toolName} global rules: ${filePath}`);
52
+ return true;
53
+ }
54
+
55
+ // Append block to existing file
56
+ const updated = existing.trimEnd() + '\n\n' + block + '\n';
57
+ writeFileSync(filePath, updated, 'utf8');
58
+ fmt.logStep(`Appended to ${toolName} global rules: ${filePath}`);
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Write AW-PROTOCOL into all supported AI tool global files.
64
+ * Called by aw init. Silent if AW-PROTOCOL.md not found.
65
+ *
66
+ * @param {string} [workspaceDir] - Optional workspace root. When provided,
67
+ * also writes to <workspaceDir>/.cursor/rules/aw-protocol.mdc so Cursor
68
+ * auto-loads it via its workspace rules mechanism (alwaysApply: true).
69
+ */
70
+ export function writeGlobalProtocolFiles(workspaceDir) {
71
+ const protocol = readProtocol();
72
+ if (!protocol) {
73
+ fmt.logWarn('AW-PROTOCOL.md not found in ~/.aw_registry — skipping global rules install');
74
+ return [];
75
+ }
76
+
77
+ const written = [];
78
+ const cursorContent = `---\nalwaysApply: true\n---\n\n${protocol}`;
79
+
80
+ // Claude Code + Claude CLI (machine-global)
81
+ const claudeMd = join(HOME, '.claude', 'CLAUDE.md');
82
+ if (injectBlock(claudeMd, protocol, 'Claude Code/CLI')) written.push(claudeMd);
83
+
84
+ // Cursor — workspace-level rules (auto-loaded by Cursor from .cursor/rules/)
85
+ // Machine-global ~/.cursor/rules/ is NOT auto-loaded by Cursor; workspace-level is.
86
+ if (workspaceDir) {
87
+ const wsCursorMdc = join(workspaceDir, '.cursor', 'rules', 'aw-protocol.mdc');
88
+ if (injectBlock(wsCursorMdc, cursorContent, 'Cursor workspace')) written.push(wsCursorMdc);
89
+ }
90
+
91
+ // Cursor — also write to ~/.cursor/rules/ as fallback for manual reference
92
+ const cursorMdc = join(HOME, '.cursor', 'rules', 'aw-protocol.mdc');
93
+ if (injectBlock(cursorMdc, cursorContent, 'Cursor (home)')) written.push(cursorMdc);
94
+
95
+ // Codex CLI (machine-global)
96
+ const codexAgents = join(HOME, '.codex', 'AGENTS.md');
97
+ if (injectBlock(codexAgents, protocol, 'Codex')) written.push(codexAgents);
98
+
99
+ // Windsurf (known reliability issues — warn user)
100
+ const windsurfRules = join(HOME, '.codeium', 'windsurf', 'memories', 'global_rules.md');
101
+ if (injectBlock(windsurfRules, protocol, 'Windsurf')) {
102
+ written.push(windsurfRules);
103
+ fmt.logWarn('Windsurf global_rules.md has known reliability issues. Verify in Cascade > Customizations > Rules.');
104
+ }
105
+
106
+ return written;
107
+ }
package/commands/pull.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  existsSync,
5
5
  lstatSync,
6
6
  } from 'node:fs';
7
- import { dirname, join, extname } from 'node:path';
7
+ import { join, extname } from 'node:path';
8
8
  import { homedir } from 'node:os';
9
9
  import { exec as execCb, execSync } from 'node:child_process';
10
10
  import { promisify } from 'node:util';
@@ -30,9 +30,7 @@ import {
30
30
  } from '../constants.mjs';
31
31
  import { collectAllPaths, syncFileTree } from '../file-tree.mjs';
32
32
  import { linkWorkspace } from '../link.mjs';
33
- import { generateCommands, copyInstructions, syncHomeHarnessInstructions } from '../integrate.mjs';
34
- import { removeWorkspaceHookDefaults } from '../codex.mjs';
35
- import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
33
+ import { generateCommands, copyInstructions } from '../integrate.mjs';
36
34
 
37
35
  const HOME = homedir();
38
36
  const AW_HOME = join(HOME, '.aw');
@@ -44,7 +42,7 @@ export async function pullCommand(args) {
44
42
  const silent = args['--silent'] === true || args._silent === true;
45
43
 
46
44
  const log = {
47
- cancel: silent ? () => { process.exit(0); } : fmt.cancel,
45
+ cancel: silent ? (msg) => { throw new fmt.CancelError(msg || 'silent cancel', { exitCode: 0 }); } : fmt.cancel,
48
46
  logInfo: silent ? () => {} : fmt.logInfo,
49
47
  logSuccess: silent ? () => {} : fmt.logSuccess,
50
48
  logStep: silent ? () => {} : fmt.logStep,
@@ -247,7 +245,6 @@ export async function pullCommand(args) {
247
245
  if (!args._skipIntegrate) {
248
246
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
249
247
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
250
- const startupCleanupDir = localAw ? dirname(localAw) : (cwd !== HOME ? cwd : null);
251
248
 
252
249
  if (!silent) {
253
250
  const ideSpinner = log.spinner();
@@ -255,18 +252,12 @@ export async function pullCommand(args) {
255
252
  const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
256
253
  ideSpinner.message('Generating commands...');
257
254
  const commands = generateCommands(HOME, { silent: true });
258
- syncHomeHarnessInstructions(HOME);
259
- ensureAwRuntimeHook(HOME);
260
- applyStoredStartupPreferences(HOME);
261
- if (startupCleanupDir) removeWorkspaceHookDefaults(startupCleanupDir);
255
+ copyInstructions(HOME, null, cfg.namespace);
262
256
  ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlink${symlinks !== 1 ? 's' : ''}, ${chalk.bold(commands)} command${commands !== 1 ? 's' : ''}`);
263
257
  } else {
264
258
  linkWorkspace(HOME, awDirForLinks, { silent: true });
265
259
  generateCommands(HOME, { silent: true });
266
- syncHomeHarnessInstructions(HOME);
267
- ensureAwRuntimeHook(HOME);
268
- applyStoredStartupPreferences(HOME);
269
- if (startupCleanupDir) removeWorkspaceHookDefaults(startupCleanupDir);
260
+ copyInstructions(HOME, null, cfg.namespace);
270
261
  }
271
262
  }
272
263
 
@@ -299,73 +290,3 @@ function registerMcp(namespace) {
299
290
  fmt.logWarn('MCP registration failed (pull still successful)');
300
291
  }
301
292
  }
302
- function printDryRun(actions, verbose) {
303
- const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0, ORPHAN: 0, UNCHANGED: 0 };
304
- const lines = [];
305
-
306
- for (const type of ['agents', 'skills', 'commands', 'evals', 'references']) {
307
- const items = actions.filter(a => a.type === type);
308
- if (items.length === 0) continue;
309
-
310
- lines.push(chalk.bold(`${type}/`));
311
- for (const act of items.sort((a, b) => a.targetFilename.localeCompare(b.targetFilename))) {
312
- counts[act.action] = (counts[act.action] || 0) + 1;
313
- if (!verbose && act.action === 'UNCHANGED') continue;
314
- const ns = act.namespacePath ? chalk.dim(` [${act.namespacePath}]`) : '';
315
- lines.push(` ${fmt.actionLabel(act.action)} ${act.targetFilename}${ns}`);
316
- }
317
- }
318
-
319
- if (lines.length > 0) {
320
- fmt.note(lines.join('\n'), 'Dry Run');
321
- }
322
-
323
- fmt.logInfo(`Summary: ${fmt.countSummary(counts)}`);
324
- fmt.logWarn('No files modified (--dry-run)');
325
- }
326
-
327
- function printSummary(actions, verbose, conflictCount) {
328
- const conflicts = actions.filter(a => a.action === 'CONFLICT');
329
-
330
- for (const type of ['agents', 'skills', 'commands', 'evals', 'references']) {
331
- const typeActions = actions.filter(a => a.type === type);
332
- if (typeActions.length === 0) continue;
333
-
334
- const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0, ORPHAN: 0, UNCHANGED: 0 };
335
- for (const a of typeActions) counts[a.action]++;
336
-
337
- const parts = [];
338
- if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
339
- if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
340
- if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
341
- if (counts.ORPHAN > 0) parts.push(chalk.yellow(`${counts.ORPHAN} removed`));
342
- const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
343
-
344
- fmt.logSuccess(`${typeActions.length} ${type} pulled${detail}`);
345
-
346
- if (verbose) {
347
- for (const a of typeActions.filter(a => a.action !== 'UNCHANGED')) {
348
- const ns = a.namespacePath ? chalk.dim(` [${a.namespacePath}]`) : '';
349
- fmt.logMessage(` ${fmt.actionLabel(a.action)} ${a.targetFilename}${ns}`);
350
- }
351
- }
352
- }
353
-
354
- if (conflicts.length > 0) {
355
- const conflictLines = conflicts.map(c => {
356
- return `${chalk.red('both modified:')} ${c.type}/${c.targetFilename}`;
357
- }).join('\n');
358
-
359
- fmt.note(
360
- conflictLines + '\n\n' +
361
- chalk.dim('Fix conflicts, then re-run pull to verify.\n') +
362
- chalk.dim('grep -r "<<<<<<< " .aw_registry/'),
363
- chalk.red('Merge Conflicts')
364
- );
365
-
366
- fmt.outro(chalk.red('Pull completed with conflicts — resolve and re-run'));
367
- process.exit(1);
368
- }
369
-
370
- fmt.outro('Pull complete');
371
- }
@@ -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) {
@@ -147,7 +147,7 @@ function pushRulesTree(sourceRoot, { repo, dryRun, cwd }) {
147
147
  const prTitle = buildRulesPrTitle(sourceRoot, cwd);
148
148
  const prBody = buildRulesPrBody(sourceRoot, sourceType, cwd);
149
149
 
150
- execSync('git commit -m "registry: sync platform rules"', { cwd: tempDir, stdio: 'pipe' });
150
+ execSync(`git commit -m "registry: sync platform rules\n\n${AW_CO_AUTHOR}"`, { cwd: tempDir, stdio: 'pipe' });
151
151
  s2.stop('Rules sync prepared');
152
152
 
153
153
  const s3 = fmt.spinner();