azclaude-copilot 0.1.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.
Files changed (108) hide show
  1. package/.claude-plugin/marketplace.json +27 -0
  2. package/.claude-plugin/plugin.json +17 -0
  3. package/LICENSE +21 -0
  4. package/README.md +477 -0
  5. package/bin/cli.js +1027 -0
  6. package/bin/copilot.js +228 -0
  7. package/hooks/README.md +3 -0
  8. package/hooks/hooks.json +40 -0
  9. package/package.json +41 -0
  10. package/templates/CLAUDE.md +51 -0
  11. package/templates/agents/cc-cli-integrator.md +104 -0
  12. package/templates/agents/cc-template-author.md +109 -0
  13. package/templates/agents/cc-test-maintainer.md +101 -0
  14. package/templates/agents/code-reviewer.md +136 -0
  15. package/templates/agents/loop-controller.md +118 -0
  16. package/templates/agents/orchestrator-init.md +196 -0
  17. package/templates/agents/test-writer.md +129 -0
  18. package/templates/capabilities/evolution/cycle2-knowledge.md +87 -0
  19. package/templates/capabilities/evolution/cycle3-topology.md +128 -0
  20. package/templates/capabilities/evolution/detect.md +103 -0
  21. package/templates/capabilities/evolution/evaluate.md +90 -0
  22. package/templates/capabilities/evolution/generate.md +123 -0
  23. package/templates/capabilities/evolution/re-derivation.md +77 -0
  24. package/templates/capabilities/intelligence/debate.md +104 -0
  25. package/templates/capabilities/intelligence/elo.md +122 -0
  26. package/templates/capabilities/intelligence/experiment.md +86 -0
  27. package/templates/capabilities/intelligence/opro.md +84 -0
  28. package/templates/capabilities/intelligence/pipeline.md +149 -0
  29. package/templates/capabilities/level-builders/level1-claudemd.md +52 -0
  30. package/templates/capabilities/level-builders/level2-mcp.md +58 -0
  31. package/templates/capabilities/level-builders/level3-skills.md +276 -0
  32. package/templates/capabilities/level-builders/level4-memory.md +72 -0
  33. package/templates/capabilities/level-builders/level5-agents.md +123 -0
  34. package/templates/capabilities/level-builders/level6-hooks.md +119 -0
  35. package/templates/capabilities/level-builders/level7-extmcp.md +60 -0
  36. package/templates/capabilities/level-builders/level8-orchestrated.md +98 -0
  37. package/templates/capabilities/manifest.md +58 -0
  38. package/templates/capabilities/shared/5-layer-agent.md +206 -0
  39. package/templates/capabilities/shared/completion-rule.md +44 -0
  40. package/templates/capabilities/shared/context-artifacts.md +96 -0
  41. package/templates/capabilities/shared/domain-advisor-generator.md +205 -0
  42. package/templates/capabilities/shared/friction-log.md +43 -0
  43. package/templates/capabilities/shared/multi-cli-paths.md +56 -0
  44. package/templates/capabilities/shared/native-tools.md +199 -0
  45. package/templates/capabilities/shared/plan-tracker.md +69 -0
  46. package/templates/capabilities/shared/pressure-test.md +88 -0
  47. package/templates/capabilities/shared/quality-check.md +83 -0
  48. package/templates/capabilities/shared/reflexes.md +159 -0
  49. package/templates/capabilities/shared/review-reception.md +70 -0
  50. package/templates/capabilities/shared/security.md +174 -0
  51. package/templates/capabilities/shared/semantic-boundary-check.md +140 -0
  52. package/templates/capabilities/shared/session-rhythm.md +42 -0
  53. package/templates/capabilities/shared/tdd.md +54 -0
  54. package/templates/capabilities/shared/vocabulary-transform.md +63 -0
  55. package/templates/commands/add.md +152 -0
  56. package/templates/commands/audit.md +123 -0
  57. package/templates/commands/blueprint.md +115 -0
  58. package/templates/commands/copilot.md +157 -0
  59. package/templates/commands/create.md +156 -0
  60. package/templates/commands/debate.md +75 -0
  61. package/templates/commands/deps.md +112 -0
  62. package/templates/commands/doc.md +100 -0
  63. package/templates/commands/dream.md +120 -0
  64. package/templates/commands/evolve.md +170 -0
  65. package/templates/commands/explain.md +25 -0
  66. package/templates/commands/find.md +100 -0
  67. package/templates/commands/fix.md +122 -0
  68. package/templates/commands/hookify.md +100 -0
  69. package/templates/commands/level-up.md +48 -0
  70. package/templates/commands/loop.md +62 -0
  71. package/templates/commands/migrate.md +119 -0
  72. package/templates/commands/persist.md +73 -0
  73. package/templates/commands/pulse.md +87 -0
  74. package/templates/commands/refactor.md +97 -0
  75. package/templates/commands/reflect.md +107 -0
  76. package/templates/commands/reflexes.md +141 -0
  77. package/templates/commands/setup.md +97 -0
  78. package/templates/commands/ship.md +131 -0
  79. package/templates/commands/snapshot.md +70 -0
  80. package/templates/commands/test.md +86 -0
  81. package/templates/hooks/post-tool-use.js +175 -0
  82. package/templates/hooks/stop.js +85 -0
  83. package/templates/hooks/user-prompt.js +96 -0
  84. package/templates/scripts/env-scan.sh +46 -0
  85. package/templates/scripts/import-graph.sh +88 -0
  86. package/templates/scripts/validate-boundaries.sh +180 -0
  87. package/templates/skills/agent-creator/SKILL.md +91 -0
  88. package/templates/skills/agent-creator/examples/sample-agent.md +80 -0
  89. package/templates/skills/agent-creator/references/agent-engineering-guide.md +596 -0
  90. package/templates/skills/agent-creator/references/quality-checklist.md +42 -0
  91. package/templates/skills/agent-creator/scripts/scaffold.sh +144 -0
  92. package/templates/skills/architecture-advisor/SKILL.md +92 -0
  93. package/templates/skills/architecture-advisor/references/database-decisions.md +61 -0
  94. package/templates/skills/architecture-advisor/references/decision-matrices.md +122 -0
  95. package/templates/skills/architecture-advisor/references/rendering-decisions.md +39 -0
  96. package/templates/skills/architecture-advisor/scripts/detect-scale.sh +67 -0
  97. package/templates/skills/debate/SKILL.md +36 -0
  98. package/templates/skills/debate/references/acemad-protocol.md +72 -0
  99. package/templates/skills/env-scanner/SKILL.md +41 -0
  100. package/templates/skills/security/SKILL.md +44 -0
  101. package/templates/skills/security/references/security-details.md +48 -0
  102. package/templates/skills/session-guard/SKILL.md +33 -0
  103. package/templates/skills/skill-creator/SKILL.md +82 -0
  104. package/templates/skills/skill-creator/examples/sample-skill.md +74 -0
  105. package/templates/skills/skill-creator/references/quality-checklist.md +36 -0
  106. package/templates/skills/skill-creator/references/skill-engineering-guide.md +365 -0
  107. package/templates/skills/skill-creator/scripts/scaffold.sh +75 -0
  108. package/templates/skills/test-first/SKILL.md +41 -0
package/bin/cli.js ADDED
@@ -0,0 +1,1027 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const crypto = require('crypto');
7
+ const { execSync } = require('child_process');
8
+
9
+ const TEMPLATE_DIR = path.join(__dirname, '..', 'templates');
10
+ const CORE_COMMANDS = ['setup', 'fix', 'add', 'audit', 'test', 'blueprint', 'ship', 'pulse', 'explain', 'snapshot', 'persist'];
11
+ const EXTENDED_COMMANDS = ['dream', 'refactor', 'doc', 'loop', 'migrate', 'deps', 'find', 'create', 'reflect', 'hookify'];
12
+ const ADVANCED_COMMANDS = ['evolve', 'debate', 'level-up', 'copilot', 'reflexes'];
13
+ const COMMANDS = [...CORE_COMMANDS, ...EXTENDED_COMMANDS, ...ADVANCED_COMMANDS];
14
+
15
+ function ok(msg) { console.log(` ✓ ${msg}`); }
16
+ function warn(msg) { console.log(` ⚠ ${msg}`); }
17
+ function info(msg) { console.log(` · ${msg}`); }
18
+
19
+ // ─── Security ─────────────────────────────────────────────────────────────────
20
+
21
+ // Path sanitization is handled in hooks: post-tool-use.js rejects paths
22
+ // outside the project root (rel.startsWith('..')) and skips node_modules/.git.
23
+
24
+ function generateIntegrityHash(hooksObj) {
25
+ const content = JSON.stringify(hooksObj, null, 2);
26
+ return crypto.createHash('sha256').update(content).digest('hex');
27
+ }
28
+
29
+ // Atomic write: write to .tmp then rename — prevents corruption on crash/power loss
30
+ function atomicWriteFileSync(filePath, data) {
31
+ const tmp = filePath + '.tmp';
32
+ fs.writeFileSync(tmp, data);
33
+ fs.renameSync(tmp, filePath);
34
+ }
35
+
36
+ function verifyIntegrity(projectDir, cfg, cli) {
37
+ // Check project-scoped first, then global
38
+ const candidates = [
39
+ { settingsPath: path.join(projectDir, cfg, 'settings.local.json'), integrityPath: path.join(projectDir, cfg, '.azclaude-integrity'), label: 'project' },
40
+ ];
41
+ if (cli.hooksDir) {
42
+ candidates.push({ settingsPath: path.join(cli.hooksDir, 'settings.json'), integrityPath: path.join(cli.hooksDir, '.azclaude-integrity'), label: 'global' });
43
+ }
44
+
45
+ for (const { settingsPath, integrityPath, label } of candidates) {
46
+ if (!fs.existsSync(integrityPath) || !fs.existsSync(settingsPath)) continue;
47
+ try {
48
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
49
+ const savedHash = fs.readFileSync(integrityPath, 'utf8').trim();
50
+ const currentHash = generateIntegrityHash(settings.hooks || {});
51
+ if (savedHash !== currentHash) {
52
+ warn(`Hook integrity mismatch (${label}) — hooks were modified since last AZCLAUDE install`);
53
+ warn('Verify your hooks manually before continuing.');
54
+ return false;
55
+ }
56
+ ok(`Hook integrity verified (${label})`);
57
+ return true;
58
+ } catch {
59
+ warn(`Could not verify hook integrity (${label})`);
60
+ }
61
+ }
62
+ return true;
63
+ }
64
+
65
+ // ─── CLI Detection ────────────────────────────────────────────────────────────
66
+
67
+ const CLI_TABLE = [
68
+ { name: 'Claude Code', exe: 'claude', cfg: '.claude', rulesFile: 'CLAUDE.md', hooksDir: path.join(os.homedir(), '.claude') },
69
+ { name: 'Gemini CLI', exe: 'gemini', cfg: '.gemini', rulesFile: 'GEMINI.md', hooksDir: null },
70
+ { name: 'OpenCode', exe: 'opencode', cfg: '.opencode', rulesFile: 'AGENTS.md', hooksDir: null },
71
+ { name: 'Codex CLI', exe: 'codex', cfg: '.codex', rulesFile: 'AGENTS.md', hooksDir: null },
72
+ { name: 'Cursor', exe: 'cursor', cfg: '.cursor', rulesFile: '.cursor/rules/project.mdc', hooksDir: null },
73
+ ];
74
+
75
+ function detectCLI() {
76
+ // 0. Env override (for testing / explicit selection)
77
+ if (process.env.AZCLAUDE_CLI) {
78
+ const forced = CLI_TABLE.find(c => c.name.toLowerCase().replace(/\s/g,'') === process.env.AZCLAUDE_CLI.toLowerCase());
79
+ if (forced) return forced;
80
+ }
81
+
82
+ // 1. Check executable in PATH
83
+ for (const cli of CLI_TABLE) {
84
+ try {
85
+ execSync(`${cli.exe} --version`, { stdio: 'ignore', timeout: 2000 });
86
+ return cli;
87
+ } catch {}
88
+ }
89
+
90
+ // 2. Fallback: global config dir in HOME (Claude Code only currently has this)
91
+ for (const cli of CLI_TABLE) {
92
+ if (cli.hooksDir && fs.existsSync(cli.hooksDir)) return cli;
93
+ }
94
+
95
+ // 3. Default
96
+ return CLI_TABLE[0];
97
+ }
98
+
99
+ // ─── Path Substitution ────────────────────────────────────────────────────────
100
+
101
+ // Replace every hardcoded .claude/ reference in a template file with the
102
+ // detected cfg path. Called at install time — once — never again at runtime.
103
+ function substitutePaths(content, cfg) {
104
+ return content.replace(/\.claude\//g, `${cfg}/`);
105
+ }
106
+
107
+ // ─── Hook Scripts ─────────────────────────────────────────────────────────────
108
+
109
+ const HOOK_SCRIPTS = ['user-prompt.js', 'stop.js', 'post-tool-use.js'];
110
+
111
+ function copyHookScripts(dstDir) {
112
+ fs.mkdirSync(dstDir, { recursive: true });
113
+ const srcDir = path.join(TEMPLATE_DIR, 'hooks');
114
+ for (const name of HOOK_SCRIPTS) {
115
+ const src = path.join(srcDir, name);
116
+ const dst = path.join(dstDir, name);
117
+ if (fs.existsSync(src)) {
118
+ fs.copyFileSync(src, dst);
119
+ try { fs.chmodSync(dst, '755'); } catch (_) {}
120
+ }
121
+ }
122
+ return dstDir;
123
+ }
124
+
125
+ function buildHookEntries(scriptsDir) {
126
+ const nodeExe = process.execPath;
127
+ const userPromptScript = path.join(scriptsDir, 'user-prompt.js');
128
+ const stopScript = path.join(scriptsDir, 'stop.js');
129
+ const postToolUseScript = path.join(scriptsDir, 'post-tool-use.js');
130
+ return {
131
+ UserPromptSubmit: [{ matcher: '', hooks: [{ type: 'command', command: `"${nodeExe}" "${userPromptScript}"` }] }],
132
+ Stop: [{ matcher: '', hooks: [{ type: 'command', command: `"${nodeExe}" "${stopScript}"` }] }],
133
+ PostToolUse: [{ matcher: 'Write|Edit', hooks: [{ type: 'command', command: `"${nodeExe}" "${postToolUseScript}"` }] }],
134
+ };
135
+ }
136
+
137
+ // ─── Project-Scoped Hooks (default) ──────────────────────────────────────────
138
+ // Installs hooks into <project>/.claude/settings.local.json + <project>/.claude/hooks/
139
+ // settings.local.json is gitignored (machine-specific absolute paths).
140
+
141
+ function installProjectHooks(projectDir, cfg) {
142
+ const hooksDir = path.join(projectDir, cfg, 'hooks');
143
+ const settingsPath = path.join(projectDir, cfg, 'settings.local.json');
144
+
145
+ // Copy hook scripts into project
146
+ const scriptsDir = copyHookScripts(hooksDir);
147
+
148
+ // Read existing settings.local.json (may have permissions, env vars, etc.)
149
+ let settings = {};
150
+ if (fs.existsSync(settingsPath)) {
151
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
152
+ }
153
+
154
+ if (settings._azclaude) {
155
+ // Already installed — just refresh scripts to latest templates
156
+ ok('Project hooks already installed — scripts refreshed');
157
+ return;
158
+ }
159
+
160
+ // Check for existing hooks from other sources
161
+ const hasExistingHooks = settings.hooks && Object.keys(settings.hooks).length > 0;
162
+ if (hasExistingHooks) {
163
+ warn(`Existing hooks in ${settingsPath} — merging AZCLAUDE hooks`);
164
+ }
165
+
166
+ // Write hook config
167
+ settings._azclaude = true;
168
+ if (!settings.hooks) settings.hooks = {};
169
+ Object.assign(settings.hooks, buildHookEntries(scriptsDir));
170
+
171
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2));
172
+
173
+ // Write integrity hash
174
+ const integrityPath = path.join(projectDir, cfg, '.azclaude-integrity');
175
+ fs.writeFileSync(integrityPath, generateIntegrityHash(settings.hooks));
176
+
177
+ // Ensure settings.local.json is gitignored (contains machine-specific absolute paths)
178
+ const gitignorePath = path.join(projectDir, '.gitignore');
179
+ if (fs.existsSync(gitignorePath)) {
180
+ const gitignore = fs.readFileSync(gitignorePath, 'utf8');
181
+ if (!gitignore.includes('settings.local.json')) {
182
+ fs.appendFileSync(gitignorePath, `\n# AZCLAUDE — machine-specific hook paths\n${cfg}/settings.local.json\n`);
183
+ ok('Added settings.local.json to .gitignore');
184
+ }
185
+ }
186
+
187
+ ok(`Project hooks installed (${cfg}/settings.local.json)`);
188
+ ok(`Hook scripts installed (${cfg}/hooks/)`);
189
+ info('Project-scoped — no global pollution, no cross-project side effects');
190
+ info('Node.js hooks — works on Windows PowerShell, CMD, Git Bash, macOS, Linux');
191
+ }
192
+
193
+ // ─── Migration: clean global hooks if project hooks now active ───────────────
194
+
195
+ function migrateFromGlobalHooks(cli, projectDir, cfg) {
196
+ if (!cli.hooksDir) return;
197
+ const globalSettings = path.join(cli.hooksDir, 'settings.json');
198
+ if (!fs.existsSync(globalSettings)) return;
199
+
200
+ let settings = {};
201
+ try { settings = JSON.parse(fs.readFileSync(globalSettings, 'utf8')); } catch { return; }
202
+ if (!settings._azclaude) return;
203
+
204
+ // Global AZCLAUDE hooks exist — remove only AZCLAUDE's entries, preserve other plugins'
205
+ delete settings._azclaude;
206
+ if (settings.hooks) {
207
+ for (const event of ['UserPromptSubmit', 'Stop', 'PostToolUse']) {
208
+ if (Array.isArray(settings.hooks[event])) {
209
+ // Keep entries that don't reference AZCLAUDE hook scripts
210
+ settings.hooks[event] = settings.hooks[event].filter(group => {
211
+ const cmds = (group.hooks || []).map(h => h.command || '');
212
+ return !cmds.some(c => c.includes('user-prompt.js') || c.includes('stop.js') || c.includes('post-tool-use.js'));
213
+ });
214
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
215
+ }
216
+ }
217
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
218
+ }
219
+
220
+ // Clean up: if settings is now empty (only had AZCLAUDE), remove the file
221
+ const remaining = Object.keys(settings).length;
222
+ if (remaining === 0) {
223
+ try { fs.unlinkSync(globalSettings); } catch (_) {}
224
+ } else {
225
+ atomicWriteFileSync(globalSettings, JSON.stringify(settings, null, 2));
226
+ }
227
+
228
+ // Remove global hook scripts
229
+ const globalHooksDir = path.join(cli.hooksDir, 'hooks');
230
+ for (const name of HOOK_SCRIPTS) {
231
+ const f = path.join(globalHooksDir, name);
232
+ try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch (_) {}
233
+ }
234
+
235
+ // Remove integrity hash
236
+ const integrityPath = path.join(cli.hooksDir, '.azclaude-integrity');
237
+ try { if (fs.existsSync(integrityPath)) fs.unlinkSync(integrityPath); } catch (_) {}
238
+
239
+ ok('Migrated: global hooks removed — project-scoped hooks take over');
240
+ }
241
+
242
+ // ─── Global Hooks (fallback for CLIs without project-scoped support) ─────────
243
+
244
+ function installGlobalHooks(cli) {
245
+ if (!cli.hooksDir) {
246
+ warn(`Hooks not supported for ${cli.name}`);
247
+ info('Session state (goals.md injection, friction stubs) requires manual /persist on this CLI');
248
+ return;
249
+ }
250
+
251
+ const settingsPath = path.join(cli.hooksDir, 'settings.json');
252
+ if (!fs.existsSync(cli.hooksDir)) fs.mkdirSync(cli.hooksDir, { recursive: true });
253
+
254
+ let settings = {};
255
+ if (fs.existsSync(settingsPath)) {
256
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
257
+ }
258
+
259
+ if (settings._azclaude) {
260
+ copyHookScripts(path.join(cli.hooksDir, 'hooks'));
261
+ ok('Global hooks already installed — scripts refreshed');
262
+ return;
263
+ }
264
+
265
+ const hasExistingHooks = settings.hooks && Object.keys(settings.hooks).length > 0;
266
+ if (hasExistingHooks) {
267
+ warn(`Existing hooks detected in ${settingsPath}`);
268
+ warn('Manually merge the AZCLAUDE hooks or back up and re-run.');
269
+ return;
270
+ }
271
+
272
+ const scriptsDir = copyHookScripts(path.join(cli.hooksDir, 'hooks'));
273
+
274
+ settings._azclaude = true;
275
+ if (!settings.hooks) settings.hooks = {};
276
+ Object.assign(settings.hooks, buildHookEntries(scriptsDir));
277
+
278
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2));
279
+
280
+ const integrityPath = path.join(cli.hooksDir, '.azclaude-integrity');
281
+ fs.writeFileSync(integrityPath, generateIntegrityHash(settings.hooks));
282
+
283
+ ok(`Global hooks installed (${settingsPath})`);
284
+ ok(`Hook scripts installed (${scriptsDir})`);
285
+ }
286
+
287
+ // ─── Capabilities ─────────────────────────────────────────────────────────────
288
+
289
+ // Core capability dirs installed by default; advanced dirs only with --full
290
+ const CORE_CAP_DIRS = ['shared', 'level-builders'];
291
+ const FULL_CAP_DIRS = ['shared', 'level-builders', 'evolution', 'intelligence'];
292
+
293
+ function installCapabilities(projectDir, cfg, full) {
294
+ const src = path.join(TEMPLATE_DIR, 'capabilities');
295
+ const dst = path.join(projectDir, cfg, 'capabilities');
296
+
297
+ if (fs.existsSync(dst)) {
298
+ // If upgrading to --full, install missing dirs
299
+ if (full) {
300
+ for (const dir of FULL_CAP_DIRS) {
301
+ const dstSub = path.join(dst, dir);
302
+ if (!fs.existsSync(dstSub)) {
303
+ copyDir(path.join(src, dir), dstSub);
304
+ ok(`${dir}/ capabilities added (--full)`);
305
+ }
306
+ }
307
+ }
308
+ ok('Capabilities already installed — checked');
309
+ return;
310
+ }
311
+
312
+ const dirs = full ? FULL_CAP_DIRS : CORE_CAP_DIRS;
313
+ fs.mkdirSync(dst, { recursive: true });
314
+ // Always install manifest.md
315
+ fs.copyFileSync(path.join(src, 'manifest.md'), path.join(dst, 'manifest.md'));
316
+ for (const dir of dirs) {
317
+ copyDir(path.join(src, dir), path.join(dst, dir));
318
+ }
319
+ ok(`Capabilities installed (${cfg}/capabilities/) — ${full ? 'full' : 'core'}`);
320
+ if (!full) info('Run npx azclaude --full to add evolution + intelligence capabilities');
321
+ info('manifest.md is your capability index — read it to find what to load');
322
+ }
323
+
324
+ // ─── Commands (Skills) ────────────────────────────────────────────────────────
325
+
326
+ function installCommands(projectDir, cfg) {
327
+ const commandsDir = path.join(projectDir, cfg, 'commands');
328
+ fs.mkdirSync(commandsDir, { recursive: true });
329
+
330
+ for (const cmd of COMMANDS) {
331
+ const src = path.join(TEMPLATE_DIR, 'commands', `${cmd}.md`);
332
+ const dst = path.join(commandsDir, `${cmd}.md`);
333
+ if (!fs.existsSync(dst) && fs.existsSync(src)) {
334
+ fs.copyFileSync(src, dst);
335
+ ok(`/${cmd} installed`);
336
+ } else if (fs.existsSync(dst)) {
337
+ info(`/${cmd} already exists — skipping`);
338
+ }
339
+ }
340
+ }
341
+
342
+ // ─── Skills (SKILL.md — model-auto-invoked) ──────────────────────────────────
343
+
344
+ const SKILLS = ['session-guard', 'test-first', 'env-scanner', 'debate', 'security', 'skill-creator', 'agent-creator', 'architecture-advisor'];
345
+
346
+ function installSkills(projectDir, cfg) {
347
+ const skillsDir = path.join(projectDir, cfg, 'skills');
348
+ fs.mkdirSync(skillsDir, { recursive: true });
349
+
350
+ for (const skill of SKILLS) {
351
+ const srcDir = path.join(TEMPLATE_DIR, 'skills', skill);
352
+ const src = path.join(srcDir, 'SKILL.md');
353
+ const dstDir = path.join(skillsDir, skill);
354
+ const dst = path.join(dstDir, 'SKILL.md');
355
+ if (!fs.existsSync(dst) && fs.existsSync(src)) {
356
+ // Copy SKILL.md + references/ + examples/ (progressive disclosure)
357
+ fs.mkdirSync(dstDir, { recursive: true });
358
+ const content = substitutePaths(fs.readFileSync(src, 'utf8'), cfg);
359
+ fs.writeFileSync(dst, content);
360
+ // Copy subdirectories (references/, examples/, scripts/)
361
+ for (const sub of ['references', 'examples', 'scripts']) {
362
+ const subSrc = path.join(srcDir, sub);
363
+ if (fs.existsSync(subSrc)) copyDir(subSrc, path.join(dstDir, sub));
364
+ }
365
+ ok(`${skill} skill installed (auto-invoked by model)`);
366
+ } else if (fs.existsSync(dst)) {
367
+ info(`${skill} skill already exists — skipping`);
368
+ }
369
+ }
370
+ }
371
+
372
+ // ─── Scripts ──────────────────────────────────────────────────────────────────
373
+
374
+ function installScripts(projectDir, cfg) {
375
+ const src = path.join(TEMPLATE_DIR, 'scripts');
376
+ const dst = path.join(projectDir, cfg, 'scripts');
377
+ if (!fs.existsSync(src)) return;
378
+ fs.mkdirSync(dst, { recursive: true });
379
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
380
+ if (entry.isDirectory()) continue;
381
+ const s = path.join(src, entry.name);
382
+ const d = path.join(dst, entry.name);
383
+ // Always overwrite scripts to keep in sync (unlike commands which preserve user edits)
384
+ fs.copyFileSync(s, d);
385
+ try { fs.chmodSync(d, '755'); } catch {}
386
+ }
387
+ ok(`Scripts installed/updated (${cfg}/scripts/)`);
388
+ }
389
+
390
+ // ─── Agents ───────────────────────────────────────────────────────────────────
391
+
392
+ const AGENTS = ['orchestrator-init', 'code-reviewer', 'test-writer', 'loop-controller', 'cc-template-author', 'cc-cli-integrator', 'cc-test-maintainer'];
393
+
394
+ function installAgents(projectDir, cfg) {
395
+ const agentsDir = path.join(projectDir, cfg, 'agents');
396
+ fs.mkdirSync(agentsDir, { recursive: true });
397
+
398
+ for (const agent of AGENTS) {
399
+ const src = path.join(TEMPLATE_DIR, 'agents', `${agent}.md`);
400
+ const dst = path.join(agentsDir, `${agent}.md`);
401
+ if (!fs.existsSync(dst) && fs.existsSync(src)) {
402
+ const content = substitutePaths(fs.readFileSync(src, 'utf8'), cfg);
403
+ fs.writeFileSync(dst, content);
404
+ ok(`${agent} agent installed`);
405
+ }
406
+ }
407
+ }
408
+
409
+ // ─── Rules File (CLAUDE.md / GEMINI.md / AGENTS.md) ──────────────────────────
410
+
411
+ function installRulesFile(projectDir, cfg, rulesFile) {
412
+ const dst = path.join(projectDir, rulesFile);
413
+
414
+ // Ensure parent dir exists (e.g. .cursor/rules/)
415
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
416
+
417
+ if (fs.existsSync(dst)) {
418
+ info(`${rulesFile} already exists — run /setup to fill in project details`);
419
+ return;
420
+ }
421
+
422
+ const src = path.join(TEMPLATE_DIR, 'CLAUDE.md');
423
+ const content = substitutePaths(fs.readFileSync(src, 'utf8'), cfg);
424
+ fs.writeFileSync(dst, content);
425
+ ok(`${rulesFile} created — run /setup to configure for this project`);
426
+ }
427
+
428
+ // ─── Directories ──────────────────────────────────────────────────────────────
429
+
430
+ function createDirectories(projectDir, cfg) {
431
+ const dirs = [
432
+ `${cfg}/memory/sessions`,
433
+ `${cfg}/memory/learnings`,
434
+ 'ops/observations',
435
+ 'shared-skills'
436
+ ];
437
+ for (const dir of dirs) {
438
+ fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
439
+ }
440
+ ok('Memory directories created');
441
+
442
+ const knowledgeDir = path.join(projectDir, 'knowledge');
443
+ const knowledgeIndex = path.join(projectDir, 'knowledge-index.md');
444
+ if (fs.existsSync(knowledgeDir) && !fs.existsSync(knowledgeIndex)) {
445
+ fs.writeFileSync(knowledgeIndex,
446
+ '| file | summary | key_questions | tags |\n' +
447
+ '|------|---------|--------------|------|\n' +
448
+ '| (run /setup to populate this index) | | | |\n'
449
+ );
450
+ ok('knowledge-index.md stub created (run /setup to populate)');
451
+ }
452
+ }
453
+
454
+ // ─── Shared Skills ────────────────────────────────────────────────────────────
455
+
456
+ function ensureSharedSkillsDir() {
457
+ const dir = path.join(os.homedir(), 'shared-skills');
458
+ if (!fs.existsSync(dir)) {
459
+ fs.mkdirSync(dir, { recursive: true });
460
+ ok('~/shared-skills created (global portable skills directory)');
461
+ } else {
462
+ info('~/shared-skills exists — checking for portable skills');
463
+ const skills = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
464
+ if (skills.length > 0) {
465
+ info(` ${skills.length} portable skill(s) available: ${skills.join(', ')}`);
466
+ info(' Run /setup to import skills that match this project\'s stack');
467
+
468
+ // Verify checksums if available
469
+ const checksumsPath = path.join(dir, '.checksums');
470
+ if (fs.existsSync(checksumsPath)) {
471
+ const checksums = fs.readFileSync(checksumsPath, 'utf8').split('\n').filter(Boolean);
472
+ const checksumMap = {};
473
+ for (const line of checksums) {
474
+ const [hash, file] = line.split(' ');
475
+ if (hash && file) checksumMap[file.trim()] = hash.trim();
476
+ }
477
+ for (const skill of skills) {
478
+ if (checksumMap[skill]) {
479
+ const content = fs.readFileSync(path.join(dir, skill), 'utf8');
480
+ const actual = crypto.createHash('sha256').update(content).digest('hex');
481
+ if (actual !== checksumMap[skill]) {
482
+ warn(`Checksum mismatch for ${skill} — file may have been tampered with`);
483
+ } else {
484
+ ok(`${skill} checksum verified`);
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ // ─── Demo ─────────────────────────────────────────────────────────────────────
494
+
495
+ function runDemo() {
496
+ const { spawnSync } = require('child_process');
497
+ const tmpBase = path.join(os.tmpdir(), `azclaude-demo-${Date.now()}`);
498
+
499
+ function step(n, label) { console.log(`\nStep ${n}: ${label}`); }
500
+ function show(label, val) { console.log(` ${label}`); if (val) console.log(` ${val.split('\n').join('\n ')}`); }
501
+
502
+ console.log('\n════════════════════════════════════════════════');
503
+ console.log(' AZCLAUDE demo — 30-second proof');
504
+ console.log(' Showing: memory survives context compaction');
505
+ console.log('════════════════════════════════════════════════');
506
+
507
+ // ── Step 1: scaffold a minimal project ───────────────────────────────────
508
+ step(1, 'Create a project with goals.md');
509
+ fs.mkdirSync(path.join(tmpBase, '.claude', 'memory'), { recursive: true });
510
+ fs.mkdirSync(path.join(tmpBase, 'ops', 'observations'), { recursive: true });
511
+ fs.mkdirSync(path.join(tmpBase, 'src'), { recursive: true });
512
+
513
+ const goalsInitial = [
514
+ '# Goals — demo-project',
515
+ 'Updated: ' + new Date().toISOString().slice(0, 10),
516
+ '',
517
+ '## Current threads',
518
+ '- Build user auth feature',
519
+ '',
520
+ '## Done this session',
521
+ '- Project scaffolded',
522
+ '',
523
+ '## Next actions',
524
+ '1. Implement login endpoint',
525
+ ''
526
+ ].join('\n');
527
+
528
+ const goalsPath = path.join(tmpBase, '.claude', 'memory', 'goals.md');
529
+ const fakeFile = path.join(tmpBase, 'src', 'auth.js');
530
+ fs.writeFileSync(goalsPath, goalsInitial);
531
+ fs.writeFileSync(fakeFile, '// auth logic here\n');
532
+
533
+ show('✓ Project created at', tmpBase);
534
+ show('✓ goals.md initialized with current thread:', '"Build user auth feature"');
535
+
536
+ // ── Step 2: simulate PostToolUse (file edit) ──────────────────────────────
537
+ step(2, 'You edit src/auth.js — PostToolUse hook fires automatically');
538
+
539
+ const postToolScript = path.join(TEMPLATE_DIR, 'hooks', 'post-tool-use.js');
540
+ if (fs.existsSync(postToolScript)) {
541
+ const toolInput = JSON.stringify({ tool_input: { file_path: fakeFile } });
542
+ spawnSync(process.execPath, [postToolScript], {
543
+ input: toolInput,
544
+ cwd: tmpBase,
545
+ env: { ...process.env, AZCLAUDE_CFG: '.claude' }
546
+ });
547
+ const after = fs.readFileSync(goalsPath, 'utf8');
548
+ const ipLine = after.split('\n').find(l => l.includes('src'));
549
+ show('✓ PostToolUse fired — goals.md updated:', ipLine || '(entry added)');
550
+ }
551
+
552
+ // ── Step 3: compaction ────────────────────────────────────────────────────
553
+ step(3, 'Claude Code compacts conversation at turn 80 — all earlier context gone');
554
+ show('→ Turns 1-79 summarized, detail lost');
555
+ show('→ But goals.md still has the In progress entry');
556
+ const mid = fs.readFileSync(goalsPath, 'utf8');
557
+ const ipSection = mid.split('\n').filter(l => l.includes('## In progress') || l.startsWith('- ')).join('\n');
558
+ show('✓ goals.md In progress section:', ipSection);
559
+
560
+ // ── Step 4: next session — UserPromptSubmit injects goals ─────────────────
561
+ step(4, 'New session starts — UserPromptSubmit hook fires automatically');
562
+
563
+ const upScript = path.join(TEMPLATE_DIR, 'hooks', 'user-prompt.js');
564
+ if (fs.existsSync(upScript)) {
565
+ // Neutralise the session-marker so it fires even in this process tree
566
+ const marker = path.join(os.tmpdir(), `.azclaude-session-${process.ppid || process.pid}`);
567
+ const markerExisted = fs.existsSync(marker);
568
+ if (markerExisted) fs.unlinkSync(marker);
569
+
570
+ const result = spawnSync(process.execPath, [upScript], {
571
+ cwd: tmpBase,
572
+ env: { ...process.env, AZCLAUDE_CFG: '.claude' }
573
+ });
574
+
575
+ if (markerExisted) fs.writeFileSync(marker, ''); // restore
576
+ show('✓ Claude receives this at session start:');
577
+ console.log(' ┌─────────────────────────────────────────');
578
+ (result.stdout || '').toString().split('\n').forEach(l => console.log(` │ ${l}`));
579
+ console.log(' └─────────────────────────────────────────');
580
+ }
581
+
582
+ // ── Cleanup ───────────────────────────────────────────────────────────────
583
+ try { fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {}
584
+
585
+ console.log('\n════════════════════════════════════════════════');
586
+ console.log(' Memory works. Context survives compaction.');
587
+ console.log(' The hook fires on every file edit — no user action needed.');
588
+ console.log('\n Install on your project: npx azclaude');
589
+ console.log(' Check health: npx azclaude doctor');
590
+ console.log('════════════════════════════════════════════════\n');
591
+ }
592
+
593
+ // ─── Audit — efficiency scoring ───────────────────────────────────────────────
594
+
595
+ function runAudit() {
596
+ const cli = detectCLI();
597
+ const projectDir = process.cwd();
598
+ const cfg = cli.cfg;
599
+
600
+ console.log('\n════════════════════════════════════════════════');
601
+ console.log(' AZCLAUDE audit — efficiency score');
602
+ console.log('════════════════════════════════════════════════\n');
603
+
604
+ const scores = {};
605
+
606
+ // 1. Tool Coverage (hooks, agents, skills, commands)
607
+ const hasHooks = fs.existsSync(path.join(projectDir, cfg, 'settings.local.json'));
608
+ const agentDir = path.join(projectDir, cfg, 'agents');
609
+ const agentCount = fs.existsSync(agentDir) ? fs.readdirSync(agentDir).filter(f => f.endsWith('.md')).length : 0;
610
+ const skillDir = path.join(projectDir, cfg, 'skills');
611
+ const skillCount = fs.existsSync(skillDir) ? fs.readdirSync(skillDir).filter(s => fs.existsSync(path.join(skillDir, s, 'SKILL.md'))).length : 0;
612
+ const cmdDir = path.join(projectDir, cfg, 'commands');
613
+ const cmdCount = fs.existsSync(cmdDir) ? fs.readdirSync(cmdDir).filter(f => f.endsWith('.md')).length : 0;
614
+ let toolScore = 0;
615
+ if (hasHooks) toolScore += 3;
616
+ if (agentCount >= 3) toolScore += 3;
617
+ if (skillCount >= 5) toolScore += 2;
618
+ if (cmdCount >= 20) toolScore += 2;
619
+ scores['Tool Coverage'] = Math.min(toolScore, 10);
620
+
621
+ // 2. Context Efficiency (manifest exists, CLAUDE.md < 500 lines)
622
+ const manifestExists = fs.existsSync(path.join(projectDir, cfg, 'capabilities', 'manifest.md'));
623
+ const rulesPath = path.join(projectDir, cli.rulesFile);
624
+ const rulesLines = fs.existsSync(rulesPath) ? fs.readFileSync(rulesPath, 'utf8').split('\n').length : 0;
625
+ let ctxScore = 0;
626
+ if (manifestExists) ctxScore += 4;
627
+ if (rulesLines > 0 && rulesLines < 500) ctxScore += 3;
628
+ else if (rulesLines >= 500) ctxScore += 1;
629
+ if (rulesLines > 0 && !fs.readFileSync(rulesPath, 'utf8').includes('{{')) ctxScore += 3;
630
+ scores['Context Efficiency'] = Math.min(ctxScore, 10);
631
+
632
+ // 3. Quality Gates (TDD rule, completion rule, tests exist)
633
+ let qualScore = 0;
634
+ if (fs.existsSync(rulesPath)) {
635
+ const rules = fs.readFileSync(rulesPath, 'utf8');
636
+ if (/tdd|test.first/i.test(rules)) qualScore += 3;
637
+ if (/completion|never say.*should work/i.test(rules)) qualScore += 3;
638
+ }
639
+ const hasTests = fs.existsSync('tests') || fs.existsSync('test') || fs.existsSync('__tests__');
640
+ if (hasTests) qualScore += 4;
641
+ scores['Quality Gates'] = Math.min(qualScore, 10);
642
+
643
+ // 4. Memory Persistence (goals.md, checkpoints, patterns)
644
+ let memScore = 0;
645
+ const goalsPath = path.join(projectDir, cfg, 'memory', 'goals.md');
646
+ const cpDir = path.join(projectDir, cfg, 'memory', 'checkpoints');
647
+ const patternsPath = path.join(projectDir, cfg, 'memory', 'patterns.md');
648
+ const reflexDir = path.join(projectDir, cfg, 'memory', 'reflexes');
649
+ if (fs.existsSync(goalsPath)) memScore += 3;
650
+ if (fs.existsSync(cpDir) && fs.readdirSync(cpDir).length > 0) memScore += 3;
651
+ if (fs.existsSync(patternsPath)) memScore += 2;
652
+ if (fs.existsSync(reflexDir)) memScore += 2;
653
+ scores['Memory Persistence'] = Math.min(memScore, 10);
654
+
655
+ // 5. Security (integrity hash, injection filter, path guard)
656
+ let secScore = 0;
657
+ if (fs.existsSync(path.join(projectDir, cfg, '.azclaude-integrity'))) secScore += 4;
658
+ if (hasHooks) secScore += 3;
659
+ const postHook = path.join(projectDir, cfg, 'hooks', 'post-tool-use.js');
660
+ if (fs.existsSync(postHook) && fs.readFileSync(postHook, 'utf8').includes('startsWith')) secScore += 3;
661
+ scores['Security'] = Math.min(secScore, 10);
662
+
663
+ // 6. Cost Awareness (costs.jsonl exists, hook profiles supported)
664
+ let costScore = 0;
665
+ const costsPath = path.join(projectDir, cfg, 'memory', 'metrics', 'costs.jsonl');
666
+ if (fs.existsSync(costsPath)) {
667
+ costScore += 5;
668
+ const costLines = fs.readFileSync(costsPath, 'utf8').split('\n').filter(Boolean);
669
+ console.log(` Tool calls tracked: ${costLines.length}`);
670
+ }
671
+ if (fs.existsSync(postHook) && fs.readFileSync(postHook, 'utf8').includes('HOOK_PROFILE')) costScore += 5;
672
+ scores['Cost Awareness'] = Math.min(costScore, 10);
673
+
674
+ // 7. Evolution Readiness (evolve command, agents from evidence, reflexes)
675
+ let evoScore = 0;
676
+ if (fs.existsSync(path.join(cmdDir, 'evolve.md'))) evoScore += 3;
677
+ if (fs.existsSync(path.join(cmdDir, 'reflexes.md'))) evoScore += 3;
678
+ if (agentCount >= 5) evoScore += 2;
679
+ if (fs.existsSync(path.join(projectDir, 'ops', 'evolution-log.md'))) evoScore += 2;
680
+ scores['Evolution Readiness'] = Math.min(evoScore, 10);
681
+
682
+ // 8. Boundary Health (no overlaps, no orphans, manifest complete)
683
+ let boundScore = 0;
684
+ const validateScript = path.join(projectDir, cfg, 'scripts', 'validate-boundaries.sh');
685
+ if (fs.existsSync(validateScript)) {
686
+ boundScore += 3;
687
+ try {
688
+ const vr = spawnSync('bash', [validateScript, path.join(projectDir, cfg)],
689
+ { encoding: 'utf8', cwd: projectDir, timeout: 10000 });
690
+ if (vr.stdout) {
691
+ // Parse machine-readable output: BOUNDARY_RESULT:pass=N:warn=N
692
+ const resultMatch = vr.stdout.match(/BOUNDARY_RESULT:pass=(\d+):warn=(\d+)/);
693
+ if (resultMatch) {
694
+ const warns = parseInt(resultMatch[2], 10);
695
+ if (warns === 0) boundScore += 7;
696
+ else if (warns <= 2) boundScore += 4;
697
+ else boundScore += 1;
698
+ if (warns > 0) console.log(` Boundary warnings: ${warns}`);
699
+ } else {
700
+ boundScore += 2; // script ran but no parseable output
701
+ }
702
+ }
703
+ } catch (_) { boundScore += 2; }
704
+ }
705
+ scores['Boundary Health'] = Math.min(boundScore, 10);
706
+
707
+ // Output scores
708
+ let total = 0;
709
+ for (const [cat, score] of Object.entries(scores)) {
710
+ const bar = '█'.repeat(score) + '░'.repeat(10 - score);
711
+ console.log(` ${bar} ${score}/10 ${cat}`);
712
+ total += score;
713
+ }
714
+
715
+ const maxTotal = Object.keys(scores).length * 10;
716
+ const pct = Math.round((total / maxTotal) * 100);
717
+
718
+ console.log('\n════════════════════════════════════════════════');
719
+ console.log(` Overall: ${total}/${maxTotal} (${pct}%)`);
720
+ if (pct >= 80) console.log(' Grade: A — production-ready environment');
721
+ else if (pct >= 60) console.log(' Grade: B — solid foundation, room to grow');
722
+ else if (pct >= 40) console.log(' Grade: C — basics in place, gaps remain');
723
+ else console.log(' Grade: D — run /setup and /level-up');
724
+ console.log('════════════════════════════════════════════════\n');
725
+
726
+ process.exit(pct >= 60 ? 0 : 1);
727
+ }
728
+
729
+ // ─── Doctor ───────────────────────────────────────────────────────────────────
730
+
731
+ function runDoctor() {
732
+ const cli = detectCLI();
733
+ const projectDir = process.cwd();
734
+ const cfg = cli.cfg;
735
+ let pass = 0, fail = 0;
736
+ const failures = [];
737
+
738
+ function chk(label, ok) {
739
+ if (ok) { console.log(` ✓ ${label}`); pass++; }
740
+ else { console.log(` ✗ ${label}`); fail++; failures.push(label); }
741
+ }
742
+
743
+ console.log('\n════════════════════════════════════════════════');
744
+ console.log(' AZCLAUDE doctor — environment health check');
745
+ console.log('════════════════════════════════════════════════\n');
746
+
747
+ // ── Node.js version ──────────────────────────────────────────────────────
748
+ console.log('[ Runtime ]');
749
+ const [major] = process.versions.node.split('.').map(Number);
750
+ chk(`Node.js ${process.versions.node} (need ≥ 16)`, major >= 16);
751
+ chk(`node binary: ${process.execPath}`, fs.existsSync(process.execPath));
752
+
753
+ // ── Hooks (project-scoped or global) ─────────────────────────────────────
754
+ const projectSettingsPath = path.join(projectDir, cfg, 'settings.local.json');
755
+ const projectHooksDir = path.join(projectDir, cfg, 'hooks');
756
+ const hasProjectHooks = fs.existsSync(projectSettingsPath);
757
+
758
+ if (hasProjectHooks) {
759
+ console.log(`\n[ Project hooks — ${cfg}/settings.local.json ]`);
760
+ let settings = {};
761
+ try { settings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf8')); } catch {}
762
+
763
+ chk('_azclaude marker present (hooks installed)', !!settings._azclaude);
764
+ chk('UserPromptSubmit hook present', !!settings.hooks?.UserPromptSubmit);
765
+ chk('Stop hook present', !!settings.hooks?.Stop);
766
+ chk('PostToolUse hook present (edit tracking)', !!settings.hooks?.PostToolUse);
767
+
768
+ const upCmd = settings.hooks?.UserPromptSubmit?.[0]?.hooks?.[0]?.command || '';
769
+ chk('UserPromptSubmit uses Node.js (not bash)', upCmd.includes('node') && !upCmd.includes('SESSION_MARKER'));
770
+
771
+ for (const script of HOOK_SCRIPTS) {
772
+ chk(`hook script exists: ${script}`, fs.existsSync(path.join(projectHooksDir, script)));
773
+ }
774
+
775
+ const integrityPath = path.join(projectDir, cfg, '.azclaude-integrity');
776
+ if (fs.existsSync(integrityPath) && settings.hooks) {
777
+ const saved = fs.readFileSync(integrityPath, 'utf8').trim();
778
+ const current = generateIntegrityHash(settings.hooks);
779
+ chk('hook integrity hash matches', saved === current);
780
+ }
781
+
782
+ // Warn if global hooks still present (should have been migrated)
783
+ if (cli.hooksDir) {
784
+ const globalSettings = path.join(cli.hooksDir, 'settings.json');
785
+ if (fs.existsSync(globalSettings)) {
786
+ try {
787
+ const gs = JSON.parse(fs.readFileSync(globalSettings, 'utf8'));
788
+ if (gs._azclaude) {
789
+ console.log(' ⚠ Global hooks still present — re-run npx azclaude to migrate');
790
+ }
791
+ } catch {}
792
+ }
793
+ }
794
+ } else if (cli.hooksDir) {
795
+ console.log('\n[ Global hooks — ~/.claude/ ]');
796
+ const settingsPath = path.join(cli.hooksDir, 'settings.json');
797
+ const integrityPath = path.join(cli.hooksDir, '.azclaude-integrity');
798
+ const hooksDir = path.join(cli.hooksDir, 'hooks');
799
+
800
+ chk(`settings.json exists (${settingsPath})`, fs.existsSync(settingsPath));
801
+
802
+ let settings = {};
803
+ if (fs.existsSync(settingsPath)) {
804
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
805
+ }
806
+
807
+ chk('_azclaude marker present (hooks installed)', !!settings._azclaude);
808
+ chk('UserPromptSubmit hook present', !!settings.hooks?.UserPromptSubmit);
809
+ chk('Stop hook present', !!settings.hooks?.Stop);
810
+ chk('PostToolUse hook present (edit tracking)', !!settings.hooks?.PostToolUse);
811
+
812
+ const upCmd = settings.hooks?.UserPromptSubmit?.[0]?.hooks?.[0]?.command || '';
813
+ chk('UserPromptSubmit uses Node.js (not bash)', upCmd.includes('node') && !upCmd.includes('SESSION_MARKER'));
814
+
815
+ for (const script of HOOK_SCRIPTS) {
816
+ chk(`hook script exists: ${script}`, fs.existsSync(path.join(hooksDir, script)));
817
+ }
818
+
819
+ if (fs.existsSync(integrityPath) && settings.hooks) {
820
+ const saved = fs.readFileSync(integrityPath, 'utf8').trim();
821
+ const current = generateIntegrityHash(settings.hooks);
822
+ chk('hook integrity hash matches', saved === current);
823
+ }
824
+
825
+ info('Tip: re-run npx azclaude to upgrade to project-scoped hooks');
826
+ } else {
827
+ console.log(`\n[ Hooks ]`);
828
+ console.log(` · hooks not supported for ${cli.name} — skipping`);
829
+ }
830
+
831
+ // ── Project structure ────────────────────────────────────────────────────
832
+ console.log('\n[ Project structure ]');
833
+ chk(`${cfg}/ config directory`, fs.existsSync(path.join(projectDir, cfg)));
834
+ chk(`${cfg}/capabilities/manifest.md`, fs.existsSync(path.join(projectDir, cfg, 'capabilities', 'manifest.md')));
835
+ chk(`${cfg}/commands/ (skills)`, fs.existsSync(path.join(projectDir, cfg, 'commands')));
836
+ chk(`${cfg}/memory/ directory`, fs.existsSync(path.join(projectDir, cfg, 'memory')));
837
+ chk(`${cfg}/agents/ directory`, fs.existsSync(path.join(projectDir, cfg, 'agents')));
838
+ chk(`ops/observations/ directory`, fs.existsSync(path.join(projectDir, 'ops', 'observations')));
839
+
840
+ const goalsPath = path.join(projectDir, cfg, 'memory', 'goals.md');
841
+ chk(`goals.md exists`, fs.existsSync(goalsPath));
842
+ if (fs.existsSync(goalsPath)) {
843
+ const g = fs.readFileSync(goalsPath, 'utf8');
844
+ chk('goals.md has an Updated date', /^Updated: \d{4}-\d{2}-\d{2}/m.test(g));
845
+ chk('goals.md has no unfilled placeholders', !g.includes('{{'));
846
+ }
847
+
848
+ const rulesPath = path.join(projectDir, cli.rulesFile);
849
+ chk(`${cli.rulesFile} exists`, fs.existsSync(rulesPath));
850
+ if (fs.existsSync(rulesPath)) {
851
+ const r = fs.readFileSync(rulesPath, 'utf8');
852
+ chk(`${cli.rulesFile} filled (no {{placeholders}})`, !r.includes('{{'));
853
+ }
854
+
855
+ // ── Commands ─────────────────────────────────────────────────────────────
856
+ console.log('\n[ Commands ]');
857
+ const cmdDir = path.join(projectDir, cfg, 'commands');
858
+ const installed = fs.existsSync(cmdDir) ? fs.readdirSync(cmdDir).filter(f => f.endsWith('.md')) : [];
859
+ chk(`${installed.length}/${COMMANDS.length} commands installed`, installed.length === COMMANDS.length);
860
+ const missing = COMMANDS.filter(c => !installed.includes(`${c}.md`));
861
+ if (missing.length) console.log(` · missing: ${missing.join(', ')}`);
862
+
863
+ // ── Skills ──────────────────────────────────────────────────────────────
864
+ console.log('\n[ Skills ]');
865
+ const skillsDir = path.join(projectDir, cfg, 'skills');
866
+ const installedSkills = fs.existsSync(skillsDir) ? fs.readdirSync(skillsDir).filter(s => {
867
+ return fs.existsSync(path.join(skillsDir, s, 'SKILL.md'));
868
+ }) : [];
869
+ chk(`${installedSkills.length}/${SKILLS.length} skills installed`, installedSkills.length === SKILLS.length);
870
+ const missingSkills = SKILLS.filter(s => !installedSkills.includes(s));
871
+ if (missingSkills.length) console.log(` · missing: ${missingSkills.join(', ')}`);
872
+
873
+ // ── Memory health ──────────────────────────────────────────────────────
874
+ console.log('\n[ Memory ]');
875
+ const memDir = path.join(projectDir, cfg, 'memory');
876
+ chk('checkpoints/ directory exists', fs.existsSync(path.join(memDir, 'checkpoints')));
877
+ chk('sessions/ directory exists', fs.existsSync(path.join(memDir, 'sessions')));
878
+ chk('codebase-map.md exists', fs.existsSync(path.join(memDir, 'codebase-map.md')));
879
+
880
+ // Stale goals warning (> 7 days)
881
+ if (fs.existsSync(goalsPath)) {
882
+ const goalsContent = fs.readFileSync(goalsPath, 'utf8');
883
+ const dateMatch = goalsContent.match(/^Updated: (\d{4}-\d{2}-\d{2})/m);
884
+ if (dateMatch) {
885
+ const updatedDate = new Date(dateMatch[1]);
886
+ const daysSince = Math.floor((Date.now() - updatedDate.getTime()) / 86400000);
887
+ chk(`goals.md is current (updated ${daysSince}d ago)`, daysSince <= 7);
888
+ }
889
+ }
890
+
891
+ // ── Git status ─────────────────────────────────────────────────────────
892
+ console.log('\n[ Git ]');
893
+ try {
894
+ const { spawnSync } = require('child_process');
895
+ const gitStatus = spawnSync('git', ['status', '--porcelain'], { cwd: projectDir, encoding: 'utf8', timeout: 5000 });
896
+ if (gitStatus.status === 0) {
897
+ // Exclude goals.md from count — PostToolUse hook auto-modifies it during sessions
898
+ const changes = gitStatus.stdout.trim().split('\n').filter(l => l.length > 0 && !l.includes('goals.md'));
899
+ chk(`working tree clean (${changes.length} uncommitted, goals.md excluded)`, changes.length === 0);
900
+ }
901
+ } catch (_) {}
902
+
903
+ // ── Hook freshness ────────────────────────────────────────────────────
904
+ console.log('\n[ Hook freshness ]');
905
+ const installedHooksDir = hasProjectHooks ? projectHooksDir : (cli.hooksDir ? path.join(cli.hooksDir, 'hooks') : null);
906
+ const templateHooksDir = path.join(__dirname, '..', 'templates', 'hooks');
907
+ if (installedHooksDir) {
908
+ for (const script of HOOK_SCRIPTS) {
909
+ const installedPath = path.join(installedHooksDir, script);
910
+ const templatePath = path.join(templateHooksDir, script);
911
+ if (fs.existsSync(installedPath) && fs.existsSync(templatePath)) {
912
+ const installedContent = fs.readFileSync(installedPath, 'utf8');
913
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
914
+ chk(`${script} is up to date`, installedContent === templateContent);
915
+ }
916
+ }
917
+ } else {
918
+ console.log(' · hooks not supported — skipping');
919
+ }
920
+
921
+ // ── Summary ──────────────────────────────────────────────────────────────
922
+ const total = pass + fail;
923
+ console.log('\n════════════════════════════════════════════════');
924
+ console.log(` ${pass}/${total} checks passed`);
925
+ if (fail > 0) {
926
+ const hasCommandFail = failures.some(f => /commands installed/.test(f));
927
+ const hasHookFail = failures.some(f => /hook|Hook|integrity/.test(f));
928
+ const hasGitFail = failures.some(f => /uncommitted/.test(f));
929
+ const hasMemoryFail = failures.some(f => /checkpoints|sessions|codebase-map|goals/.test(f));
930
+ console.log('');
931
+ if (hasCommandFail || hasHookFail) console.log(' Fix: re-run npx azclaude to install missing files');
932
+ if (hasHookFail) console.log(' If hooks still fail: check that Node.js ≥ 16 is in PATH');
933
+ if (hasGitFail) console.log(' Git: commit or stash uncommitted changes');
934
+ if (hasMemoryFail) console.log(' Memory: run /setup or /persist to create missing files');
935
+ } else {
936
+ console.log(' Environment is healthy.');
937
+ }
938
+ console.log('════════════════════════════════════════════════\n');
939
+
940
+ process.exit(fail > 0 ? 1 : 0);
941
+ }
942
+
943
+ // ─── Utils ────────────────────────────────────────────────────────────────────
944
+
945
+ function copyDir(src, dst) {
946
+ fs.mkdirSync(dst, { recursive: true });
947
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
948
+ const s = path.join(src, entry.name);
949
+ const d = path.join(dst, entry.name);
950
+ if (entry.isDirectory()) copyDir(s, d);
951
+ else fs.copyFileSync(s, d);
952
+ }
953
+ }
954
+
955
+ // ─── Main ─────────────────────────────────────────────────────────────────────
956
+
957
+ if (process.argv[2] === 'doctor' && process.argv[3] === '--audit') { runAudit(); process.exit(0); }
958
+ if (process.argv[2] === 'doctor') { runDoctor(); process.exit(0); }
959
+ if (process.argv[2] === 'demo') { runDemo(); process.exit(0); }
960
+ if (process.argv[2] === 'copilot') {
961
+ // Delegate to copilot runner
962
+ const copilotScript = path.join(__dirname, 'copilot.js');
963
+ const copilotArgs = process.argv.slice(3);
964
+ const { spawnSync } = require('child_process');
965
+ const result = spawnSync(process.execPath, [copilotScript, ...copilotArgs], { stdio: 'inherit' });
966
+ process.exit(result.status || 0);
967
+ }
968
+
969
+ const fullInstall = process.argv.includes('--full');
970
+ const projectDir = process.cwd();
971
+ const cli = detectCLI();
972
+
973
+ console.log('\n════════════════════════════════════════════════');
974
+ console.log(' AZCLAUDE — AI Coding Environment');
975
+ console.log(` CLI: ${cli.name} → installing to ${cli.cfg}/`);
976
+ console.log('════════════════════════════════════════════════\n');
977
+
978
+ verifyIntegrity(projectDir, cli.cfg, cli);
979
+
980
+ // Hooks: project-scoped by default (settings.local.json), global as fallback
981
+ if (cli.cfg === '.claude') {
982
+ installProjectHooks(projectDir, cli.cfg);
983
+ migrateFromGlobalHooks(cli, projectDir, cli.cfg);
984
+ } else {
985
+ installGlobalHooks(cli);
986
+ }
987
+ installCapabilities(projectDir, cli.cfg, fullInstall);
988
+ installCommands(projectDir, cli.cfg);
989
+ installSkills(projectDir, cli.cfg);
990
+ installScripts(projectDir, cli.cfg);
991
+ installAgents(projectDir, cli.cfg);
992
+ installRulesFile(projectDir, cli.cfg, cli.rulesFile);
993
+ createDirectories(projectDir, cli.cfg);
994
+ ensureSharedSkillsDir();
995
+
996
+ // ── Evolution log directory ───────────────────────────────────────────────────
997
+ const evolLogPath = path.join(projectDir, 'ops', 'evolution-log.md');
998
+ if (!fs.existsSync(evolLogPath)) {
999
+ const header = '# Evolution History\n\n| Date | Before | After | Delta | Summary |\n|------|--------|-------|-------|---------|\n';
1000
+ try { fs.writeFileSync(evolLogPath, header); } catch (_) {}
1001
+ }
1002
+
1003
+ // ── Post-install boundary check (warn if collisions detected) ────────────────
1004
+ const postInstallValidator = path.join(projectDir, cli.cfg, 'scripts', 'validate-boundaries.sh');
1005
+ if (fs.existsSync(postInstallValidator)) {
1006
+ try {
1007
+ const vr = spawnSync('bash', [postInstallValidator, path.join(projectDir, cli.cfg)],
1008
+ { encoding: 'utf8', cwd: projectDir, timeout: 10000 });
1009
+ const match = (vr.stdout || '').match(/BOUNDARY_RESULT:pass=(\d+):warn=(\d+)/);
1010
+ if (match && parseInt(match[2], 10) > 0) {
1011
+ warn(`Boundary check: ${match[2]} warning(s) detected — run /evolve to resolve`);
1012
+ } else if (match) {
1013
+ ok('Boundary check: no collisions or overlaps');
1014
+ }
1015
+ } catch (_) {}
1016
+ }
1017
+
1018
+ console.log('\n════════════════════════════════════════════════');
1019
+ console.log(` Install mode: ${fullInstall ? 'full (all capabilities)' : 'core (shared + level-builders)'}`);
1020
+ console.log(' Architecture: lazy-loaded, manifest-driven');
1021
+ console.log(' Token cost per task: ~200-600 (vs ~21,000 monolith)');
1022
+ console.log(` Next step: run /setup to configure this project`);
1023
+ if (!fullInstall) {
1024
+ console.log('');
1025
+ console.log(' When ready for Level 5+: npx azclaude --full');
1026
+ }
1027
+ console.log('════════════════════════════════════════════════\n');