@ghl-ai/aw 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.
package/integrate.mjs ADDED
@@ -0,0 +1,466 @@
1
+ // integrate.mjs — Generate commands for all IDEs, instructions (CLAUDE.md, AGENTS.md)
2
+
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import * as fmt from './fmt.mjs';
6
+
7
+ // AW CLI commands to generate
8
+ const AW_COMMANDS = [
9
+ { name: 'pull', description: 'Pull agents & skills from registry', hint: '<path>' },
10
+ { name: 'push', description: 'Push local changes to registry', hint: '<path>' },
11
+ { name: 'status', description: 'Show workspace sync status', hint: '' },
12
+ { name: 'drop', description: 'Stop syncing a path', hint: '<path>' },
13
+ { name: 'search', description: 'Search local and remote registry', hint: '<query>' },
14
+ { name: 'nuke', description: 'Remove entire .aw_registry/', hint: '' },
15
+ ];
16
+
17
+ /**
18
+ * Generate /aw:* commands directly into namespace commands/ dirs.
19
+ * Writes CLI commands + agent invoke commands alongside hand-written commands.
20
+ * link.mjs symlinks everything from commands/ → .claude/commands/aw/ for discovery.
21
+ *
22
+ * A .generated-manifest.json tracks which files were generated so they can be
23
+ * cleaned on rebuild without needing filename prefixes.
24
+ */
25
+ export function generateCommands(cwd) {
26
+ const awDir = join(cwd, '.aw_registry');
27
+
28
+ // Clean old .generated-commands if it exists (migration)
29
+ const oldGenDir = join(awDir, '.generated-commands');
30
+ if (existsSync(oldGenDir)) rmSync(oldGenDir, { recursive: true, force: true });
31
+
32
+ let count = 0;
33
+ const namespaces = listNamespaceDirs(awDir);
34
+
35
+ for (const ns of namespaces) {
36
+ const commandsDir = join(awDir, ns, 'commands');
37
+ mkdirSync(commandsDir, { recursive: true });
38
+
39
+ // 1. CLI commands
40
+ for (const cmd of AW_COMMANDS) {
41
+ const fileName = `${cmd.name}.md`;
42
+ // Skip if a hand-written command with same name exists
43
+ if (existsSync(join(commandsDir, fileName))) continue;
44
+
45
+ const content = [
46
+ '---',
47
+ `name: aw:${cmd.name}`,
48
+ `description: ${cmd.description}`,
49
+ cmd.hint ? `argument-hint: "${cmd.hint}"` : null,
50
+ '---',
51
+ '',
52
+ 'Run the following command:',
53
+ '',
54
+ '```',
55
+ `node bin/aw ${cmd.name} $ARGUMENTS`,
56
+ '```',
57
+ '',
58
+ ].filter(v => v !== null).join('\n');
59
+
60
+ writeFileSync(join(commandsDir, fileName), content);
61
+ count++;
62
+ }
63
+
64
+ }
65
+
66
+ // Codex skills — .agents/skills/<name>/SKILL.md
67
+ const agentsSkillsDir = join(cwd, '.agents/skills');
68
+ mkdirSync(agentsSkillsDir, { recursive: true });
69
+ for (const cmd of AW_COMMANDS) {
70
+ const skillDir = join(agentsSkillsDir, cmd.name);
71
+ mkdirSync(skillDir, { recursive: true });
72
+ const content = [
73
+ '---',
74
+ `name: ${cmd.name}`,
75
+ `description: ${cmd.description}`,
76
+ '---',
77
+ '',
78
+ `Run: \`node bin/aw ${cmd.name}\` followed by the user's arguments.`,
79
+ '',
80
+ ].join('\n');
81
+ writeFileSync(join(skillDir, 'SKILL.md'), content);
82
+ }
83
+
84
+ if (count > 0) {
85
+ fmt.logSuccess(`Generated ${count} aw commands`);
86
+ }
87
+
88
+ return count;
89
+ }
90
+
91
+ /**
92
+ * Copy CLAUDE.md and AGENTS.md to project root.
93
+ */
94
+ export function copyInstructions(cwd, tempDir, namespace) {
95
+ for (const file of ['CLAUDE.md', 'AGENTS.md']) {
96
+ const dest = join(cwd, file);
97
+ if (existsSync(dest)) continue;
98
+
99
+ if (tempDir) {
100
+ const src = join(tempDir, 'registry', file);
101
+ if (existsSync(src)) {
102
+ let content = readFileSync(src, 'utf8');
103
+ if (namespace) {
104
+ content = content.replace(/\{\{TEAM\}\}/g, namespace);
105
+ }
106
+ writeFileSync(dest, content);
107
+ fmt.logSuccess(`Created ${file}`);
108
+ continue;
109
+ }
110
+ }
111
+
112
+ const content = file === 'CLAUDE.md'
113
+ ? generateClaudeMd(cwd, namespace)
114
+ : generateAgentsMd(cwd, namespace);
115
+ if (content) {
116
+ writeFileSync(dest, content);
117
+ fmt.logSuccess(`Created ${file}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ function generateClaudeMd(cwd, namespace) {
123
+ const team = namespace || 'my-team';
124
+ return `# CLAUDE.md — ${team}
125
+
126
+ Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\` (shared knowledge), \`git-jenkins\` (CI/CD), \`grafana\` (observability)
127
+
128
+ ## Routing Rule (ABSOLUTE)
129
+
130
+ > **Every non-trivial task MUST call \`Skill(skill: "ghl-ai-task-router")\` BEFORE any response.**
131
+ >
132
+ > **Trivial** (do directly): typo fixes, single-line edits, git ops, file exploration, factual code questions.
133
+ >
134
+ > Everything else — including tasks phrased as questions, suggestions, or discussions — routes first.
135
+ > **No conversational responses first. No planning first. Route first.**
136
+ > **Execute, don't just recommend:** run the steps (tools, edits, workflows) instead of listing "next steps" for the user.
137
+
138
+ ## Architecture
139
+
140
+ \`\`\`
141
+ .aw_registry/
142
+ ├── ghl/ # Platform layer (shared, read-only)
143
+ │ ├── agents/ # 15 platform agents (reviewers, engineers, designer)
144
+ │ ├── skills/ # 77 platform skills (SKILL.md files)
145
+ │ ├── commands/ # Platform commands (health-check, help, discover)
146
+ │ └── evals/ # Platform agent & skill quality tests
147
+
148
+ ├── ${team}/ # Team layer (from template, customizable)
149
+ │ ├── agents/ # Team agents (developers, testers, PM, coordinator)
150
+ │ ├── skills/ # Team-specific skills
151
+ │ ├── commands/ # Workflow commands (ship, plan, review, deploy, etc.)
152
+ │ ├── blueprints/ # Development workflow blueprints
153
+ │ └── evals/ # Team agent quality tests
154
+
155
+ └── .sync-config.json # Tracks synced paths and team namespace
156
+
157
+ .aw_docs/ # Local orchestration state
158
+ ├── STATE.md # Active run, recent history
159
+ ├── config.json # Sync settings
160
+ ├── runs/<run-id>/ # Per-run state
161
+ │ ├── RUN.md # Run metadata + execution log
162
+ │ ├── steps/<NN>-<name>.md # Per-step state (YAML frontmatter)
163
+ │ └── transparency/ # _transparency JSONs
164
+ ├── learnings/<agent>.md # Accumulated learnings per agent
165
+ ├── learnings/_pending-sync.jsonl # Sync queue → MCP memory/store
166
+ └── tasks/BOARD.md # Task board
167
+ \`\`\`
168
+
169
+ Symlinked to \`.claude/{agents,skills,commands/aw,blueprints,evals}\`.
170
+
171
+ ## Local-First Operations
172
+
173
+ All orchestration is file-based. No MCP calls for orchestration.
174
+
175
+ | Operation | How |
176
+ |-----------|-----|
177
+ | **Load agent** | Read \`.aw_registry/<ns>/agents/<slug>.md\` → skills list + identity |
178
+ | **Load skill** | Read \`.aw_registry/<ns>/skills/<slug>/SKILL.md\` → full body |
179
+ | **Create run** | Generate \`run-YYYYMMDDTHHMMSS\`, write \`.aw_docs/runs/<id>/RUN.md\` + step files |
180
+ | **Step result** | Update step .md frontmatter + write transparency JSON |
181
+ | **Close run** | Update RUN.md, append \`.aw_docs/learnings/\`, sync queue, eager \`memory/store\` |
182
+ | **Track task** | Update \`.aw_docs/tasks/BOARD.md\` YAML frontmatter |
183
+ | **Learn** | Append to \`.aw_docs/learnings/<agent>.md\` + \`_pending-sync.jsonl\` |
184
+ | **Recall** | Read \`.aw_docs/learnings/<agent>.md\` + MCP \`memory/search\` |
185
+ | **Discover agents** | Read \`.aw_registry/<ns>/\` or \`ls .claude/agents/\` |
186
+
187
+ ## MCP (Remote Only)
188
+
189
+ Only these MCP calls are used:
190
+
191
+ \`\`\`
192
+ memory/search → Search shared team knowledge base
193
+ memory/store → Push learnings to shared knowledge (eager sync after runs)
194
+ memory/get → Fetch specific memory by ID
195
+ grafana/* → External observability
196
+ git-jenkins/* → External CI/CD pipelines
197
+ stitch/* → External design generation
198
+ \`\`\`
199
+
200
+ ## Symlink Naming
201
+
202
+ All symlinks are prefixed with their namespace (\`ghl-\` or \`${team}-\`):
203
+ - \`.aw_registry/ghl/agents/security-reviewer.md\` → \`.claude/agents/ghl-security-reviewer.md\`
204
+ - \`.aw_registry/${team}/agents/frontend-developer.md\` → \`.claude/agents/${team}-frontend-developer.md\`
205
+ - \`.aw_registry/${team}/commands/ship.md\` → \`.claude/commands/aw/${team}-ship.md\`
206
+
207
+ ## Dependency Rule
208
+
209
+ - **ghl/ is self-contained** — platform agents reference only platform skills
210
+ - **${team}/ can reference ghl/** — team agents use \`ghl-*\` prefixed skill names
211
+ - **ghl/ never references ${team}/** — platform layer knows nothing about team-specific resources
212
+
213
+ ## How Agents Connect to Skills
214
+
215
+ Agents declare skills in frontmatter \`skills:\` list:
216
+ \`\`\`yaml
217
+ # ghl/agents/security-reviewer.md — platform skills directly
218
+ skills:
219
+ - security-review
220
+ - platform-services-authentication-authorization
221
+
222
+ # ${team}/agents/frontend-developer.md — ghl- prefix for platform skills
223
+ skills:
224
+ - ghl-vue-development # → ghl/skills/vue-development
225
+ - create-prd # → ${team}/skills/create-prd (local)
226
+ \`\`\`
227
+
228
+ ## Quick Reference
229
+
230
+ | Action | Command |
231
+ |--------|---------|
232
+ | Discover commands | Type \`/aw:\` and autocomplete |
233
+ | List all agents | \`ls .claude/agents/\` |
234
+ | List all skills | \`ls .claude/skills/\` |
235
+ | Pull updates | \`aw pull\` |
236
+ | Push changes | \`aw push <path>\` |
237
+ | Workspace status | \`aw status\` |
238
+ | Reset workspace | \`aw nuke\` |
239
+
240
+ ## Workflow: 11 Quality Gates
241
+
242
+ \`\`\`
243
+ Search → Intake → Architecture → Design → Implementation (Ralph Loop) →
244
+ Testing → QA Validation → Code Review (5 parallel) → PR → Staging → Production
245
+ \`\`\`
246
+
247
+ Gates are shell scripts in \`scripts/gates/\` — CANNOT be bypassed by LLM judgment.
248
+
249
+ ## Cost Optimization Rules
250
+
251
+ - **Commands**: Use parameterized single commands, not variant files
252
+ - **Skills**: Link to \`platform-docs/content/...\`, max 60 lines inline
253
+ - **Agents**: Definitions ≤5 KB, reference material in linked skills
254
+ - **Local-first**: No MCP calls for orchestration — file reads are faster and always available
255
+ `;
256
+ }
257
+
258
+ function generateAgentsMd(cwd, namespace) {
259
+ const team = namespace || 'my-team';
260
+ return `# AGENTS.md — ${team}
261
+
262
+ ## Agent System
263
+
264
+ Two-layer agent system with local-first orchestration. Agents are loaded from \`.aw_registry/\` (symlinked to \`.claude/agents/\`).
265
+
266
+ ### How Agents Are Loaded (Local-First)
267
+
268
+ Commands load agents entirely from local files — no MCP calls for agent/skill loading:
269
+
270
+ \`\`\`
271
+ 1. Read .aw_registry/ghl/agents/<slug>.md → Agent identity, skills[] list, capabilities
272
+ OR .aw_registry/${team}/agents/<slug>.md
273
+
274
+ 2. For each skill in the agent's skills[] frontmatter:
275
+ Read .aw_registry/ghl/skills/<slug>/SKILL.md → Full skill body (patterns, checklists)
276
+ OR .aw_registry/${team}/skills/<slug>/SKILL.md
277
+
278
+ 3. Read .aw_docs/learnings/<slug>.md → Prior learnings from past runs
279
+
280
+ 4. Call MCP memory/search({query: "..."}) → Recalled memories (only MCP call)
281
+ \`\`\`
282
+
283
+ ### Platform Agents (ghl-)
284
+
285
+ Shared infrastructure agents — always available, self-contained. Loaded from \`.aw_registry/ghl/agents/\`.
286
+
287
+ | Agent | Role | Model Tier |
288
+ |-------|------|------------|
289
+ | \`ghl-architecture-reviewer\` | Architecture review, ADR compliance, coupling detection | Sonnet |
290
+ | \`ghl-security-reviewer\` | Security review, auth patterns, tenant isolation | Sonnet |
291
+ | \`ghl-performance-reviewer\` | Performance review, bundle analysis, N+1 detection | Sonnet |
292
+ | \`ghl-frontend-engineer\` | MFA architecture, event-bus, shared packages | Sonnet |
293
+ | \`ghl-services-engineer\` | Inter-service comms, workers, circuit breakers | Sonnet |
294
+ | \`ghl-infra-engineer\` | Kubernetes, Terraform, Jenkins, deployment | Sonnet |
295
+ | \`ghl-database-engineer\` | MongoDB, Redis, Elasticsearch, Firestore, migrations | Sonnet |
296
+ | \`ghl-product-designer\` | HighRise design system, accessibility, typography | Sonnet |
297
+ | \`ghl-frontend-core-reviewer\` | Vue 3 patterns, composables, Pinia stores | Sonnet |
298
+ | \`ghl-frontend-i18n-reviewer\` | i18n compliance, translation keys | Haiku |
299
+ | \`ghl-maintainability-reviewer\` | Code duplication, naming, complexity | Haiku |
300
+ | \`ghl-reliability-reviewer\` | Error handling, null guards, SSR safety | Haiku |
301
+ | \`ghl-sdet-engineer\` | Playwright POM, quality gates, performance testing | Haiku |
302
+ | \`ghl-devops-automator\` | Docker, Jenkins pipelines, deployment strategies | Haiku |
303
+ | \`ghl-data-engineer\` | CDC pipelines, ClickHouse, analytics data models | Haiku |
304
+
305
+ ### Team Agents (${team}-)
306
+
307
+ Team-specific agents — customizable. Loaded from \`.aw_registry/${team}/agents/\`. Can reference platform skills via \`ghl-*\` prefix.
308
+
309
+ | Agent | Role | Model Tier |
310
+ |-------|------|------------|
311
+ | \`${team}-coordinator\` | Task decomposition, quality gate enforcement, orchestration | Opus |
312
+ | \`${team}-architect\` | ADR creation, data flow design, system architecture | Opus |
313
+ | \`${team}-product-manager\` | PRDs, feature prioritization, sprint planning | Opus |
314
+ | \`${team}-frontend-developer\` | Vue components, composables, TypeScript | Sonnet |
315
+ | \`${team}-backend-developer\` | NestJS services, API endpoints, workers | Sonnet |
316
+ | \`${team}-manager\` | Spec writing, user stories, team coordination | Sonnet |
317
+ | \`${team}-unit-tester\` | Component tests, composable tests, BDD | Haiku |
318
+ | \`${team}-e2e-tester\` | Cypress tests, Gherkin scenarios | Haiku |
319
+ | \`${team}-mutation-tester\` | Stryker mutation analysis, test improvement | Haiku |
320
+ | \`${team}-release-engineer\` | Release checklists, rollback plans, deploy | Sonnet |
321
+ | \`${team}-design-reviewer\` | Design token audit, accessibility review | Haiku |
322
+ | \`${team}-design-auditor\` | Dark mode audit, design token verification | Haiku |
323
+ | \`${team}-technical-writer\` | API docs, migration guides | Haiku |
324
+ | \`${team}-api-tester\` | API contract tests, auth flow tests | Haiku |
325
+ | \`${team}-visual-tester\` | Screenshot comparison, visual test plans | Haiku |
326
+ | \`${team}-responsive-tester\` | Breakpoint audit, touch target compliance | Haiku |
327
+ | \`${team}-performance-benchmarker\` | Core Web Vitals, k6 scripts | Haiku |
328
+ | \`${team}-stitch-designer\` | Multi-screen flows, screen prompts | Haiku |
329
+ | \`${team}-animation-specialist\` | Transitions, reduced motion compliance | Haiku |
330
+ | \`${team}-ux-researcher\` | Interview scripts, usability test plans | Haiku |
331
+ | \`${team}-onboarding-optimizer\` | Empty state design, onboarding flow critique | Haiku |
332
+
333
+ ### Skill Resolution
334
+
335
+ Agents declare skills in their frontmatter \`skills:\` list. Commands read SKILL.md files directly from disk:
336
+
337
+ - **No prefix** → \`.aw_registry/${team}/skills/<name>/SKILL.md\`
338
+ - **\`ghl-\` prefix** → \`.aw_registry/ghl/skills/<name-without-ghl->/SKILL.md\`
339
+
340
+ \`\`\`yaml
341
+ # .aw_registry/ghl/agents/security-reviewer.md — platform skills directly
342
+ skills:
343
+ - security-review # → .aw_registry/ghl/skills/security-review/SKILL.md
344
+ - platform-services-authentication-authorization # → .aw_registry/ghl/skills/platform-services-.../SKILL.md
345
+
346
+ # .aw_registry/${team}/agents/frontend-developer.md — ghl- prefix for platform skills
347
+ skills:
348
+ - ghl-vue-development # → .aw_registry/ghl/skills/vue-development/SKILL.md
349
+ - create-prd # → .aw_registry/${team}/skills/create-prd/SKILL.md
350
+ \`\`\`
351
+
352
+ ### How to Invoke
353
+
354
+ \`\`\`
355
+ # Via workflow commands (recommended — orchestrates multiple agents with .aw_docs/ tracking)
356
+ /aw:${team}-ship # Full ship: research → spec → design → plan → implement → test → review → deploy
357
+ /aw:${team}-work # Implementation: 6 agents, parallel DB/frontend, quality review + test
358
+ /aw:${team}-plan # Architecture + PM planning
359
+ /aw:${team}-review-all # Parallel code review with 10 reviewers in 3 waves
360
+ /aw:${team}-brainstorm # Requirement exploration with architect + PM
361
+
362
+ # Via Agent tool (direct invocation — no run tracking)
363
+ Agent(subagent_type: "ghl:security-reviewer", prompt: "Review this PR for security issues")
364
+ Agent(subagent_type: "${team}:frontend-developer", prompt: "Create a Vue component for...")
365
+ \`\`\`
366
+
367
+ ### Learnings & Knowledge
368
+
369
+ Agent knowledge accumulates across runs via \`.aw_docs/learnings/\`:
370
+
371
+ \`\`\`
372
+ .aw_docs/learnings/
373
+ ├── backend-developer.md # Learnings from all runs where this agent worked
374
+ ├── frontend-developer.md # Appended after each workflow close
375
+ ├── architecture-reviewer.md # Prior learnings loaded before each run
376
+ └── _pending-sync.jsonl # Queue for eager sync to MCP memory/store
377
+ \`\`\`
378
+
379
+ Before each run, prior learnings are read from these files and injected into agent prompts. After each run, new learnings are appended. Optionally synced to shared MCP memory for cross-project recall.
380
+
381
+ ### Evals
382
+
383
+ Each agent has quality test cases in \`.claude/evals/\`:
384
+ \`\`\`
385
+ .claude/evals/ghl-agents-security-reviewer/ # Platform agent evals
386
+ .claude/evals/${team}-agents-frontend-developer/ # Team agent evals
387
+ .claude/evals/ghl-skills-vue-development/ # Skill evals
388
+ \`\`\`
389
+
390
+ Run with: \`/ghl:eval agent:<slug>\` or \`/ghl:eval skill:<slug>\`
391
+ `;
392
+ }
393
+
394
+ /**
395
+ * Initialize .aw_docs/ directory for local-first orchestration state.
396
+ * Creates run tracking, learnings, task board, and cache directories.
397
+ */
398
+ export function initAwDocs(cwd) {
399
+ const awDocsDir = join(cwd, '.aw_docs');
400
+ if (existsSync(awDocsDir)) return; // Already initialized
401
+
402
+ const dirs = [
403
+ '.aw_docs/runs',
404
+ '.aw_docs/learnings',
405
+ '.aw_docs/tasks',
406
+ '.aw_docs/cache',
407
+ ];
408
+
409
+ for (const dir of dirs) {
410
+ mkdirSync(join(cwd, dir), { recursive: true });
411
+ }
412
+
413
+ // STATE.md — workspace state
414
+ writeFileSync(join(awDocsDir, 'STATE.md'), `---
415
+ active_run: null
416
+ last_run: null
417
+ last_workflow: null
418
+ last_agent: null
419
+ updated_at: null
420
+ pending_sync_count: 0
421
+ ---
422
+
423
+ # Workspace State
424
+
425
+ No active runs. Use \`/aw:<team>-<command>\` to start a workflow.
426
+ `);
427
+
428
+ // config.json — registry paths, sync settings
429
+ writeFileSync(join(awDocsDir, 'config.json'), JSON.stringify({
430
+ sync: {
431
+ eager: true,
432
+ batch_threshold: 10,
433
+ mcp_memory_enabled: true,
434
+ },
435
+ paths: {
436
+ runs: '.aw_docs/runs',
437
+ learnings: '.aw_docs/learnings',
438
+ tasks: '.aw_docs/tasks',
439
+ cache: '.aw_docs/cache',
440
+ },
441
+ }, null, 2) + '\n');
442
+
443
+ // BOARD.md — task board
444
+ writeFileSync(join(awDocsDir, 'tasks', 'BOARD.md'), `---
445
+ tasks: []
446
+ updated_at: null
447
+ ---
448
+
449
+ # Task Board
450
+
451
+ No active tasks. Tasks are created during workflow execution.
452
+ `);
453
+
454
+ // _pending-sync.jsonl — sync queue (empty)
455
+ writeFileSync(join(awDocsDir, 'learnings', '_pending-sync.jsonl'), '');
456
+
457
+ fmt.logSuccess('Created .aw_docs/ (local orchestration state)');
458
+ }
459
+
460
+ function listNamespaceDirs(awDir) {
461
+ if (!existsSync(awDir)) return [];
462
+ return readdirSync(awDir, { withFileTypes: true })
463
+ .filter(d => d.isDirectory() && !d.name.startsWith('.'))
464
+ .map(d => d.name);
465
+ }
466
+
package/link.mjs ADDED
@@ -0,0 +1,209 @@
1
+ // link.mjs — Create symlinks from IDE dirs → .aw_registry/
2
+
3
+ import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
4
+ import { join, relative } from 'node:path';
5
+ import { execSync } from 'node:child_process';
6
+ import * as fmt from './fmt.mjs';
7
+
8
+ const IDE_DIRS = ['.claude', '.cursor', '.codex'];
9
+ // Per-file symlink types
10
+ const FILE_TYPES = ['agents', 'blueprints'];
11
+
12
+ /**
13
+ * List namespace directories inside .aw_registry/ (skip dotfiles).
14
+ */
15
+ function listNamespaceDirs(awDir) {
16
+ if (!existsSync(awDir)) return [];
17
+ return readdirSync(awDir, { withFileTypes: true })
18
+ .filter(d => d.isDirectory() && !d.name.startsWith('.'))
19
+ .map(d => d.name);
20
+ }
21
+
22
+ /**
23
+ * List subdirectories (non-dot) in a directory.
24
+ */
25
+ function listDirs(dir) {
26
+ if (!existsSync(dir)) return [];
27
+ return readdirSync(dir, { withFileTypes: true })
28
+ .filter(d => d.isDirectory() && !d.name.startsWith('.'))
29
+ .map(d => d.name);
30
+ }
31
+
32
+ /**
33
+ * Clean all symlinks from IDE type directories before re-linking.
34
+ */
35
+ function cleanIdeSymlinks(cwd) {
36
+ for (const ide of IDE_DIRS) {
37
+ for (const type of [...FILE_TYPES, 'skills', 'evals']) {
38
+ const dir = join(cwd, ide, type);
39
+ if (!existsSync(dir)) continue;
40
+ for (const entry of readdirSync(dir)) {
41
+ const p = join(dir, entry);
42
+ try {
43
+ if (lstatSync(p).isSymbolicLink()) unlinkSync(p);
44
+ } catch { /* best effort */ }
45
+ }
46
+ }
47
+ // Clean command symlinks (both flat legacy and namespaced aw/)
48
+ const cmdDir = join(cwd, ide, 'commands');
49
+ if (!existsSync(cmdDir)) continue;
50
+ for (const entry of readdirSync(cmdDir)) {
51
+ const p = join(cmdDir, entry);
52
+ try {
53
+ if (lstatSync(p).isSymbolicLink()) unlinkSync(p);
54
+ } catch { /* best effort */ }
55
+ }
56
+ // Clean inside commands/aw/ namespace dir
57
+ const awCmdDir = join(cmdDir, 'aw');
58
+ if (existsSync(awCmdDir)) {
59
+ for (const entry of readdirSync(awCmdDir)) {
60
+ const p = join(awCmdDir, entry);
61
+ try {
62
+ if (lstatSync(p).isSymbolicLink()) unlinkSync(p);
63
+ } catch { /* best effort */ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Compute flat IDE name: namespace-slug (always includes namespace).
71
+ */
72
+ function flatName(ns, name) {
73
+ return `${ns}-${name}`;
74
+ }
75
+
76
+ /**
77
+ * Create/refresh all IDE symlinks.
78
+ *
79
+ * - agents, blueprints: per-file symlinks with flat names
80
+ * - skills: per-skill directory symlinks with flat names
81
+ * - commands: directory symlink per namespace → .claude/commands/<ns>/
82
+ * (Claude Code needs a directory at .claude/commands/<ns>/ for /<ns>:* autocomplete)
83
+ */
84
+ export function linkWorkspace(cwd) {
85
+ const awDir = join(cwd, '.aw_registry');
86
+ if (!existsSync(awDir)) return 0;
87
+
88
+ let created = 0;
89
+
90
+ // Clean old symlinks first
91
+ cleanIdeSymlinks(cwd);
92
+
93
+ const namespaces = listNamespaceDirs(awDir);
94
+
95
+ for (const ns of namespaces) {
96
+ // Per-file types: agents, blueprints
97
+ for (const type of FILE_TYPES) {
98
+ const typeDir = join(awDir, ns, type);
99
+ if (!existsSync(typeDir)) continue;
100
+
101
+ for (const file of readdirSync(typeDir).filter(f => f.endsWith('.md') && !f.startsWith('.'))) {
102
+ const flat = flatName(ns, file);
103
+
104
+ for (const ide of IDE_DIRS) {
105
+ const linkDir = join(cwd, ide, type);
106
+ mkdirSync(linkDir, { recursive: true });
107
+ const linkPath = join(linkDir, flat);
108
+ const targetPath = join(awDir, ns, type, file);
109
+ const relTarget = relative(linkDir, targetPath);
110
+ try {
111
+ execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
112
+ created++;
113
+ } catch { /* best effort */ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // Skills: per-skill directory symlinks
119
+ const skillsDir = join(awDir, ns, 'skills');
120
+ if (!existsSync(skillsDir)) continue;
121
+ for (const skill of listDirs(skillsDir)) {
122
+ const flat = flatName(ns, skill);
123
+
124
+ for (const ide of IDE_DIRS) {
125
+ const linkDir = join(cwd, ide, 'skills');
126
+ mkdirSync(linkDir, { recursive: true });
127
+ const linkPath = join(linkDir, flat);
128
+ const targetPath = join(skillsDir, skill);
129
+ const relTarget = relative(linkDir, targetPath);
130
+ try {
131
+ execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
132
+ created++;
133
+ } catch { /* best effort */ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // Evals: per-eval-dir symlinks (evals/agents/<name>/, evals/skills/<name>/)
139
+ for (const ns of namespaces) {
140
+ const evalsDir = join(awDir, ns, 'evals');
141
+ if (!existsSync(evalsDir)) continue;
142
+
143
+ for (const subType of listDirs(evalsDir)) {
144
+ const subDir = join(evalsDir, subType);
145
+ for (const evalName of listDirs(subDir)) {
146
+ const flat = `${ns}-${subType}-${evalName}`;
147
+
148
+ for (const ide of IDE_DIRS) {
149
+ const linkDir = join(cwd, ide, 'evals');
150
+ mkdirSync(linkDir, { recursive: true });
151
+ const linkPath = join(linkDir, flat);
152
+ const targetPath = join(subDir, evalName);
153
+ const relTarget = relative(linkDir, targetPath);
154
+ try {
155
+ execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
156
+ created++;
157
+ } catch { /* best effort */ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // Codex per-skill symlinks: .agents/skills/<name>
164
+ const agentsSkillsDir = join(cwd, '.agents/skills');
165
+ for (const ns of namespaces) {
166
+ const skillsDir = join(awDir, ns, 'skills');
167
+ if (!existsSync(skillsDir)) continue;
168
+ mkdirSync(agentsSkillsDir, { recursive: true });
169
+ for (const skill of listDirs(skillsDir)) {
170
+ const flat = flatName(ns, skill);
171
+ const linkPath = join(agentsSkillsDir, flat);
172
+ const targetPath = join(skillsDir, skill);
173
+ const relTarget = relative(agentsSkillsDir, targetPath);
174
+ try {
175
+ execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
176
+ created++;
177
+ } catch { /* best effort */ }
178
+ }
179
+ }
180
+
181
+ // Commands: per-file symlinks from <ns>/commands/ → .claude/commands/aw/
182
+ // All commands (hand-written + generated) live in namespace/commands/ as single source of truth
183
+ for (const ns of namespaces) {
184
+ const commandsDir = join(awDir, ns, 'commands');
185
+ if (!existsSync(commandsDir)) continue;
186
+
187
+ for (const file of readdirSync(commandsDir).filter(f => f.endsWith('.md') && !f.startsWith('.'))) {
188
+ const cmdFileName = `${ns}-${file}`;
189
+
190
+ for (const ide of IDE_DIRS) {
191
+ const linkDir = join(cwd, ide, 'commands', 'aw');
192
+ mkdirSync(linkDir, { recursive: true });
193
+ const linkPath = join(linkDir, cmdFileName);
194
+ const targetPath = join(commandsDir, file);
195
+ const relTarget = relative(linkDir, targetPath);
196
+ try {
197
+ execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
198
+ created++;
199
+ } catch { /* best effort */ }
200
+ }
201
+ }
202
+ }
203
+
204
+ if (created > 0) {
205
+ fmt.logSuccess(`Linked ${created} IDE symlink${created > 1 ? 's' : ''}`);
206
+ }
207
+
208
+ return created;
209
+ }