@ghl-ai/aw 0.1.70-beta.2 → 0.1.70-beta.3

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/commands/c4.mjs CHANGED
@@ -101,13 +101,11 @@ function safeReaddirSync(dir) {
101
101
  try { return readdirSync(dir); } catch { return []; }
102
102
  }
103
103
 
104
- function safeListNamespaceDirs(dir, fsApi = {}) {
105
- const readDir = fsApi.readdirSync ?? readdirSync;
106
- const stat = fsApi.statSync ?? statSync;
104
+ function safeListNamespaceDirs(dir) {
107
105
  try {
108
- return readDir(dir).filter((entry) => {
106
+ return readdirSync(dir).filter((entry) => {
109
107
  if (entry.startsWith('.')) return false;
110
- try { return stat(join(dir, entry)).isDirectory(); } catch { return false; }
108
+ try { return statSync(join(dir, entry)).isDirectory(); } catch { return false; }
111
109
  });
112
110
  } catch { return []; }
113
111
  }
@@ -134,15 +132,17 @@ function setupGitAuth({ token, c4, home, cwd }) {
134
132
  c4.ensureOriginRemote({ cwd });
135
133
  }
136
134
 
137
- function resolveCodexRouterSkillPath({ awHome }) {
138
- return join(awHome, '.aw_registry/aw/skills/using-aw-skills/SKILL.md');
135
+ function resolveCodexRouterSkillPath({ awHome, eccHome, existsSync = fsExistsSync }) {
136
+ const eccRouterSkillPath = join(eccHome, 'skills/using-aw-skills/SKILL.md');
137
+ if (existsSync(eccRouterSkillPath)) return eccRouterSkillPath;
138
+ return join(awHome, '.aw_registry/platform/core/skills/using-aw-skills/SKILL.md');
139
139
  }
140
140
 
141
141
  /* ─────────────────────────────────────────────────────────────────────────
142
142
  * Per-harness branch (step 10).
143
143
  *
144
144
  * Returns `{ ok, bridge, injector }` so the summary line can pick up
145
- * codex-specific status ticks. Hard failures (injector / claude
145
+ * codex-specific status ticks. Hard failures (bridge / injector / claude
146
146
  * marketplace / slim card) bubble up as thrown Errors.
147
147
  * ───────────────────────────────────────────────────────────────────────── */
148
148
 
@@ -165,8 +165,9 @@ function runHarnessBranch({ harness, c4, home, eccHome, cwd, writer, existsSync
165
165
  }
166
166
  if (harness === 'codex-web') {
167
167
  const awHome = join(home, '.aw');
168
+ c4.applyEccRegistryBridge({ awHome, eccHome });
168
169
  c4.ensureCodexHooksFlag(home);
169
- const routerSkillPath = resolveCodexRouterSkillPath({ awHome });
170
+ const routerSkillPath = resolveCodexRouterSkillPath({ awHome, eccHome, existsSync });
170
171
  // Pass hookPath explicitly: defaultHookPath() reads os.homedir() at module
171
172
  // load time, which ignores the orchestrator's `home` (env.HOME). Without
172
173
  // this, installs land in the real ~/.codex even when HOME is overridden
@@ -192,7 +193,7 @@ function runSelfTests({ harness, c4, home, awHome, eccHome }) {
192
193
  if (!skill.ok) failures.push('skill-resolution');
193
194
 
194
195
  // diagnoseCommandResolution is warn-only — does NOT add to failures.
195
- const command = c4.diagnoseCommandResolution({ harness, home, awHome });
196
+ const command = c4.diagnoseCommandResolution({ harness, home });
196
197
 
197
198
  let view;
198
199
  let injector;
@@ -410,27 +411,12 @@ export async function c4Command(rawArgs, overrides = {}) {
410
411
  // check, so we specifically look for directories (namespaces like
411
412
  // "platform") to confirm the registry content was actually pulled.
412
413
  {
413
- const registryNamespaces = safeListNamespaceDirs(awRegistry, fs);
414
+ const registryNamespaces = safeListNamespaceDirs(awRegistry);
414
415
  if (registryNamespaces.length === 0) {
415
416
  writer.stderr('[aw-c4] registry has no namespace directories after init — retrying with aw pull\n');
416
417
  const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
417
- if (pullRes?.status !== 0 || safeListNamespaceDirs(awRegistry, fs).length === 0) {
418
- writer.stderr('[aw-c4] FATAL: registry skills were not fetched\n');
419
- return exit(1);
420
- }
421
- }
422
- }
423
-
424
- // Step 9b — verify active AW skills exist. `/aw:*` adapters point directly
425
- // at ~/.aw/.aw_registry/aw/skills/<skill>/SKILL.md, so an empty registry is
426
- // not enough; the AW namespace must be present.
427
- {
428
- const awSkillsDir = join(awRegistry, 'aw', 'skills');
429
- if (!fs.existsSync(awSkillsDir)) {
430
- writer.stderr('[aw-c4] AW skills missing after init — retrying with aw pull\n');
431
- const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
432
- if (pullRes?.status !== 0 || !fs.existsSync(awSkillsDir)) {
433
- writer.stderr('[aw-c4] FATAL: AW skills were not fetched\n');
418
+ if (pullRes?.status !== 0 || safeListNamespaceDirs(awRegistry).length === 0) {
419
+ writer.stderr('[aw-c4] FATAL: registry commands were not fetched\n');
434
420
  return exit(1);
435
421
  }
436
422
  }
@@ -453,9 +439,26 @@ export async function c4Command(rawArgs, overrides = {}) {
453
439
  writer.stdout('[aw-c4] MCP disabled; skipping registerGhlAiMcp\n');
454
440
  }
455
441
 
456
- // Step 12 — slash surface backed by active AW skills. No ECC command files
457
- // are linked; each adapter points directly at a registry SKILL.md.
458
- safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, awHome }), writer);
442
+ // Step 12 — slash command surface (10 stage commands from ECC).
443
+ safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
444
+
445
+ // Step 12a — full registry command surface.
446
+ // `ensureCommandSurface` only links the 10 AW routing-stage commands from
447
+ // ~/.aw-ecc/commands/. The full registry (~100+ domain commands like
448
+ // platform-review-security-hardening) lives in ~/.aw/.aw_registry/ and is
449
+ // populated by `aw init` (step 7). Link them all into the harness command
450
+ // dir so `/aw:*` resolution works for every registered command.
451
+ const registryCmdResult = safe(
452
+ 'ensureRegistryCommandSurface',
453
+ () => c4.ensureRegistryCommandSurface({ harness, home, awRegistryDir: awRegistry }),
454
+ writer,
455
+ );
456
+ if (registryCmdResult.ok) {
457
+ const { linked, skipped } = registryCmdResult.value;
458
+ if (linked > 0) {
459
+ writer.stdout(`[aw-c4] registry commands: ${linked} linked, ${skipped} skipped\n`);
460
+ }
461
+ }
459
462
 
460
463
  // Step 12b — Cursor Cloud slash-expand rule (no-op on other harnesses).
461
464
  // The model-side workaround for Cursor Cloud's chat UI not pre-expanding
@@ -523,14 +523,22 @@ function findBrokenRuleReferences(filePaths) {
523
523
  return broken;
524
524
  }
525
525
 
526
- function missingCoreAwSkillFiles(awRegistryDir) {
527
- const expectedSkills = [
528
- 'using-aw-skills',
529
- ...EXPECTED_AW_ROUTES.map(route => `aw-${route}`),
530
- ];
531
- return expectedSkills
532
- .map(skillName => `aw/skills/${skillName}/SKILL.md`)
533
- .filter(relativePath => !awRegistryDir || !existsSync(join(awRegistryDir, relativePath)));
526
+ function missingCorePromptFiles(promptsDir) {
527
+ return EXPECTED_AW_ROUTES
528
+ .map(route => `aw-${route}.md`)
529
+ .filter(fileName => !existsSync(join(promptsDir, fileName)));
530
+ }
531
+
532
+ function missingCoreCursorCommandFiles(commandsDir) {
533
+ return EXPECTED_AW_ROUTES
534
+ .map(route => `${route}.md`)
535
+ .filter(fileName => !existsSync(join(commandsDir, fileName)));
536
+ }
537
+
538
+ function missingCoreClaudeCommandFiles(pluginRoot) {
539
+ return EXPECTED_AW_ROUTES
540
+ .map(route => `commands/${route}.md`)
541
+ .filter(relativePath => !existsSync(join(pluginRoot, relativePath)));
534
542
  }
535
543
 
536
544
  function buildDoctorChecks(homeDir, cwd) {
@@ -540,8 +548,8 @@ function buildDoctorChecks(homeDir, cwd) {
540
548
  const rulesDir = resolveRulesDir(homeDir);
541
549
 
542
550
  const usingAwHookPath = [
543
- join(homeDir, '.aw_registry', 'aw', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
544
- join(homeDir, '.aw', '.aw_registry', 'aw', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
551
+ join(homeDir, '.aw_registry', 'platform', 'core', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
552
+ join(homeDir, '.aw', '.aw_registry', 'platform', 'core', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
545
553
  ].find(existsSync);
546
554
 
547
555
  checks.push(usingAwHookPath
@@ -578,19 +586,6 @@ function buildDoctorChecks(homeDir, cwd) {
578
586
  );
579
587
  }
580
588
 
581
- const missingAwSkills = missingCoreAwSkillFiles(awRegistryDir);
582
- checks.push(
583
- missingAwSkills.length === 0
584
- ? makeCheck('aw-skills-source', 'AW skills source', 'pass', 'AW core route skills are synced under ~/.aw/.aw_registry/aw/skills')
585
- : makeCheck(
586
- 'aw-skills-source',
587
- 'AW skills source',
588
- 'fail',
589
- `AW registry is missing core skill files: ${missingAwSkills.join(', ')}`,
590
- 'Run `aw init` or `aw pull platform` to sync AW skills into ~/.aw/.aw_registry/aw/skills.',
591
- ),
592
- );
593
-
594
589
  if (cwd !== homeDir) {
595
590
  const projectAgentsPath = join(cwd, 'AGENTS.md');
596
591
  const projectClaudePath = join(cwd, 'CLAUDE.md');
@@ -651,10 +646,12 @@ function buildDoctorChecks(homeDir, cwd) {
651
646
  const missingBundleFiles = missingFiles(claudePluginRoot, [
652
647
  '.claude-plugin/plugin.json',
653
648
  'hooks/hooks.json',
649
+ 'skills/using-aw-skills/SKILL.md',
650
+ 'skills/using-aw-skills/hooks/session-start.sh',
654
651
  ]);
655
652
  checks.push(
656
653
  missingBundleFiles.length === 0 && claudeSessionStartStatus.ok
657
- ? makeCheck('claude-plugin-bundle', 'Claude plugin bundle', 'pass', 'Claude plugin bundle contains AW routing hooks')
654
+ ? makeCheck('claude-plugin-bundle', 'Claude plugin bundle', 'pass', 'Claude plugin bundle contains AW routing hooks and router skill files')
658
655
  : makeCheck(
659
656
  'claude-plugin-bundle',
660
657
  'Claude plugin bundle',
@@ -670,8 +667,21 @@ function buildDoctorChecks(homeDir, cwd) {
670
667
  ),
671
668
  );
672
669
 
670
+ const missingPluginCommands = missingCoreClaudeCommandFiles(claudePluginRoot);
671
+ checks.push(
672
+ missingPluginCommands.length === 0
673
+ ? makeCheck('claude-plugin-commands', 'Claude public commands', 'pass', 'Claude plugin bundle exposes the current AW command surface (primary, conditional, and compatibility routes)')
674
+ : makeCheck(
675
+ 'claude-plugin-commands',
676
+ 'Claude public commands',
677
+ 'fail',
678
+ `Claude plugin bundle is missing core command files: ${missingPluginCommands.join(', ')}`,
679
+ 'Refresh the AW Claude plugin so the plugin bundle includes the full AW public command surface.',
680
+ ),
681
+ );
673
682
  } else {
674
683
  checks.push(makeCheck('claude-plugin-bundle', 'Claude plugin bundle', 'warn', 'Claude plugin bundle is not installed', 'Enable/install the AW Claude plugin, then rerun `aw doctor`.'));
684
+ checks.push(makeCheck('claude-plugin-commands', 'Claude public commands', 'warn', 'Claude plugin command bundle could not be inspected because the plugin is not installed'));
675
685
  }
676
686
 
677
687
  const claudeLegacyHooks = parseLegacyClaudeHookTargets(claudeSettings?.hooks?.SessionStart || []);
@@ -857,6 +867,19 @@ function buildDoctorChecks(homeDir, cwd) {
857
867
  : makeCheck('codex-references', 'Codex shared references', 'fail', 'Codex shared references are missing', projectRelinkFix(homeDir, cwd, '~/.codex/references')),
858
868
  );
859
869
 
870
+ const missingPrompts = missingCorePromptFiles(join(homeDir, '.codex', 'prompts'));
871
+ checks.push(
872
+ missingPrompts.length === 0
873
+ ? makeCheck('codex-prompts', 'Codex prompts', 'pass', 'Codex prompt sync produced the current AW prompt surface (primary, conditional, and compatibility routes)')
874
+ : makeCheck(
875
+ 'codex-prompts',
876
+ 'Codex prompts',
877
+ 'fail',
878
+ `Codex is missing core prompt files: ${missingPrompts.join(', ')}`,
879
+ 'Run `aw init` or refresh the AW ECC bundle to regenerate the Codex prompts.',
880
+ ),
881
+ );
882
+
860
883
  const codexAgentsPath = join(homeDir, '.codex', 'AGENTS.md');
861
884
  checks.push(
862
885
  existsSync(codexAgentsPath) && textHasManagedRouterBridge(readText(codexAgentsPath)) && textHasRulesReference(readText(codexAgentsPath))
@@ -921,6 +944,20 @@ function buildDoctorChecks(homeDir, cwd) {
921
944
  : makeCheck('cursor-install-state', 'Cursor install state', 'fail', 'Cursor install-state file is missing', globalInstallStateFix(homeDir, cwd, 'Cursor install state')),
922
945
  );
923
946
 
947
+ const cursorCommandsDir = join(homeDir, '.cursor', 'commands', 'aw');
948
+ const missingCursorCommands = missingCoreCursorCommandFiles(cursorCommandsDir);
949
+ checks.push(
950
+ missingCursorCommands.length === 0
951
+ ? makeCheck('cursor-commands', 'Cursor public commands', 'pass', 'Cursor has the current AW command surface under ~/.cursor/commands/aw/')
952
+ : makeCheck(
953
+ 'cursor-commands',
954
+ 'Cursor public commands',
955
+ 'fail',
956
+ `Cursor is missing core command files: ${missingCursorCommands.join(', ')}`,
957
+ projectRelinkFix(homeDir, cwd, 'AW command files under ~/.cursor/commands/aw/'),
958
+ ),
959
+ );
960
+
924
961
  const cursorMcp = jsonMcpHealth(join(homeDir, '.cursor', 'mcp.json'));
925
962
  checks.push(
926
963
  cursorMcp.present && cursorMcp.url && cursorMcp.authorization
package/commands/init.mjs CHANGED
@@ -49,16 +49,7 @@ import {
49
49
  syncWorktreeSparseCheckout,
50
50
  findNearestWorktree,
51
51
  } from '../git.mjs';
52
- import {
53
- REGISTRY_DIR,
54
- REGISTRY_REPO,
55
- REGISTRY_URL,
56
- AW_REGISTRY_NAMESPACE_DIR,
57
- DOCS_SOURCE_DIR,
58
- AW_DOCS_DIR,
59
- RULES_SOURCE_DIR,
60
- RULES_RUNTIME_DIR,
61
- } from '../constants.mjs';
52
+ import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, RULES_RUNTIME_DIR } from '../constants.mjs';
62
53
  import { syncFileTree } from '../file-tree.mjs';
63
54
  import { installAwUsageHooks, formatAwUsageHooksInstallReport } from '../install-aw-usage-hooks.mjs';
64
55
 
@@ -282,6 +273,7 @@ function installIdeTasks() {
282
273
  }
283
274
 
284
275
  const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
276
+ const DEFAULT_NAMESPACE = 'platform';
285
277
 
286
278
  export async function initCommand(args) {
287
279
  let namespace = args['--namespace'] || null;
@@ -307,6 +299,7 @@ export async function initCommand(args) {
307
299
  let team = nsParts[0] || null;
308
300
  let subTeam = nsParts[1] || null;
309
301
  let folderName = subTeam ? `${team}/${subTeam}` : team;
302
+ const activeNamespace = () => team || DEFAULT_NAMESPACE;
310
303
 
311
304
  if (team && !ALLOWED_NAMESPACES.includes(team)) {
312
305
  const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
@@ -399,7 +392,7 @@ export async function initCommand(args) {
399
392
  const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
400
393
  if (isNewSubTeam) {
401
394
  if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
402
- const newSparsePaths = [AW_REGISTRY_NAMESPACE_DIR, `.aw_registry/${folderName}`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR];
395
+ const newSparsePaths = [`.aw_registry/${folderName}`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR];
403
396
  addToSparseCheckout(AW_HOME, newSparsePaths);
404
397
  config.addPattern(GLOBAL_AW_DIR, folderName);
405
398
  scaffoldNamespace(AW_HOME, folderName);
@@ -447,8 +440,9 @@ export async function initCommand(args) {
447
440
  await installAwEcc(cwd, { silent });
448
441
 
449
442
  ensureAwRuntimeHook(HOME);
450
- syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
451
- await setupMcp(HOME, freshCfg?.namespace || team, { silent });
443
+ const effectiveNamespace = freshCfg?.namespace || activeNamespace();
444
+ syncHomeAndProjectInstructions(cwd, effectiveNamespace);
445
+ await setupMcp(HOME, effectiveNamespace, { silent });
452
446
  await maybeConfigureContextMode(args, { silent });
453
447
  applyStoredStartupPreferences(HOME);
454
448
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
@@ -494,7 +488,7 @@ export async function initCommand(args) {
494
488
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
495
489
  let installedIntegrations = [];
496
490
  if (!silent && !skipIntegrations && !isNewSubTeam) {
497
- installedIntegrations = await autoInstallIntegrations(freshCfg?.namespace || team, { silent });
491
+ installedIntegrations = await autoInstallIntegrations(effectiveNamespace, { silent });
498
492
  }
499
493
 
500
494
  if (silent) {
@@ -546,13 +540,13 @@ export async function initCommand(args) {
546
540
  }
547
541
 
548
542
  // Determine sparse paths
549
- const sparsePaths = [AW_REGISTRY_NAMESPACE_DIR, `.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
543
+ const sparsePaths = [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
550
544
  if (folderName) {
551
545
  sparsePaths.push(`.aw_registry/${folderName}`);
552
546
  }
553
547
 
554
548
  fmt.note([
555
- folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
549
+ `${chalk.dim('namespace:')} ${folderName || activeNamespace()}`,
556
550
  user ? `${chalk.dim('user:')} ${user}` : null,
557
551
  `${chalk.dim('version:')} v${VERSION}`,
558
552
  ].filter(Boolean).join('\n'), 'Config');
@@ -592,7 +586,7 @@ export async function initCommand(args) {
592
586
  }
593
587
 
594
588
  // Create sync config — default to 'platform' when no namespace specified
595
- const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
589
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: activeNamespace(), user });
596
590
  if (folderName) {
597
591
  config.addPattern(GLOBAL_AW_DIR, folderName);
598
592
  scaffoldNamespace(AW_HOME, folderName);
@@ -614,8 +608,8 @@ export async function initCommand(args) {
614
608
 
615
609
  // Parallel batch B: post-ECC setup (instructions and MCP are independent)
616
610
  const [, mcpFiles] = await Promise.all([
617
- Promise.resolve(syncHomeAndProjectInstructions(cwd, team)),
618
- setupMcp(HOME, team, { silent }),
611
+ Promise.resolve(syncHomeAndProjectInstructions(cwd, activeNamespace())),
612
+ setupMcp(HOME, activeNamespace(), { silent }),
619
613
  ]);
620
614
  await maybeConfigureContextMode(args, { silent });
621
615
  // applyStoredStartupPreferences reads settings written by ECC — keep after batch B
@@ -687,7 +681,7 @@ export async function initCommand(args) {
687
681
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
688
682
  let installedIntegrations = [];
689
683
  if (!silent && !skipIntegrations) {
690
- installedIntegrations = await autoInstallIntegrations(team, { silent });
684
+ installedIntegrations = await autoInstallIntegrations(activeNamespace(), { silent });
691
685
  }
692
686
 
693
687
  // Offer to update if a newer version is available
package/commands/pull.mjs CHANGED
@@ -26,7 +26,6 @@ import {
26
26
  import {
27
27
  REGISTRY_DIR,
28
28
  REGISTRY_URL,
29
- AW_REGISTRY_NAMESPACE_DIR,
30
29
  DOCS_SOURCE_DIR,
31
30
  AW_DOCS_DIR,
32
31
  RULES_SOURCE_DIR,
@@ -94,7 +93,7 @@ export async function pullCommand(args) {
94
93
  // Ensure platform pulls also fetch docs and rules on older installs that
95
94
  // pre-date the new sparse-checkout paths.
96
95
  if (input === 'platform') {
97
- addToSparseCheckout(AW_HOME, [AW_REGISTRY_NAMESPACE_DIR, `.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR]);
96
+ addToSparseCheckout(AW_HOME, [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR]);
98
97
  if (!cfg.include.includes('platform')) {
99
98
  config.addPattern(GLOBAL_AW_DIR, 'platform');
100
99
  }
package/constants.mjs CHANGED
@@ -19,9 +19,6 @@ export const REGISTRY_URL = process.env.AW_REGISTRY_URL
19
19
  /** Directory inside the registry repo that holds platform/ and [template]/ */
20
20
  export const REGISTRY_DIR = '.aw_registry';
21
21
 
22
- /** Top-level AW namespace in the registry. Holds active AW-owned skills. */
23
- export const AW_REGISTRY_NAMESPACE_DIR = `${REGISTRY_DIR}/aw`;
24
-
25
22
  /** Directory in platform-docs repo containing documentation (pulled into platform/docs/) */
26
23
  export const DOCS_SOURCE_DIR = 'content';
27
24
 
package/ecc.mjs CHANGED
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
3
3
  const execAsync = promisify(execCb);
4
4
  import {
5
5
  existsSync, readFileSync, readdirSync,
6
- mkdirSync, rmSync, writeFileSync,
6
+ mkdirSync, rmSync, writeFileSync, renameSync,
7
7
  } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import { homedir } from "node:os";
@@ -16,6 +16,7 @@ export const AW_ECC_TAG = "v1.4.66";
16
16
  const REQUIRED_ECC_FILES = [
17
17
  "package.json",
18
18
  "scripts/install-apply.js",
19
+ "scripts/sync-ecc-to-codex.sh",
19
20
  ];
20
21
 
21
22
  const MARKETPLACE_NAME = "aw-marketplace";
@@ -23,31 +24,19 @@ const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
23
24
 
24
25
  function eccDir() { return join(homedir(), ".aw-ecc"); }
25
26
 
26
- // File-copy targets use explicit non-skill, non-command module sets so hooks,
27
- // rules, shared references, and install-state can remain available while AW
28
- // skills come from platform-docs/.aw_registry/aw. Slash access is backed by
29
- // those skills directly, so aw-ecc command artifacts are intentionally skipped.
27
+ // "claude" uses a no-commands module set so hooks, rules, shared references,
28
+ // and install-state land in ~/.claude/ while slash commands stay owned by
29
+ // the marketplace plugin under /aw:*.
30
+ // Using `--without baseline:commands` with broader profiles is unsafe because
31
+ // some optional modules depend on commands-core and cause install-apply to fail.
30
32
  const FILE_COPY_TARGETS = ["claude", "cursor", "codex"];
31
- const FILE_COPY_MODULES_BY_TARGET = {
32
- claude: [
33
- "rules-core",
34
- "agents-core",
35
- "hooks-runtime",
36
- "platform-configs",
37
- ],
38
- cursor: [
39
- "rules-core",
40
- "agents-core",
41
- "hooks-runtime",
42
- "platform-configs",
43
- ],
44
- codex: [
45
- "rules-core",
46
- "agents-core",
47
- "hooks-runtime",
48
- "platform-configs",
49
- ],
50
- };
33
+ const CLAUDE_FILE_COPY_MODULES = [
34
+ "rules-core",
35
+ "agents-core",
36
+ "hooks-runtime",
37
+ "platform-configs",
38
+ "workflow-quality",
39
+ ];
51
40
 
52
41
  const TARGET_STATE = {
53
42
  claude: { state: ".claude/ecc/install-state.json" },
@@ -252,11 +241,12 @@ function cloneOrUpdate(tag, dest) {
252
241
 
253
242
  /**
254
243
  * Transform canonical /aw: references to Cursor-compatible /aw- in installed
255
- * rule files. Cursor's model-side rule still sees hyphenated route mentions in
256
- * some generated home instructions.
244
+ * skill and rule files. Cursor namespaces commands via directory structure
245
+ * (commands/aw/plan.md /aw-plan) rather than colons.
257
246
  */
258
247
  function transformCursorAwRefs(home) {
259
248
  const dirs = [
249
+ join(home, ".cursor", "skills"),
260
250
  join(home, ".cursor", "rules"),
261
251
  ];
262
252
  for (const dir of dirs) {
@@ -290,16 +280,57 @@ function uninstallClaudePlugin() {
290
280
  try { run(`claude plugin marketplace remove ${MARKETPLACE_NAME}`); } catch { /* not registered */ }
291
281
  }
292
282
 
293
- function pruneEccAuthoredSkillsAndCommands(repoDir) {
294
- // The local aw-ecc clone is now only a compatibility runtime for hooks,
295
- // rules, agents, and configs. Active AW skills live in platform-docs under
296
- // ~/.aw/.aw_registry/aw/skills, and slash adapters point at those SKILL.md
297
- // files directly.
298
- for (const relPath of ["commands", "skills"]) {
283
+ /**
284
+ * Move ecc command files from ~/.cursor/commands/*.md ~/.cursor/commands/aw/*.md
285
+ * so Cursor exposes them as /aw:tdd, /aw:plan consistent with Claude Code's plugin namespace.
286
+ * Also updates the ecc-install-state.json paths so nuke can clean them up correctly.
287
+ */
288
+ function namespaceCursorCommands(home) {
289
+ const commandsDir = join(home, ".cursor", "commands");
290
+ const awDir = join(commandsDir, "aw");
291
+ if (!existsSync(commandsDir)) return;
292
+
293
+ // Move any flat .md files (not already in a subdirectory) into aw/
294
+ const moved = [];
295
+ for (const file of readdirSync(commandsDir)) {
296
+ if (!file.endsWith(".md") || file.startsWith(".")) continue;
297
+ const src = join(commandsDir, file);
298
+ mkdirSync(awDir, { recursive: true });
299
+ const dest = join(awDir, file);
299
300
  try {
300
- rmSync(join(repoDir, relPath), { recursive: true, force: true });
301
+ renameSync(src, dest);
302
+ moved.push({ from: src, to: dest });
301
303
  } catch { /* best effort */ }
302
304
  }
305
+
306
+ if (moved.length === 0) return;
307
+
308
+ // Update install-state so nuke removes files from the new aw/ location
309
+ const statePath = join(home, ".cursor", "ecc-install-state.json");
310
+ if (!existsSync(statePath)) return;
311
+ try {
312
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
313
+ const pathMap = new Map(moved.map(({ from, to }) => [from, to]));
314
+ state.operations = (state.operations || []).map((op) => {
315
+ const newPath = op.destinationPath && pathMap.get(op.destinationPath);
316
+ return newPath ? { ...op, destinationPath: newPath } : op;
317
+ });
318
+ writeFileSync(statePath, JSON.stringify(state, null, 2));
319
+ } catch { /* best effort */ }
320
+ }
321
+
322
+ /**
323
+ * Run scripts/sync-ecc-to-codex.sh from the cloned ecc repo.
324
+ * Generates ~/.codex/prompts/*.md (Codex equivalent of slash commands),
325
+ * using aw-*.md for AW-namespaced commands and ecc-*.md for the rest,
326
+ * and merges ~/.codex/AGENTS.md. Best-effort — failure doesn't block init.
327
+ */
328
+ function syncEccToCodex(repoDir) {
329
+ const syncScript = join(repoDir, "scripts", "sync-ecc-to-codex.sh");
330
+ if (!existsSync(syncScript)) return;
331
+ try {
332
+ run(`bash "${syncScript}"`, { cwd: homedir() });
333
+ } catch { /* best effort — codex sync failure is non-blocking */ }
303
334
  }
304
335
 
305
336
  export async function installAwEcc(
@@ -316,7 +347,6 @@ export async function installAwEcc(
316
347
  try {
317
348
  if (eccSpinner) eccSpinner.start('Cloning aw-ecc engine...');
318
349
  await cloneOrUpdateAsync(AW_ECC_TAG, repoDir);
319
- pruneEccAuthoredSkillsAndCommands(repoDir);
320
350
  if (eccSpinner) eccSpinner.message('Installing aw-ecc dependencies...');
321
351
 
322
352
  // Claude Code: plugin install via marketplace CLI (proper agent dispatch)
@@ -351,17 +381,20 @@ export async function installAwEcc(
351
381
 
352
382
  // Always use HOME as cwd so files land in ~/.<target>/ globally.
353
383
  const runCwd = homedir();
354
- const installModules = FILE_COPY_MODULES_BY_TARGET[target] || [];
355
- const installArgs = installModules.length > 0
356
- ? `--target ${target} --modules ${installModules.join(",")}`
357
- : `--target ${target}`;
384
+ const installArgs = target === "claude"
385
+ ? `--target ${target} --modules ${CLAUDE_FILE_COPY_MODULES.join(",")}`
386
+ : `--target ${target} --profile full`;
358
387
  run(
359
388
  `node ${join(repoDir, "scripts/install-apply.js")} ${installArgs}`,
360
389
  { cwd: runCwd },
361
390
  );
362
391
  if (target === "cursor") {
392
+ namespaceCursorCommands(runCwd);
363
393
  transformCursorAwRefs(home);
364
394
  }
395
+ if (target === "codex") {
396
+ syncEccToCodex(repoDir);
397
+ }
365
398
  restoreProtectedConfigs(snapshot);
366
399
  } catch { /* target not supported — skip */ }
367
400
  }));
@@ -413,8 +446,8 @@ export function uninstallAwEcc({ silent = false } = {}) {
413
446
  } catch { /* corrupted state — skip */ }
414
447
  }
415
448
 
416
- // Codex: remove legacy generated prompt files from older ECC sync flows
417
- // (not tracked in install-state — cleaned via manifests, with prefix fallback).
449
+ // Codex: remove generated prompt files from sync-ecc-to-codex.sh
450
+ // (not tracked in install-state — cleaned via manifests, with prefix fallback)
418
451
  const codexPromptsDir = join(HOME, ".codex", "prompts");
419
452
  if (existsSync(codexPromptsDir)) {
420
453
  try {
package/git.mjs CHANGED
@@ -5,14 +5,7 @@ import { mkdtempSync, existsSync, lstatSync, rmSync, readFileSync, symlinkSync,
5
5
  import { join, basename, dirname } from 'node:path';
6
6
  import { homedir, tmpdir } from 'node:os';
7
7
  import { promisify } from 'node:util';
8
- import {
9
- REGISTRY_BASE_BRANCH,
10
- REGISTRY_DIR,
11
- AW_REGISTRY_NAMESPACE_DIR,
12
- DOCS_SOURCE_DIR,
13
- AW_DOCS_DIR,
14
- RULES_SOURCE_DIR,
15
- } from './constants.mjs';
8
+ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR } from './constants.mjs';
16
9
 
17
10
  const exec = promisify(execCb);
18
11
 
@@ -162,7 +155,6 @@ export function includeToSparsePaths(paths) {
162
155
  }
163
156
  result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
164
157
  if (paths.includes('platform')) {
165
- result.add(AW_REGISTRY_NAMESPACE_DIR);
166
158
  result.add(DOCS_SOURCE_DIR);
167
159
  result.add(AW_DOCS_DIR);
168
160
  result.add(RULES_SOURCE_DIR);
@@ -397,9 +389,14 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
397
389
  // git 2.46+ bug on blob:none + no-cone sparse-checkout repos that silently
398
390
  // drops bare-name patterns (e.g. "content", "CODEOWNERS") when HEAD advances.
399
391
  //
400
- // --autostash: AW can have local registry edits pending when sync runs.
401
- // Without autostash, git refuses to rebase and the pull silently aborts.
402
- // Autostash stashes dirty bytes, rebases cleanly, and reapplies them.
392
+ // --autostash: AW writes into the registry working tree from external
393
+ // sources (ensureAwRuntimeHook copies ~/.aw-ecc/.../session-start.sh into a
394
+ // tracked path; transformCursorAwRefs rewrites /aw: /aw- through Cursor
395
+ // skill directory symlinks that resolve into .aw_registry/). When those
396
+ // versions drift, the working tree is dirty at rebase time and rebase
397
+ // refuses to run, silently aborting the entire pull. Autostash stashes the
398
+ // dirty bytes, rebases cleanly, and reapplies them — so the registry stays
399
+ // in sync even when external sources have leaked into the tracked tree.
403
400
  try {
404
401
  await exec(`git -C "${awHome}" rebase --autostash origin/${REGISTRY_BASE_BRANCH}`);
405
402
  updated = true;
@@ -26,10 +26,10 @@ const CODEX_HOME_PHASE_BLUEPRINTS = {
26
26
  marker: this.scriptMarker,
27
27
  phase: 'SessionStart',
28
28
  targetCandidates: [
29
- '$HOME/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh',
30
- '$HOME/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh',
29
+ '$HOME/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh',
30
+ '$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh',
31
31
  ],
32
- warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry/aw. Run aw init or aw pull platform.',
32
+ warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry. Run aw init or aw pull platform.',
33
33
  telemetryHookPath: '$HOME/.aw-ecc/scripts/hooks/aw-usage-session-start.js',
34
34
  harnessEnv: 'codex',
35
35
  });