@ghl-ai/aw 0.1.39-beta.9 → 0.1.40-beta.0
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/ecc.mjs +1 -1
- package/git.mjs +101 -19
- package/integrate.mjs +54 -33
- package/package.json +1 -1
- package/render-rules.mjs +141 -12
package/ecc.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
|
|
|
10
10
|
|
|
11
11
|
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
12
12
|
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
13
|
-
export const AW_ECC_TAG = "v1.4.
|
|
13
|
+
export const AW_ECC_TAG = "v1.4.37";
|
|
14
14
|
|
|
15
15
|
const MARKETPLACE_NAME = "aw-marketplace";
|
|
16
16
|
const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
package/git.mjs
CHANGED
|
@@ -9,6 +9,49 @@ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, RULES_SOURCE_DIR }
|
|
|
9
9
|
|
|
10
10
|
const exec = promisify(execCb);
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Env vars applied to every git command that touches the network.
|
|
14
|
+
* GIT_TERMINAL_PROMPT=0 prevents git from hanging when it would otherwise
|
|
15
|
+
* prompt for credentials (e.g. HTTPS URL with SSH-only auth configured).
|
|
16
|
+
* Instead, git exits immediately with a non-zero code that we can catch.
|
|
17
|
+
*/
|
|
18
|
+
const GIT_NET_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert an HTTPS GitHub URL to its SSH equivalent.
|
|
22
|
+
* e.g. https://github.com/Org/Repo.git → git@github.com:Org/Repo.git
|
|
23
|
+
*/
|
|
24
|
+
function toSshUrl(httpsUrl) {
|
|
25
|
+
const m = httpsUrl.match(/^https:\/\/github\.com\/(.+)$/);
|
|
26
|
+
return m ? `git@github.com:${m[1]}` : httpsUrl;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect whether the user's git is configured to prefer SSH for github.com.
|
|
31
|
+
* Checks: 1) git insteadOf config 2) gh CLI auth status
|
|
32
|
+
* Returns true if SSH is preferred.
|
|
33
|
+
*/
|
|
34
|
+
function prefersSsh() {
|
|
35
|
+
// Check git url."git@github.com:".insteadOf
|
|
36
|
+
try {
|
|
37
|
+
const out = execSync(
|
|
38
|
+
'git config --global --get-regexp "url\\.git@github\\.com.*\\.insteadOf"',
|
|
39
|
+
{ stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV },
|
|
40
|
+
).trim();
|
|
41
|
+
if (out.includes('https://github.com')) return true;
|
|
42
|
+
} catch { /* not configured */ }
|
|
43
|
+
|
|
44
|
+
// Check gh auth — if protocol is ssh, prefer SSH
|
|
45
|
+
try {
|
|
46
|
+
const out = execSync('gh auth status 2>&1', {
|
|
47
|
+
stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV, timeout: 5000,
|
|
48
|
+
});
|
|
49
|
+
if (/git protocol:\s*ssh/i.test(out)) return true;
|
|
50
|
+
} catch { /* gh not installed or not authed */ }
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
// ── Backward-compat: temp-dir sparse checkout (used by search.mjs) ────────────
|
|
13
56
|
|
|
14
57
|
/**
|
|
@@ -18,12 +61,23 @@ const exec = promisify(execCb);
|
|
|
18
61
|
export function sparseCheckout(repo, paths) {
|
|
19
62
|
const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
|
|
20
63
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
64
|
+
const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
|
|
65
|
+
const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
|
|
66
|
+
|
|
67
|
+
let cloned = false;
|
|
68
|
+
for (const url of urls) {
|
|
69
|
+
try {
|
|
70
|
+
execSync(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, {
|
|
71
|
+
stdio: 'pipe', env: GIT_NET_ENV,
|
|
72
|
+
});
|
|
73
|
+
cloned = true;
|
|
74
|
+
break;
|
|
75
|
+
} catch {
|
|
76
|
+
// Clean up partial clone so next attempt can use the same tempDir
|
|
77
|
+
try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!cloned) {
|
|
27
81
|
throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
|
|
28
82
|
}
|
|
29
83
|
|
|
@@ -47,10 +101,20 @@ export function sparseCheckout(repo, paths) {
|
|
|
47
101
|
export async function sparseCheckoutAsync(repo, paths) {
|
|
48
102
|
const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
|
|
49
103
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
104
|
+
const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
|
|
105
|
+
const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
|
|
106
|
+
|
|
107
|
+
let cloned = false;
|
|
108
|
+
for (const url of urls) {
|
|
109
|
+
try {
|
|
110
|
+
await exec(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, { env: GIT_NET_ENV });
|
|
111
|
+
cloned = true;
|
|
112
|
+
break;
|
|
113
|
+
} catch {
|
|
114
|
+
try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!cloned) {
|
|
54
118
|
throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
|
|
55
119
|
}
|
|
56
120
|
|
|
@@ -106,7 +170,9 @@ export function isValidClone(awHome, repoUrl) {
|
|
|
106
170
|
if (!existsSync(join(awHome, '.git'))) return false;
|
|
107
171
|
try {
|
|
108
172
|
const remote = execSync('git remote get-url origin', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' }).trim();
|
|
109
|
-
|
|
173
|
+
// Normalize both sides to bare repo path for comparison (handles HTTPS ↔ SSH)
|
|
174
|
+
const normalize = (u) => u.replace(/\.git$/, '').replace(/^git@github\.com:/, 'https://github.com/');
|
|
175
|
+
return normalize(remote) === normalize(repoUrl);
|
|
110
176
|
} catch {
|
|
111
177
|
return false;
|
|
112
178
|
}
|
|
@@ -123,10 +189,26 @@ export async function initPersistentClone(repoUrl, awHome, sparsePaths) {
|
|
|
123
189
|
try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
|
|
124
190
|
}
|
|
125
191
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
192
|
+
const urls = prefersSsh()
|
|
193
|
+
? [toSshUrl(repoUrl), repoUrl]
|
|
194
|
+
: [repoUrl, toSshUrl(repoUrl)];
|
|
195
|
+
|
|
196
|
+
let cloned = false;
|
|
197
|
+
for (const url of urls) {
|
|
198
|
+
// Clean up any partial clone from a previous attempt
|
|
199
|
+
if (existsSync(awHome) && !isValidClone(awHome, url)) {
|
|
200
|
+
try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await exec(`git clone --filter=blob:none --no-checkout "${url}" "${awHome}"`, { env: GIT_NET_ENV });
|
|
204
|
+
cloned = true;
|
|
205
|
+
break;
|
|
206
|
+
} catch {
|
|
207
|
+
// try next URL
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!cloned) {
|
|
211
|
+
throw new Error(`Failed to clone ${repoUrl}. Check your git credentials and repo access (HTTPS and SSH both failed).`);
|
|
130
212
|
}
|
|
131
213
|
|
|
132
214
|
try {
|
|
@@ -286,7 +368,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
|
|
|
286
368
|
|
|
287
369
|
// ── 2. Fetch ──────────────────────────────────────────────────────────────
|
|
288
370
|
try {
|
|
289
|
-
await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}
|
|
371
|
+
await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`, { env: GIT_NET_ENV });
|
|
290
372
|
} catch (e) {
|
|
291
373
|
throw new Error(`Failed to fetch from origin: ${e.message}`);
|
|
292
374
|
}
|
|
@@ -315,7 +397,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
|
|
|
315
397
|
// someone else pushed to the remote tracking branch since our last fetch.
|
|
316
398
|
if (isPushBranch) {
|
|
317
399
|
try {
|
|
318
|
-
await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"
|
|
400
|
+
await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`, { env: GIT_NET_ENV });
|
|
319
401
|
} catch { /* non-blocking — divergence will be resolved on next aw push */ }
|
|
320
402
|
}
|
|
321
403
|
} catch {
|
|
@@ -442,7 +524,7 @@ export function updatePushBranch(awHome, pushBranchName) {
|
|
|
442
524
|
}
|
|
443
525
|
|
|
444
526
|
try {
|
|
445
|
-
execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe' });
|
|
527
|
+
execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe', env: GIT_NET_ENV });
|
|
446
528
|
} catch (e) {
|
|
447
529
|
throw new Error(`Failed to push branch: ${e.message}`);
|
|
448
530
|
}
|
|
@@ -485,7 +567,7 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
|
|
|
485
567
|
}
|
|
486
568
|
|
|
487
569
|
try {
|
|
488
|
-
await exec(`git -C "${awHome}" push -u origin "${branchName}"
|
|
570
|
+
await exec(`git -C "${awHome}" push -u origin "${branchName}"`, { env: GIT_NET_ENV });
|
|
489
571
|
} catch (e) {
|
|
490
572
|
throw new Error(`Failed to push branch: ${e.message}`);
|
|
491
573
|
}
|
package/integrate.mjs
CHANGED
|
@@ -114,12 +114,41 @@ function shouldResetHomeInstructionFile(content, file) {
|
|
|
114
114
|
return legacyMarkers.some(marker => content.includes(marker));
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function stripLegacyRepoInstructionContent(content, file) {
|
|
118
|
+
const legacyMarkers = file === 'CLAUDE.md'
|
|
119
|
+
? [
|
|
120
|
+
'# CLAUDE.md — ',
|
|
121
|
+
'## Routing Rule (ABSOLUTE)',
|
|
122
|
+
'This supplements the root `AGENTS.md` with Codex-specific guidance.',
|
|
123
|
+
'<!-- BEGIN ECC -->',
|
|
124
|
+
]
|
|
125
|
+
: [
|
|
126
|
+
'# AGENTS.md — ',
|
|
127
|
+
'# ECC for Codex CLI',
|
|
128
|
+
'# AW SDLC Repo Instructions',
|
|
129
|
+
'Use the repo-local AW SDLC files as the source of truth for routing and stage behavior.',
|
|
130
|
+
'## Agent System',
|
|
131
|
+
'<!-- BEGIN ECC -->',
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const startIndexes = legacyMarkers
|
|
135
|
+
.map(marker => content.indexOf(marker))
|
|
136
|
+
.filter(idx => idx !== -1);
|
|
137
|
+
|
|
138
|
+
if (startIndexes.length === 0) return content;
|
|
139
|
+
|
|
140
|
+
const startIdx = Math.min(...startIndexes);
|
|
141
|
+
const preserved = content.slice(0, startIdx).trimEnd();
|
|
142
|
+
return preserved ? `${preserved}\n` : '';
|
|
143
|
+
}
|
|
144
|
+
|
|
117
145
|
function applyManagedInstructionSections(content, file, rulesSections = {}, options = {}) {
|
|
118
146
|
const rulesHeader = file === 'CLAUDE.md' ? CLAUDE_RULES_HEADER : AGENTS_RULES_HEADER;
|
|
119
147
|
const rulesSection = file === 'CLAUDE.md' ? rulesSections.claudeSection : rulesSections.agentsSection;
|
|
120
148
|
const includeBridge = options.includeBridge !== false;
|
|
121
149
|
|
|
122
|
-
let next =
|
|
150
|
+
let next = stripLegacyRepoInstructionContent(content, file);
|
|
151
|
+
next = stripManagedBlock(next, AW_ROUTER_BRIDGE_START_MARKER, AW_ROUTER_BRIDGE_END_MARKER);
|
|
123
152
|
next = stripManagedSection(next, AW_ROUTER_BRIDGE_HEADER, [rulesHeader]);
|
|
124
153
|
next = stripManagedSection(next, rulesHeader);
|
|
125
154
|
next = next.trimEnd();
|
|
@@ -129,8 +158,15 @@ function applyManagedInstructionSections(content, file, rulesSections = {}, opti
|
|
|
129
158
|
return next ? `${next}\n` : '';
|
|
130
159
|
}
|
|
131
160
|
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
// Marker tells users (and aw init) where the managed section starts.
|
|
162
|
+
// Everything BEFORE this marker is repo-owned and never touched by aw.
|
|
163
|
+
// Everything AFTER is managed by aw — re-rendered on every aw init.
|
|
164
|
+
const managedBoundary = '<!-- aw-managed: content below is regenerated by `aw init` — put your own content above this line -->';
|
|
165
|
+
const appended = [managedBoundary, ...sections].join('\n\n').trim();
|
|
166
|
+
// Strip any prior managedBoundary line from `next` so we don't accumulate them
|
|
167
|
+
// when re-running aw init.
|
|
168
|
+
const cleaned = next.split('\n').filter(line => line.trim() !== managedBoundary).join('\n').trimEnd();
|
|
169
|
+
return cleaned ? `${cleaned}\n\n${appended}\n` : `${appended}\n`;
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
/**
|
|
@@ -188,49 +224,34 @@ function findFiles(dir, typeName) {
|
|
|
188
224
|
}
|
|
189
225
|
|
|
190
226
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
227
|
+
* Refresh rules sections in any existing AGENTS.md/CLAUDE.md at the repo
|
|
228
|
+
* root.
|
|
229
|
+
*
|
|
230
|
+
* Repo instruction files are user-owned. aw init no longer creates or updates
|
|
231
|
+
* managed sections in repo-local AGENTS.md / CLAUDE.md.
|
|
232
|
+
*
|
|
233
|
+
* The only repo-file behavior left is cleanup: if a repo still contains old
|
|
234
|
+
* aw-managed sections from prior versions, strip those sections while leaving
|
|
235
|
+
* the user's own content intact.
|
|
194
236
|
*/
|
|
195
237
|
export function copyInstructions(cwd, tempDir, namespace) {
|
|
196
238
|
const rulesSections = renderRules(cwd);
|
|
197
239
|
const createdFiles = [];
|
|
240
|
+
|
|
198
241
|
for (const file of ['AGENTS.md', 'CLAUDE.md']) {
|
|
199
242
|
const dest = join(cwd, file);
|
|
200
243
|
if (existsSync(dest)) {
|
|
201
244
|
const existing = readFileSync(dest, 'utf8');
|
|
202
|
-
const updated = applyManagedInstructionSections(existing, file,
|
|
245
|
+
const updated = applyManagedInstructionSections(existing, file, {}, { includeBridge: false });
|
|
203
246
|
|
|
204
247
|
if (updated !== existing) {
|
|
205
248
|
writeFileSync(dest, updated);
|
|
206
|
-
fmt.
|
|
249
|
+
fmt.logStep(`Stripped aw-managed sections from ${file} (now in ~/.claude/CLAUDE.md / ~/.codex/AGENTS.md)`);
|
|
207
250
|
}
|
|
208
251
|
continue;
|
|
209
252
|
}
|
|
210
253
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (tempDir) {
|
|
214
|
-
const src = join(tempDir, '.aw_registry', file);
|
|
215
|
-
if (existsSync(src)) {
|
|
216
|
-
let content = readFileSync(src, 'utf8');
|
|
217
|
-
if (namespace) {
|
|
218
|
-
content = content.replace(/\{\{TEAM\}\}/g, namespace);
|
|
219
|
-
}
|
|
220
|
-
content = applyManagedInstructionSections(content, file, rulesSections, { includeBridge: false });
|
|
221
|
-
writeFileSync(dest, content);
|
|
222
|
-
fmt.logSuccess(`Created ${file}`);
|
|
223
|
-
createdFiles.push(dest);
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const content = generateAgentsMd(cwd, namespace, rulesSections);
|
|
229
|
-
if (content) {
|
|
230
|
-
writeFileSync(dest, applyManagedInstructionSections(content, file, rulesSections, { includeBridge: false }));
|
|
231
|
-
fmt.logSuccess(`Created ${file}`);
|
|
232
|
-
createdFiles.push(dest);
|
|
233
|
-
}
|
|
254
|
+
// Never create repo instruction files anymore.
|
|
234
255
|
}
|
|
235
256
|
return createdFiles;
|
|
236
257
|
}
|
|
@@ -286,7 +307,7 @@ function generateClaudeMd(cwd, namespace, rulesSections = {}) {
|
|
|
286
307
|
const team = namespace || 'my-team';
|
|
287
308
|
let base = `# CLAUDE.md — ${team}
|
|
288
309
|
|
|
289
|
-
Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\` (shared knowledge), \`
|
|
310
|
+
Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\` (shared knowledge), \`jenkins_*\` (CI/CD via ghl-ai MCP), \`grafana\` (observability)
|
|
290
311
|
|
|
291
312
|
## Routing Rule (ABSOLUTE)
|
|
292
313
|
|
|
@@ -357,7 +378,7 @@ memory/search → Search shared team knowledge base
|
|
|
357
378
|
memory/store → Push learnings to shared knowledge (eager sync after runs)
|
|
358
379
|
memory/get → Fetch specific memory by ID
|
|
359
380
|
grafana/* → External observability
|
|
360
|
-
|
|
381
|
+
jenkins_* → CI/CD pipelines (provided by ghl-ai MCP)
|
|
361
382
|
stitch/* → External design generation
|
|
362
383
|
\`\`\`
|
|
363
384
|
|
package/package.json
CHANGED
package/render-rules.mjs
CHANGED
|
@@ -6,7 +6,12 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
7
7
|
import { RULES_RUNTIME_DIR } from './constants.mjs';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// The marker is placed AFTER the YAML frontmatter so Cursor/Markdown parsers
|
|
10
|
+
// see the frontmatter at byte 0. Putting an HTML comment before --- breaks
|
|
11
|
+
// Cursor's alwaysApply/globs detection.
|
|
12
|
+
const GENERATED_MARKER = '<!-- Generated by aw — do not edit manually -->';
|
|
13
|
+
// Legacy header (pre-pattern comment first) — kept for pruning old files.
|
|
14
|
+
const LEGACY_GENERATED_HEADER = '<!-- Generated by aw — do not edit manually -->\n\n';
|
|
10
15
|
const STACK_OVERLAY_FLAG = 'AW_ENABLE_STACK_OVERLAY_RULES';
|
|
11
16
|
|
|
12
17
|
/** Rule scope → Cursor .mdc glob patterns */
|
|
@@ -196,7 +201,8 @@ function pruneStaleGeneratedRules(outputDir, expectedFilenames) {
|
|
|
196
201
|
|
|
197
202
|
const fullPath = join(outputDir, entry.name);
|
|
198
203
|
const content = readOrNull(fullPath);
|
|
199
|
-
|
|
204
|
+
// Match both new pattern (marker after frontmatter) and legacy (marker at top).
|
|
205
|
+
if (!content || (!content.includes(GENERATED_MARKER) && !content.startsWith(LEGACY_GENERATED_HEADER))) continue;
|
|
200
206
|
|
|
201
207
|
try {
|
|
202
208
|
unlinkSync(fullPath);
|
|
@@ -303,7 +309,9 @@ function renderCursorRules(cwd, rulesDir, options = {}) {
|
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
306
|
-
|
|
312
|
+
// Frontmatter MUST be at byte 0 for Cursor's YAML parser.
|
|
313
|
+
// Generated marker goes on the line immediately after the closing ---.
|
|
314
|
+
const content = frontmatter.join('\n') + '\n' + GENERATED_MARKER + '\n\n' + agentsMd.trim() + '\n' + refSection;
|
|
307
315
|
writeFileSync(join(cursorRulesDir, `${scopeToFilename(scope)}.mdc`), content);
|
|
308
316
|
count++;
|
|
309
317
|
}
|
|
@@ -318,10 +326,13 @@ function renderCursorRules(cwd, rulesDir, options = {}) {
|
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
function generateCursorAwRoutingRule() {
|
|
321
|
-
|
|
329
|
+
// Frontmatter MUST be at byte 0 for Cursor's alwaysApply/globs detection.
|
|
330
|
+
return `---
|
|
322
331
|
description: "AW global routing: select route, READ stage skill, then respond"
|
|
323
332
|
alwaysApply: true
|
|
324
333
|
---
|
|
334
|
+
${GENERATED_MARKER}
|
|
335
|
+
|
|
325
336
|
# AW Global Routing
|
|
326
337
|
|
|
327
338
|
## Hard Gate (MUST — do not skip)
|
|
@@ -460,7 +471,8 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
|
|
|
460
471
|
}
|
|
461
472
|
}
|
|
462
473
|
|
|
463
|
-
|
|
474
|
+
// Claude Code reads .md frontmatter similarly — keep marker after frontmatter.
|
|
475
|
+
const content = frontmatter + GENERATED_MARKER + '\n\n' + agentsMd.trim() + '\n' + refSection;
|
|
464
476
|
writeFileSync(join(claudeRulesDir, `${scopeToFilename(scope)}.md`), content);
|
|
465
477
|
count++;
|
|
466
478
|
}
|
|
@@ -471,21 +483,120 @@ function renderClaudeRules(cwd, rulesDir, options = {}) {
|
|
|
471
483
|
/**
|
|
472
484
|
* Generate a rules section for CLAUDE.md from runtime AW rules.
|
|
473
485
|
*/
|
|
474
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Resolve the final scope set for AGENTS.md/CLAUDE.md filtering.
|
|
488
|
+
* Precedence: explicit option > .aw/config.json awRuleScopes > auto-detect.
|
|
489
|
+
* Returns undefined if user explicitly disabled filtering (awRuleScopes: "all").
|
|
490
|
+
*/
|
|
491
|
+
export function resolveApplicableScopes(cwd, options = {}) {
|
|
492
|
+
if (options.applicableScopes) return options.applicableScopes;
|
|
493
|
+
|
|
494
|
+
// Read .aw/config.json if present.
|
|
495
|
+
const configPath = join(cwd, '.aw', 'config.json');
|
|
496
|
+
if (existsSync(configPath)) {
|
|
497
|
+
try {
|
|
498
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
499
|
+
const setting = config.awRuleScopes;
|
|
500
|
+
if (setting === 'all') return undefined; // disable filtering
|
|
501
|
+
if (Array.isArray(setting)) return new Set(setting);
|
|
502
|
+
} catch { /* fall through to auto-detect */ }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return detectApplicableScopes(cwd);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Detect applicable rule scopes for a consumer repo based on filesystem signals.
|
|
510
|
+
* Returns a Set of scope names (e.g. 'frontend', 'backend', 'mobile').
|
|
511
|
+
* 'universal' and 'security' are ALWAYS returned (cross-cutting, apply everywhere).
|
|
512
|
+
*/
|
|
513
|
+
export function detectApplicableScopes(cwd) {
|
|
514
|
+
const scopes = new Set(['universal', 'security']);
|
|
515
|
+
|
|
516
|
+
const has = (rel) => existsSync(join(cwd, rel));
|
|
517
|
+
const readPkg = () => {
|
|
518
|
+
try {
|
|
519
|
+
if (!has('package.json')) return null;
|
|
520
|
+
return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const pkg = readPkg();
|
|
527
|
+
if (pkg) {
|
|
528
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
529
|
+
// Frontend signals
|
|
530
|
+
if (deps.vue || deps.nuxt || deps['@vue/cli-service'] || deps.react || deps.next || deps.svelte) {
|
|
531
|
+
scopes.add('frontend');
|
|
532
|
+
}
|
|
533
|
+
// Backend signals
|
|
534
|
+
if (deps['@nestjs/core'] || deps.express || deps.fastify || deps.koa || deps['@platform-core/base-service']) {
|
|
535
|
+
scopes.add('backend');
|
|
536
|
+
scopes.add('api-design');
|
|
537
|
+
}
|
|
538
|
+
// Data signals
|
|
539
|
+
if (deps.mongoose || deps.typeorm || deps.prisma || deps.knex || deps['@platform-core/firestore']) {
|
|
540
|
+
scopes.add('data');
|
|
541
|
+
}
|
|
542
|
+
// SDET signals
|
|
543
|
+
if (deps.playwright || deps['@playwright/test'] || deps['@gohighlevel/sdet-platform-core']) {
|
|
544
|
+
scopes.add('sdet');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (has('pubspec.yaml')) scopes.add('mobile'); // Flutter/Dart
|
|
549
|
+
if (has('pom.xml') || has('build.gradle') || has('build.gradle.kts')) scopes.add('backend');
|
|
550
|
+
if (has('go.mod')) scopes.add('backend');
|
|
551
|
+
if (has('Cargo.toml')) scopes.add('backend');
|
|
552
|
+
if (has('requirements.txt') || has('pyproject.toml')) scopes.add('backend');
|
|
553
|
+
|
|
554
|
+
// Infra signals
|
|
555
|
+
if (has('Dockerfile') || has('Jenkinsfile') || has('helm') || has('terraform')) {
|
|
556
|
+
scopes.add('infra');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// If we only found the defaults (universal + security) with no other signals,
|
|
560
|
+
// return undefined to preserve current behavior (show all rules). Filtering
|
|
561
|
+
// only kicks in when we positively detect a framework or stack.
|
|
562
|
+
if (scopes.size <= 2) return undefined;
|
|
563
|
+
return scopes;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function filterRulesByScopes(rules, applicableScopes) {
|
|
567
|
+
if (!applicableScopes || applicableScopes.size === 0) return rules;
|
|
568
|
+
return rules.filter(r => {
|
|
569
|
+
const scope = String(r.id || '').split('/')[0];
|
|
570
|
+
return applicableScopes.has(scope);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function generateClaudeMdRulesSection(rulesDir, options = {}) {
|
|
475
575
|
const manifest = readManifest(rulesDir);
|
|
476
576
|
if (!manifest) return '';
|
|
477
577
|
|
|
478
578
|
const mustRules = manifest.rules.filter(r => r.severity === 'MUST');
|
|
479
|
-
|
|
579
|
+
const applicableScopes = options.applicableScopes;
|
|
580
|
+
const filteredRules = applicableScopes
|
|
581
|
+
? filterRulesByScopes(mustRules, applicableScopes)
|
|
582
|
+
: mustRules;
|
|
583
|
+
if (filteredRules.length === 0) return '';
|
|
480
584
|
|
|
481
585
|
const lines = [
|
|
482
586
|
'## Platform Rules (MUST)',
|
|
483
587
|
'',
|
|
484
588
|
'> Rendered from platform `.aw/.aw_rules/`. Full details in reference files.',
|
|
485
|
-
'',
|
|
486
589
|
];
|
|
487
590
|
|
|
488
|
-
|
|
591
|
+
if (applicableScopes) {
|
|
592
|
+
lines.push(
|
|
593
|
+
`> Filtered to scopes detected in this repo: \`${[...applicableScopes].sort().join('`, `')}\`. ` +
|
|
594
|
+
`To override, set \`awRuleScopes\` in \`.aw/config.json\`.`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
lines.push('');
|
|
598
|
+
|
|
599
|
+
for (const rule of filteredRules) {
|
|
489
600
|
lines.push(`- [ ] **${rule.id}** — ${rule.description}`);
|
|
490
601
|
}
|
|
491
602
|
|
|
@@ -516,14 +627,28 @@ export function generateAgentsMdRulesSection(rulesDir, options = {}) {
|
|
|
516
627
|
// Reference table — tells all IDEs (especially Codex) where to read domain rules.
|
|
517
628
|
// Codex can't auto-trigger by glob, but it CAN read these files when working in
|
|
518
629
|
// the matching area. Keep AGENTS.md lean; full content stays in the source files.
|
|
519
|
-
const
|
|
630
|
+
const allScopes = listRuleScopes(rulesDir, {
|
|
520
631
|
includeNestedScopes: stackOverlaysEnabled(options),
|
|
521
632
|
});
|
|
633
|
+
// Filter domain scopes by what's relevant to this consumer repo.
|
|
634
|
+
// 'universal' and 'security' always apply; other domains require detection.
|
|
635
|
+
const applicableScopes = options.applicableScopes;
|
|
636
|
+
const isApplicable = (scope) => !applicableScopes
|
|
637
|
+
|| applicableScopes.has(scope)
|
|
638
|
+
|| applicableScopes.has(scope.split('/')[0]);
|
|
639
|
+
const scopes = allScopes.filter(({ scope }) => isApplicable(scope));
|
|
522
640
|
const domains = scopes.filter(({ scope }) => !scope.includes('/'));
|
|
523
641
|
const overlays = scopes.filter(({ scope }) => scope.includes('/'));
|
|
524
642
|
if (domains.length > 0) {
|
|
525
643
|
lines.push('### Domain Rules');
|
|
526
644
|
lines.push('');
|
|
645
|
+
if (applicableScopes) {
|
|
646
|
+
lines.push(
|
|
647
|
+
`> Filtered to scopes detected in this repo: \`${[...applicableScopes].sort().join('`, `')}\`. ` +
|
|
648
|
+
`To override, set \`awRuleScopes\` (array or "all") in \`.aw/config.json\`.`,
|
|
649
|
+
);
|
|
650
|
+
lines.push('');
|
|
651
|
+
}
|
|
527
652
|
lines.push('When working in a specific domain, read the matching rules file:');
|
|
528
653
|
lines.push('');
|
|
529
654
|
lines.push('| Domain | Read when editing | Rules file |');
|
|
@@ -566,10 +691,14 @@ export function renderRules(cwd, options = {}) {
|
|
|
566
691
|
const rulesDir = resolveRulesSourceDir(cwd, options);
|
|
567
692
|
if (!rulesDir) return { cursorCount: 0, claudeSection: '', agentsSection: '' };
|
|
568
693
|
|
|
694
|
+
// Resolve applicable scopes for AGENTS.md / CLAUDE.md MUST-rule list.
|
|
695
|
+
// Order: explicit option → .aw/config.json awRuleScopes → auto-detect.
|
|
696
|
+
const applicableScopes = resolveApplicableScopes(cwd, options);
|
|
697
|
+
|
|
569
698
|
const cursorCount = renderCursorRules(cwd, rulesDir, options);
|
|
570
699
|
const claudeCount = renderClaudeRules(cwd, rulesDir, options);
|
|
571
|
-
const claudeSection = generateClaudeMdRulesSection(rulesDir);
|
|
572
|
-
const agentsSection = generateAgentsMdRulesSection(rulesDir, { ...options, outputDir: cwd });
|
|
700
|
+
const claudeSection = generateClaudeMdRulesSection(rulesDir, { applicableScopes });
|
|
701
|
+
const agentsSection = generateAgentsMdRulesSection(rulesDir, { ...options, outputDir: cwd, applicableScopes });
|
|
573
702
|
|
|
574
703
|
// Also render to global dirs so rules apply everywhere
|
|
575
704
|
const HOME = options.homeDir || homedir();
|