@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/cli.mjs +8 -1
- package/codex.mjs +839 -0
- package/commands/doctor.mjs +1086 -0
- package/commands/init.mjs +71 -81
- package/commands/link-project.mjs +12 -1
- package/commands/nuke.mjs +14 -4
- package/commands/pull.mjs +111 -11
- package/commands/push-rules.mjs +4 -15
- package/commands/push.mjs +4 -4
- package/commands/search.mjs +1 -1
- package/commands/startup.mjs +87 -0
- package/constants.mjs +3 -1
- package/ecc.mjs +130 -42
- package/git.mjs +4 -23
- package/hook-manifest.mjs +195 -0
- package/hooks/codex-home.mjs +184 -0
- package/hooks/shared-phase-scripts.mjs +69 -0
- package/integrate.mjs +219 -47
- package/link.mjs +36 -1
- package/mcp.mjs +2 -10
- package/package.json +8 -2
- package/paths.mjs +1 -1
- package/registry.mjs +1 -1
- package/render-rules.mjs +267 -27
- package/startup.mjs +562 -0
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import * as fmt from '../fmt.mjs';
|
|
7
|
+
import { chalk } from '../fmt.mjs';
|
|
8
|
+
import { AW_ECC_TAG } from '../ecc.mjs';
|
|
9
|
+
import { getSupportedHarnessPhaseEntries } from '../hook-manifest.mjs';
|
|
10
|
+
import { getStartupStatus, hasLegacyRepoStartupDefaults } from '../startup.mjs';
|
|
11
|
+
|
|
12
|
+
const STATUS_PRIORITY = { pass: 0, warn: 1, fail: 2 };
|
|
13
|
+
const STATUS_LABEL = {
|
|
14
|
+
pass: chalk.green('PASS'),
|
|
15
|
+
warn: chalk.yellow('WARN'),
|
|
16
|
+
fail: chalk.red('FAIL'),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const GENERATED_RULE_HEADER = '<!-- Generated by aw — do not edit manually -->';
|
|
20
|
+
const AW_ROUTER_BRIDGE_START_MARKER = '<!-- aw-managed:start router-bridge -->';
|
|
21
|
+
const AW_ROUTER_BRIDGE_END_MARKER = '<!-- aw-managed:end router-bridge -->';
|
|
22
|
+
const PRIMARY_AW_ROUTES = ['plan', 'build', 'test', 'review', 'deploy', 'ship'];
|
|
23
|
+
const CONDITIONAL_AW_ROUTES = ['investigate'];
|
|
24
|
+
const COMPAT_AW_ROUTES = ['execute', 'verify'];
|
|
25
|
+
const EXPECTED_AW_ROUTES = [...PRIMARY_AW_ROUTES, ...CONDITIONAL_AW_ROUTES, ...COMPAT_AW_ROUTES];
|
|
26
|
+
const CLAUDE_PLUGIN_NAME = 'aw@aw-marketplace';
|
|
27
|
+
const CLAUDE_POSIX_HOOK_COMMAND = 'bash "${CLAUDE_PLUGIN_ROOT}/hooks/session-start"';
|
|
28
|
+
const CLAUDE_PORTABLE_HOOK_COMMAND = 'bash "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/hooks/session-start"';
|
|
29
|
+
const CLAUDE_PORTABLE_BASH_LC_COMMAND = 'bash -lc \'exec bash "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/hooks/session-start"\'';
|
|
30
|
+
const CLAUDE_POSIX_BASH_LC_COMMAND = 'bash -lc \'exec bash "${CLAUDE_PLUGIN_ROOT}/hooks/session-start"\'';
|
|
31
|
+
const CLAUDE_WINDOWS_HOOK_COMMAND = '"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd" session-start';
|
|
32
|
+
const REPO_CLAUDE_SESSION_START_COMMAND = '"$CLAUDE_PROJECT_DIR"/hooks/aw-session-start';
|
|
33
|
+
const CODEX_HOOK_MATCHER = 'startup|resume';
|
|
34
|
+
const CODEX_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-session-start.sh"';
|
|
35
|
+
const CODEX_PROMPT_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-user-prompt-submit.sh"';
|
|
36
|
+
const CODEX_PRE_TOOL_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-pre-tool-use.sh"';
|
|
37
|
+
const CODEX_POST_TOOL_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-post-tool-use.sh"';
|
|
38
|
+
const CODEX_STOP_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-stop.sh"';
|
|
39
|
+
const CURSOR_HOOK_COMMAND = 'node .cursor/hooks/session-start.js';
|
|
40
|
+
const CURSOR_BEFORE_PROMPT_COMMAND = 'node .cursor/hooks/before-submit-prompt.js';
|
|
41
|
+
|
|
42
|
+
function hasManagedHookCommand(entry, command) {
|
|
43
|
+
return Array.isArray(entry?.hooks)
|
|
44
|
+
&& entry.hooks.some(hook => hook?.type === 'command' && hook?.command === command);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatMissingParts(parts) {
|
|
48
|
+
return parts.length > 0 ? parts.join(', ') : 'none';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatPhaseSummary(parts) {
|
|
52
|
+
if (parts.length === 0) return 'none';
|
|
53
|
+
if (parts.length === 1) return parts[0];
|
|
54
|
+
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
|
|
55
|
+
return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function projectRelinkFix(homeDir, cwd, targetDescription) {
|
|
59
|
+
return cwd !== homeDir
|
|
60
|
+
? `Run \`aw link\` from the project root to refresh ${targetDescription}; use \`aw init\` if the global AW install is missing.`
|
|
61
|
+
: `Run \`aw init\` to refresh ${targetDescription}.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function globalInstallStateFix(homeDir, cwd, targetDescription) {
|
|
65
|
+
return cwd !== homeDir
|
|
66
|
+
? `Run \`aw init\` to repair the global ${targetDescription}; use \`aw link\` separately if the current project still needs relinking.`
|
|
67
|
+
: `Run \`aw init\` to repair the global ${targetDescription}.`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readJson(filePath, fallback = {}) {
|
|
71
|
+
if (!existsSync(filePath)) return fallback;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
74
|
+
} catch {
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readText(filePath, fallback = '') {
|
|
80
|
+
if (!existsSync(filePath)) return fallback;
|
|
81
|
+
try {
|
|
82
|
+
return readFileSync(filePath, 'utf8');
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeCheck(id, title, status, summary, fix = null) {
|
|
89
|
+
return { id, title, status, summary, fix };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function overallStatus(checks) {
|
|
93
|
+
return checks.reduce((current, check) => (
|
|
94
|
+
STATUS_PRIORITY[check.status] > STATUS_PRIORITY[current] ? check.status : current
|
|
95
|
+
), 'pass');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseLegacyClaudeHookTargets(sessionStartEntries = []) {
|
|
99
|
+
const targets = [];
|
|
100
|
+
for (const entry of sessionStartEntries) {
|
|
101
|
+
if (!Array.isArray(entry?.hooks)) continue;
|
|
102
|
+
for (const hook of entry.hooks) {
|
|
103
|
+
const command = String(hook?.command || '');
|
|
104
|
+
if (!command.includes('.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh')) continue;
|
|
105
|
+
const match = command.match(/bash\s+("?)(.+?session-start\.sh)\1(?:\s|$)/);
|
|
106
|
+
const rawPath = (match?.[2] || '').replace(/\\ /g, ' ').trim();
|
|
107
|
+
targets.push({ command, path: rawPath || null });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return targets;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compareVersion(a, b) {
|
|
114
|
+
const aParts = String(a).split('.').map(part => Number.parseInt(part, 10) || 0);
|
|
115
|
+
const bParts = String(b).split('.').map(part => Number.parseInt(part, 10) || 0);
|
|
116
|
+
const max = Math.max(aParts.length, bParts.length);
|
|
117
|
+
for (let i = 0; i < max; i += 1) {
|
|
118
|
+
const left = aParts[i] || 0;
|
|
119
|
+
const right = bParts[i] || 0;
|
|
120
|
+
if (left > right) return 1;
|
|
121
|
+
if (left < right) return -1;
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function latestVersion(dirPath) {
|
|
127
|
+
if (!existsSync(dirPath)) return null;
|
|
128
|
+
try {
|
|
129
|
+
return readdirSync(dirPath, { withFileTypes: true })
|
|
130
|
+
.filter(entry => entry.isDirectory())
|
|
131
|
+
.map(entry => entry.name)
|
|
132
|
+
.sort(compareVersion)
|
|
133
|
+
.at(-1) || null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseExactGitTag(repoDir) {
|
|
140
|
+
if (!existsSync(join(repoDir, '.git'))) return null;
|
|
141
|
+
try {
|
|
142
|
+
return execSync(`git -C "${repoDir}" describe --tags --exact-match`, {
|
|
143
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
timeout: 3000,
|
|
146
|
+
}).trim();
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseCodexHooksFile(filePath) {
|
|
153
|
+
const content = readText(filePath);
|
|
154
|
+
return /^\s*codex_hooks\s*=\s*true\b/m.test(content);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveAwRegistryDir(homeDir) {
|
|
158
|
+
return [
|
|
159
|
+
join(homeDir, '.aw_registry'),
|
|
160
|
+
join(homeDir, '.aw', '.aw_registry'),
|
|
161
|
+
].find(existsSync) || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveRulesDir(homeDir) {
|
|
165
|
+
const candidates = [
|
|
166
|
+
join(homeDir, '.aw', '.aw_rules'),
|
|
167
|
+
join(homeDir, '.aw', '.aw_registry', '.aw_rules'),
|
|
168
|
+
];
|
|
169
|
+
return candidates.find(existsSync) || null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function findClaudePlugin(homeDir) {
|
|
173
|
+
const cacheRoot = join(homeDir, '.claude', 'plugins', 'cache', 'aw-marketplace', 'aw');
|
|
174
|
+
const version = latestVersion(cacheRoot);
|
|
175
|
+
return {
|
|
176
|
+
cacheRoot,
|
|
177
|
+
version,
|
|
178
|
+
root: version ? join(cacheRoot, version) : null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function missingFiles(rootDir, relativePaths) {
|
|
183
|
+
if (!rootDir) return [...relativePaths];
|
|
184
|
+
return relativePaths.filter(relativePath => !existsSync(join(rootDir, relativePath)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function countDomainRuleFiles(rulesDir) {
|
|
188
|
+
const platformDir = join(rulesDir, 'platform');
|
|
189
|
+
if (!existsSync(platformDir)) return 0;
|
|
190
|
+
|
|
191
|
+
let count = 0;
|
|
192
|
+
const stack = [platformDir];
|
|
193
|
+
while (stack.length > 0) {
|
|
194
|
+
const current = stack.pop();
|
|
195
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
196
|
+
const fullPath = join(current, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
stack.push(fullPath);
|
|
199
|
+
} else if (entry.isFile() && entry.name === 'AGENTS.md') {
|
|
200
|
+
count += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return count;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function directoryContainsGeneratedRuleFiles(dirPath, extension) {
|
|
208
|
+
if (!existsSync(dirPath)) return false;
|
|
209
|
+
try {
|
|
210
|
+
return readdirSync(dirPath, { withFileTypes: true })
|
|
211
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(extension))
|
|
212
|
+
.some(entry => readText(join(dirPath, entry.name)).startsWith(GENERATED_RULE_HEADER));
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function directoryContainsRegistrySymlink(dirPath) {
|
|
219
|
+
if (!existsSync(dirPath)) return false;
|
|
220
|
+
try {
|
|
221
|
+
return readdirSync(dirPath, { withFileTypes: true }).some((entry) => {
|
|
222
|
+
const fullPath = join(dirPath, entry.name);
|
|
223
|
+
if (!lstatSync(fullPath).isSymbolicLink()) return false;
|
|
224
|
+
return readlinkSync(fullPath).includes('.aw_registry');
|
|
225
|
+
});
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function jsonMcpHealth(filePath) {
|
|
232
|
+
const server = readJson(filePath).mcpServers?.['ghl-ai'];
|
|
233
|
+
return {
|
|
234
|
+
present: Boolean(server),
|
|
235
|
+
url: /^https?:\/\//.test(String(server?.url || '')),
|
|
236
|
+
authorization: typeof server?.headers?.Authorization === 'string' && server.headers.Authorization.length > 0,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function tomlMcpHealth(filePath) {
|
|
241
|
+
const content = readText(filePath);
|
|
242
|
+
return {
|
|
243
|
+
present: /\[mcp_servers\.ghl-ai\]/.test(content),
|
|
244
|
+
url: /\[mcp_servers\.ghl-ai\][\s\S]*?url\s*=\s*"https?:\/\/[^"]+"/.test(content),
|
|
245
|
+
authorization: /\[mcp_servers\.ghl-ai\.headers\][\s\S]*?Authorization\s*=\s*".+"/.test(content),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function hasManagedCodexSessionStart(filePath) {
|
|
250
|
+
const config = readJson(filePath, {});
|
|
251
|
+
return Array.isArray(config?.hooks?.SessionStart)
|
|
252
|
+
&& config.hooks.SessionStart.some(entry =>
|
|
253
|
+
entry?.matcher === CODEX_HOOK_MATCHER
|
|
254
|
+
&& Array.isArray(entry?.hooks)
|
|
255
|
+
&& entry.hooks.some(hook =>
|
|
256
|
+
hook?.type === 'command'
|
|
257
|
+
&& hook?.command === CODEX_HOOK_COMMAND
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function hasManagedCursorSessionStart(filePath) {
|
|
263
|
+
const config = readJson(filePath, {});
|
|
264
|
+
return Array.isArray(config?.hooks?.sessionStart)
|
|
265
|
+
&& config.hooks.sessionStart.some(entry =>
|
|
266
|
+
String(entry?.command || '') === CURSOR_HOOK_COMMAND
|
|
267
|
+
&& String(entry?.event || '') === 'sessionStart'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getClaudeHomeHookCoverage(homeDir) {
|
|
272
|
+
const supportedEntries = getSupportedHarnessPhaseEntries('claude', 'home');
|
|
273
|
+
const supportedPhases = supportedEntries.map(entry => entry.phase);
|
|
274
|
+
const hooksPath = join(homeDir, '.claude', 'hooks', 'hooks.json');
|
|
275
|
+
if (!existsSync(hooksPath)) {
|
|
276
|
+
return { ok: false, supportedPhases, missingPhases: supportedPhases };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const hooksConfig = readJson(hooksPath, {});
|
|
280
|
+
const sessionStartStatus = getClaudeHomeSessionStartStatus(homeDir);
|
|
281
|
+
const phaseChecks = {
|
|
282
|
+
SessionStart: sessionStartStatus.present && sessionStartStatus.ok && sessionStartStatus.reason !== 'missing',
|
|
283
|
+
UserPromptSubmit: Array.isArray(hooksConfig?.hooks?.UserPromptSubmit)
|
|
284
|
+
&& hooksConfig.hooks.UserPromptSubmit.some(entry =>
|
|
285
|
+
Array.isArray(entry?.hooks)
|
|
286
|
+
&& entry.hooks.some(hook => String(hook?.command || '').includes('session-start-rules-context.sh'))
|
|
287
|
+
),
|
|
288
|
+
PreToolUse: Array.isArray(hooksConfig?.hooks?.PreToolUse) && hooksConfig.hooks.PreToolUse.length > 0,
|
|
289
|
+
PostToolUse: Array.isArray(hooksConfig?.hooks?.PostToolUse) && hooksConfig.hooks.PostToolUse.length > 0,
|
|
290
|
+
Stop: Array.isArray(hooksConfig?.hooks?.Stop) && hooksConfig.hooks.Stop.length > 0,
|
|
291
|
+
PostToolUseFailure: Array.isArray(hooksConfig?.hooks?.PostToolUseFailure) && hooksConfig.hooks.PostToolUseFailure.length > 0,
|
|
292
|
+
PreCompact: Array.isArray(hooksConfig?.hooks?.PreCompact) && hooksConfig.hooks.PreCompact.length > 0,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const missingPhases = supportedPhases.filter(phase => !phaseChecks[phase]);
|
|
296
|
+
|
|
297
|
+
return { ok: missingPhases.length === 0, supportedPhases, missingPhases };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function getCodexHomeHookCoverage(homeDir) {
|
|
301
|
+
const supportedEntries = getSupportedHarnessPhaseEntries('codex', 'home');
|
|
302
|
+
const supportedPhases = supportedEntries.map(entry => entry.phase);
|
|
303
|
+
const hooksPath = join(homeDir, '.codex', 'hooks.json');
|
|
304
|
+
const hooksConfig = readJson(hooksPath, {});
|
|
305
|
+
const phaseChecks = {
|
|
306
|
+
SessionStart: Array.isArray(hooksConfig?.hooks?.SessionStart)
|
|
307
|
+
&& hooksConfig.hooks.SessionStart.some(entry =>
|
|
308
|
+
entry?.matcher === CODEX_HOOK_MATCHER
|
|
309
|
+
&& hasManagedHookCommand(entry, CODEX_HOOK_COMMAND)
|
|
310
|
+
),
|
|
311
|
+
UserPromptSubmit: Array.isArray(hooksConfig?.hooks?.UserPromptSubmit)
|
|
312
|
+
&& hooksConfig.hooks.UserPromptSubmit.some(entry => hasManagedHookCommand(entry, CODEX_PROMPT_HOOK_COMMAND)),
|
|
313
|
+
PreToolUse: Array.isArray(hooksConfig?.hooks?.PreToolUse)
|
|
314
|
+
&& hooksConfig.hooks.PreToolUse.some(entry => entry?.matcher === '*' && hasManagedHookCommand(entry, CODEX_PRE_TOOL_HOOK_COMMAND)),
|
|
315
|
+
PostToolUse: Array.isArray(hooksConfig?.hooks?.PostToolUse)
|
|
316
|
+
&& hooksConfig.hooks.PostToolUse.some(entry => entry?.matcher === '*' && hasManagedHookCommand(entry, CODEX_POST_TOOL_HOOK_COMMAND)),
|
|
317
|
+
Stop: Array.isArray(hooksConfig?.hooks?.Stop)
|
|
318
|
+
&& hooksConfig.hooks.Stop.some(entry => hasManagedHookCommand(entry, CODEX_STOP_HOOK_COMMAND)),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const scriptChecks = {
|
|
322
|
+
SessionStart: existsSync(join(homeDir, '.codex', 'hooks', 'aw-session-start.sh')),
|
|
323
|
+
UserPromptSubmit: existsSync(join(homeDir, '.codex', 'hooks', 'aw-user-prompt-submit.sh')),
|
|
324
|
+
PreToolUse: existsSync(join(homeDir, '.codex', 'hooks', 'aw-pre-tool-use.sh')),
|
|
325
|
+
PostToolUse: existsSync(join(homeDir, '.codex', 'hooks', 'aw-post-tool-use.sh')),
|
|
326
|
+
Stop: existsSync(join(homeDir, '.codex', 'hooks', 'aw-stop.sh')),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const missingPhases = supportedPhases.filter(phase => !phaseChecks[phase]);
|
|
330
|
+
const missingScripts = supportedPhases.filter(phase => !scriptChecks[phase]);
|
|
331
|
+
|
|
332
|
+
return { ok: missingPhases.length === 0 && missingScripts.length === 0, supportedPhases, missingPhases, missingScripts };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getCursorHomeHookCoverage(homeDir) {
|
|
336
|
+
const supportedEntries = getSupportedHarnessPhaseEntries('cursor', 'home');
|
|
337
|
+
const supportedPhases = supportedEntries.map(entry => entry.phase);
|
|
338
|
+
const hooksPath = join(homeDir, '.cursor', 'hooks.json');
|
|
339
|
+
const hooksConfig = readJson(hooksPath, {});
|
|
340
|
+
const phaseChecks = {
|
|
341
|
+
SessionStart: Array.isArray(hooksConfig?.hooks?.sessionStart) && hooksConfig.hooks.sessionStart.length > 0,
|
|
342
|
+
UserPromptSubmit: Array.isArray(hooksConfig?.hooks?.beforeSubmitPrompt) && hooksConfig.hooks.beforeSubmitPrompt.length > 0,
|
|
343
|
+
PreToolUse: Array.isArray(hooksConfig?.hooks?.beforeShellExecution)
|
|
344
|
+
&& hooksConfig.hooks.beforeShellExecution.length > 0
|
|
345
|
+
&& Array.isArray(hooksConfig?.hooks?.beforeMCPExecution)
|
|
346
|
+
&& hooksConfig.hooks.beforeMCPExecution.length > 0,
|
|
347
|
+
PostToolUse: Array.isArray(hooksConfig?.hooks?.afterShellExecution)
|
|
348
|
+
&& hooksConfig.hooks.afterShellExecution.length > 0
|
|
349
|
+
&& Array.isArray(hooksConfig?.hooks?.afterFileEdit)
|
|
350
|
+
&& hooksConfig.hooks.afterFileEdit.length > 0
|
|
351
|
+
&& Array.isArray(hooksConfig?.hooks?.afterMCPExecution)
|
|
352
|
+
&& hooksConfig.hooks.afterMCPExecution.length > 0,
|
|
353
|
+
Stop: Array.isArray(hooksConfig?.hooks?.stop) && hooksConfig.hooks.stop.length > 0,
|
|
354
|
+
SessionEnd: Array.isArray(hooksConfig?.hooks?.sessionEnd) && hooksConfig.hooks.sessionEnd.length > 0,
|
|
355
|
+
PreCompact: Array.isArray(hooksConfig?.hooks?.preCompact) && hooksConfig.hooks.preCompact.length > 0,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const scriptChecks = {
|
|
359
|
+
SessionStart: existsSync(join(homeDir, '.cursor', 'hooks', 'session-start.js')),
|
|
360
|
+
UserPromptSubmit: existsSync(join(homeDir, '.cursor', 'hooks', 'before-submit-prompt.js')),
|
|
361
|
+
PreToolUse: existsSync(join(homeDir, '.cursor', 'hooks', 'before-shell-execution.js'))
|
|
362
|
+
&& existsSync(join(homeDir, '.cursor', 'hooks', 'before-mcp-execution.js')),
|
|
363
|
+
PostToolUse: existsSync(join(homeDir, '.cursor', 'hooks', 'after-shell-execution.js'))
|
|
364
|
+
&& existsSync(join(homeDir, '.cursor', 'hooks', 'after-file-edit.js'))
|
|
365
|
+
&& existsSync(join(homeDir, '.cursor', 'hooks', 'after-mcp-execution.js')),
|
|
366
|
+
Stop: existsSync(join(homeDir, '.cursor', 'hooks', 'stop.js')),
|
|
367
|
+
SessionEnd: existsSync(join(homeDir, '.cursor', 'hooks', 'session-end.js')),
|
|
368
|
+
PreCompact: existsSync(join(homeDir, '.cursor', 'hooks', 'pre-compact.js')),
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const missingPhases = supportedPhases.filter(phase => !phaseChecks[phase]);
|
|
372
|
+
const missingScripts = supportedPhases.filter(phase => !scriptChecks[phase]);
|
|
373
|
+
|
|
374
|
+
return { ok: missingPhases.length === 0 && missingScripts.length === 0, supportedPhases, missingPhases, missingScripts };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getClaudePluginSessionStartStatus(pluginRoot) {
|
|
378
|
+
const hooksConfig = readJson(join(pluginRoot, 'hooks', 'hooks.json'), {});
|
|
379
|
+
if (!Array.isArray(hooksConfig?.hooks?.SessionStart)) {
|
|
380
|
+
return { ok: false, reason: 'missing' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const entry of hooksConfig.hooks.SessionStart) {
|
|
384
|
+
if (!String(entry?.matcher || '').includes('startup') || !Array.isArray(entry?.hooks)) continue;
|
|
385
|
+
for (const hook of entry.hooks) {
|
|
386
|
+
if (hook?.type !== 'command') continue;
|
|
387
|
+
const command = String(hook?.command || '');
|
|
388
|
+
if (
|
|
389
|
+
command === CLAUDE_POSIX_HOOK_COMMAND
|
|
390
|
+
|| command === CLAUDE_PORTABLE_HOOK_COMMAND
|
|
391
|
+
|| command === CLAUDE_POSIX_BASH_LC_COMMAND
|
|
392
|
+
|| command === CLAUDE_PORTABLE_BASH_LC_COMMAND
|
|
393
|
+
) {
|
|
394
|
+
return { ok: true, command };
|
|
395
|
+
}
|
|
396
|
+
if (command === CLAUDE_WINDOWS_HOOK_COMMAND) {
|
|
397
|
+
return process.platform === 'win32'
|
|
398
|
+
? { ok: true, command }
|
|
399
|
+
: { ok: false, reason: 'platform-incompatible', command };
|
|
400
|
+
}
|
|
401
|
+
if (command.includes('session-start')) {
|
|
402
|
+
return { ok: false, reason: 'unexpected-command', command };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { ok: false, reason: 'missing' };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function getClaudeHomeSessionStartStatus(homeDir) {
|
|
411
|
+
const hooksPath = join(homeDir, '.claude', 'hooks', 'hooks.json');
|
|
412
|
+
if (!existsSync(hooksPath)) {
|
|
413
|
+
return { present: false, ok: true, reason: 'absent' };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const hooksConfig = readJson(hooksPath, {});
|
|
417
|
+
if (!Array.isArray(hooksConfig?.hooks?.SessionStart)) {
|
|
418
|
+
return { present: true, ok: true, reason: 'missing' };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const entry of hooksConfig.hooks.SessionStart) {
|
|
422
|
+
if (!String(entry?.matcher || '').includes('startup') || !Array.isArray(entry?.hooks)) continue;
|
|
423
|
+
for (const hook of entry.hooks) {
|
|
424
|
+
if (hook?.type !== 'command') continue;
|
|
425
|
+
const command = String(hook?.command || '');
|
|
426
|
+
if (command === CLAUDE_PORTABLE_HOOK_COMMAND || command === CLAUDE_PORTABLE_BASH_LC_COMMAND) {
|
|
427
|
+
return { present: true, ok: true, command };
|
|
428
|
+
}
|
|
429
|
+
if (command === CLAUDE_POSIX_HOOK_COMMAND || command === CLAUDE_POSIX_BASH_LC_COMMAND) {
|
|
430
|
+
return { present: true, ok: true, command };
|
|
431
|
+
}
|
|
432
|
+
if (command === CLAUDE_WINDOWS_HOOK_COMMAND) {
|
|
433
|
+
return process.platform === 'win32'
|
|
434
|
+
? { present: true, ok: true, command }
|
|
435
|
+
: { present: true, ok: false, reason: 'platform-incompatible', command };
|
|
436
|
+
}
|
|
437
|
+
if (command.includes('session-start')) {
|
|
438
|
+
return { present: true, ok: false, reason: 'unexpected-command', command };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { present: true, ok: true, reason: 'missing' };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function getProjectClaudeSessionStartStatus(cwd) {
|
|
447
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
448
|
+
if (!existsSync(settingsPath)) {
|
|
449
|
+
return { present: false, ok: true, reason: 'absent' };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const settings = readJson(settingsPath, {});
|
|
453
|
+
if (!Array.isArray(settings?.hooks?.SessionStart)) {
|
|
454
|
+
return { present: true, ok: true, reason: 'missing' };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const entry of settings.hooks.SessionStart) {
|
|
458
|
+
if (!Array.isArray(entry?.hooks)) continue;
|
|
459
|
+
for (const hook of entry.hooks) {
|
|
460
|
+
if (hook?.type !== 'command') continue;
|
|
461
|
+
const command = String(hook?.command || '');
|
|
462
|
+
if (command !== REPO_CLAUDE_SESSION_START_COMMAND && !command.includes('hooks/aw-session-start')) continue;
|
|
463
|
+
const targetPath = join(cwd, 'hooks', 'aw-session-start');
|
|
464
|
+
if (!existsSync(targetPath)) {
|
|
465
|
+
return { present: true, ok: false, reason: 'missing-target', command, targetPath };
|
|
466
|
+
}
|
|
467
|
+
return { present: true, ok: false, reason: 'legacy-present', command, targetPath };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { present: true, ok: true, reason: 'missing' };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function textHasRulesReference(text) {
|
|
475
|
+
return text.includes('Platform Rules') && text.includes('.aw/.aw_rules/platform/');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function textHasManagedRouterBridge(text) {
|
|
479
|
+
return text.includes(AW_ROUTER_BRIDGE_START_MARKER)
|
|
480
|
+
&& text.includes(AW_ROUTER_BRIDGE_END_MARKER)
|
|
481
|
+
&& text.includes('## AW Router Bridge')
|
|
482
|
+
&& text.includes('### 1. Route First (MANDATORY)')
|
|
483
|
+
&& text.includes('### 5. Verify With Proof')
|
|
484
|
+
&& text.includes('using-aw-skills')
|
|
485
|
+
&& text.includes('incremental-implementation');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function extractRuleReferencePaths(text) {
|
|
489
|
+
const matches = [];
|
|
490
|
+
const pattern = /(?:\]\(|`)([^)\n`]*(?:\.aw_rules|\.aw_registry\/\.aw_rules)\/[^)\n`]+\.md)(?:\)|`)/g;
|
|
491
|
+
for (const match of text.matchAll(pattern)) {
|
|
492
|
+
if (match[1]) matches.push(match[1]);
|
|
493
|
+
}
|
|
494
|
+
return [...new Set(matches)];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function listGeneratedRuleFiles(dirPath, extension) {
|
|
498
|
+
if (!existsSync(dirPath)) return [];
|
|
499
|
+
try {
|
|
500
|
+
return readdirSync(dirPath, { withFileTypes: true })
|
|
501
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(extension))
|
|
502
|
+
.map(entry => join(dirPath, entry.name))
|
|
503
|
+
.filter(filePath => readText(filePath).startsWith(GENERATED_RULE_HEADER));
|
|
504
|
+
} catch {
|
|
505
|
+
return [];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function findBrokenRuleReferences(filePaths) {
|
|
510
|
+
const broken = [];
|
|
511
|
+
for (const filePath of filePaths) {
|
|
512
|
+
const content = readText(filePath);
|
|
513
|
+
const refs = extractRuleReferencePaths(content);
|
|
514
|
+
for (const ref of refs) {
|
|
515
|
+
if (ref.includes('<domain>')) continue;
|
|
516
|
+
const resolvedPath = join(dirname(filePath), ref);
|
|
517
|
+
if (!existsSync(resolvedPath)) {
|
|
518
|
+
broken.push({ filePath, ref, resolvedPath });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return broken;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function missingCorePromptFiles(promptsDir) {
|
|
526
|
+
return EXPECTED_AW_ROUTES
|
|
527
|
+
.map(route => `aw-${route}.md`)
|
|
528
|
+
.filter(fileName => !existsSync(join(promptsDir, fileName)));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function missingCoreCursorCommandFiles(commandsDir) {
|
|
532
|
+
return EXPECTED_AW_ROUTES
|
|
533
|
+
.map(route => `${route}.md`)
|
|
534
|
+
.filter(fileName => !existsSync(join(commandsDir, fileName)));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function missingCoreClaudeCommandFiles(pluginRoot) {
|
|
538
|
+
return EXPECTED_AW_ROUTES
|
|
539
|
+
.map(route => `commands/${route}.md`)
|
|
540
|
+
.filter(relativePath => !existsSync(join(pluginRoot, relativePath)));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function buildDoctorChecks(homeDir, cwd) {
|
|
544
|
+
const startup = getStartupStatus(homeDir);
|
|
545
|
+
const checks = [];
|
|
546
|
+
const awRegistryDir = resolveAwRegistryDir(homeDir);
|
|
547
|
+
const rulesDir = resolveRulesDir(homeDir);
|
|
548
|
+
|
|
549
|
+
const usingAwHookPath = [
|
|
550
|
+
join(homeDir, '.aw_registry', 'platform', 'core', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
|
|
551
|
+
join(homeDir, '.aw', '.aw_registry', 'platform', 'core', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh'),
|
|
552
|
+
].find(existsSync);
|
|
553
|
+
|
|
554
|
+
checks.push(usingAwHookPath
|
|
555
|
+
? makeCheck('runtime-hook', 'AW runtime hook', 'pass', `Found using-aw-skills hook at ${usingAwHookPath.replace(`${homeDir}/`, '~/')}`)
|
|
556
|
+
: makeCheck('runtime-hook', 'AW runtime hook', 'fail', 'using-aw-skills hook is missing from ~/.aw_registry', 'Run `aw init` or `aw pull platform` to restore the AW runtime hook.'),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
checks.push(startup.mode === 'enabled'
|
|
560
|
+
? makeCheck('routing-mode', 'Routing mode', 'pass', `Global routing mode is ${startup.mode}`)
|
|
561
|
+
: makeCheck('routing-mode', 'Routing mode', 'warn', 'Global routing mode is disabled', 'Run `aw routing enable` if you want automatic AW startup behavior.'),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
if (!rulesDir) {
|
|
565
|
+
checks.push(makeCheck(
|
|
566
|
+
'aw-rules-source',
|
|
567
|
+
'AW rules source',
|
|
568
|
+
'fail',
|
|
569
|
+
'No synced .aw/.aw_rules tree found under ~/.aw/.aw_rules',
|
|
570
|
+
'Run `aw init` or `aw pull platform` to sync the AW rules source into ~/.aw/.aw_rules.',
|
|
571
|
+
));
|
|
572
|
+
} else {
|
|
573
|
+
const topLevelRulesAgents = join(rulesDir, 'AGENTS.md');
|
|
574
|
+
const domainRuleCount = countDomainRuleFiles(rulesDir);
|
|
575
|
+
checks.push(
|
|
576
|
+
existsSync(topLevelRulesAgents) && domainRuleCount > 0
|
|
577
|
+
? makeCheck('aw-rules-source', 'AW rules source', 'pass', `Rules source is synced (${domainRuleCount} domain AGENTS.md files detected)`)
|
|
578
|
+
: makeCheck(
|
|
579
|
+
'aw-rules-source',
|
|
580
|
+
'AW rules source',
|
|
581
|
+
'fail',
|
|
582
|
+
'Rules source is incomplete under ~/.aw/.aw_rules',
|
|
583
|
+
'Run `aw pull platform` to refresh .aw/.aw_rules, then rerun `aw doctor`.',
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (cwd !== homeDir) {
|
|
589
|
+
const projectAgentsPath = join(cwd, 'AGENTS.md');
|
|
590
|
+
const projectClaudePath = join(cwd, 'CLAUDE.md');
|
|
591
|
+
const projectAgentsHasBridge = existsSync(projectAgentsPath) && textHasManagedRouterBridge(readText(projectAgentsPath));
|
|
592
|
+
const projectClaudeExists = existsSync(projectClaudePath);
|
|
593
|
+
const projectClaudeHasBridge = projectClaudeExists && textHasManagedRouterBridge(readText(projectClaudePath));
|
|
594
|
+
|
|
595
|
+
checks.push(
|
|
596
|
+
!projectAgentsHasBridge && !projectClaudeHasBridge
|
|
597
|
+
? makeCheck('project-router-bridge', 'Current repo router bridge', 'pass', 'Current repo does not carry a stale managed AW Router Bridge block')
|
|
598
|
+
: makeCheck(
|
|
599
|
+
'project-router-bridge',
|
|
600
|
+
'Current repo router bridge',
|
|
601
|
+
'warn',
|
|
602
|
+
projectAgentsHasBridge
|
|
603
|
+
? 'Current repo AGENTS.md still carries a stale managed AW Router Bridge block'
|
|
604
|
+
: 'Current repo CLAUDE.md still carries a stale managed AW Router Bridge block',
|
|
605
|
+
'Run `aw init` to move the managed AW Router Bridge into the user-home harness files and remove stale repo-local copies.',
|
|
606
|
+
),
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const projectClaudeSessionStartStatus = getProjectClaudeSessionStartStatus(cwd);
|
|
610
|
+
if (!projectClaudeSessionStartStatus.ok) {
|
|
611
|
+
checks.push(makeCheck(
|
|
612
|
+
'project-claude-session-start',
|
|
613
|
+
'Current repo Claude session-start hook',
|
|
614
|
+
projectClaudeSessionStartStatus.reason === 'missing-target' ? 'fail' : 'warn',
|
|
615
|
+
projectClaudeSessionStartStatus.reason === 'missing-target'
|
|
616
|
+
? `Current repo .claude/settings.json points SessionStart at missing ${projectClaudeSessionStartStatus.targetPath.replace(`${cwd}/`, '')}`
|
|
617
|
+
: 'Current repo still has a legacy Claude SessionStart hook wired through repo-local hooks/aw-session-start',
|
|
618
|
+
'Run `aw link` from the project root to remove or refresh legacy repo-local Claude startup hooks.',
|
|
619
|
+
));
|
|
620
|
+
} else {
|
|
621
|
+
checks.push(makeCheck('project-claude-session-start', 'Current repo Claude session-start hook', 'pass', 'No legacy project-local Claude SessionStart hook detected'));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
checks.push(
|
|
625
|
+
hasLegacyRepoStartupDefaults(cwd)
|
|
626
|
+
? makeCheck('repo-legacy-routing', 'Current repo routing defaults', 'warn', 'Legacy repo-local routing files are still present in the current workspace', 'Run `aw routing enable` to clean repo-local startup defaults and prefer the global-managed setup.')
|
|
627
|
+
: makeCheck('repo-legacy-routing', 'Current repo routing defaults', 'pass', 'No legacy repo-local routing files detected in the current workspace'),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');
|
|
632
|
+
const claudeSettings = readJson(claudeSettingsPath, {});
|
|
633
|
+
const claudePluginEnabled = claudeSettings.enabledPlugins?.[CLAUDE_PLUGIN_NAME] === true;
|
|
634
|
+
const { version: claudePluginVersion, root: claudePluginRoot } = findClaudePlugin(homeDir);
|
|
635
|
+
if (claudePluginEnabled && claudePluginVersion) {
|
|
636
|
+
checks.push(makeCheck('claude-plugin', 'Claude plugin', 'pass', `${CLAUDE_PLUGIN_NAME} is enabled (cache version ${claudePluginVersion})`));
|
|
637
|
+
} else if (claudePluginEnabled) {
|
|
638
|
+
checks.push(makeCheck('claude-plugin', 'Claude plugin', 'fail', 'Claude settings enable the AW plugin, but no plugin cache version is installed', 'Reinstall the AW Claude plugin by re-running `aw init` or the Claude plugin install flow.'));
|
|
639
|
+
} else {
|
|
640
|
+
checks.push(makeCheck('claude-plugin', 'Claude plugin', 'warn', 'Claude plugin is not enabled in ~/.claude/settings.json', 'Enable or reinstall the AW Claude plugin if you expect Claude startup hydration.'));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (claudePluginRoot) {
|
|
644
|
+
const claudeSessionStartStatus = getClaudePluginSessionStartStatus(claudePluginRoot);
|
|
645
|
+
const missingBundleFiles = missingFiles(claudePluginRoot, [
|
|
646
|
+
'.claude-plugin/plugin.json',
|
|
647
|
+
'hooks/hooks.json',
|
|
648
|
+
'skills/using-aw-skills/SKILL.md',
|
|
649
|
+
'skills/using-aw-skills/hooks/session-start.sh',
|
|
650
|
+
]);
|
|
651
|
+
checks.push(
|
|
652
|
+
missingBundleFiles.length === 0 && claudeSessionStartStatus.ok
|
|
653
|
+
? makeCheck('claude-plugin-bundle', 'Claude plugin bundle', 'pass', 'Claude plugin bundle contains AW routing hooks and router skill files')
|
|
654
|
+
: makeCheck(
|
|
655
|
+
'claude-plugin-bundle',
|
|
656
|
+
'Claude plugin bundle',
|
|
657
|
+
'fail',
|
|
658
|
+
missingBundleFiles.length > 0
|
|
659
|
+
? `Claude plugin bundle is missing: ${missingBundleFiles.join(', ')}`
|
|
660
|
+
: claudeSessionStartStatus.reason === 'platform-incompatible'
|
|
661
|
+
? `Claude plugin bundle uses a Windows-only SessionStart command on ${process.platform}: ${claudeSessionStartStatus.command}`
|
|
662
|
+
: claudeSessionStartStatus.reason === 'unexpected-command'
|
|
663
|
+
? `Claude plugin bundle has an unexpected SessionStart command: ${claudeSessionStartStatus.command}`
|
|
664
|
+
: 'Claude plugin bundle is installed but hooks/hooks.json is missing the managed SessionStart routing hook',
|
|
665
|
+
'Reinstall or refresh the AW Claude plugin so hooks/hooks.json uses the managed SessionStart shell entrypoint for the current platform.',
|
|
666
|
+
),
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
const missingPluginCommands = missingCoreClaudeCommandFiles(claudePluginRoot);
|
|
670
|
+
checks.push(
|
|
671
|
+
missingPluginCommands.length === 0
|
|
672
|
+
? makeCheck('claude-plugin-commands', 'Claude public commands', 'pass', 'Claude plugin bundle exposes the current AW command surface (primary, conditional, and compatibility routes)')
|
|
673
|
+
: makeCheck(
|
|
674
|
+
'claude-plugin-commands',
|
|
675
|
+
'Claude public commands',
|
|
676
|
+
'fail',
|
|
677
|
+
`Claude plugin bundle is missing core command files: ${missingPluginCommands.join(', ')}`,
|
|
678
|
+
'Refresh the AW Claude plugin so the plugin bundle includes the full AW public command surface.',
|
|
679
|
+
),
|
|
680
|
+
);
|
|
681
|
+
} else {
|
|
682
|
+
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`.'));
|
|
683
|
+
checks.push(makeCheck('claude-plugin-commands', 'Claude public commands', 'warn', 'Claude plugin command bundle could not be inspected because the plugin is not installed'));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const claudeLegacyHooks = parseLegacyClaudeHookTargets(claudeSettings?.hooks?.SessionStart || []);
|
|
687
|
+
const claudeHomeSessionStartStatus = getClaudeHomeSessionStartStatus(homeDir);
|
|
688
|
+
const claudeHomeHookCoverage = getClaudeHomeHookCoverage(homeDir);
|
|
689
|
+
if (claudeLegacyHooks.length > 0) {
|
|
690
|
+
const brokenTargets = claudeLegacyHooks.filter(target => !target.path || !existsSync(target.path));
|
|
691
|
+
checks.push(makeCheck(
|
|
692
|
+
'claude-session-start',
|
|
693
|
+
'Claude session-start hook',
|
|
694
|
+
brokenTargets.length > 0 ? 'fail' : 'warn',
|
|
695
|
+
brokenTargets.length > 0
|
|
696
|
+
? `Legacy Claude SessionStart hook points at missing path ${brokenTargets[0].path || '<unknown>'}`
|
|
697
|
+
: 'Legacy Claude SessionStart hook is still present',
|
|
698
|
+
'Run `aw routing enable` to clean legacy Claude SessionStart hooks.',
|
|
699
|
+
));
|
|
700
|
+
} else if (startup.claudeDisabled) {
|
|
701
|
+
checks.push(makeCheck('claude-session-start', 'Claude session-start hook', 'warn', 'Claude automatic startup routing is disabled via ~/.claude/settings.json', 'Run `aw routing enable` to restore Claude startup routing.'));
|
|
702
|
+
} else if (!claudeHomeSessionStartStatus.ok) {
|
|
703
|
+
checks.push(makeCheck(
|
|
704
|
+
'claude-session-start',
|
|
705
|
+
'Claude session-start hook',
|
|
706
|
+
'fail',
|
|
707
|
+
claudeHomeSessionStartStatus.reason === 'platform-incompatible'
|
|
708
|
+
? `~/.claude/hooks/hooks.json uses a Windows-only SessionStart command on ${process.platform}: ${claudeHomeSessionStartStatus.command}`
|
|
709
|
+
: `~/.claude/hooks/hooks.json has an unexpected SessionStart command: ${claudeHomeSessionStartStatus.command}`,
|
|
710
|
+
'Refresh the home-level Claude hooks so SessionStart uses the managed portable shell entrypoint.',
|
|
711
|
+
));
|
|
712
|
+
} else {
|
|
713
|
+
checks.push(makeCheck('claude-session-start', 'Claude session-start hook', 'pass', 'No stale Claude SessionStart override detected'));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
checks.push(
|
|
717
|
+
claudeHomeHookCoverage.ok
|
|
718
|
+
? makeCheck(
|
|
719
|
+
'claude-home-hooks',
|
|
720
|
+
'Claude home hook phases',
|
|
721
|
+
'pass',
|
|
722
|
+
`Claude home hooks cover all supported AW phases: ${formatPhaseSummary(claudeHomeHookCoverage.supportedPhases)}`,
|
|
723
|
+
)
|
|
724
|
+
: makeCheck(
|
|
725
|
+
'claude-home-hooks',
|
|
726
|
+
'Claude home hook phases',
|
|
727
|
+
'fail',
|
|
728
|
+
`Claude home hooks are missing core phases: ${formatMissingParts(claudeHomeHookCoverage.missingPhases)}`,
|
|
729
|
+
'Run `aw init` to refresh ~/.claude/hooks/hooks.json with the full home-level AW hook phase set.',
|
|
730
|
+
),
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const claudeInstallStatePath = join(homeDir, '.claude', 'ecc', 'install-state.json');
|
|
734
|
+
checks.push(existsSync(claudeInstallStatePath)
|
|
735
|
+
? makeCheck('claude-install-state', 'Claude install state', 'pass', 'Claude install-state file is present')
|
|
736
|
+
: makeCheck('claude-install-state', 'Claude install state', 'fail', 'Claude install-state file is missing', globalInstallStateFix(homeDir, cwd, 'Claude install state')),
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const claudeMcp = jsonMcpHealth(join(homeDir, '.claude.json'));
|
|
740
|
+
checks.push(
|
|
741
|
+
claudeMcp.present && claudeMcp.url && claudeMcp.authorization
|
|
742
|
+
? makeCheck('claude-mcp', 'Claude MCP config', 'pass', 'Claude has a ghl-ai MCP server with URL and Authorization header')
|
|
743
|
+
: makeCheck(
|
|
744
|
+
'claude-mcp',
|
|
745
|
+
'Claude MCP config',
|
|
746
|
+
'fail',
|
|
747
|
+
`Claude MCP is incomplete (present=${claudeMcp.present}, url=${claudeMcp.url}, authorization=${claudeMcp.authorization})`,
|
|
748
|
+
'Run `aw init` to rewrite ~/.claude.json with a healthy ghl-ai MCP configuration.',
|
|
749
|
+
),
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
const claudeRefPath = join(homeDir, '.claude', 'references', 'route-selection-patterns.md');
|
|
753
|
+
checks.push(
|
|
754
|
+
existsSync(claudeRefPath)
|
|
755
|
+
? makeCheck('claude-references', 'Claude shared references', 'pass', 'Claude shared references are materialized')
|
|
756
|
+
: makeCheck('claude-references', 'Claude shared references', 'fail', 'Claude shared references are missing', projectRelinkFix(homeDir, cwd, '~/.claude/references')),
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
const claudeRulesDir = join(homeDir, '.claude', 'rules', 'platform');
|
|
760
|
+
const claudeHomeInstructionsPath = join(homeDir, '.claude', 'CLAUDE.md');
|
|
761
|
+
checks.push(
|
|
762
|
+
existsSync(claudeHomeInstructionsPath) && textHasManagedRouterBridge(readText(claudeHomeInstructionsPath))
|
|
763
|
+
? makeCheck('claude-home-instructions', 'Claude home instructions', 'pass', 'Claude home CLAUDE.md includes the managed AW Router Bridge block')
|
|
764
|
+
: makeCheck(
|
|
765
|
+
'claude-home-instructions',
|
|
766
|
+
'Claude home instructions',
|
|
767
|
+
'fail',
|
|
768
|
+
existsSync(claudeHomeInstructionsPath)
|
|
769
|
+
? 'Claude home CLAUDE.md is present but missing the managed AW Router Bridge block'
|
|
770
|
+
: 'Claude home CLAUDE.md is missing',
|
|
771
|
+
'Run `aw init` to regenerate ~/.claude/CLAUDE.md with the managed AW Router Bridge block.',
|
|
772
|
+
),
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
checks.push(
|
|
776
|
+
rulesDir && directoryContainsGeneratedRuleFiles(claudeRulesDir, '.md')
|
|
777
|
+
? makeCheck('claude-rules', 'Claude AW rules', 'pass', 'Claude has generated platform rules under ~/.claude/rules/platform/')
|
|
778
|
+
: makeCheck(
|
|
779
|
+
'claude-rules',
|
|
780
|
+
'Claude AW rules',
|
|
781
|
+
rulesDir ? 'fail' : 'warn',
|
|
782
|
+
rulesDir ? 'Claude platform rules are missing or not generated by AW' : 'Claude platform rules were not checked because .aw/.aw_rules is not synced',
|
|
783
|
+
'Run `aw pull platform` or `aw init` to render Claude platform rules.',
|
|
784
|
+
),
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
const claudeRuleFiles = listGeneratedRuleFiles(claudeRulesDir, '.md');
|
|
788
|
+
const brokenClaudeRuleRefs = findBrokenRuleReferences(claudeRuleFiles);
|
|
789
|
+
checks.push(
|
|
790
|
+
claudeRuleFiles.length === 0
|
|
791
|
+
? makeCheck(
|
|
792
|
+
'claude-rule-references',
|
|
793
|
+
'Claude rule references',
|
|
794
|
+
rulesDir ? 'fail' : 'warn',
|
|
795
|
+
rulesDir ? 'Claude generated rule files are missing, so reference links could not be validated' : 'Claude rule references were not checked because .aw/.aw_rules is not synced',
|
|
796
|
+
'Run `aw pull platform` or `aw init` to render Claude rules, then rerun `aw doctor`.',
|
|
797
|
+
)
|
|
798
|
+
: brokenClaudeRuleRefs.length === 0
|
|
799
|
+
? makeCheck('claude-rule-references', 'Claude rule references', 'pass', 'Claude generated rule references resolve to real files in ~/.aw/.aw_rules')
|
|
800
|
+
: makeCheck(
|
|
801
|
+
'claude-rule-references',
|
|
802
|
+
'Claude rule references',
|
|
803
|
+
'fail',
|
|
804
|
+
`Claude rule reference is broken in ${brokenClaudeRuleRefs[0].filePath.replace(`${homeDir}/`, '~/')}: ${brokenClaudeRuleRefs[0].ref}`,
|
|
805
|
+
'Refresh the rendered Claude rules so embedded reference links point at real files under ~/.aw/.aw_rules.',
|
|
806
|
+
),
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const claudeAgentsDir = join(homeDir, '.claude', 'agents');
|
|
810
|
+
checks.push(
|
|
811
|
+
directoryContainsRegistrySymlink(claudeAgentsDir)
|
|
812
|
+
? makeCheck('claude-agents', 'Claude linked agents', 'pass', 'Claude agents include symlinks into ~/.aw_registry')
|
|
813
|
+
: makeCheck(
|
|
814
|
+
'claude-agents',
|
|
815
|
+
'Claude linked agents',
|
|
816
|
+
awRegistryDir ? 'fail' : 'warn',
|
|
817
|
+
'Claude agents directory is missing registry-backed symlinks',
|
|
818
|
+
projectRelinkFix(homeDir, cwd, 'registry agents under ~/.claude/agents'),
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const codexConfigPath = join(homeDir, '.codex', 'config.toml');
|
|
823
|
+
const codexHooksPath = join(homeDir, '.codex', 'hooks.json');
|
|
824
|
+
const codexRuntimePath = join(homeDir, '.codex', 'hooks', 'aw-session-start.sh');
|
|
825
|
+
const codexHomeHookCoverage = getCodexHomeHookCoverage(homeDir);
|
|
826
|
+
const codexHealthy = startup.codexHooksEnabled && codexHomeHookCoverage.ok;
|
|
827
|
+
checks.push(codexHealthy
|
|
828
|
+
? makeCheck(
|
|
829
|
+
'codex-routing',
|
|
830
|
+
'Codex routing',
|
|
831
|
+
'pass',
|
|
832
|
+
`Codex home hooks cover all supported AW phases: ${formatPhaseSummary(codexHomeHookCoverage.supportedPhases)}`,
|
|
833
|
+
)
|
|
834
|
+
: makeCheck(
|
|
835
|
+
'codex-routing',
|
|
836
|
+
'Codex routing',
|
|
837
|
+
startup.mode === 'disabled' ? 'warn' : 'fail',
|
|
838
|
+
`Codex routing incomplete (codex_hooks=${parseCodexHooksFile(codexConfigPath)}, missing phases=${formatMissingParts(codexHomeHookCoverage.missingPhases)}, missing scripts=${formatMissingParts(codexHomeHookCoverage.missingScripts)})`,
|
|
839
|
+
'Run `aw routing enable` or `aw init` to restore Codex startup wiring.',
|
|
840
|
+
),
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
const codexInstallStatePath = join(homeDir, '.codex', 'ecc-install-state.json');
|
|
844
|
+
checks.push(existsSync(codexInstallStatePath)
|
|
845
|
+
? makeCheck('codex-install-state', 'Codex install state', 'pass', 'Codex install-state file is present')
|
|
846
|
+
: makeCheck('codex-install-state', 'Codex install state', 'fail', 'Codex install-state file is missing', globalInstallStateFix(homeDir, cwd, 'Codex install state')),
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
const codexMcp = tomlMcpHealth(codexConfigPath);
|
|
850
|
+
checks.push(
|
|
851
|
+
codexMcp.present && codexMcp.url && codexMcp.authorization
|
|
852
|
+
? makeCheck('codex-mcp', 'Codex MCP config', 'pass', 'Codex has a ghl-ai MCP server with URL and Authorization header')
|
|
853
|
+
: makeCheck(
|
|
854
|
+
'codex-mcp',
|
|
855
|
+
'Codex MCP config',
|
|
856
|
+
'fail',
|
|
857
|
+
`Codex MCP is incomplete (present=${codexMcp.present}, url=${codexMcp.url}, authorization=${codexMcp.authorization})`,
|
|
858
|
+
'Run `aw init` to rewrite ~/.codex/config.toml with a healthy ghl-ai MCP configuration.',
|
|
859
|
+
),
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
const codexRefPath = join(homeDir, '.codex', 'references', 'route-selection-patterns.md');
|
|
863
|
+
checks.push(
|
|
864
|
+
existsSync(codexRefPath)
|
|
865
|
+
? makeCheck('codex-references', 'Codex shared references', 'pass', 'Codex shared references are materialized')
|
|
866
|
+
: makeCheck('codex-references', 'Codex shared references', 'fail', 'Codex shared references are missing', projectRelinkFix(homeDir, cwd, '~/.codex/references')),
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const missingPrompts = missingCorePromptFiles(join(homeDir, '.codex', 'prompts'));
|
|
870
|
+
checks.push(
|
|
871
|
+
missingPrompts.length === 0
|
|
872
|
+
? makeCheck('codex-prompts', 'Codex prompts', 'pass', 'Codex prompt sync produced the current AW prompt surface (primary, conditional, and compatibility routes)')
|
|
873
|
+
: makeCheck(
|
|
874
|
+
'codex-prompts',
|
|
875
|
+
'Codex prompts',
|
|
876
|
+
'fail',
|
|
877
|
+
`Codex is missing core prompt files: ${missingPrompts.join(', ')}`,
|
|
878
|
+
'Run `aw init` or refresh the AW ECC bundle to regenerate the Codex prompts.',
|
|
879
|
+
),
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
const codexAgentsPath = join(homeDir, '.codex', 'AGENTS.md');
|
|
883
|
+
checks.push(
|
|
884
|
+
existsSync(codexAgentsPath) && textHasManagedRouterBridge(readText(codexAgentsPath)) && textHasRulesReference(readText(codexAgentsPath))
|
|
885
|
+
? makeCheck('codex-agents-md', 'Codex AGENTS.md', 'pass', 'Codex AGENTS.md includes the managed AW Router Bridge block and AW rules reference table')
|
|
886
|
+
: makeCheck(
|
|
887
|
+
'codex-agents-md',
|
|
888
|
+
'Codex AGENTS.md',
|
|
889
|
+
rulesDir ? 'fail' : 'warn',
|
|
890
|
+
existsSync(codexAgentsPath)
|
|
891
|
+
? 'Codex AGENTS.md is present but missing the managed AW Router Bridge block or AW rules reference table'
|
|
892
|
+
: 'Codex AGENTS.md is missing',
|
|
893
|
+
'Re-run `aw init` so Codex instructions are regenerated with the managed AW Router Bridge block and AW rules reference table.',
|
|
894
|
+
),
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
const codexAgentsText = readText(codexAgentsPath);
|
|
898
|
+
const codexBrokenRuleRefs = existsSync(codexAgentsPath) ? findBrokenRuleReferences([codexAgentsPath]) : [];
|
|
899
|
+
checks.push(
|
|
900
|
+
existsSync(codexAgentsPath) && textHasRulesReference(codexAgentsText)
|
|
901
|
+
? codexBrokenRuleRefs.length === 0
|
|
902
|
+
? makeCheck('codex-rule-references', 'Codex rule references', 'pass', 'Codex AGENTS.md points at real AW rule files')
|
|
903
|
+
: makeCheck(
|
|
904
|
+
'codex-rule-references',
|
|
905
|
+
'Codex rule references',
|
|
906
|
+
'fail',
|
|
907
|
+
`Codex AGENTS.md has a broken AW rule path: ${codexBrokenRuleRefs[0].ref}`,
|
|
908
|
+
'Regenerate ~/.codex/AGENTS.md so its AW rule paths resolve correctly from the Codex home directory.',
|
|
909
|
+
)
|
|
910
|
+
: makeCheck(
|
|
911
|
+
'codex-rule-references',
|
|
912
|
+
'Codex rule references',
|
|
913
|
+
rulesDir ? 'warn' : 'warn',
|
|
914
|
+
existsSync(codexAgentsPath)
|
|
915
|
+
? 'Codex rule references were not checked because ~/.codex/AGENTS.md is missing the AW rules reference table'
|
|
916
|
+
: 'Codex rule references were not checked because ~/.codex/AGENTS.md is missing',
|
|
917
|
+
),
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
const cursorHooksPath = join(homeDir, '.cursor', 'hooks.json');
|
|
921
|
+
const cursorRuntimePath = join(homeDir, '.cursor', 'hooks', 'session-start.js');
|
|
922
|
+
const cursorHomeHookCoverage = getCursorHomeHookCoverage(homeDir);
|
|
923
|
+
const cursorHealthy = cursorHomeHookCoverage.ok;
|
|
924
|
+
checks.push(cursorHealthy
|
|
925
|
+
? makeCheck(
|
|
926
|
+
'cursor-routing',
|
|
927
|
+
'Cursor routing',
|
|
928
|
+
'pass',
|
|
929
|
+
`Cursor home hooks cover all supported AW phases: ${formatPhaseSummary(cursorHomeHookCoverage.supportedPhases)}`,
|
|
930
|
+
)
|
|
931
|
+
: makeCheck(
|
|
932
|
+
'cursor-routing',
|
|
933
|
+
'Cursor routing',
|
|
934
|
+
startup.mode === 'disabled' ? 'warn' : 'fail',
|
|
935
|
+
`Cursor routing incomplete (missing phases=${formatMissingParts(cursorHomeHookCoverage.missingPhases)}, missing scripts=${formatMissingParts(cursorHomeHookCoverage.missingScripts)})`,
|
|
936
|
+
'Run `aw routing enable` or `aw init` to restore Cursor startup wiring.',
|
|
937
|
+
),
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
const cursorInstallStatePath = join(homeDir, '.cursor', 'ecc-install-state.json');
|
|
941
|
+
checks.push(existsSync(cursorInstallStatePath)
|
|
942
|
+
? makeCheck('cursor-install-state', 'Cursor install state', 'pass', 'Cursor install-state file is present')
|
|
943
|
+
: makeCheck('cursor-install-state', 'Cursor install state', 'fail', 'Cursor install-state file is missing', globalInstallStateFix(homeDir, cwd, 'Cursor install state')),
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
const cursorCommandsDir = join(homeDir, '.cursor', 'commands', 'aw');
|
|
947
|
+
const missingCursorCommands = missingCoreCursorCommandFiles(cursorCommandsDir);
|
|
948
|
+
checks.push(
|
|
949
|
+
missingCursorCommands.length === 0
|
|
950
|
+
? makeCheck('cursor-commands', 'Cursor public commands', 'pass', 'Cursor has the current AW command surface under ~/.cursor/commands/aw/')
|
|
951
|
+
: makeCheck(
|
|
952
|
+
'cursor-commands',
|
|
953
|
+
'Cursor public commands',
|
|
954
|
+
'fail',
|
|
955
|
+
`Cursor is missing core command files: ${missingCursorCommands.join(', ')}`,
|
|
956
|
+
projectRelinkFix(homeDir, cwd, 'AW command files under ~/.cursor/commands/aw/'),
|
|
957
|
+
),
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
const cursorMcp = jsonMcpHealth(join(homeDir, '.cursor', 'mcp.json'));
|
|
961
|
+
checks.push(
|
|
962
|
+
cursorMcp.present && cursorMcp.url && cursorMcp.authorization
|
|
963
|
+
? makeCheck('cursor-mcp', 'Cursor MCP config', 'pass', 'Cursor has a ghl-ai MCP server with URL and Authorization header')
|
|
964
|
+
: makeCheck(
|
|
965
|
+
'cursor-mcp',
|
|
966
|
+
'Cursor MCP config',
|
|
967
|
+
'fail',
|
|
968
|
+
`Cursor MCP is incomplete (present=${cursorMcp.present}, url=${cursorMcp.url}, authorization=${cursorMcp.authorization})`,
|
|
969
|
+
'Run `aw init` to rewrite ~/.cursor/mcp.json with a healthy ghl-ai MCP configuration.',
|
|
970
|
+
),
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
const cursorRefPath = join(homeDir, '.cursor', 'references', 'route-selection-patterns.md');
|
|
974
|
+
const cursorAgentsPath = join(homeDir, '.cursor', 'AGENTS.md');
|
|
975
|
+
checks.push(
|
|
976
|
+
existsSync(cursorAgentsPath) && textHasManagedRouterBridge(readText(cursorAgentsPath))
|
|
977
|
+
? makeCheck('cursor-home-instructions', 'Cursor home instructions', 'pass', 'Cursor home AGENTS.md includes the managed AW Router Bridge block')
|
|
978
|
+
: makeCheck(
|
|
979
|
+
'cursor-home-instructions',
|
|
980
|
+
'Cursor home instructions',
|
|
981
|
+
'fail',
|
|
982
|
+
existsSync(cursorAgentsPath)
|
|
983
|
+
? 'Cursor home AGENTS.md is present but missing the managed AW Router Bridge block'
|
|
984
|
+
: 'Cursor home AGENTS.md is missing',
|
|
985
|
+
'Run `aw init` to regenerate ~/.cursor/AGENTS.md with the managed AW Router Bridge block.',
|
|
986
|
+
),
|
|
987
|
+
);
|
|
988
|
+
checks.push(
|
|
989
|
+
existsSync(cursorRefPath)
|
|
990
|
+
? makeCheck('cursor-references', 'Cursor shared references', 'pass', 'Cursor shared references are materialized')
|
|
991
|
+
: makeCheck('cursor-references', 'Cursor shared references', 'fail', 'Cursor shared references are missing', projectRelinkFix(homeDir, cwd, '~/.cursor/references')),
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
checks.push(
|
|
995
|
+
rulesDir && directoryContainsGeneratedRuleFiles(join(homeDir, '.cursor', 'rules'), '.mdc')
|
|
996
|
+
? makeCheck('cursor-rules', 'Cursor AW rules', 'pass', 'Cursor has generated AW rules under ~/.cursor/rules/')
|
|
997
|
+
: makeCheck(
|
|
998
|
+
'cursor-rules',
|
|
999
|
+
'Cursor AW rules',
|
|
1000
|
+
rulesDir ? 'fail' : 'warn',
|
|
1001
|
+
rulesDir ? 'Cursor rules are missing or not generated by AW' : 'Cursor rules were not checked because .aw/.aw_rules is not synced',
|
|
1002
|
+
'Run `aw pull platform` or `aw init` to render Cursor rule files.',
|
|
1003
|
+
),
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
const cursorRulesDir = join(homeDir, '.cursor', 'rules');
|
|
1007
|
+
const cursorRuleFiles = listGeneratedRuleFiles(cursorRulesDir, '.mdc');
|
|
1008
|
+
const brokenCursorRuleRefs = findBrokenRuleReferences(cursorRuleFiles);
|
|
1009
|
+
checks.push(
|
|
1010
|
+
cursorRuleFiles.length === 0
|
|
1011
|
+
? makeCheck(
|
|
1012
|
+
'cursor-rule-references',
|
|
1013
|
+
'Cursor rule references',
|
|
1014
|
+
rulesDir ? 'fail' : 'warn',
|
|
1015
|
+
rulesDir ? 'Cursor generated rule files are missing, so reference links could not be validated' : 'Cursor rule references were not checked because .aw/.aw_rules is not synced',
|
|
1016
|
+
'Run `aw pull platform` or `aw init` to render Cursor rules, then rerun `aw doctor`.',
|
|
1017
|
+
)
|
|
1018
|
+
: brokenCursorRuleRefs.length === 0
|
|
1019
|
+
? makeCheck('cursor-rule-references', 'Cursor rule references', 'pass', 'Cursor generated rule references resolve to real files in ~/.aw/.aw_rules')
|
|
1020
|
+
: makeCheck(
|
|
1021
|
+
'cursor-rule-references',
|
|
1022
|
+
'Cursor rule references',
|
|
1023
|
+
'fail',
|
|
1024
|
+
`Cursor rule reference is broken in ${brokenCursorRuleRefs[0].filePath.replace(`${homeDir}/`, '~/')}: ${brokenCursorRuleRefs[0].ref}`,
|
|
1025
|
+
'Refresh the rendered Cursor rules so embedded reference links point at real files under ~/.aw/.aw_rules.',
|
|
1026
|
+
),
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
const eccDir = join(homeDir, '.aw-ecc');
|
|
1030
|
+
const eccSyncScript = join(eccDir, 'scripts', 'sync-ecc-to-codex.sh');
|
|
1031
|
+
if (!existsSync(eccDir)) {
|
|
1032
|
+
checks.push(makeCheck('aw-ecc', 'AW ECC bundle', 'warn', '~/.aw-ecc is not installed', 'Run `aw init` to install the bundled AW ECC workspace assets.'));
|
|
1033
|
+
} else if (!existsSync(eccSyncScript)) {
|
|
1034
|
+
checks.push(makeCheck('aw-ecc', 'AW ECC bundle', 'fail', 'AW ECC clone exists but sync-ecc-to-codex.sh is missing', 'Reinstall AW ECC with `aw init` to restore the bundled assets.'));
|
|
1035
|
+
} else {
|
|
1036
|
+
const currentTag = parseExactGitTag(eccDir);
|
|
1037
|
+
const tagSummary = currentTag ? `tag ${currentTag}` : 'no exact tag';
|
|
1038
|
+
checks.push(currentTag === AW_ECC_TAG
|
|
1039
|
+
? makeCheck('aw-ecc', 'AW ECC bundle', 'pass', `AW ECC clone present at expected ${tagSummary}`)
|
|
1040
|
+
: makeCheck('aw-ecc', 'AW ECC bundle', 'warn', `AW ECC clone present but not on expected tag ${AW_ECC_TAG} (${tagSummary})`, 'Re-run `aw init` if you want the bundled AW ECC clone reset to the expected release tag.'),
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return checks;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export function getDoctorReport(homeDir = homedir(), cwd = process.cwd()) {
|
|
1048
|
+
const checks = buildDoctorChecks(homeDir, cwd);
|
|
1049
|
+
const status = overallStatus(checks);
|
|
1050
|
+
const fixes = [...new Set(checks.map(check => check.fix).filter(Boolean))];
|
|
1051
|
+
return {
|
|
1052
|
+
status,
|
|
1053
|
+
checks,
|
|
1054
|
+
fixes,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function statusIcon(status) {
|
|
1059
|
+
switch (status) {
|
|
1060
|
+
case 'pass': return chalk.green('✓');
|
|
1061
|
+
case 'warn': return chalk.yellow('!');
|
|
1062
|
+
default: return chalk.red('✖');
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export function doctorCommand() {
|
|
1067
|
+
const homeDir = homedir();
|
|
1068
|
+
const report = getDoctorReport(homeDir, process.cwd());
|
|
1069
|
+
|
|
1070
|
+
fmt.intro('aw doctor');
|
|
1071
|
+
|
|
1072
|
+
const summary = report.checks
|
|
1073
|
+
.map(check => `${statusIcon(check.status)} ${check.title}: ${check.summary}`)
|
|
1074
|
+
.join('\n');
|
|
1075
|
+
fmt.note(summary, `Health ${STATUS_LABEL[report.status]}`);
|
|
1076
|
+
|
|
1077
|
+
if (report.fixes.length > 0) {
|
|
1078
|
+
fmt.note(report.fixes.map(fix => `- ${fix}`).join('\n'), 'Suggested Fixes');
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (report.status === 'fail') {
|
|
1082
|
+
process.exitCode = 1;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
fmt.outro(`⟁ aw doctor complete (${report.status.toUpperCase()})`);
|
|
1086
|
+
}
|