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

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/git.mjs CHANGED
@@ -5,7 +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 { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR } from './constants.mjs';
8
+ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, RULES_SOURCE_DIR } from './constants.mjs';
9
9
 
10
10
  const exec = promisify(execCb);
11
11
 
@@ -91,6 +91,7 @@ export function includeToSparsePaths(paths) {
91
91
  result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
92
92
  if (paths.includes('platform')) {
93
93
  result.add(DOCS_SOURCE_DIR);
94
+ result.add(RULES_SOURCE_DIR);
94
95
  }
95
96
  return [...result];
96
97
  }
package/hooks.mjs CHANGED
@@ -9,6 +9,7 @@ import { execSync } from 'node:child_process';
9
9
  import { join } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import * as fmt from './fmt.mjs';
12
+ import { AW_CO_AUTHOR } from './constants.mjs';
12
13
 
13
14
  const HOME = homedir();
14
15
  const HOOKS_DIR = join(HOME, '.aw', 'hooks');
@@ -113,6 +114,27 @@ fi
113
114
  exit 0
114
115
  `;
115
116
 
117
+ // prepare-commit-msg: synchronous hook — appends Co-Authored-By trailer to commits
118
+ // in AW-linked repos. Skips merge commits and respects AW_NO_COAUTHOR=1 opt-out.
119
+ const PREPARE_COMMIT_MSG = makeDispatcher('prepare-commit-msg', `\
120
+ # Inject co-author trailer unless it's a merge commit or opt-out is set
121
+ if [ "$2" != "merge" ] && [ "$AW_NO_COAUTHOR" != "1" ]; then
122
+ # Only brand AW-linked repos (check for .aw/ symlink or worktree)
123
+ if [ -L ".aw" ] || [ -f ".aw/.git" ]; then
124
+ TRAILER="${AW_CO_AUTHOR}"
125
+ # Replace Claude/Cursor co-author trailers with AW
126
+ sed -i.bak '/^Co-Authored-By:.*Claude/d' "$1" 2>/dev/null || true
127
+ sed -i.bak '/^Co-Authored-By:.*noreply@anthropic\\.com/d' "$1" 2>/dev/null || true
128
+ sed -i.bak '/^Co-Authored-By:.*Cursor/d' "$1" 2>/dev/null || true
129
+ rm -f "$1.bak"
130
+ # Only append if not already present (idempotent)
131
+ if ! grep -qF "$TRAILER" "$1"; then
132
+ echo "" >> "$1"
133
+ echo "$TRAILER" >> "$1"
134
+ fi
135
+ fi
136
+ fi`);
137
+
116
138
  /**
117
139
  * Install global git hooks at ~/.aw/hooks/ and set core.hooksPath.
118
140
  * If core.hooksPath is already set by another tool, saves the previous
@@ -136,6 +158,9 @@ export function installGlobalHooks() {
136
158
  writeFileSync(join(HOOKS_DIR, 'post-commit'), POST_COMMIT);
137
159
  chmodSync(join(HOOKS_DIR, 'post-commit'), '755');
138
160
 
161
+ writeFileSync(join(HOOKS_DIR, 'prepare-commit-msg'), PREPARE_COMMIT_MSG);
162
+ chmodSync(join(HOOKS_DIR, 'prepare-commit-msg'), '755');
163
+
139
164
  // Detect and preserve existing core.hooksPath
140
165
  let previousPath = null;
141
166
  try {
@@ -199,3 +224,79 @@ export function removeGlobalHooks() {
199
224
 
200
225
  fmt.logStep('Global git hooks removed');
201
226
  }
227
+
228
+ // Standalone prepare-commit-msg for local .git/hooks/ installation.
229
+ // Unlike the global dispatcher, this does NOT chain to repo-local hooks
230
+ // (because it IS the repo-local hook — chaining would cause infinite recursion).
231
+ const LOCAL_PREPARE_COMMIT_MSG = `#!/bin/sh
232
+ # aw: local prepare-commit-msg hook (installed by aw init)
233
+
234
+ # Skip aw temp dirs
235
+ case "$(pwd)" in /tmp/aw-*|/var/folders/*/aw-*|*/.aw|*/.aw/*) exit 0 ;; esac
236
+
237
+ # Skip merge commits
238
+ [ "$2" = "merge" ] && exit 0
239
+
240
+ # Opt-out escape hatch
241
+ [ "$AW_NO_COAUTHOR" = "1" ] && exit 0
242
+
243
+ # Only brand AW-linked repos (check for .aw/ symlink or worktree)
244
+ if [ -L ".aw" ] || [ -f ".aw/.git" ]; then
245
+ TRAILER="${AW_CO_AUTHOR}"
246
+ # Replace Claude/Cursor co-author trailers with AW
247
+ sed -i.bak '/^Co-Authored-By:.*Claude/d' "$1" 2>/dev/null || true
248
+ sed -i.bak '/^Co-Authored-By:.*noreply@anthropic\\.com/d' "$1" 2>/dev/null || true
249
+ sed -i.bak '/^Co-Authored-By:.*Cursor/d' "$1" 2>/dev/null || true
250
+ rm -f "$1.bak"
251
+ # Only append if not already present (idempotent)
252
+ if ! grep -qF "$TRAILER" "$1"; then
253
+ echo "" >> "$1"
254
+ echo "$TRAILER" >> "$1"
255
+ fi
256
+ fi
257
+
258
+ exit 0
259
+ `;
260
+
261
+ /**
262
+ * Install prepare-commit-msg hook into a project's local .git/hooks/.
263
+ * This covers repos where another tool (e.g. Claude Code) sets a local
264
+ * core.hooksPath that overrides the global one.
265
+ *
266
+ * @param {string} projectDir — root of the project (must contain .git/)
267
+ */
268
+ export function installLocalCommitHook(projectDir) {
269
+ if (process.env.AW_NO_HOOKS === '1') return;
270
+ try {
271
+ const gitDir = join(projectDir, '.git');
272
+ if (!existsSync(gitDir)) return;
273
+
274
+ // Only install if a local core.hooksPath is set (overrides our global hooks)
275
+ let localHooksPath;
276
+ try {
277
+ localHooksPath = execSync('git config --local core.hooksPath', {
278
+ cwd: projectDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
279
+ }).trim();
280
+ } catch { return; /* no local override — global hooks will handle it */ }
281
+
282
+ if (!localHooksPath) return;
283
+
284
+ // Resolve relative paths against projectDir
285
+ const hooksDir = localHooksPath.startsWith('/')
286
+ ? localHooksPath
287
+ : join(projectDir, localHooksPath);
288
+
289
+ mkdirSync(hooksDir, { recursive: true });
290
+
291
+ const hookPath = join(hooksDir, 'prepare-commit-msg');
292
+ // Don't overwrite if a non-aw hook already exists
293
+ if (existsSync(hookPath)) {
294
+ const content = readFileSync(hookPath, 'utf8');
295
+ if (!content.includes('aw:') && !content.includes('Co-Authored-By')) return;
296
+ }
297
+
298
+ writeFileSync(hookPath, LOCAL_PREPARE_COMMIT_MSG);
299
+ chmodSync(hookPath, '755');
300
+ } catch { /* best effort */ }
301
+ }
302
+
package/integrate.mjs CHANGED
@@ -6,6 +6,19 @@ import { homedir } from 'node:os';
6
6
  import * as fmt from './fmt.mjs';
7
7
  import * as config from './config.mjs';
8
8
  import { getLocalRegistryDir } from './git.mjs';
9
+ import { renderRules } from './render-rules.mjs';
10
+
11
+ function upsertRulesSection(content, header, section) {
12
+ if (!section) return content;
13
+
14
+ const marker = `## ${header}`;
15
+ const idx = content.indexOf(marker);
16
+ if (idx === -1) {
17
+ return `${content.trimEnd()}\n\n${section}\n`;
18
+ }
19
+
20
+ return `${content.slice(0, idx).trimEnd()}\n\n${section}\n`;
21
+ }
9
22
 
10
23
  /**
11
24
  * Count hand-written commands already present in the registry.
@@ -62,15 +75,29 @@ function findFiles(dir, typeName) {
62
75
  }
63
76
 
64
77
  /**
65
- * Copy AGENTS.md to project root.
66
- * CLAUDE.md is intentionally NOT generated its routing rule hijacks plugin
67
- * commands like /aw:plan, preventing proper agent dispatch.
78
+ * Copy AGENTS.md to project root and refresh rules sections in any existing
79
+ * CLAUDE.md. New CLAUDE.md files are intentionally not generated because their
80
+ * routing rules can hijack plugin command dispatch in some workspaces.
68
81
  */
69
82
  export function copyInstructions(cwd, tempDir, namespace) {
83
+ const rulesSections = renderRules(cwd);
70
84
  const createdFiles = [];
71
- for (const file of ['AGENTS.md']) {
85
+ for (const file of ['AGENTS.md', 'CLAUDE.md']) {
72
86
  const dest = join(cwd, file);
73
- if (existsSync(dest)) continue;
87
+ if (existsSync(dest)) {
88
+ const existing = readFileSync(dest, 'utf8');
89
+ const updated = file === 'CLAUDE.md'
90
+ ? upsertRulesSection(existing, 'Platform Rules (MUST)', rulesSections.claudeSection)
91
+ : upsertRulesSection(existing, 'Platform Rules — Non-Negotiables', rulesSections.agentsSection);
92
+
93
+ if (updated !== existing) {
94
+ writeFileSync(dest, updated);
95
+ fmt.logSuccess(`Updated ${file}`);
96
+ }
97
+ continue;
98
+ }
99
+
100
+ if (file === 'CLAUDE.md') continue;
74
101
 
75
102
  if (tempDir) {
76
103
  const src = join(tempDir, '.aw_registry', file);
@@ -86,7 +113,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
86
113
  }
87
114
  }
88
115
 
89
- const content = generateAgentsMd(cwd, namespace);
116
+ const content = generateAgentsMd(cwd, namespace, rulesSections);
90
117
  if (content) {
91
118
  writeFileSync(dest, content);
92
119
  fmt.logSuccess(`Created ${file}`);
@@ -96,9 +123,9 @@ export function copyInstructions(cwd, tempDir, namespace) {
96
123
  return createdFiles;
97
124
  }
98
125
 
99
- function generateClaudeMd(cwd, namespace) {
126
+ function generateClaudeMd(cwd, namespace, rulesSections = {}) {
100
127
  const team = namespace || 'my-team';
101
- return `# CLAUDE.md — ${team}
128
+ let base = `# CLAUDE.md — ${team}
102
129
 
103
130
  Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\` (shared knowledge), \`git-jenkins\` (CI/CD), \`grafana\` (observability)
104
131
 
@@ -231,11 +258,17 @@ Gates are shell scripts in \`scripts/gates/\` — CANNOT be bypassed by LLM judg
231
258
  - **Agents**: Definitions ≤5 KB, reference material in linked skills
232
259
  - **Local-first**: No MCP calls for orchestration — file reads are faster and always available
233
260
  `;
261
+
262
+ if (rulesSections.claudeSection) {
263
+ base += '\n' + rulesSections.claudeSection;
264
+ }
265
+
266
+ return base;
234
267
  }
235
268
 
236
- function generateAgentsMd(cwd, namespace) {
269
+ function generateAgentsMd(cwd, namespace, rulesSections = {}) {
237
270
  const team = namespace || 'my-team';
238
- return `# AGENTS.md — ${team}
271
+ let base = `# AGENTS.md — ${team}
239
272
 
240
273
  ## Agent System
241
274
 
@@ -367,6 +400,12 @@ Each agent has quality test cases in \`.claude/evals/\`:
367
400
 
368
401
  Run with: \`/platform:eval agent:<slug>\` or \`/platform:eval skill:<slug>\`
369
402
  `;
403
+
404
+ if (rulesSections.agentsSection) {
405
+ base += '\n' + rulesSections.agentsSection;
406
+ }
407
+
408
+ return base;
370
409
  }
371
410
 
372
411
  /**
@@ -450,4 +489,3 @@ function getTeamNamespaces(awDir) {
450
489
  .map(p => p.split('/')[0]),
451
490
  )];
452
491
  }
453
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.37-beta.8",
3
+ "version": "0.1.37",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": "bin.js",
@@ -20,10 +20,13 @@
20
20
  "paths.mjs",
21
21
  "plan.mjs",
22
22
  "registry.mjs",
23
+ "slack-sim/",
24
+ "file-tree.mjs",
23
25
  "apply.mjs",
24
26
  "update.mjs",
25
27
  "hooks.mjs",
26
28
  "ecc.mjs",
29
+ "render-rules.mjs",
27
30
  "telemetry.mjs"
28
31
  ],
29
32
  "engines": {
@@ -39,8 +42,10 @@
39
42
  "author": "GoHighLevel",
40
43
  "license": "MIT",
41
44
  "scripts": {
42
- "test": "vitest run --reporter=verbose",
43
- "test:watch": "vitest --reporter=verbose",
45
+ "test": "yarn test:vitest && yarn test:node",
46
+ "test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs",
47
+ "test:node": "node tests/run-node-tests.mjs",
48
+ "test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs",
44
49
  "preuninstall": "node bin.js nuke 2>/dev/null || true"
45
50
  },
46
51
  "publishConfig": {
@@ -49,7 +54,8 @@
49
54
  "dependencies": {
50
55
  "@clack/prompts": "0.8.2",
51
56
  "chalk": "^5.6.2",
52
- "figlet": "^1.11.0"
57
+ "figlet": "^1.11.0",
58
+ "pg": "^8.13.0"
53
59
  },
54
60
  "devDependencies": {
55
61
  "vitest": "^4.1.2"