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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/push.mjs CHANGED
@@ -29,7 +29,7 @@ import { hasRulesChanges, isRulesPushInput, pushRulesCommand } from './push-rule
29
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
30
30
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
31
31
 
32
- const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals'];
32
+ const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals', 'references'];
33
33
 
34
34
  // ── PR content generation ────────────────────────────────────────────
35
35
 
@@ -635,8 +635,8 @@ export async function pushCommand(args) {
635
635
  const registryAbsPath = join(registrySubDir, relFromRegistry);
636
636
  const allFiles = collectBatchFiles(registryAbsPath, registrySubDir);
637
637
  if (allFiles.length === 0) {
638
- fmt.cancel(`Nothing to push in ${chalk.cyan(input)} no agents, skills, commands, or evals found.`);
639
- return;
638
+ // Folder/namespace input batch push
639
+ fmt.cancel(`Nothing to push in ${chalk.cyan(input)} — no agents, skills, commands, evals, or references found.`);
640
640
  }
641
641
  // Only include files that actually have changes (new or modified)
642
642
  const folderChanges = detectChanges(awHome, REGISTRY_DIR);
@@ -688,7 +688,7 @@ export async function pushCommand(args) {
688
688
  fmt.cancel([
689
689
  `Invalid push path: ${chalk.red(resolved.registryPath)}`,
690
690
  '',
691
- ` Content must live under an ${chalk.bold('agents/')}, ${chalk.bold('skills/')}, ${chalk.bold('commands/')}, or ${chalk.bold('evals/')} directory.`,
691
+ ` Content must live under an ${chalk.bold('agents/')}, ${chalk.bold('skills/')}, ${chalk.bold('commands/')}, ${chalk.bold('evals/')}, or ${chalk.bold('references/')} directory.`,
692
692
  '',
693
693
  ` ${chalk.dim('Valid examples:')}`,
694
694
  ` aw push .aw_registry/commerce/quality/agents/unit-tester.md`,
@@ -98,7 +98,7 @@ function searchLocal(workspaceDir, query) {
98
98
  if (!nsEntry.isDirectory() || nsEntry.name.startsWith('.')) continue;
99
99
  const ns = nsEntry.name;
100
100
 
101
- for (const type of ['agents', 'skills', 'commands', 'evals']) {
101
+ for (const type of ['agents', 'skills', 'commands', 'evals', 'references']) {
102
102
  const typeDir = join(workspaceDir, ns, type);
103
103
  if (!existsSync(typeDir)) continue;
104
104
 
@@ -0,0 +1,87 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+
4
+ import * as fmt from '../fmt.mjs';
5
+ import { chalk } from '../fmt.mjs';
6
+ import { removeWorkspaceHookDefaults } from '../codex.mjs';
7
+ import {
8
+ applyStoredStartupPreferences,
9
+ getStartupStatus,
10
+ hasLegacyRepoStartupDefaults,
11
+ saveStartupPreferences,
12
+ } from '../startup.mjs';
13
+
14
+ const HOME = homedir();
15
+
16
+ export function routingCommand(args) {
17
+ const action = String(args._positional?.[0] || 'status').toLowerCase();
18
+
19
+ if (!['status', 'enable', 'disable'].includes(action)) {
20
+ fmt.cancel(`Unknown routing action: ${action}. Use: aw routing status|enable|disable`);
21
+ return;
22
+ }
23
+
24
+ fmt.intro(`aw routing ${action}`);
25
+
26
+ if (action === 'status') {
27
+ return renderStatus();
28
+ }
29
+
30
+ const mode = action === 'disable' ? 'disabled' : 'enabled';
31
+ const cwd = process.cwd();
32
+ const removedLegacyFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
33
+
34
+ saveStartupPreferences(mode, HOME);
35
+ const updatedFiles = applyStoredStartupPreferences(HOME);
36
+ const status = getStartupStatus(HOME);
37
+
38
+ fmt.outro([
39
+ `⟁ Session routing ${action === 'disable' ? 'disabled' : 'enabled'}`,
40
+ '',
41
+ ` ${chalk.green('✓')} Preference saved: ${chalk.dim(status.preferencesPath.replace(`${HOME}/`, '~/'))}`,
42
+ ` ${chalk.green('✓')} Claude session routing: ${status.claudeDisabled ? chalk.yellow('disabled') : chalk.green('enabled')}`,
43
+ ` ${chalk.green('✓')} Codex session routing: ${status.codexHooksEnabled && status.codexSessionStartPresent ? chalk.green('enabled') : chalk.yellow('disabled')}`,
44
+ ` ${chalk.green('✓')} Cursor session routing: ${status.cursorSessionStartPresent ? chalk.green('enabled') : chalk.yellow('disabled')}`,
45
+ removedLegacyFiles.length > 0
46
+ ? ` ${chalk.green('✓')} Removed ${removedLegacyFiles.length} legacy repo routing file${removedLegacyFiles.length > 1 ? 's' : ''} from the current project`
47
+ : null,
48
+ updatedFiles.length === 0
49
+ ? ` ${chalk.dim('Note:')} no global routing files needed changes`
50
+ : null,
51
+ ].filter(Boolean).join('\n'));
52
+ }
53
+
54
+ function renderStatus() {
55
+ const status = getStartupStatus(HOME);
56
+ const codexStatus = status.codexHooksEnabled && status.codexSessionStartPresent
57
+ ? 'enabled'
58
+ : status.codexSessionStartPresent && !status.codexHooksEnabled
59
+ ? 'hook installed but codex_hooks is disabled'
60
+ : 'disabled';
61
+ const claudeStatus = status.claudeDisabled
62
+ ? 'disabled via ~/.claude/settings.json override'
63
+ : status.claudeLegacySessionStartPresent
64
+ ? 'enabled (legacy hook present; run aw routing enable to clean)'
65
+ : status.claudePluginEnabled && status.claudePluginInstalled
66
+ ? 'enabled (plugin installed)'
67
+ : status.claudePluginEnabled && !status.claudePluginInstalled
68
+ ? 'misconfigured (plugin enabled in settings but not installed)'
69
+ : 'disabled (plugin not installed)';
70
+
71
+ fmt.note([
72
+ `${chalk.dim('mode:')} ${status.mode}`,
73
+ `${chalk.dim('prefs:')} ${status.preferencesPath.replace(`${HOME}/`, '~/')}`,
74
+ `${chalk.dim('claude session routing:')} ${claudeStatus}`,
75
+ `${chalk.dim('codex session routing:')} ${codexStatus}`,
76
+ `${chalk.dim('codex runtime:')} ${status.codexSessionStartScriptInstalled ? 'installed' : 'not installed'}`,
77
+ `${chalk.dim('cursor session routing:')} ${status.cursorSessionStartPresent ? 'enabled' : 'disabled'}`,
78
+ `${chalk.dim('cursor runtime:')} ${status.cursorSessionStartScriptInstalled ? 'installed' : 'not installed'}`,
79
+ process.cwd() !== HOME
80
+ ? `${chalk.dim('current repo legacy routing files:')} ${hasLegacyRepoStartupDefaults(process.cwd()) ? 'present' : 'not detected'}`
81
+ : null,
82
+ ].join('\n'), 'Routing');
83
+
84
+ fmt.outro('⟁ Routing status complete');
85
+ }
86
+
87
+ export const startupCommand = routingCommand;
package/constants.mjs CHANGED
@@ -25,8 +25,10 @@ export const DOCS_SOURCE_DIR = 'content';
25
25
  /** Persistent git clone root — ~/.aw/ */
26
26
  export const AW_HOME = join(homedir(), '.aw');
27
27
 
28
- /** Directory in platform-docs repo containing platform rules (pulled into .aw_registry/.aw_rules/) */
28
+ /** Directory in platform-docs repo containing platform rules (synced into root-level .aw_rules/) */
29
29
  export const RULES_SOURCE_DIR = '.aw_rules';
30
+ /** Runtime location exposed to harnesses and generated instructions */
31
+ export const RULES_RUNTIME_DIR = '.aw/.aw_rules';
30
32
  /** Telemetry endpoint — override with AW_TELEMETRY_URL env var */
31
33
  export const TELEMETRY_URL = process.env.AW_TELEMETRY_URL || 'https://services.leadconnectorhq.com/agentic-workspace/api/telemetry/events';
32
34
 
package/ecc.mjs CHANGED
@@ -6,21 +6,30 @@ import {
6
6
  import { dirname, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import * as fmt from "./fmt.mjs";
9
+ import { applyStoredStartupPreferences } from "./startup.mjs";
9
10
 
10
11
  const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
11
12
  const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
12
- const AW_ECC_TAG = "v1.4.32";
13
+ export const AW_ECC_TAG = "v1.4.37";
13
14
 
14
15
  const MARKETPLACE_NAME = "aw-marketplace";
15
16
  const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
16
17
 
17
18
  function eccDir() { return join(homedir(), ".aw-ecc"); }
18
19
 
19
- // "claude" runs install-apply.js --without baseline:commands so hooks, rules,
20
- // and agents land in ~/.claude/ — but commands are skipped because the plugin
21
- // marketplace (installClaudePlugin) already registers them under /aw:tdd.
22
- // File-copying commands would create a duplicate flat /tdd alongside /aw:tdd.
20
+ // "claude" uses a no-commands module set so hooks, rules, shared references,
21
+ // and install-state land in ~/.claude/ — while slash commands stay owned by
22
+ // the marketplace plugin under /aw:*.
23
+ // Using `--without baseline:commands` with broader profiles is unsafe because
24
+ // some optional modules depend on commands-core and cause install-apply to fail.
23
25
  const FILE_COPY_TARGETS = ["claude", "cursor", "codex"];
26
+ const CLAUDE_FILE_COPY_MODULES = [
27
+ "rules-core",
28
+ "agents-core",
29
+ "hooks-runtime",
30
+ "platform-configs",
31
+ "workflow-quality",
32
+ ];
24
33
 
25
34
  const TARGET_STATE = {
26
35
  claude: { state: ".claude/ecc/install-state.json" },
@@ -40,6 +49,20 @@ function run(cmd, opts = {}) {
40
49
  return execSync(cmd, { stdio: "pipe", ...opts });
41
50
  }
42
51
 
52
+ function fetchOverrideRef(dest, ref) {
53
+ run(`git -C ${dest} fetch --quiet --depth 1 origin ${ref}`);
54
+ run(`git -C ${dest} checkout --quiet FETCH_HEAD`);
55
+ }
56
+
57
+ function cloneWithRef(url, ref, dest) {
58
+ try {
59
+ run(`git clone --quiet --depth 1 --branch ${ref} "${url}" "${dest}"`);
60
+ } catch {
61
+ run(`git clone --quiet --depth 1 "${url}" "${dest}"`);
62
+ fetchOverrideRef(dest, ref);
63
+ }
64
+ }
65
+
43
66
  function readIfExists(path) {
44
67
  try { return existsSync(path) ? readFileSync(path, "utf8") : null; } catch { return null; }
45
68
  }
@@ -96,20 +119,39 @@ function relProtectedPath(absPath, home) {
96
119
  function cloneOrUpdate(tag, dest) {
97
120
  // AW_ECC_CLONE_URL overrides the remote (used in tests to point at a local fake repo)
98
121
  const overrideUrl = process.env.AW_ECC_CLONE_URL;
122
+ // AW_ECC_CLONE_REF lets aw init install ECC from a non-default branch or tag.
123
+ const overrideRef = process.env.AW_ECC_CLONE_REF?.trim();
99
124
 
100
125
  if (existsSync(join(dest, ".git"))) {
101
126
  try {
102
- if (!overrideUrl) {
127
+ if (!overrideUrl && !overrideRef) {
103
128
  run(`git -C ${dest} fetch --quiet --depth 1 origin tag ${tag}`);
104
129
  run(`git -C ${dest} checkout --quiet ${tag}`);
130
+ } else {
131
+ if (overrideUrl) {
132
+ run(`git -C ${dest} remote set-url origin "${overrideUrl}"`);
133
+ }
134
+ fetchOverrideRef(dest, overrideRef || tag);
105
135
  }
106
136
  return;
107
137
  } catch { /* fall through to fresh clone */ }
108
138
  }
109
139
  if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
110
140
  if (overrideUrl) {
111
- run(`git clone --quiet --depth 1 "${overrideUrl}" "${dest}"`);
141
+ if (overrideRef) {
142
+ cloneWithRef(overrideUrl, overrideRef, dest);
143
+ } else {
144
+ run(`git clone --quiet --depth 1 "${overrideUrl}" "${dest}"`);
145
+ }
112
146
  } else {
147
+ if (overrideRef) {
148
+ try {
149
+ cloneWithRef(AW_ECC_REPO_SSH, overrideRef, dest);
150
+ } catch {
151
+ cloneWithRef(AW_ECC_REPO_HTTPS, overrideRef, dest);
152
+ }
153
+ return;
154
+ }
113
155
  try {
114
156
  run(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_SSH} ${dest}`);
115
157
  } catch {
@@ -118,6 +160,32 @@ function cloneOrUpdate(tag, dest) {
118
160
  }
119
161
  }
120
162
 
163
+ /**
164
+ * Transform canonical /aw: references to Cursor-compatible /aw- in installed
165
+ * skill and rule files. Cursor namespaces commands via directory structure
166
+ * (commands/aw/plan.md → /aw-plan) rather than colons.
167
+ */
168
+ function transformCursorAwRefs(home) {
169
+ const dirs = [
170
+ join(home, ".cursor", "skills"),
171
+ join(home, ".cursor", "rules"),
172
+ ];
173
+ for (const dir of dirs) {
174
+ if (!existsSync(dir)) continue;
175
+ for (const entry of readdirSync(dir, { withFileTypes: true, recursive: true })) {
176
+ if (!entry.isFile()) continue;
177
+ const ext = entry.name.split(".").pop();
178
+ if (!["md", "mdc"].includes(ext)) continue;
179
+ const filePath = join(entry.parentPath || entry.path, entry.name);
180
+ try {
181
+ const content = readFileSync(filePath, "utf8");
182
+ if (!content.includes("/aw:")) continue;
183
+ writeFileSync(filePath, content.replace(/\/aw:/g, "/aw-"));
184
+ } catch { /* best effort */ }
185
+ }
186
+ }
187
+ }
188
+
121
189
  function installClaudePlugin(repoDir) {
122
190
  // Always remove first so we re-register from the correct repoDir.
123
191
  // Falling back to `marketplace update` is unsafe — it updates from the
@@ -174,7 +242,8 @@ function namespaceCursorCommands(home) {
174
242
 
175
243
  /**
176
244
  * Run scripts/sync-ecc-to-codex.sh from the cloned ecc repo.
177
- * Generates ~/.codex/prompts/ecc-*.md (Codex equivalent of slash commands)
245
+ * Generates ~/.codex/prompts/*.md (Codex equivalent of slash commands),
246
+ * using aw-*.md for AW-namespaced commands and ecc-*.md for the rest,
178
247
  * and merges ~/.codex/AGENTS.md. Best-effort — failure doesn't block init.
179
248
  */
180
249
  function syncEccToCodex(repoDir) {
@@ -185,25 +254,6 @@ function syncEccToCodex(repoDir) {
185
254
  } catch { /* best effort — codex sync failure is non-blocking */ }
186
255
  }
187
256
 
188
- /**
189
- * Run ECC repair after install so managed home manifests self-heal when local
190
- * hook/config files have drifted. This is especially important for Codex where
191
- * hook registration and wrapper scripts can get out of sync with ~/.aw-ecc.
192
- */
193
- function repairEccInstall(repoDir, targets) {
194
- const repairScript = join(repoDir, "scripts", "repair.js");
195
- if (!existsSync(repairScript) || !Array.isArray(targets) || targets.length === 0) return;
196
-
197
- const targetArgs = targets
198
- .filter(Boolean)
199
- .map((target) => ` --target ${target}`)
200
- .join("");
201
-
202
- try {
203
- run(`node "${repairScript}"${targetArgs}`, { cwd: homedir() });
204
- } catch { /* best effort — repair failure is non-blocking */ }
205
- }
206
-
207
257
  export async function installAwEcc(
208
258
  cwd,
209
259
  { targets = ["cursor", "claude", "codex"], silent = false } = {},
@@ -232,25 +282,41 @@ export async function installAwEcc(
232
282
  run("npm install --no-audit --no-fund --ignore-scripts --loglevel=error", {
233
283
  cwd: repoDir,
234
284
  });
285
+ // generate-aw-hooks.js produces hooks.json and hook script sources that
286
+ // install-apply.js needs. npm install uses --ignore-scripts so the
287
+ // prepare script (which normally runs this) is skipped — call explicitly.
288
+ const generateHooksScript = join(repoDir, "scripts", "generate-aw-hooks.js");
289
+ if (existsSync(generateHooksScript)) {
290
+ try {
291
+ run(`node "${generateHooksScript}"`, { cwd: repoDir });
292
+ } catch { /* best effort — older engine versions may not have this script */ }
293
+ }
235
294
  for (const target of fileCopyTargets) {
236
295
  try {
237
296
  const snapshot = snapshotProtectedConfigs(home, target);
238
297
 
239
298
  // Always use HOME as cwd so files land in ~/.<target>/ globally.
240
299
  const runCwd = homedir();
241
- // For claude: skip commands (plugin handles them as /aw:tdd) but
242
- // install hooks, rules, agents into ~/.claude/.
243
- const withoutFlag = target === "claude" ? " --without baseline:commands" : "";
300
+ // For claude: install the safe no-commands module set. The plugin
301
+ // already owns /aw:* command registration, and broader profiles with
302
+ // `--without baseline:commands` can fail on commands-core deps.
303
+ const installArgs = target === "claude"
304
+ ? `--target ${target} --modules ${CLAUDE_FILE_COPY_MODULES.join(",")}`
305
+ : `--target ${target} --profile full`;
244
306
  run(
245
- `node ${join(repoDir, "scripts/install-apply.js")} --target ${target} --profile full${withoutFlag}`,
307
+ `node ${join(repoDir, "scripts/install-apply.js")} ${installArgs}`,
246
308
  { cwd: runCwd },
247
309
  );
248
310
  // Move cursor commands into aw/ subfolder for namespace consistency
249
311
  // so they're accessible as /aw:tdd, /aw:plan — same as Claude Code plugin.
250
312
  if (target === "cursor") {
251
313
  namespaceCursorCommands(runCwd);
314
+ // Cursor commands use hyphens (commands/aw/plan.md -> /aw-plan)
315
+ // but source skill/rule files use canonical colons (/aw:plan).
316
+ // Transform /aw: -> /aw- in installed cursor skill/rule files.
317
+ transformCursorAwRefs(home);
252
318
  }
253
- // Run sync script for codex: generates ~/.codex/prompts/ecc-*.md and
319
+ // Run sync script for codex: generates ~/.codex/prompts/*.md and
254
320
  // merges AGENTS.md — Codex has no slash commands, so prompts are the
255
321
  // equivalent of commands.
256
322
  if (target === "codex") {
@@ -262,13 +328,10 @@ export async function installAwEcc(
262
328
  restoreProtectedConfigs(snapshot);
263
329
  } catch { /* target not supported — skip */ }
264
330
  }
265
-
266
- // Enforce post-install parity for managed home files after all target-
267
- // specific steps have run. This catches drifted local manifests that the
268
- // initial install/sync path may otherwise leave partially updated.
269
- repairEccInstall(repoDir, fileCopyTargets);
270
331
  }
271
332
 
333
+ applyStoredStartupPreferences();
334
+
272
335
  if (!silent) fmt.logSuccess("aw-ecc engine installed");
273
336
  } catch (err) {
274
337
  if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
@@ -312,17 +375,42 @@ export function uninstallAwEcc({ silent = false } = {}) {
312
375
  } catch { /* corrupted state — skip */ }
313
376
  }
314
377
 
315
- // Codex: remove ecc prompt files generated by sync-ecc-to-codex.sh
316
- // (not tracked in install-state — cleaned by prefix pattern ecc-*.md)
378
+ // Codex: remove generated prompt files from sync-ecc-to-codex.sh
379
+ // (not tracked in install-state — cleaned via manifests, with prefix fallback)
317
380
  const codexPromptsDir = join(HOME, ".codex", "prompts");
318
381
  if (existsSync(codexPromptsDir)) {
319
382
  try {
320
- for (const file of readdirSync(codexPromptsDir)) {
321
- if (file.startsWith("ecc-") && file.endsWith(".md")) {
322
- rmSync(join(codexPromptsDir, file), { force: true });
383
+ const promptFiles = new Set();
384
+ for (const manifestName of ["ecc-prompts-manifest.txt", "ecc-extension-prompts-manifest.txt"]) {
385
+ const manifestPath = join(codexPromptsDir, manifestName);
386
+ if (!existsSync(manifestPath)) continue;
387
+ try {
388
+ const entries = readFileSync(manifestPath, "utf8")
389
+ .split(/\r?\n/)
390
+ .map((line) => line.trim())
391
+ .filter(Boolean);
392
+ for (const entry of entries) promptFiles.add(entry);
393
+ } catch { /* best effort */ }
394
+ rmSync(manifestPath, { force: true });
395
+ }
396
+
397
+ for (const file of promptFiles) {
398
+ const promptPath = join(codexPromptsDir, file);
399
+ if (existsSync(promptPath)) {
400
+ rmSync(promptPath, { force: true });
323
401
  removed++;
324
402
  }
325
403
  }
404
+
405
+ for (const file of readdirSync(codexPromptsDir)) {
406
+ const isLegacyGeneratedPrompt = file.endsWith(".md")
407
+ && (file.startsWith("ecc-") || file.startsWith("aw-"));
408
+ if (!isLegacyGeneratedPrompt) continue;
409
+
410
+ rmSync(join(codexPromptsDir, file), { force: true });
411
+ removed++;
412
+ }
413
+
326
414
  pruneEmptyParents(codexPromptsDir, join(HOME, ".codex"));
327
415
  } catch { /* best effort */ }
328
416
  }
package/git.mjs CHANGED
@@ -124,34 +124,15 @@ export async function initPersistentClone(repoUrl, awHome, sparsePaths) {
124
124
  }
125
125
 
126
126
  try {
127
- execSync(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${awHome}"`, {
128
- stdio: 'pipe',
129
- encoding: 'utf8',
130
- env: {
131
- ...process.env,
132
- GIT_TERMINAL_PROMPT: process.env.GIT_TERMINAL_PROMPT ?? '0',
133
- },
134
- });
127
+ await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${awHome}"`);
135
128
  } catch (e) {
136
129
  throw new Error(`Failed to clone ${repoUrl}: ${e.message}`);
137
130
  }
138
131
 
139
132
  try {
140
- execSync('git sparse-checkout init --no-cone', {
141
- cwd: awHome,
142
- stdio: 'pipe',
143
- encoding: 'utf8',
144
- });
145
- execSync(`git sparse-checkout set ${sparsePaths.map(p => `"${p}"`).join(' ')}`, {
146
- cwd: awHome,
147
- stdio: 'pipe',
148
- encoding: 'utf8',
149
- });
150
- execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, {
151
- cwd: awHome,
152
- stdio: 'pipe',
153
- encoding: 'utf8',
154
- });
133
+ await exec('git sparse-checkout init --no-cone', { cwd: awHome });
134
+ await exec(`git sparse-checkout set ${sparsePaths.map(p => `"${p}"`).join(' ')}`, { cwd: awHome });
135
+ await exec(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: awHome });
155
136
  } catch (e) {
156
137
  throw new Error(`Failed to configure sparse checkout: ${e.message}`);
157
138
  }
@@ -0,0 +1,195 @@
1
+ export const AW_HOOK_PHASES = [
2
+ 'SessionStart',
3
+ 'UserPromptSubmit',
4
+ 'PreToolUse',
5
+ 'PostToolUse',
6
+ 'Stop',
7
+ 'SessionEnd',
8
+ 'PostToolUseFailure',
9
+ 'PreCompact',
10
+ ];
11
+
12
+ const REQUIRED = 'required';
13
+ const EXTENDED = 'extended';
14
+
15
+ const NATIVE = 'native';
16
+ const EQUIVALENT = 'equivalent';
17
+ const UNSUPPORTED = 'unsupported';
18
+
19
+ export const AW_HOOK_MANIFEST = [
20
+ {
21
+ phase: 'SessionStart',
22
+ tier: REQUIRED,
23
+ sharedImplementation: 'session-start',
24
+ harnesses: {
25
+ claude: {
26
+ home: { status: NATIVE, events: ['SessionStart'] },
27
+ workspace: { status: NATIVE, events: ['SessionStart'] },
28
+ },
29
+ codex: {
30
+ home: { status: NATIVE, events: ['SessionStart'] },
31
+ workspace: { status: NATIVE, events: ['SessionStart'] },
32
+ },
33
+ cursor: {
34
+ home: { status: NATIVE, events: ['sessionStart'] },
35
+ workspace: { status: NATIVE, events: ['sessionStart'] },
36
+ },
37
+ },
38
+ },
39
+ {
40
+ phase: 'UserPromptSubmit',
41
+ tier: REQUIRED,
42
+ sharedImplementation: 'user-prompt-submit',
43
+ harnesses: {
44
+ claude: {
45
+ home: { status: NATIVE, events: ['UserPromptSubmit'] },
46
+ workspace: { status: NATIVE, events: ['UserPromptSubmit'] },
47
+ },
48
+ codex: {
49
+ home: { status: NATIVE, events: ['UserPromptSubmit'] },
50
+ workspace: { status: NATIVE, events: ['UserPromptSubmit'] },
51
+ },
52
+ cursor: {
53
+ home: { status: EQUIVALENT, events: ['beforeSubmitPrompt'] },
54
+ workspace: { status: UNSUPPORTED, events: [] },
55
+ },
56
+ },
57
+ },
58
+ {
59
+ phase: 'PreToolUse',
60
+ tier: REQUIRED,
61
+ sharedImplementation: 'pre-tool-use',
62
+ harnesses: {
63
+ claude: {
64
+ home: { status: NATIVE, events: ['PreToolUse'] },
65
+ workspace: { status: UNSUPPORTED, events: [] },
66
+ },
67
+ codex: {
68
+ home: { status: NATIVE, events: ['PreToolUse'] },
69
+ workspace: { status: UNSUPPORTED, events: [] },
70
+ },
71
+ cursor: {
72
+ home: { status: EQUIVALENT, events: ['beforeShellExecution', 'beforeMCPExecution'] },
73
+ workspace: { status: UNSUPPORTED, events: [] },
74
+ },
75
+ },
76
+ },
77
+ {
78
+ phase: 'PostToolUse',
79
+ tier: REQUIRED,
80
+ sharedImplementation: 'post-tool-use',
81
+ harnesses: {
82
+ claude: {
83
+ home: { status: NATIVE, events: ['PostToolUse'] },
84
+ workspace: { status: UNSUPPORTED, events: [] },
85
+ },
86
+ codex: {
87
+ home: { status: NATIVE, events: ['PostToolUse'] },
88
+ workspace: { status: UNSUPPORTED, events: [] },
89
+ },
90
+ cursor: {
91
+ home: { status: EQUIVALENT, events: ['afterShellExecution', 'afterFileEdit', 'afterMCPExecution'] },
92
+ workspace: { status: UNSUPPORTED, events: [] },
93
+ },
94
+ },
95
+ },
96
+ {
97
+ phase: 'Stop',
98
+ tier: REQUIRED,
99
+ sharedImplementation: 'stop',
100
+ harnesses: {
101
+ claude: {
102
+ home: { status: NATIVE, events: ['Stop'] },
103
+ workspace: { status: UNSUPPORTED, events: [] },
104
+ },
105
+ codex: {
106
+ home: { status: NATIVE, events: ['Stop'] },
107
+ workspace: { status: UNSUPPORTED, events: [] },
108
+ },
109
+ cursor: {
110
+ home: { status: NATIVE, events: ['stop'] },
111
+ workspace: { status: UNSUPPORTED, events: [] },
112
+ },
113
+ },
114
+ },
115
+ {
116
+ phase: 'SessionEnd',
117
+ tier: EXTENDED,
118
+ sharedImplementation: 'session-end',
119
+ harnesses: {
120
+ claude: {
121
+ home: { status: UNSUPPORTED, events: [] },
122
+ workspace: { status: UNSUPPORTED, events: [] },
123
+ },
124
+ codex: {
125
+ home: { status: UNSUPPORTED, events: [] },
126
+ workspace: { status: UNSUPPORTED, events: [] },
127
+ },
128
+ cursor: {
129
+ home: { status: NATIVE, events: ['sessionEnd'] },
130
+ workspace: { status: UNSUPPORTED, events: [] },
131
+ },
132
+ },
133
+ },
134
+ {
135
+ phase: 'PostToolUseFailure',
136
+ tier: EXTENDED,
137
+ sharedImplementation: 'post-tool-use-failure',
138
+ harnesses: {
139
+ claude: {
140
+ home: { status: NATIVE, events: ['PostToolUseFailure'] },
141
+ workspace: { status: UNSUPPORTED, events: [] },
142
+ },
143
+ codex: {
144
+ home: { status: UNSUPPORTED, events: [] },
145
+ workspace: { status: UNSUPPORTED, events: [] },
146
+ },
147
+ cursor: {
148
+ home: { status: UNSUPPORTED, events: [] },
149
+ workspace: { status: UNSUPPORTED, events: [] },
150
+ },
151
+ },
152
+ },
153
+ {
154
+ phase: 'PreCompact',
155
+ tier: EXTENDED,
156
+ sharedImplementation: 'pre-compact',
157
+ harnesses: {
158
+ claude: {
159
+ home: { status: NATIVE, events: ['PreCompact'] },
160
+ workspace: { status: UNSUPPORTED, events: [] },
161
+ },
162
+ codex: {
163
+ home: { status: UNSUPPORTED, events: [] },
164
+ workspace: { status: UNSUPPORTED, events: [] },
165
+ },
166
+ cursor: {
167
+ home: { status: NATIVE, events: ['preCompact'] },
168
+ workspace: { status: UNSUPPORTED, events: [] },
169
+ },
170
+ },
171
+ },
172
+ ];
173
+
174
+ export function getHookManifestEntry(phase) {
175
+ const entry = AW_HOOK_MANIFEST.find(item => item.phase === phase);
176
+ if (!entry) {
177
+ throw new Error(`Unknown AW hook phase: ${phase}`);
178
+ }
179
+ return entry;
180
+ }
181
+
182
+ export function getHarnessPhaseEntries(harness, surface = 'home') {
183
+ return AW_HOOK_MANIFEST.map(entry => ({
184
+ ...entry,
185
+ harness: entry.harnesses[harness]?.[surface] || { status: UNSUPPORTED, events: [] },
186
+ }));
187
+ }
188
+
189
+ export function getSupportedHarnessPhaseEntries(harness, surface = 'home') {
190
+ return getHarnessPhaseEntries(harness, surface).filter(entry => entry.harness.status !== UNSUPPORTED);
191
+ }
192
+
193
+ export function getRequiredHarnessPhaseEntries(harness, surface = 'home') {
194
+ return getSupportedHarnessPhaseEntries(harness, surface).filter(entry => entry.tier === REQUIRED);
195
+ }