cc-workspace 4.0.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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +382 -0
  3. package/bin/cli.js +735 -0
  4. package/global-skills/agents/implementer.md +39 -0
  5. package/global-skills/agents/team-lead.md +143 -0
  6. package/global-skills/agents/workspace-init.md +207 -0
  7. package/global-skills/bootstrap-repo/SKILL.md +70 -0
  8. package/global-skills/constitution.md +58 -0
  9. package/global-skills/cross-service-check/SKILL.md +67 -0
  10. package/global-skills/cycle-retrospective/SKILL.md +133 -0
  11. package/global-skills/dispatch-feature/SKILL.md +168 -0
  12. package/global-skills/dispatch-feature/references/anti-patterns.md +31 -0
  13. package/global-skills/dispatch-feature/references/frontend-ux-standards.md +73 -0
  14. package/global-skills/dispatch-feature/references/spawn-templates.md +109 -0
  15. package/global-skills/hooks/notify-user.sh +30 -0
  16. package/global-skills/hooks/permission-auto-approve.sh +16 -0
  17. package/global-skills/hooks/session-start-context.sh +85 -0
  18. package/global-skills/hooks/subagent-start-context.sh +35 -0
  19. package/global-skills/hooks/task-completed-check.sh +21 -0
  20. package/global-skills/hooks/teammate-idle-check.sh +29 -0
  21. package/global-skills/hooks/track-file-modifications.sh +20 -0
  22. package/global-skills/hooks/user-prompt-guard.sh +19 -0
  23. package/global-skills/hooks/validate-spawn-prompt.sh +79 -0
  24. package/global-skills/hooks/worktree-create-context.sh +22 -0
  25. package/global-skills/incident-debug/SKILL.md +86 -0
  26. package/global-skills/merge-prep/SKILL.md +87 -0
  27. package/global-skills/plan-review/SKILL.md +70 -0
  28. package/global-skills/qa-ruthless/SKILL.md +102 -0
  29. package/global-skills/refresh-profiles/SKILL.md +22 -0
  30. package/global-skills/rules/constitution-en.md +67 -0
  31. package/global-skills/rules/context-hygiene.md +43 -0
  32. package/global-skills/rules/model-routing.md +42 -0
  33. package/global-skills/templates/claude-md.template.md +124 -0
  34. package/global-skills/templates/constitution.template.md +18 -0
  35. package/global-skills/templates/workspace.template.md +33 -0
  36. package/package.json +28 -0
package/bin/cli.js ADDED
@@ -0,0 +1,735 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execSync } = require("child_process");
6
+
7
+ // ─── Package info ───────────────────────────────────────────
8
+ const PKG = require("../package.json");
9
+ const PACKAGE_DIR = path.resolve(__dirname, "..");
10
+ const SKILLS_DIR = path.join(PACKAGE_DIR, "global-skills");
11
+
12
+ // ─── Claude global dirs ─────────────────────────────────────
13
+ const HOME = process.env.HOME || process.env.USERPROFILE;
14
+ const CLAUDE_DIR = path.join(HOME, ".claude");
15
+ const VERSION_FILE = path.join(CLAUDE_DIR, ".orchestrator-version");
16
+ const GLOBAL_SKILLS = path.join(CLAUDE_DIR, "skills");
17
+ const GLOBAL_RULES = path.join(CLAUDE_DIR, "rules");
18
+ const GLOBAL_AGENTS = path.join(CLAUDE_DIR, "agents");
19
+
20
+ // ─── ANSI Colors (zero deps) ────────────────────────────────
21
+ const isColor = process.stdout.isTTY && !process.env.NO_COLOR;
22
+ const c = {
23
+ reset: isColor ? "\x1b[0m" : "",
24
+ bold: isColor ? "\x1b[1m" : "",
25
+ dim: isColor ? "\x1b[2m" : "",
26
+ green: isColor ? "\x1b[32m" : "",
27
+ yellow: isColor ? "\x1b[33m" : "",
28
+ red: isColor ? "\x1b[31m" : "",
29
+ cyan: isColor ? "\x1b[36m" : "",
30
+ blue: isColor ? "\x1b[34m" : "",
31
+ magenta: isColor ? "\x1b[35m" : "",
32
+ gray: isColor ? "\x1b[90m" : "",
33
+ white: isColor ? "\x1b[37m" : "",
34
+ bgGreen: isColor ? "\x1b[42m\x1b[30m" : "",
35
+ bgRed: isColor ? "\x1b[41m\x1b[37m" : "",
36
+ };
37
+
38
+ // ─── Banner ─────────────────────────────────────────────────
39
+ const BANNER = `
40
+ ${c.cyan}${c.bold} ██████╗ ██████╗ ██╗ ██╗███████╗${c.reset}
41
+ ${c.cyan} ██╔════╝██╔════╝ ██║ ██║██╔════╝${c.reset}
42
+ ${c.cyan} ██║ ██║ ██║ █╗ ██║███████╗${c.reset}
43
+ ${c.cyan} ██║ ██║ ██║███╗██║╚════██║${c.reset}
44
+ ${c.cyan} ╚██████╗╚██████╗ ╚███╔███╔╝███████║${c.reset}
45
+ ${c.cyan} ╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝${c.reset}
46
+ ${c.dim} Claude Code Workspace Orchestrator${c.reset} ${c.bold}v${PKG.version}${c.reset}
47
+ `;
48
+
49
+ const BANNER_SMALL = `${c.cyan}${c.bold}cc-workspace${c.reset} ${c.dim}v${PKG.version}${c.reset}`;
50
+
51
+ // ─── Output helpers ─────────────────────────────────────────
52
+ function log(msg = "") { console.log(msg); }
53
+ function ok(msg) { console.log(` ${c.green}✓${c.reset} ${msg}`); }
54
+ function warn(msg) { console.log(` ${c.yellow}⚠${c.reset} ${c.yellow}${msg}${c.reset}`); }
55
+ function fail(msg) { console.error(` ${c.red}✗${c.reset} ${c.red}${msg}${c.reset}`); }
56
+ function info(msg) { console.log(` ${c.blue}▸${c.reset} ${msg}`); }
57
+ function step(msg) { console.log(`\n${c.bold}${c.white} ${msg}${c.reset}`); }
58
+ function hr() { console.log(`${c.dim} ${"─".repeat(50)}${c.reset}`); }
59
+
60
+ // ─── FS helpers ─────────────────────────────────────────────
61
+ function mkdirp(dir) { fs.mkdirSync(dir, { recursive: true }); }
62
+
63
+ function copyFile(src, dest) { fs.copyFileSync(src, dest); }
64
+
65
+ function copyDir(src, dest) {
66
+ mkdirp(dest);
67
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
68
+ const srcPath = path.join(src, entry.name);
69
+ const destPath = path.join(dest, entry.name);
70
+ entry.isDirectory() ? copyDir(srcPath, destPath) : copyFile(srcPath, destPath);
71
+ }
72
+ }
73
+
74
+ // ─── Version helpers ────────────────────────────────────────
75
+ function readVersion() {
76
+ try { return fs.readFileSync(VERSION_FILE, "utf8").trim(); }
77
+ catch { return null; }
78
+ }
79
+
80
+ function writeVersion(v) {
81
+ mkdirp(CLAUDE_DIR);
82
+ fs.writeFileSync(VERSION_FILE, v + "\n");
83
+ }
84
+
85
+ function semverCompare(a, b) {
86
+ const pa = a.split(".").map(Number);
87
+ const pb = b.split(".").map(Number);
88
+ for (let i = 0; i < 3; i++) {
89
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
90
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
91
+ }
92
+ return 0;
93
+ }
94
+
95
+ function needsUpdate(force) {
96
+ if (force) return true;
97
+ const installed = readVersion();
98
+ if (!installed) return true;
99
+ return semverCompare(PKG.version, installed) > 0;
100
+ }
101
+
102
+ // ─── Detect project type ────────────────────────────────────
103
+ function detectProjectType(dir) {
104
+ const has = (f) => fs.existsSync(path.join(dir, f));
105
+ const pkgHas = (kw) => {
106
+ try { return fs.readFileSync(path.join(dir, "package.json"), "utf8").includes(kw); }
107
+ catch { return false; }
108
+ };
109
+ if (has("composer.json")) return "PHP/Laravel";
110
+ if (has("pom.xml")) return "Java/Spring";
111
+ if (has("build.gradle")) return "Java/Gradle";
112
+ if (has("requirements.txt") || has("pyproject.toml")) return "Python";
113
+ if (has("go.mod")) return "Go";
114
+ if (has("Cargo.toml")) return "Rust";
115
+ if (has("package.json")) {
116
+ if (pkgHas("quasar")) return "Vue/Quasar";
117
+ if (pkgHas("nuxt")) return "Vue/Nuxt";
118
+ if (pkgHas("next")) return "React/Next";
119
+ if (pkgHas('"vue"')) return "Vue";
120
+ if (pkgHas('"react"')) return "React";
121
+ return "Node.js";
122
+ }
123
+ return "unknown";
124
+ }
125
+
126
+ // ─── Type badge ─────────────────────────────────────────────
127
+ function typeBadge(type) {
128
+ const badges = {
129
+ "PHP/Laravel": `${c.magenta}PHP/Laravel${c.reset}`,
130
+ "Java/Spring": `${c.red}Java/Spring${c.reset}`,
131
+ "Java/Gradle": `${c.red}Java/Gradle${c.reset}`,
132
+ "Python": `${c.yellow}Python${c.reset}`,
133
+ "Go": `${c.cyan}Go${c.reset}`,
134
+ "Rust": `${c.red}Rust${c.reset}`,
135
+ "Vue/Quasar": `${c.green}Vue/Quasar${c.reset}`,
136
+ "Vue/Nuxt": `${c.green}Vue/Nuxt${c.reset}`,
137
+ "Vue": `${c.green}Vue${c.reset}`,
138
+ "React/Next": `${c.blue}React/Next${c.reset}`,
139
+ "React": `${c.blue}React${c.reset}`,
140
+ "Node.js": `${c.green}Node.js${c.reset}`,
141
+ };
142
+ return badges[type] || `${c.dim}${type}${c.reset}`;
143
+ }
144
+
145
+ // ─── Install global components ──────────────────────────────
146
+ function installGlobals(force) {
147
+ const installed = readVersion();
148
+ const shouldUpdate = needsUpdate(force);
149
+
150
+ if (!shouldUpdate) {
151
+ ok(`Global components up to date ${c.dim}(v${installed})${c.reset}`);
152
+ return false;
153
+ }
154
+
155
+ step(installed
156
+ ? `Updating globals: ${c.dim}v${installed}${c.reset} → ${c.green}v${PKG.version}${c.reset}`
157
+ : `Installing global components`
158
+ );
159
+
160
+ mkdirp(GLOBAL_SKILLS);
161
+ mkdirp(GLOBAL_RULES);
162
+ mkdirp(GLOBAL_AGENTS);
163
+
164
+ // Skills
165
+ const skipDirs = new Set(["rules", "agents", "hooks", "templates"]);
166
+ let skillCount = 0;
167
+ for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
168
+ if (!entry.isDirectory() || skipDirs.has(entry.name)) continue;
169
+ copyDir(path.join(SKILLS_DIR, entry.name), path.join(GLOBAL_SKILLS, entry.name));
170
+ skillCount++;
171
+ }
172
+ ok(`${skillCount} skills`);
173
+
174
+ // Rules
175
+ const rulesDir = path.join(SKILLS_DIR, "rules");
176
+ if (fs.existsSync(rulesDir)) {
177
+ let n = 0;
178
+ for (const f of fs.readdirSync(rulesDir)) {
179
+ if (f.endsWith(".md")) { copyFile(path.join(rulesDir, f), path.join(GLOBAL_RULES, f)); n++; }
180
+ }
181
+ ok(`${n} rules`);
182
+ }
183
+
184
+ // Agents
185
+ const agentsDir = path.join(SKILLS_DIR, "agents");
186
+ if (fs.existsSync(agentsDir)) {
187
+ let n = 0;
188
+ for (const f of fs.readdirSync(agentsDir)) {
189
+ if (f.endsWith(".md")) { copyFile(path.join(agentsDir, f), path.join(GLOBAL_AGENTS, f)); n++; }
190
+ }
191
+ ok(`${n} agents`);
192
+ }
193
+
194
+ // Constitution (FR)
195
+ const constitutionFr = path.join(SKILLS_DIR, "constitution.md");
196
+ if (fs.existsSync(constitutionFr)) {
197
+ copyFile(constitutionFr, path.join(CLAUDE_DIR, "constitution.md"));
198
+ ok("constitution ${c.dim}(FR)${c.reset}");
199
+ }
200
+
201
+ writeVersion(PKG.version);
202
+ return true;
203
+ }
204
+
205
+ // ─── Generate settings.json with hooks ──────────────────────
206
+ function generateSettings(orchDir) {
207
+ const hp = ".claude/hooks";
208
+ const settings = {
209
+ env: {
210
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
211
+ CLAUDE_CODE_SUBAGENT_MODEL: "sonnet"
212
+ },
213
+ hooks: {
214
+ PreToolUse: [
215
+ { matcher: "Write|Edit|MultiEdit", command: `bash ${hp}/block-orchestrator-writes.sh`, timeout: 5 },
216
+ { matcher: "Teammate", command: `bash ${hp}/validate-spawn-prompt.sh`, timeout: 5 }
217
+ ],
218
+ SessionStart: [
219
+ { command: `bash ${hp}/session-start-context.sh`, timeout: 10 }
220
+ ],
221
+ UserPromptSubmit: [
222
+ { command: `bash ${hp}/user-prompt-guard.sh`, timeout: 3 }
223
+ ],
224
+ SubagentStart: [
225
+ { command: `bash ${hp}/subagent-start-context.sh`, timeout: 5 }
226
+ ],
227
+ PermissionRequest: [
228
+ { command: `bash ${hp}/permission-auto-approve.sh`, timeout: 3 }
229
+ ],
230
+ PostToolUse: [
231
+ { matcher: "Write|Edit|MultiEdit", command: `bash ${hp}/track-file-modifications.sh`, timeout: 3 }
232
+ ],
233
+ TeammateIdle: [
234
+ { command: `bash ${hp}/teammate-idle-check.sh`, timeout: 5 }
235
+ ],
236
+ TaskCompleted: [
237
+ { command: `bash ${hp}/task-completed-check.sh`, timeout: 3 }
238
+ ],
239
+ WorktreeCreate: [
240
+ { command: `bash ${hp}/worktree-create-context.sh`, timeout: 3 }
241
+ ],
242
+ Notification: [
243
+ { command: `bash ${hp}/notify-user.sh`, timeout: 5 }
244
+ ]
245
+ }
246
+ };
247
+ fs.writeFileSync(path.join(orchDir, ".claude", "settings.json"), JSON.stringify(settings, null, 2) + "\n");
248
+ }
249
+
250
+ // ─── Block hook (inline, always regenerated) ────────────────
251
+ function generateBlockHook(hooksDir) {
252
+ const blockHook = `#!/usr/bin/env bash
253
+ # block-orchestrator-writes.sh v${PKG.version}
254
+ # PreToolUse hook: blocks writes to sibling repos. Allows writes within orchestrator/.
255
+ set -euo pipefail
256
+
257
+ INPUT=$(cat)
258
+
259
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null) || FILE_PATH=""
260
+
261
+ if [ -z "$FILE_PATH" ]; then
262
+ cat << 'EOF'
263
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cannot determine target path. Delegate to a teammate."}}
264
+ EOF
265
+ exit 0
266
+ fi
267
+
268
+ ORCH_DIR="\${CLAUDE_PROJECT_DIR:-.}"
269
+ ORCH_ABS="$(cd "$ORCH_DIR" 2>/dev/null && pwd)" || ORCH_ABS=""
270
+
271
+ if [ -d "$(dirname "$FILE_PATH")" ]; then
272
+ TARGET_ABS="$(cd "$(dirname "$FILE_PATH")" 2>/dev/null && pwd)/$(basename "$FILE_PATH")"
273
+ else
274
+ TARGET_ABS="$FILE_PATH"
275
+ fi
276
+
277
+ if [ -n "$ORCH_ABS" ]; then
278
+ case "$TARGET_ABS" in
279
+ "$ORCH_ABS"/*)
280
+ exit 0
281
+ ;;
282
+ esac
283
+ fi
284
+
285
+ PARENT_DIR="$(dirname "$ORCH_ABS" 2>/dev/null)" || PARENT_DIR=""
286
+ if [ -n "$PARENT_DIR" ]; then
287
+ for repo_dir in "$PARENT_DIR"/*/; do
288
+ [ -d "$repo_dir/.git" ] || continue
289
+ REPO_ABS="$(cd "$repo_dir" 2>/dev/null && pwd)"
290
+ case "$TARGET_ABS" in
291
+ "$REPO_ABS"/*)
292
+ REPO_NAME=$(basename "$REPO_ABS")
293
+ cat << EOF
294
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: Cannot write in repo $REPO_NAME/. Delegate to a teammate via Agent Teams."}}
295
+ EOF
296
+ exit 0
297
+ ;;
298
+ esac
299
+ done
300
+ fi
301
+
302
+ cat << 'EOF'
303
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: Write target is outside orchestrator/. Delegate to a teammate."}}
304
+ EOF
305
+ exit 0
306
+ `;
307
+ fs.writeFileSync(path.join(hooksDir, "block-orchestrator-writes.sh"), blockHook, { mode: 0o755 });
308
+ }
309
+
310
+ // ─── CLAUDE.md content ──────────────────────────────────────
311
+ function claudeMdContent() {
312
+ return `# Orchestrateur v${PKG.version}
313
+
314
+ Tu es le lead technique. Tu ne codes jamais dans les repos — tu peux écrire dans orchestrator/.
315
+ Tu clarifies, planifies, délègues, traces.
316
+
317
+ ## Sécurité
318
+ - \`disallowedTools: Bash\` — pas de shell direct
319
+ - \`allowed-tools\` : Read, Write, Edit, Glob, Grep, Task, Teammate, SendMessage
320
+ - Hook \`PreToolUse\` path-aware : autorise orchestrator/, bloque les repos frères
321
+
322
+ > settings.json contient env vars + hooks registration.
323
+
324
+ ## Lancement
325
+ \`\`\`
326
+ cd orchestrator/
327
+ claude --agent workspace-init # première fois : diagnostic + config
328
+ claude --agent team-lead # sessions de travail
329
+ \`\`\`
330
+
331
+ ## Initialisation (workspace-init)
332
+ L'agent \`workspace-init\` vérifie la structure, scanne les repos frères (type, CLAUDE.md,
333
+ .claude/, tests), et configure interactivement workspace.md et constitution.md.
334
+ Se lance une seule fois. Idempotent — peut être relancé pour re-diagnostiquer.
335
+
336
+ ## 4 modes de session
337
+ | Mode | Description |
338
+ |------|-------------|
339
+ | **A — Complet** | Clarify → Plan → Validate → Dispatch en waves → QA |
340
+ | **B — Plan rapide** | Specs → Plan → Dispatch |
341
+ | **C — Go direct** | Dispatch immédiat |
342
+ | **D — Single-service** | 1 repo, pas de waves |
343
+
344
+ ## Config
345
+ - Contexte projet : \`./workspace.md\`
346
+ - Constitution projet : \`./constitution.md\`
347
+ - Templates : \`./templates/\`
348
+ - Profils services : \`./plans/service-profiles.md\`
349
+ - Plans actifs : \`./plans/*.md\`
350
+
351
+ ## Skills (9)
352
+ - **dispatch-feature** : 4 modes, clarify → plan → waves → collect → verify
353
+ - **qa-ruthless** : QA adversarial, min 3 findings par service
354
+ - **cross-service-check** : cohérence inter-repos
355
+ - **incident-debug** : diagnostic multi-couche
356
+ - **plan-review** : sanity check du plan (haiku)
357
+ - **merge-prep** : pré-merge, conflits, PR summaries
358
+ - **cycle-retrospective** : capitalisation post-cycle (haiku)
359
+ - **refresh-profiles** : relit les CLAUDE.md des repos (haiku)
360
+ - **bootstrap-repo** : génère un CLAUDE.md pour un repo (haiku)
361
+
362
+ ## Règles
363
+ 1. Pas de code dans les repos — délègue aux teammates
364
+ 2. Peut écrire dans orchestrator/ (plans, workspace.md, constitution.md)
365
+ 3. Clarifie les ambiguïtés AVANT de planifier (sauf mode C)
366
+ 4. Tout plan en markdown dans \`./plans/\`
367
+ 5. Dispatch via Agent Teams (Teammate tool) en waves
368
+ 6. Constitution complète (12 principes + règles projet) dans chaque spawn prompt
369
+ 7. Standards UX injectés aux teammates frontend
370
+ 8. Chaque teammate détecte le code mort
371
+ 9. Escalade des choix archi non couverts par le plan
372
+ 10. QA impitoyable — violations UX = bloquant
373
+ 11. Compact après chaque cycle
374
+ 12. Hooks en warning uniquement — jamais bloquants
375
+ 13. Cycle rétrospective après chaque feature terminée
376
+ `;
377
+ }
378
+
379
+ // ─── Plan template content ──────────────────────────────────
380
+ function planTemplateContent() {
381
+ return `# Plan: [NOM]
382
+ > Cree le : [DATE]
383
+ > Statut : En cours
384
+
385
+ ## Contexte
386
+ [Pourquoi cette feature]
387
+
388
+ ## Clarifications
389
+ [Reponses clarify]
390
+
391
+ ## Services impactes
392
+ | Service | Impacte | Branche | Teammate | Statut |
393
+ |---------|---------|---------|----------|--------|
394
+ | | oui/non | | | ⏳ |
395
+
396
+ ## Waves
397
+ - Wave 1: [producteurs]
398
+ - Wave 2: [consommateurs]
399
+ - Wave 3: [infra]
400
+
401
+ ## Contrat API
402
+ [Shapes exactes]
403
+
404
+ ## Taches
405
+
406
+ ### [service]
407
+ - ⏳ [tache]
408
+
409
+ ## QA
410
+ - ⏳ Cross-service check
411
+ - ⏳ QA ruthless
412
+ - ⏳ Merge prep
413
+
414
+ ## Session log
415
+ - [DATE HH:MM] : Plan cree
416
+ `;
417
+ }
418
+
419
+ // ─── Setup workspace ────────────────────────────────────────
420
+ function setupWorkspace(workspacePath, projectName) {
421
+ const wsAbs = path.resolve(workspacePath);
422
+ const orchDir = path.join(wsAbs, "orchestrator");
423
+
424
+ // ── Structure ──
425
+ step("Creating orchestrator/");
426
+ mkdirp(path.join(orchDir, ".claude", "hooks"));
427
+ mkdirp(path.join(orchDir, "plans"));
428
+ mkdirp(path.join(orchDir, "templates"));
429
+ ok("Structure created");
430
+
431
+ // ── Templates ──
432
+ const templatesDir = path.join(SKILLS_DIR, "templates");
433
+ if (fs.existsSync(templatesDir)) {
434
+ for (const f of fs.readdirSync(templatesDir)) {
435
+ if (f.endsWith(".md")) copyFile(path.join(templatesDir, f), path.join(orchDir, "templates", f));
436
+ }
437
+ ok("Templates");
438
+ }
439
+
440
+ // ── workspace.md ──
441
+ const wsMd = path.join(orchDir, "workspace.md");
442
+ if (!fs.existsSync(wsMd)) {
443
+ const tpl = path.join(orchDir, "templates", "workspace.template.md");
444
+ if (fs.existsSync(tpl)) copyFile(tpl, wsMd);
445
+ else fs.writeFileSync(wsMd, `# Workspace: ${projectName}\n\n## Projet\n[UNCONFIGURED]\n`);
446
+ ok(`workspace.md ${c.dim}[UNCONFIGURED]${c.reset}`);
447
+ } else {
448
+ warn("workspace.md exists — skipped");
449
+ }
450
+
451
+ // ── constitution.md ──
452
+ const constMd = path.join(orchDir, "constitution.md");
453
+ if (!fs.existsSync(constMd)) {
454
+ const tpl = path.join(orchDir, "templates", "constitution.template.md");
455
+ if (fs.existsSync(tpl)) copyFile(tpl, constMd);
456
+ else fs.writeFileSync(constMd, [
457
+ `# Constitution — ${projectName}`, "",
458
+ "> The 12 universal principles apply automatically.",
459
+ "> Add project-specific rules starting at 13.", "",
460
+ "## Project-specific rules", "",
461
+ '13. **[Rule name].** [Description]', ""
462
+ ].join("\n"));
463
+ ok(`constitution.md ${c.dim}(template)${c.reset}`);
464
+ } else {
465
+ warn("constitution.md exists — skipped");
466
+ }
467
+
468
+ // ── Hooks ──
469
+ step("Installing hooks");
470
+ const hooksDir = path.join(orchDir, ".claude", "hooks");
471
+ generateBlockHook(hooksDir);
472
+ const hooksSrc = path.join(SKILLS_DIR, "hooks");
473
+ let hookCount = 1;
474
+ if (fs.existsSync(hooksSrc)) {
475
+ for (const f of fs.readdirSync(hooksSrc)) {
476
+ if (!f.endsWith(".sh") || f === "verify-cycle-complete.sh") continue;
477
+ copyFile(path.join(hooksSrc, f), path.join(hooksDir, f));
478
+ fs.chmodSync(path.join(hooksDir, f), 0o755);
479
+ hookCount++;
480
+ }
481
+ }
482
+ ok(`${hookCount} hooks ${c.dim}(all warning-only)${c.reset}`);
483
+
484
+ // ── Settings ──
485
+ generateSettings(orchDir);
486
+ ok(`settings.json ${c.dim}(env + hooks)${c.reset}`);
487
+
488
+ // ── CLAUDE.md ──
489
+ const claudeMd = path.join(orchDir, "CLAUDE.md");
490
+ if (!fs.existsSync(claudeMd)) {
491
+ fs.writeFileSync(claudeMd, claudeMdContent());
492
+ ok("CLAUDE.md");
493
+ } else {
494
+ warn("CLAUDE.md exists — skipped");
495
+ }
496
+
497
+ // ── Plan template ──
498
+ const planTpl = path.join(orchDir, "plans", "_TEMPLATE.md");
499
+ if (!fs.existsSync(planTpl)) {
500
+ fs.writeFileSync(planTpl, planTemplateContent());
501
+ ok("Plan template");
502
+ }
503
+
504
+ // ── .gitignore ──
505
+ const gi = path.join(orchDir, ".gitignore");
506
+ if (!fs.existsSync(gi)) {
507
+ fs.writeFileSync(gi, [
508
+ ".claude/bash-commands.log", ".claude/worktrees/", ".claude/modified-files.log",
509
+ "plans/*.md", "!plans/_TEMPLATE.md", "!plans/service-profiles.md", ""
510
+ ].join("\n"));
511
+ ok(".gitignore");
512
+ }
513
+
514
+ // ── Scan repos ──
515
+ step("Scanning sibling repos");
516
+ const repos = [];
517
+ const reposWithoutClaude = [];
518
+
519
+ for (const entry of fs.readdirSync(wsAbs, { withFileTypes: true })) {
520
+ if (!entry.isDirectory() || entry.name === "orchestrator") continue;
521
+ const repoDir = path.join(wsAbs, entry.name);
522
+ if (!fs.existsSync(path.join(repoDir, ".git"))) continue;
523
+ const type = detectProjectType(repoDir);
524
+ const hasClaude = fs.existsSync(path.join(repoDir, "CLAUDE.md"));
525
+ repos.push({ name: entry.name, type, hasClaude });
526
+ if (!hasClaude) reposWithoutClaude.push(entry.name);
527
+ }
528
+
529
+ if (repos.length === 0) {
530
+ info(`${c.dim}No sibling repos found${c.reset}`);
531
+ } else {
532
+ // Tree-view output
533
+ repos.forEach((r, i) => {
534
+ const isLast = i === repos.length - 1;
535
+ const branch = isLast ? "└──" : "├──";
536
+ const claudeIcon = r.hasClaude
537
+ ? `${c.green}CLAUDE.md ✓${c.reset}`
538
+ : `${c.red}CLAUDE.md ✗${c.reset}`;
539
+ const name = r.name.padEnd(22);
540
+ console.log(` ${c.dim}${branch}${c.reset} ${c.bold}${name}${c.reset} ${typeBadge(r.type).padEnd(25)} ${claudeIcon}`);
541
+ });
542
+ }
543
+
544
+ // ── Service profiles ──
545
+ const profileLines = [
546
+ `# Service Profiles — ${projectName}`,
547
+ `> Generated: ${new Date().toISOString().slice(0, 10)}`,
548
+ "> Regenerate with `/refresh-profiles`", ""
549
+ ];
550
+ for (const r of repos) {
551
+ profileLines.push(`## ${r.name} (../${r.name}/)`);
552
+ profileLines.push(`- **Type** : ${r.type}`);
553
+ profileLines.push(`- **CLAUDE.md** : ${r.hasClaude ? "present" : "ABSENT — /bootstrap-repo"}`);
554
+ profileLines.push("");
555
+ }
556
+ fs.writeFileSync(path.join(orchDir, "plans", "service-profiles.md"), profileLines.join("\n"));
557
+
558
+ // ── Final summary ──
559
+ log("");
560
+ log(`${c.green}${c.bold} ══════════════════════════════════════════════════${c.reset}`);
561
+ log(`${c.green}${c.bold} Ready!${c.reset} ${c.dim}Orchestrator v${PKG.version}${c.reset}`);
562
+ log(`${c.green}${c.bold} ══════════════════════════════════════════════════${c.reset}`);
563
+ log("");
564
+ log(` ${c.dim}Directory${c.reset} ${orchDir}`);
565
+ log(` ${c.dim}Repos${c.reset} ${repos.length} detected`);
566
+ log(` ${c.dim}Hooks${c.reset} ${hookCount} scripts`);
567
+ log(` ${c.dim}Skills${c.reset} 9 ${c.dim}(~/.claude/skills/)${c.reset}`);
568
+ log("");
569
+ log(` ${c.bold}Next steps:${c.reset}`);
570
+ log(` ${c.cyan}cd orchestrator/${c.reset}`);
571
+ log(` ${c.cyan}claude --agent workspace-init${c.reset} ${c.dim}# first time: diagnostic + config${c.reset}`);
572
+ log(` ${c.cyan}claude --agent team-lead${c.reset} ${c.dim}# orchestration sessions${c.reset}`);
573
+ if (reposWithoutClaude.length > 0) {
574
+ log("");
575
+ warn(`${reposWithoutClaude.length} repo(s) without CLAUDE.md: ${c.bold}${reposWithoutClaude.join(", ")}${c.reset}`);
576
+ info(`workspace-init can generate them`);
577
+ }
578
+ log("");
579
+ }
580
+
581
+ // ─── Doctor ─────────────────────────────────────────────────
582
+ function doctor() {
583
+ log(BANNER_SMALL);
584
+ step("Diagnostic");
585
+
586
+ const checks = [];
587
+ function check(name, isOk, detail) {
588
+ checks.push({ name, ok: isOk, detail });
589
+ isOk ? ok(name) : fail(`${name} — ${detail}`);
590
+ }
591
+
592
+ // Version
593
+ const installed = readVersion();
594
+ check("Installed version",
595
+ installed === PKG.version,
596
+ installed ? `v${installed} (package is v${PKG.version})` : "not installed — run: npx cc-workspace update"
597
+ );
598
+
599
+ // Global dirs
600
+ check("~/.claude/skills/", fs.existsSync(GLOBAL_SKILLS), "missing");
601
+ check("~/.claude/rules/", fs.existsSync(GLOBAL_RULES), "missing");
602
+ check("~/.claude/agents/", fs.existsSync(GLOBAL_AGENTS), "missing");
603
+
604
+ // Skills count
605
+ if (fs.existsSync(GLOBAL_SKILLS)) {
606
+ const skills = fs.readdirSync(GLOBAL_SKILLS, { withFileTypes: true }).filter(e => e.isDirectory());
607
+ check(`Skills (${skills.length}/9)`, skills.length >= 9, `only ${skills.length} found`);
608
+ }
609
+
610
+ // Rules
611
+ for (const r of ["constitution-en.md", "context-hygiene.md", "model-routing.md"]) {
612
+ check(`Rule: ${r}`, fs.existsSync(path.join(GLOBAL_RULES, r)), "missing");
613
+ }
614
+
615
+ // Agents
616
+ for (const a of ["team-lead.md", "implementer.md", "workspace-init.md"]) {
617
+ check(`Agent: ${a}`, fs.existsSync(path.join(GLOBAL_AGENTS, a)), "missing");
618
+ }
619
+
620
+ // jq
621
+ let jqOk = false;
622
+ try { execSync("jq --version", { stdio: "pipe" }); jqOk = true; } catch {}
623
+ check("jq installed", jqOk, "required for hooks — brew install jq");
624
+
625
+ // Local orchestrator/ check
626
+ const cwd = process.cwd();
627
+ const orchDir = path.join(cwd, "orchestrator");
628
+ const inOrch = fs.existsSync(path.join(cwd, "workspace.md"));
629
+ const hasOrch = fs.existsSync(orchDir);
630
+
631
+ if (inOrch) {
632
+ step("Local workspace (inside orchestrator/)");
633
+ check("workspace.md", true, "");
634
+ check("constitution.md", fs.existsSync(path.join(cwd, "constitution.md")), "missing");
635
+ check("plans/", fs.existsSync(path.join(cwd, "plans")), "missing");
636
+ check("templates/", fs.existsSync(path.join(cwd, "templates")), "missing");
637
+ check(".claude/hooks/", fs.existsSync(path.join(cwd, ".claude", "hooks")), "missing");
638
+ const configured = !fs.readFileSync(path.join(cwd, "workspace.md"), "utf8").includes("[UNCONFIGURED]");
639
+ check("workspace.md configured", configured, "[UNCONFIGURED] — run: claude --agent workspace-init");
640
+ } else if (hasOrch) {
641
+ step("Local workspace (orchestrator/ found)");
642
+ check("orchestrator/workspace.md", fs.existsSync(path.join(orchDir, "workspace.md")), "missing — run init");
643
+ } else {
644
+ log(`\n ${c.dim}No orchestrator/ found in cwd.${c.reset}`);
645
+ }
646
+
647
+ // Summary
648
+ const failed = checks.filter(x => !x.ok);
649
+ log("");
650
+ hr();
651
+ if (failed.length === 0) {
652
+ log(` ${c.bgGreen} ALL CHECKS PASSED ${c.reset}`);
653
+ } else {
654
+ log(` ${c.bgRed} ${failed.length} ISSUE(S) FOUND ${c.reset}`);
655
+ }
656
+ log("");
657
+ }
658
+
659
+ // ─── CLI ────────────────────────────────────────────────────
660
+ const args = process.argv.slice(2);
661
+ const command = args[0];
662
+
663
+ switch (command) {
664
+ case "init": {
665
+ const workspace = args[1] || ".";
666
+ const name = args[2] || "Mon Projet";
667
+ log(BANNER);
668
+ installGlobals(false);
669
+ setupWorkspace(workspace, name);
670
+ break;
671
+ }
672
+
673
+ case "update": {
674
+ const force = args.includes("--force");
675
+ log(BANNER);
676
+ const updated = installGlobals(force);
677
+ if (!updated) {
678
+ log(`\n ${c.dim}Already up to date. Use --force to reinstall.${c.reset}\n`);
679
+ } else {
680
+ log(`\n ${c.green}${c.bold}Update complete.${c.reset}\n`);
681
+ }
682
+ break;
683
+ }
684
+
685
+ case "doctor": {
686
+ doctor();
687
+ break;
688
+ }
689
+
690
+ case "version":
691
+ case "--version":
692
+ case "-v": {
693
+ const installed = readVersion();
694
+ log(`${c.cyan}${c.bold}cc-workspace${c.reset} v${PKG.version}`);
695
+ if (installed && installed !== PKG.version) {
696
+ log(` ${c.dim}installed globals: v${installed}${c.reset}`);
697
+ }
698
+ break;
699
+ }
700
+
701
+ case "help":
702
+ case "--help":
703
+ case "-h":
704
+ case undefined: {
705
+ log(BANNER);
706
+ log(` ${c.bold}Usage:${c.reset}`);
707
+ log("");
708
+ log(` ${c.cyan}npx cc-workspace init${c.reset} ${c.dim}[path] ["Project Name"]${c.reset}`);
709
+ log(` Setup orchestrator/ in the target workspace.`);
710
+ log(` Installs global skills/rules/agents if version is newer.`);
711
+ log("");
712
+ log(` ${c.cyan}npx cc-workspace update${c.reset} ${c.dim}[--force]${c.reset}`);
713
+ log(` Update global components to this package version.`);
714
+ log("");
715
+ log(` ${c.cyan}npx cc-workspace doctor${c.reset}`);
716
+ log(` Check all components are installed and consistent.`);
717
+ log("");
718
+ log(` ${c.cyan}npx cc-workspace version${c.reset}`);
719
+ log(` Show package and installed versions.`);
720
+ log("");
721
+ hr();
722
+ log(` ${c.bold}After init:${c.reset}`);
723
+ log(` ${c.cyan}cd orchestrator/${c.reset}`);
724
+ log(` ${c.cyan}claude --agent workspace-init${c.reset} ${c.dim}# first time${c.reset}`);
725
+ log(` ${c.cyan}claude --agent team-lead${c.reset} ${c.dim}# work sessions${c.reset}`);
726
+ log("");
727
+ break;
728
+ }
729
+
730
+ default: {
731
+ fail(`Unknown command: ${command}`);
732
+ log(` Run: ${c.cyan}npx cc-workspace --help${c.reset}`);
733
+ process.exit(1);
734
+ }
735
+ }