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.
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/cli.js +735 -0
- package/global-skills/agents/implementer.md +39 -0
- package/global-skills/agents/team-lead.md +143 -0
- package/global-skills/agents/workspace-init.md +207 -0
- package/global-skills/bootstrap-repo/SKILL.md +70 -0
- package/global-skills/constitution.md +58 -0
- package/global-skills/cross-service-check/SKILL.md +67 -0
- package/global-skills/cycle-retrospective/SKILL.md +133 -0
- package/global-skills/dispatch-feature/SKILL.md +168 -0
- package/global-skills/dispatch-feature/references/anti-patterns.md +31 -0
- package/global-skills/dispatch-feature/references/frontend-ux-standards.md +73 -0
- package/global-skills/dispatch-feature/references/spawn-templates.md +109 -0
- package/global-skills/hooks/notify-user.sh +30 -0
- package/global-skills/hooks/permission-auto-approve.sh +16 -0
- package/global-skills/hooks/session-start-context.sh +85 -0
- package/global-skills/hooks/subagent-start-context.sh +35 -0
- package/global-skills/hooks/task-completed-check.sh +21 -0
- package/global-skills/hooks/teammate-idle-check.sh +29 -0
- package/global-skills/hooks/track-file-modifications.sh +20 -0
- package/global-skills/hooks/user-prompt-guard.sh +19 -0
- package/global-skills/hooks/validate-spawn-prompt.sh +79 -0
- package/global-skills/hooks/worktree-create-context.sh +22 -0
- package/global-skills/incident-debug/SKILL.md +86 -0
- package/global-skills/merge-prep/SKILL.md +87 -0
- package/global-skills/plan-review/SKILL.md +70 -0
- package/global-skills/qa-ruthless/SKILL.md +102 -0
- package/global-skills/refresh-profiles/SKILL.md +22 -0
- package/global-skills/rules/constitution-en.md +67 -0
- package/global-skills/rules/context-hygiene.md +43 -0
- package/global-skills/rules/model-routing.md +42 -0
- package/global-skills/templates/claude-md.template.md +124 -0
- package/global-skills/templates/constitution.template.md +18 -0
- package/global-skills/templates/workspace.template.md +33 -0
- 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
|
+
}
|