cc-workspace 4.7.1 → 5.2.1
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/CHANGELOG.md +291 -0
- package/README.md +123 -41
- package/bin/cli.js +313 -134
- package/global-skills/agents/e2e-validator.md +151 -32
- package/global-skills/agents/implementer.md +77 -71
- package/global-skills/agents/reviewer.md +192 -0
- package/global-skills/agents/security-auditor.md +345 -0
- package/global-skills/agents/team-lead.md +93 -101
- package/global-skills/agents/workspace-init.md +16 -5
- package/global-skills/bootstrap-repo/SKILL.md +1 -0
- package/global-skills/cleanup/SKILL.md +35 -25
- package/global-skills/cross-service-check/SKILL.md +1 -0
- package/global-skills/cycle-retrospective/SKILL.md +6 -4
- package/global-skills/dispatch-feature/SKILL.md +225 -173
- package/global-skills/dispatch-feature/references/anti-patterns.md +52 -35
- package/global-skills/dispatch-feature/references/spawn-templates.md +140 -97
- package/global-skills/doctor/SKILL.md +124 -25
- package/global-skills/e2e-validator/references/container-strategies.md +55 -23
- package/global-skills/hooks/orphan-cleanup.sh +60 -0
- package/global-skills/hooks/permission-auto-approve.sh +61 -4
- package/global-skills/hooks/session-start-context.sh +10 -47
- package/global-skills/hooks/test_hooks.sh +242 -0
- package/global-skills/hooks/user-prompt-guard.sh +6 -6
- package/global-skills/hooks/validate-spawn-prompt.sh +40 -30
- package/global-skills/incident-debug/SKILL.md +1 -0
- package/global-skills/merge-prep/SKILL.md +1 -0
- package/global-skills/metrics/SKILL.md +139 -0
- package/global-skills/plan-review/SKILL.md +2 -1
- package/global-skills/qa-ruthless/SKILL.md +2 -0
- package/global-skills/refresh-profiles/SKILL.md +1 -0
- package/global-skills/rules/context-hygiene.md +4 -19
- package/global-skills/rules/model-routing.md +31 -18
- package/global-skills/session/SKILL.md +41 -20
- package/global-skills/templates/workspace.template.md +1 -1
- package/package.json +4 -3
package/bin/cli.js
CHANGED
|
@@ -56,18 +56,28 @@ function fail(msg) { console.error(` ${c.red}✗${c.reset} ${c.red}${msg}${
|
|
|
56
56
|
function info(msg) { console.log(` ${c.blue}▸${c.reset} ${msg}`); }
|
|
57
57
|
function step(msg) { console.log(`\n${c.bold}${c.white} ${msg}${c.reset}`); }
|
|
58
58
|
function hr() { console.log(`${c.dim} ${"─".repeat(50)}${c.reset}`); }
|
|
59
|
+
function dryOk(msg) { console.log(` ${c.cyan}→${c.reset} ${c.dim}(dry-run)${c.reset} ${msg}`); }
|
|
59
60
|
|
|
60
61
|
// ─── FS helpers ─────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
+
let DRY_RUN = false;
|
|
62
63
|
|
|
63
|
-
function
|
|
64
|
+
function mkdirp(dir) {
|
|
65
|
+
if (DRY_RUN) { dryOk(`mkdir -p ${dir}`); return; }
|
|
66
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function copyFile(src, dest) {
|
|
70
|
+
if (DRY_RUN) { dryOk(`copy ${path.basename(src)} → ${dest}`); return; }
|
|
71
|
+
fs.copyFileSync(src, dest);
|
|
72
|
+
}
|
|
64
73
|
|
|
65
74
|
function copyDir(src, dest) {
|
|
66
|
-
|
|
75
|
+
if (DRY_RUN) { dryOk(`copy dir ${path.basename(src)}/ → ${dest}`); return; }
|
|
76
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
67
77
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
68
78
|
const srcPath = path.join(src, entry.name);
|
|
69
79
|
const destPath = path.join(dest, entry.name);
|
|
70
|
-
entry.isDirectory() ? copyDir(srcPath, destPath) :
|
|
80
|
+
entry.isDirectory() ? copyDir(srcPath, destPath) : fs.copyFileSync(srcPath, destPath);
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -78,10 +88,16 @@ function readVersion() {
|
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
function writeVersion(v) {
|
|
91
|
+
if (DRY_RUN) { dryOk(`write version ${v} → ${VERSION_FILE}`); return; }
|
|
81
92
|
mkdirp(CLAUDE_DIR);
|
|
82
93
|
fs.writeFileSync(VERSION_FILE, v + "\n");
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
function writeFile(filePath, content) {
|
|
97
|
+
if (DRY_RUN) { dryOk(`write ${filePath}`); return; }
|
|
98
|
+
fs.writeFileSync(filePath, content);
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
function semverCompare(a, b) {
|
|
86
102
|
const pa = a.split(".").map(Number);
|
|
87
103
|
const pb = b.split(".").map(Number);
|
|
@@ -102,9 +118,12 @@ function needsUpdate(force) {
|
|
|
102
118
|
// ─── Detect project type ────────────────────────────────────
|
|
103
119
|
function detectProjectType(dir) {
|
|
104
120
|
const has = (f) => fs.existsSync(path.join(dir, f));
|
|
105
|
-
const
|
|
106
|
-
try {
|
|
107
|
-
|
|
121
|
+
const pkgDeps = () => {
|
|
122
|
+
try {
|
|
123
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
124
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
125
|
+
return (name) => name in all;
|
|
126
|
+
} catch { return () => false; }
|
|
108
127
|
};
|
|
109
128
|
if (has("composer.json")) return "PHP/Laravel";
|
|
110
129
|
if (has("pom.xml")) return "Java/Spring";
|
|
@@ -113,11 +132,12 @@ function detectProjectType(dir) {
|
|
|
113
132
|
if (has("go.mod")) return "Go";
|
|
114
133
|
if (has("Cargo.toml")) return "Rust";
|
|
115
134
|
if (has("package.json")) {
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
if (
|
|
119
|
-
if (
|
|
120
|
-
if (
|
|
135
|
+
const hasDep = pkgDeps();
|
|
136
|
+
if (hasDep("@quasar/app") || hasDep("@quasar/app-vite") || hasDep("@quasar/app-webpack") || hasDep("quasar")) return "Vue/Quasar";
|
|
137
|
+
if (hasDep("nuxt") || hasDep("nuxt3")) return "Vue/Nuxt";
|
|
138
|
+
if (hasDep("next")) return "React/Next";
|
|
139
|
+
if (hasDep("vue")) return "Vue";
|
|
140
|
+
if (hasDep("react")) return "React";
|
|
121
141
|
return "Node.js";
|
|
122
142
|
}
|
|
123
143
|
return "unknown";
|
|
@@ -143,6 +163,9 @@ function typeBadge(type) {
|
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
// ─── Install global components ──────────────────────────────
|
|
166
|
+
// v5.2: Only agents are global (needed for --agent CLI).
|
|
167
|
+
// Skills and rules are now LOCAL to orchestrator/.claude/ to avoid
|
|
168
|
+
// polluting non-orchestrator Claude sessions.
|
|
146
169
|
function installGlobals(force) {
|
|
147
170
|
const installed = readVersion();
|
|
148
171
|
const shouldUpdate = needsUpdate(force);
|
|
@@ -157,44 +180,64 @@ function installGlobals(force) {
|
|
|
157
180
|
: `Installing global components`
|
|
158
181
|
);
|
|
159
182
|
|
|
160
|
-
mkdirp(GLOBAL_SKILLS);
|
|
161
|
-
mkdirp(GLOBAL_RULES);
|
|
162
183
|
mkdirp(GLOBAL_AGENTS);
|
|
163
184
|
|
|
164
|
-
//
|
|
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
|
+
// Agents (global — required for claude --agent)
|
|
185
186
|
const agentsDir = path.join(SKILLS_DIR, "agents");
|
|
186
187
|
if (fs.existsSync(agentsDir)) {
|
|
187
188
|
let n = 0;
|
|
188
189
|
for (const f of fs.readdirSync(agentsDir)) {
|
|
189
190
|
if (f.endsWith(".md")) { copyFile(path.join(agentsDir, f), path.join(GLOBAL_AGENTS, f)); n++; }
|
|
190
191
|
}
|
|
191
|
-
ok(`${n} agents`);
|
|
192
|
+
ok(`${n} agents ${c.dim}(global — ~/.claude/agents/)${c.reset}`);
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
// ── Migration: clean old global skills & rules from previous versions ──
|
|
196
|
+
cleanLegacyGlobals();
|
|
197
|
+
|
|
194
198
|
writeVersion(PKG.version);
|
|
195
199
|
return true;
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
// ─── Clean legacy global skills & rules ─────────────────────
|
|
203
|
+
// Previous versions installed skills and rules into ~/.claude/skills/
|
|
204
|
+
// and ~/.claude/rules/. These now live in orchestrator/.claude/ locally.
|
|
205
|
+
// This function removes only cc-workspace-owned entries, not user-created ones.
|
|
206
|
+
function cleanLegacyGlobals() {
|
|
207
|
+
let cleaned = 0;
|
|
208
|
+
|
|
209
|
+
// Skills that cc-workspace installed globally
|
|
210
|
+
if (fs.existsSync(GLOBAL_SKILLS)) {
|
|
211
|
+
const skipDirs = new Set(["rules", "agents", "hooks", "templates"]);
|
|
212
|
+
const ourSkills = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
213
|
+
.filter(e => e.isDirectory() && !skipDirs.has(e.name))
|
|
214
|
+
.map(e => e.name);
|
|
215
|
+
for (const name of ourSkills) {
|
|
216
|
+
const target = path.join(GLOBAL_SKILLS, name);
|
|
217
|
+
if (fs.existsSync(target)) {
|
|
218
|
+
if (DRY_RUN) { dryOk(`remove legacy global skill ${name}/`); }
|
|
219
|
+
else { fs.rmSync(target, { recursive: true }); }
|
|
220
|
+
cleaned++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Rules that cc-workspace installed globally
|
|
226
|
+
const ourRules = ["context-hygiene.md", "model-routing.md"];
|
|
227
|
+
for (const f of ourRules) {
|
|
228
|
+
const fp = path.join(GLOBAL_RULES, f);
|
|
229
|
+
if (fs.existsSync(fp)) {
|
|
230
|
+
if (DRY_RUN) { dryOk(`remove legacy global rule ${f}`); }
|
|
231
|
+
else { fs.unlinkSync(fp); }
|
|
232
|
+
cleaned++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (cleaned > 0) {
|
|
237
|
+
ok(`${cleaned} legacy global entries cleaned ${c.dim}(skills & rules now local to orchestrator/)${c.reset}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
198
241
|
// ─── Generate settings.json with hooks ──────────────────────
|
|
199
242
|
// Claude Code hook format: { matcher: { tools: [...] }, hooks: [{ type: "command", command: "...", timeout: N }] }
|
|
200
243
|
// For hooks without tool matcher: { hooks: [{ type: "command", command: "...", timeout: N }] }
|
|
@@ -231,6 +274,7 @@ function generateSettings(orchDir) {
|
|
|
231
274
|
withMatcher("Teammate", "validate-spawn-prompt.sh", 5)
|
|
232
275
|
],
|
|
233
276
|
SessionStart: [
|
|
277
|
+
withoutMatcher("orphan-cleanup.sh", 10),
|
|
234
278
|
withoutMatcher("session-start-context.sh", 10)
|
|
235
279
|
],
|
|
236
280
|
UserPromptSubmit: [
|
|
@@ -256,25 +300,20 @@ function generateSettings(orchDir) {
|
|
|
256
300
|
]
|
|
257
301
|
}
|
|
258
302
|
};
|
|
259
|
-
|
|
303
|
+
writeFile(path.join(orchDir, ".claude", "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
260
304
|
}
|
|
261
305
|
|
|
262
|
-
// ─── Block hook ──────────────────────────────────────────────
|
|
263
|
-
// block-orchestrator-writes is now ONLY in team-lead agent frontmatter.
|
|
264
|
-
// It is NOT in settings.json (would be inherited by teammates, blocking their writes).
|
|
265
|
-
// The generateBlockHook() function was removed in v4.1.4.
|
|
266
|
-
|
|
267
306
|
// ─── CLAUDE.md content ──────────────────────────────────────
|
|
268
307
|
function claudeMdContent() {
|
|
269
308
|
return `# Orchestrator v${PKG.version}
|
|
270
309
|
|
|
271
|
-
You are the tech lead. You never code in repos
|
|
272
|
-
You clarify, plan, delegate,
|
|
310
|
+
You are the tech lead. You never write application code in repos.
|
|
311
|
+
You clarify, plan, manage git directly, delegate to teammates, run micro-QA.
|
|
273
312
|
|
|
274
313
|
## Security
|
|
275
|
-
- \`
|
|
276
|
-
-
|
|
277
|
-
- Hook \`PreToolUse\` path-aware:
|
|
314
|
+
- \`tools\`: Read, Write, Edit, Bash, Glob, Grep, Task(implementer, Explore), Teammate, SendMessage
|
|
315
|
+
- Bash allowed for: git (branch, worktree, log), test/typecheck in /tmp/ worktrees (micro-QA)
|
|
316
|
+
- Hook \`PreToolUse\` path-aware: Write/Edit/MultiEdit only allowed in orchestrator/
|
|
278
317
|
|
|
279
318
|
> settings.json contains env vars + hooks registration.
|
|
280
319
|
|
|
@@ -283,6 +322,8 @@ You clarify, plan, delegate, track.
|
|
|
283
322
|
cd orchestrator/
|
|
284
323
|
claude --agent workspace-init # first time: diagnostic + config
|
|
285
324
|
claude --agent team-lead # work sessions
|
|
325
|
+
claude --agent reviewer # evidence-based code review (standalone or Phase 5)
|
|
326
|
+
claude --agent security-auditor # security audit (standalone or Phase 5 when needed)
|
|
286
327
|
claude --agent e2e-validator # E2E validation of completed plans
|
|
287
328
|
\`\`\`
|
|
288
329
|
|
|
@@ -294,11 +335,18 @@ Run once. Idempotent — can be re-run to re-diagnose.
|
|
|
294
335
|
## 4 session modes
|
|
295
336
|
| Mode | Description |
|
|
296
337
|
|------|-------------|
|
|
297
|
-
| **A — Full** | Clarify → Plan → Validate → Dispatch
|
|
338
|
+
| **A — Full** | Clarify → Plan → Validate → Git setup → Dispatch teammates → QA |
|
|
298
339
|
| **B — Quick plan** | Specs → Plan → Dispatch |
|
|
299
340
|
| **C — Go direct** | Immediate dispatch |
|
|
300
341
|
| **D — Single-service** | 1 repo, no waves |
|
|
301
342
|
|
|
343
|
+
## Git model (v5)
|
|
344
|
+
- Opus creates session branches and worktrees directly via Bash AFTER plan validation
|
|
345
|
+
- ONE teammate per repo handles all its commit units sequentially
|
|
346
|
+
- Micro-QA (Bash tests + Haiku diff) runs after EVERY commit before greenlighting next
|
|
347
|
+
- Worktrees persist in /tmp/ until session close (/session close <n>)
|
|
348
|
+
- Source branch: from workspace.md per repo, or override in initial prompt
|
|
349
|
+
|
|
302
350
|
## Config
|
|
303
351
|
- Project context: \`./workspace.md\`
|
|
304
352
|
- Project constitution: \`./constitution.md\`
|
|
@@ -306,41 +354,48 @@ Run once. Idempotent — can be re-run to re-diagnose.
|
|
|
306
354
|
- Service profiles: \`./plans/service-profiles.md\`
|
|
307
355
|
- Active plans: \`./plans/*.md\`
|
|
308
356
|
- Active sessions: \`./.sessions/*.json\`
|
|
357
|
+
- Skills: \`./.claude/skills/\` (local to orchestrator)
|
|
358
|
+
- Rules: \`./.claude/rules/\` (local to orchestrator)
|
|
359
|
+
- Agents: \`~/.claude/agents/\` (global — needed for --agent CLI)
|
|
309
360
|
- E2E config: \`./e2e/e2e-config.md\`
|
|
310
361
|
- E2E reports: \`./e2e/reports/\`
|
|
311
362
|
|
|
312
|
-
## Skills (13)
|
|
313
|
-
- **dispatch-feature**: 4 modes, clarify → plan →
|
|
314
|
-
- **qa-ruthless**: adversarial QA, min 3 findings per service
|
|
363
|
+
## Skills (13) + Agents (6)
|
|
364
|
+
- **dispatch-feature**: 4 modes, clarify → plan → git setup → 1 teammate/repo → micro-QA → verify
|
|
365
|
+
- **qa-ruthless**: adversarial QA (opus), min 3 findings per service
|
|
315
366
|
- **cross-service-check**: inter-repo consistency
|
|
316
367
|
- **incident-debug**: multi-layer diagnostic
|
|
317
|
-
- **plan-review**: plan sanity check (
|
|
368
|
+
- **plan-review**: plan sanity check (sonnet — constitution compliance)
|
|
318
369
|
- **merge-prep**: pre-merge, conflicts, PR summaries
|
|
319
|
-
- **cycle-retrospective**: post-cycle learning
|
|
370
|
+
- **cycle-retrospective**: MANDATORY post-cycle learning
|
|
320
371
|
- **refresh-profiles**: re-reads repo CLAUDE.md files (haiku)
|
|
321
372
|
- **bootstrap-repo**: generates a CLAUDE.md for a repo (haiku)
|
|
322
373
|
- **e2e-validator**: E2E validation of completed plans (beta) — containers + Chrome
|
|
323
|
-
- **/session**: list, status, close parallel sessions
|
|
374
|
+
- **/session**: list, status, close parallel sessions (includes worktree cleanup)
|
|
324
375
|
- **/doctor**: full workspace diagnostic
|
|
325
|
-
- **/cleanup**: remove orphan worktrees + stale sessions
|
|
376
|
+
- **/cleanup**: remove orphan worktrees (session-aware) + stale sessions
|
|
377
|
+
- **reviewer** (agent): evidence-based code review — scope check, architecture, constitution compliance
|
|
378
|
+
- **security-auditor** (agent): security audit — auth flows, tenant isolation, secrets, CVEs, input validation
|
|
326
379
|
|
|
327
380
|
## Rules
|
|
328
|
-
1. No code in repos — delegate to teammates
|
|
381
|
+
1. No application code in repos — delegate to teammates
|
|
329
382
|
2. Can write in orchestrator/ (plans, workspace.md, constitution.md)
|
|
330
|
-
3.
|
|
331
|
-
4.
|
|
332
|
-
5.
|
|
333
|
-
6.
|
|
334
|
-
7.
|
|
335
|
-
8.
|
|
336
|
-
9.
|
|
337
|
-
10.
|
|
338
|
-
11.
|
|
339
|
-
12.
|
|
340
|
-
13.
|
|
341
|
-
14.
|
|
342
|
-
15.
|
|
343
|
-
16.
|
|
383
|
+
3. Can run git commands on repos (branch, worktree, log) and tests in /tmp/ worktrees
|
|
384
|
+
4. Clarify ambiguities BEFORE planning (except mode C)
|
|
385
|
+
5. All plans in markdown in \`./plans/\`
|
|
386
|
+
6. Create worktrees AFTER plan validation, not before
|
|
387
|
+
7. ONE teammate per repo — handles all commit units sequentially
|
|
388
|
+
8. Full constitution (all rules from constitution.md) in every spawn prompt
|
|
389
|
+
9. UX standards injected for frontend teammates
|
|
390
|
+
10. Micro-QA (Bash + Haiku) after every commit — mandatory
|
|
391
|
+
11. Escalate arch decisions not covered by the plan
|
|
392
|
+
12. Ruthless QA — UX violations = blocking
|
|
393
|
+
13. Teammates must run tests before signaling — reject signals without test results
|
|
394
|
+
14. Hooks are warning-only — never blocking
|
|
395
|
+
15. cycle-retrospective is MANDATORY in Phase 5 — not optional
|
|
396
|
+
16. Worktrees live until session close — never prune active session worktrees
|
|
397
|
+
17. Never \`git checkout -b\` in repos — use \`git branch\` (no checkout)
|
|
398
|
+
18. E2E validation via \`claude --agent e2e-validator\` after plans are complete
|
|
344
399
|
`;
|
|
345
400
|
}
|
|
346
401
|
|
|
@@ -433,14 +488,17 @@ function updateLocal() {
|
|
|
433
488
|
const obsoleteHooks = ["block-orchestrator-writes.sh", "worktree-create-context.sh", "verify-cycle-complete.sh", "guard-session-checkout.sh"];
|
|
434
489
|
for (const f of obsoleteHooks) {
|
|
435
490
|
const fp = path.join(hooksDir, f);
|
|
436
|
-
if (fs.existsSync(fp))
|
|
491
|
+
if (fs.existsSync(fp)) {
|
|
492
|
+
if (DRY_RUN) dryOk(`remove obsolete ${f}`);
|
|
493
|
+
else fs.unlinkSync(fp);
|
|
494
|
+
}
|
|
437
495
|
}
|
|
438
496
|
const hooksSrc = path.join(SKILLS_DIR, "hooks");
|
|
439
497
|
if (fs.existsSync(hooksSrc)) {
|
|
440
498
|
for (const f of fs.readdirSync(hooksSrc)) {
|
|
441
499
|
if (!f.endsWith(".sh")) continue;
|
|
442
500
|
copyFile(path.join(hooksSrc, f), path.join(hooksDir, f));
|
|
443
|
-
fs.chmodSync(path.join(hooksDir, f), 0o755);
|
|
501
|
+
if (!DRY_RUN) fs.chmodSync(path.join(hooksDir, f), 0o755);
|
|
444
502
|
count++;
|
|
445
503
|
}
|
|
446
504
|
}
|
|
@@ -454,6 +512,30 @@ function updateLocal() {
|
|
|
454
512
|
ok("settings.json regenerated");
|
|
455
513
|
}
|
|
456
514
|
|
|
515
|
+
// ── Skills (local — always overwrite) ──
|
|
516
|
+
const localSkills = path.join(orchDir, ".claude", "skills");
|
|
517
|
+
mkdirp(localSkills);
|
|
518
|
+
const skipDirs = new Set(["rules", "agents", "hooks", "templates"]);
|
|
519
|
+
let skillCount = 0;
|
|
520
|
+
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
|
521
|
+
if (!entry.isDirectory() || skipDirs.has(entry.name)) continue;
|
|
522
|
+
copyDir(path.join(SKILLS_DIR, entry.name), path.join(localSkills, entry.name));
|
|
523
|
+
skillCount++;
|
|
524
|
+
}
|
|
525
|
+
ok(`${skillCount} skills ${c.dim}(local — orchestrator/.claude/skills/)${c.reset}`);
|
|
526
|
+
|
|
527
|
+
// ── Rules (local — always overwrite) ──
|
|
528
|
+
const localRules = path.join(orchDir, ".claude", "rules");
|
|
529
|
+
mkdirp(localRules);
|
|
530
|
+
const rulesDir = path.join(SKILLS_DIR, "rules");
|
|
531
|
+
if (fs.existsSync(rulesDir)) {
|
|
532
|
+
let ruleCount = 0;
|
|
533
|
+
for (const f of fs.readdirSync(rulesDir)) {
|
|
534
|
+
if (f.endsWith(".md")) { copyFile(path.join(rulesDir, f), path.join(localRules, f)); ruleCount++; }
|
|
535
|
+
}
|
|
536
|
+
ok(`${ruleCount} rules ${c.dim}(local — orchestrator/.claude/rules/)${c.reset}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
457
539
|
// ── Templates (always overwrite — reference docs) ──
|
|
458
540
|
const templatesDir = path.join(SKILLS_DIR, "templates");
|
|
459
541
|
const localTemplates = path.join(orchDir, "templates");
|
|
@@ -470,13 +552,13 @@ function updateLocal() {
|
|
|
470
552
|
|
|
471
553
|
// ── CLAUDE.md (always overwrite — generated file, not user content) ──
|
|
472
554
|
const claudeMd = path.join(orchDir, "CLAUDE.md");
|
|
473
|
-
|
|
555
|
+
writeFile(claudeMd, claudeMdContent());
|
|
474
556
|
ok("CLAUDE.md updated");
|
|
475
557
|
|
|
476
558
|
// ── Plan template (always overwrite — structure only) ──
|
|
477
559
|
const planTpl = path.join(orchDir, "plans", "_TEMPLATE.md");
|
|
478
560
|
if (fs.existsSync(path.join(orchDir, "plans"))) {
|
|
479
|
-
|
|
561
|
+
writeFile(planTpl, planTemplateContent());
|
|
480
562
|
ok("_TEMPLATE.md updated");
|
|
481
563
|
}
|
|
482
564
|
|
|
@@ -536,7 +618,7 @@ function setupWorkspace(workspacePath, projectName) {
|
|
|
536
618
|
if (!fs.existsSync(wsMd)) {
|
|
537
619
|
const tpl = path.join(orchDir, "templates", "workspace.template.md");
|
|
538
620
|
if (fs.existsSync(tpl)) copyFile(tpl, wsMd);
|
|
539
|
-
else
|
|
621
|
+
else writeFile(wsMd, `# Workspace: ${projectName}\n\n## Projet\n[UNCONFIGURED]\n`);
|
|
540
622
|
ok(`workspace.md ${c.dim}[UNCONFIGURED]${c.reset}`);
|
|
541
623
|
} else {
|
|
542
624
|
warn("workspace.md exists — skipped");
|
|
@@ -547,7 +629,7 @@ function setupWorkspace(workspacePath, projectName) {
|
|
|
547
629
|
if (!fs.existsSync(constMd)) {
|
|
548
630
|
const tpl = path.join(orchDir, "templates", "constitution.template.md");
|
|
549
631
|
if (fs.existsSync(tpl)) copyFile(tpl, constMd);
|
|
550
|
-
else
|
|
632
|
+
else writeFile(constMd, [
|
|
551
633
|
`# Constitution — ${projectName}`, "",
|
|
552
634
|
"> Define your project's non-negotiable engineering principles here.",
|
|
553
635
|
"> The orchestrator and every teammate must follow these rules without exception.", "",
|
|
@@ -570,12 +652,37 @@ function setupWorkspace(workspacePath, projectName) {
|
|
|
570
652
|
for (const f of fs.readdirSync(hooksSrc)) {
|
|
571
653
|
if (!f.endsWith(".sh")) continue;
|
|
572
654
|
copyFile(path.join(hooksSrc, f), path.join(hooksDir, f));
|
|
573
|
-
fs.chmodSync(path.join(hooksDir, f), 0o755);
|
|
655
|
+
if (!DRY_RUN) fs.chmodSync(path.join(hooksDir, f), 0o755);
|
|
574
656
|
hookCount++;
|
|
575
657
|
}
|
|
576
658
|
}
|
|
577
659
|
ok(`${hookCount} hooks ${c.dim}(all warning-only)${c.reset}`);
|
|
578
660
|
|
|
661
|
+
// ── Skills (local to orchestrator/) ──
|
|
662
|
+
step("Installing skills & rules");
|
|
663
|
+
const localSkills = path.join(orchDir, ".claude", "skills");
|
|
664
|
+
mkdirp(localSkills);
|
|
665
|
+
const skipDirs = new Set(["rules", "agents", "hooks", "templates"]);
|
|
666
|
+
let skillCount = 0;
|
|
667
|
+
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
|
668
|
+
if (!entry.isDirectory() || skipDirs.has(entry.name)) continue;
|
|
669
|
+
copyDir(path.join(SKILLS_DIR, entry.name), path.join(localSkills, entry.name));
|
|
670
|
+
skillCount++;
|
|
671
|
+
}
|
|
672
|
+
ok(`${skillCount} skills ${c.dim}(local — orchestrator/.claude/skills/)${c.reset}`);
|
|
673
|
+
|
|
674
|
+
// ── Rules (local to orchestrator/) ──
|
|
675
|
+
const localRules = path.join(orchDir, ".claude", "rules");
|
|
676
|
+
mkdirp(localRules);
|
|
677
|
+
const rulesSrc = path.join(SKILLS_DIR, "rules");
|
|
678
|
+
let ruleCount = 0;
|
|
679
|
+
if (fs.existsSync(rulesSrc)) {
|
|
680
|
+
for (const f of fs.readdirSync(rulesSrc)) {
|
|
681
|
+
if (f.endsWith(".md")) { copyFile(path.join(rulesSrc, f), path.join(localRules, f)); ruleCount++; }
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
ok(`${ruleCount} rules ${c.dim}(local — orchestrator/.claude/rules/)${c.reset}`);
|
|
685
|
+
|
|
579
686
|
// ── Settings ──
|
|
580
687
|
generateSettings(orchDir);
|
|
581
688
|
ok(`settings.json ${c.dim}(env + hooks)${c.reset}`);
|
|
@@ -583,7 +690,7 @@ function setupWorkspace(workspacePath, projectName) {
|
|
|
583
690
|
// ── CLAUDE.md ──
|
|
584
691
|
const claudeMd = path.join(orchDir, "CLAUDE.md");
|
|
585
692
|
if (!fs.existsSync(claudeMd)) {
|
|
586
|
-
|
|
693
|
+
writeFile(claudeMd, claudeMdContent());
|
|
587
694
|
ok("CLAUDE.md");
|
|
588
695
|
} else {
|
|
589
696
|
warn("CLAUDE.md exists — skipped");
|
|
@@ -592,14 +699,14 @@ function setupWorkspace(workspacePath, projectName) {
|
|
|
592
699
|
// ── Plan template ──
|
|
593
700
|
const planTpl = path.join(orchDir, "plans", "_TEMPLATE.md");
|
|
594
701
|
if (!fs.existsSync(planTpl)) {
|
|
595
|
-
|
|
702
|
+
writeFile(planTpl, planTemplateContent());
|
|
596
703
|
ok("Plan template");
|
|
597
704
|
}
|
|
598
705
|
|
|
599
706
|
// ── .gitignore ──
|
|
600
707
|
const gi = path.join(orchDir, ".gitignore");
|
|
601
708
|
if (!fs.existsSync(gi)) {
|
|
602
|
-
|
|
709
|
+
writeFile(gi, [
|
|
603
710
|
".claude/bash-commands.log", ".claude/worktrees/", ".claude/modified-files.log",
|
|
604
711
|
".sessions/",
|
|
605
712
|
"plans/*.md", "!plans/_TEMPLATE.md", "!plans/service-profiles.md",
|
|
@@ -651,7 +758,7 @@ function setupWorkspace(workspacePath, projectName) {
|
|
|
651
758
|
profileLines.push(`- **CLAUDE.md** : ${r.hasClaude ? "present" : "ABSENT — /bootstrap-repo"}`);
|
|
652
759
|
profileLines.push("");
|
|
653
760
|
}
|
|
654
|
-
|
|
761
|
+
writeFile(path.join(orchDir, "plans", "service-profiles.md"), profileLines.join("\n"));
|
|
655
762
|
|
|
656
763
|
// ── Final summary ──
|
|
657
764
|
log("");
|
|
@@ -661,14 +768,18 @@ function setupWorkspace(workspacePath, projectName) {
|
|
|
661
768
|
log("");
|
|
662
769
|
log(` ${c.dim}Directory${c.reset} ${orchDir}`);
|
|
663
770
|
log(` ${c.dim}Repos${c.reset} ${repos.length} detected`);
|
|
664
|
-
log(` ${c.dim}Hooks${c.reset} ${hookCount} scripts`);
|
|
665
|
-
log(` ${c.dim}Skills${c.reset}
|
|
771
|
+
log(` ${c.dim}Hooks${c.reset} ${hookCount} scripts ${c.dim}(local)${c.reset}`);
|
|
772
|
+
log(` ${c.dim}Skills${c.reset} ${skillCount} ${c.dim}(local — orchestrator/.claude/skills/)${c.reset}`);
|
|
773
|
+
log(` ${c.dim}Rules${c.reset} ${ruleCount} ${c.dim}(local — orchestrator/.claude/rules/)${c.reset}`);
|
|
774
|
+
log(` ${c.dim}Agents${c.reset} 6 ${c.dim}(global — ~/.claude/agents/)${c.reset}`);
|
|
666
775
|
log("");
|
|
667
776
|
log(` ${c.bold}Next steps:${c.reset}`);
|
|
668
777
|
log(` ${c.cyan}cd orchestrator/${c.reset}`);
|
|
669
778
|
log(` ${c.cyan}claude --agent workspace-init${c.reset} ${c.dim}# first time: diagnostic + config${c.reset}`);
|
|
670
779
|
log(` ${c.dim} └─ type "go" to start the diagnostic${c.reset}`);
|
|
671
|
-
log(` ${c.cyan}claude --agent team-lead${c.reset} ${c.dim}# orchestration sessions${c.reset}`);
|
|
780
|
+
log(` ${c.cyan}claude --agent team-lead${c.reset} ${c.dim}# orchestration sessions (v5: git + micro-QA)${c.reset}`);
|
|
781
|
+
log(` ${c.cyan}claude --agent reviewer${c.reset} ${c.dim}# evidence-based code review${c.reset}`);
|
|
782
|
+
log(` ${c.cyan}claude --agent security-auditor${c.reset} ${c.dim}# security audit${c.reset}`);
|
|
672
783
|
log(` ${c.cyan}claude --agent e2e-validator${c.reset} ${c.dim}# E2E validation (beta)${c.reset}`);
|
|
673
784
|
if (reposWithoutClaude.length > 0) {
|
|
674
785
|
log("");
|
|
@@ -696,27 +807,22 @@ function doctor() {
|
|
|
696
807
|
installed ? `v${installed} (package is v${PKG.version})` : "not installed — run: npx cc-workspace update"
|
|
697
808
|
);
|
|
698
809
|
|
|
699
|
-
// Global
|
|
700
|
-
check("~/.claude/skills/", fs.existsSync(GLOBAL_SKILLS), "missing");
|
|
701
|
-
check("~/.claude/rules/", fs.existsSync(GLOBAL_RULES), "missing");
|
|
810
|
+
// Global: only agents expected
|
|
702
811
|
check("~/.claude/agents/", fs.existsSync(GLOBAL_AGENTS), "missing");
|
|
703
812
|
|
|
704
|
-
// Skills count
|
|
705
|
-
if (fs.existsSync(GLOBAL_SKILLS)) {
|
|
706
|
-
const skills = fs.readdirSync(GLOBAL_SKILLS, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
707
|
-
check(`Skills (${skills.length}/13)`, skills.length >= 13, `only ${skills.length} found`);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Rules
|
|
711
|
-
for (const r of ["context-hygiene.md", "model-routing.md"]) {
|
|
712
|
-
check(`Rule: ${r}`, fs.existsSync(path.join(GLOBAL_RULES, r)), "missing");
|
|
713
|
-
}
|
|
714
|
-
|
|
715
813
|
// Agents
|
|
716
|
-
for (const a of ["team-lead.md", "implementer.md", "workspace-init.md", "e2e-validator.md"]) {
|
|
814
|
+
for (const a of ["team-lead.md", "implementer.md", "workspace-init.md", "e2e-validator.md", "reviewer.md", "security-auditor.md"]) {
|
|
717
815
|
check(`Agent: ${a}`, fs.existsSync(path.join(GLOBAL_AGENTS, a)), "missing");
|
|
718
816
|
}
|
|
719
817
|
|
|
818
|
+
// Legacy detection: warn if skills/rules still in global
|
|
819
|
+
const legacySkills = fs.existsSync(GLOBAL_SKILLS) &&
|
|
820
|
+
fs.readdirSync(GLOBAL_SKILLS, { withFileTypes: true }).some(e => e.isDirectory() && e.name === "dispatch-feature");
|
|
821
|
+
const legacyRules = fs.existsSync(path.join(GLOBAL_RULES, "model-routing.md"));
|
|
822
|
+
if (legacySkills || legacyRules) {
|
|
823
|
+
warn(`Legacy global skills/rules detected in ~/.claude/ — run: npx cc-workspace update --force`);
|
|
824
|
+
}
|
|
825
|
+
|
|
720
826
|
// jq
|
|
721
827
|
let jqOk = false;
|
|
722
828
|
try { execSync("jq --version", { stdio: "pipe" }); jqOk = true; } catch {}
|
|
@@ -727,21 +833,42 @@ function doctor() {
|
|
|
727
833
|
const orchDir = path.join(cwd, "orchestrator");
|
|
728
834
|
const inOrch = fs.existsSync(path.join(cwd, "workspace.md"));
|
|
729
835
|
const hasOrch = fs.existsSync(orchDir);
|
|
836
|
+
const localDir = inOrch ? cwd : hasOrch ? orchDir : null;
|
|
837
|
+
|
|
838
|
+
if (localDir) {
|
|
839
|
+
step(inOrch ? "Local workspace (inside orchestrator/)" : "Local workspace (orchestrator/ found)");
|
|
840
|
+
|
|
841
|
+
if (inOrch) {
|
|
842
|
+
check("workspace.md", true, "");
|
|
843
|
+
check("constitution.md", fs.existsSync(path.join(localDir, "constitution.md")), "missing");
|
|
844
|
+
check("plans/", fs.existsSync(path.join(localDir, "plans")), "missing");
|
|
845
|
+
check("templates/", fs.existsSync(path.join(localDir, "templates")), "missing");
|
|
846
|
+
check(".claude/hooks/", fs.existsSync(path.join(localDir, ".claude", "hooks")), "missing");
|
|
847
|
+
check(".sessions/", fs.existsSync(path.join(localDir, ".sessions")), "missing — run: npx cc-workspace update");
|
|
848
|
+
check("e2e/", fs.existsSync(path.join(localDir, "e2e")), "missing — run: npx cc-workspace update");
|
|
849
|
+
const configured = !fs.readFileSync(path.join(localDir, "workspace.md"), "utf8").includes("[UNCONFIGURED]");
|
|
850
|
+
check("workspace.md configured", configured, "[UNCONFIGURED] — run: claude --agent workspace-init");
|
|
851
|
+
} else {
|
|
852
|
+
check("orchestrator/workspace.md", fs.existsSync(path.join(orchDir, "workspace.md")), "missing — run init");
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Local skills (v5.2+)
|
|
856
|
+
const localSkills = path.join(localDir, ".claude", "skills");
|
|
857
|
+
if (fs.existsSync(localSkills)) {
|
|
858
|
+
const skills = fs.readdirSync(localSkills, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
859
|
+
const skipDirsDoctor = new Set(["rules", "agents", "hooks", "templates"]);
|
|
860
|
+
const expectedSkillCount = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
861
|
+
.filter(e => e.isDirectory() && !skipDirsDoctor.has(e.name)).length;
|
|
862
|
+
check(`Local skills (${skills.length}/${expectedSkillCount})`, skills.length >= expectedSkillCount, `only ${skills.length} found — run: npx cc-workspace update --force`);
|
|
863
|
+
} else {
|
|
864
|
+
check("Local skills", false, "missing .claude/skills/ — run: npx cc-workspace update --force");
|
|
865
|
+
}
|
|
730
866
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
check("templates/", fs.existsSync(path.join(cwd, "templates")), "missing");
|
|
737
|
-
check(".claude/hooks/", fs.existsSync(path.join(cwd, ".claude", "hooks")), "missing");
|
|
738
|
-
check(".sessions/", fs.existsSync(path.join(cwd, ".sessions")), "missing — run: npx cc-workspace update");
|
|
739
|
-
check("e2e/", fs.existsSync(path.join(cwd, "e2e")), "missing — run: npx cc-workspace update");
|
|
740
|
-
const configured = !fs.readFileSync(path.join(cwd, "workspace.md"), "utf8").includes("[UNCONFIGURED]");
|
|
741
|
-
check("workspace.md configured", configured, "[UNCONFIGURED] — run: claude --agent workspace-init");
|
|
742
|
-
} else if (hasOrch) {
|
|
743
|
-
step("Local workspace (orchestrator/ found)");
|
|
744
|
-
check("orchestrator/workspace.md", fs.existsSync(path.join(orchDir, "workspace.md")), "missing — run init");
|
|
867
|
+
// Local rules (v5.2+)
|
|
868
|
+
const localRules = path.join(localDir, ".claude", "rules");
|
|
869
|
+
for (const r of ["context-hygiene.md", "model-routing.md"]) {
|
|
870
|
+
check(`Local rule: ${r}`, fs.existsSync(path.join(localRules, r)), `missing — run: npx cc-workspace update --force`);
|
|
871
|
+
}
|
|
745
872
|
} else {
|
|
746
873
|
log(`\n ${c.dim}No orchestrator/ found in cwd.${c.reset}`);
|
|
747
874
|
}
|
|
@@ -795,26 +922,32 @@ const command = args[0];
|
|
|
795
922
|
|
|
796
923
|
switch (command) {
|
|
797
924
|
case "init": {
|
|
798
|
-
|
|
799
|
-
const
|
|
925
|
+
DRY_RUN = args.includes("--dry-run");
|
|
926
|
+
const filteredArgs = args.filter(a => a !== "--dry-run");
|
|
927
|
+
const workspace = filteredArgs[1] || ".";
|
|
928
|
+
const name = filteredArgs[2] || "My Project";
|
|
800
929
|
log(BANNER);
|
|
930
|
+
if (DRY_RUN) log(` ${c.yellow}${c.bold}DRY RUN${c.reset} — no files will be written\n`);
|
|
801
931
|
installGlobals(false);
|
|
802
932
|
setupWorkspace(workspace, name);
|
|
803
933
|
break;
|
|
804
934
|
}
|
|
805
935
|
|
|
806
936
|
case "update": {
|
|
937
|
+
DRY_RUN = args.includes("--dry-run");
|
|
807
938
|
const force = args.includes("--force");
|
|
808
939
|
log(BANNER);
|
|
940
|
+
if (DRY_RUN) log(` ${c.yellow}${c.bold}DRY RUN${c.reset} — no files will be written\n`);
|
|
809
941
|
const updated = installGlobals(force);
|
|
810
942
|
const localUpdated = (updated || force) ? updateLocal() : false;
|
|
811
943
|
if (!updated && !force) {
|
|
812
944
|
log(`\n ${c.dim}Already up to date. Use --force to reinstall.${c.reset}\n`);
|
|
813
945
|
} else {
|
|
814
946
|
if (localUpdated) {
|
|
815
|
-
log(`\n ${c.green}${c.bold}Update complete (
|
|
947
|
+
log(`\n ${c.green}${c.bold}Update complete (agents global + skills/rules/hooks local).${c.reset}\n`);
|
|
816
948
|
} else {
|
|
817
|
-
log(`\n ${c.green}${c.bold}Update complete (
|
|
949
|
+
log(`\n ${c.green}${c.bold}Update complete (agents only — no local orchestrator/ found).${c.reset}`);
|
|
950
|
+
log(` ${c.dim}Run from workspace root or orchestrator/ to also update local skills, rules, and hooks.${c.reset}\n`);
|
|
818
951
|
}
|
|
819
952
|
}
|
|
820
953
|
break;
|
|
@@ -843,7 +976,14 @@ switch (command) {
|
|
|
843
976
|
return;
|
|
844
977
|
}
|
|
845
978
|
|
|
846
|
-
//
|
|
979
|
+
// Agents (global)
|
|
980
|
+
for (const f of ["team-lead.md", "implementer.md", "workspace-init.md", "e2e-validator.md", "reviewer.md", "security-auditor.md"]) {
|
|
981
|
+
const fp = path.join(GLOBAL_AGENTS, f);
|
|
982
|
+
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
983
|
+
}
|
|
984
|
+
ok("Agents removed");
|
|
985
|
+
|
|
986
|
+
// Legacy global skills (from versions < 5.2)
|
|
847
987
|
if (fs.existsSync(GLOBAL_SKILLS)) {
|
|
848
988
|
const skipDirs = new Set(["rules", "agents", "hooks", "templates"]);
|
|
849
989
|
const skillDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
@@ -854,22 +994,15 @@ switch (command) {
|
|
|
854
994
|
const target = path.join(GLOBAL_SKILLS, name);
|
|
855
995
|
if (fs.existsSync(target)) { fs.rmSync(target, { recursive: true }); n++; }
|
|
856
996
|
}
|
|
857
|
-
ok(`${n} skills removed`);
|
|
997
|
+
if (n > 0) ok(`${n} legacy global skills removed`);
|
|
858
998
|
}
|
|
859
999
|
|
|
860
|
-
//
|
|
1000
|
+
// Legacy global rules (from versions < 5.2)
|
|
861
1001
|
for (const f of ["context-hygiene.md", "model-routing.md"]) {
|
|
862
1002
|
const fp = path.join(GLOBAL_RULES, f);
|
|
863
1003
|
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
864
1004
|
}
|
|
865
|
-
ok("
|
|
866
|
-
|
|
867
|
-
// Agents
|
|
868
|
-
for (const f of ["team-lead.md", "implementer.md", "workspace-init.md", "e2e-validator.md"]) {
|
|
869
|
-
const fp = path.join(GLOBAL_AGENTS, f);
|
|
870
|
-
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
871
|
-
}
|
|
872
|
-
ok("Agents removed");
|
|
1005
|
+
ok("Legacy global rules removed (if any)");
|
|
873
1006
|
|
|
874
1007
|
// Version file
|
|
875
1008
|
if (fs.existsSync(VERSION_FILE)) fs.unlinkSync(VERSION_FILE);
|
|
@@ -878,6 +1011,7 @@ switch (command) {
|
|
|
878
1011
|
log("");
|
|
879
1012
|
log(` ${c.green}${c.bold}Uninstall complete.${c.reset}`);
|
|
880
1013
|
log(` ${c.dim}Local orchestrator/ directories are preserved — remove them manually if needed.${c.reset}`);
|
|
1014
|
+
log(` ${c.dim}(Skills, rules, hooks, settings, plans live in orchestrator/.claude/)${c.reset}`);
|
|
881
1015
|
log("");
|
|
882
1016
|
rl.close();
|
|
883
1017
|
})();
|
|
@@ -902,14 +1036,16 @@ switch (command) {
|
|
|
902
1036
|
log(BANNER);
|
|
903
1037
|
log(` ${c.bold}Usage:${c.reset}`);
|
|
904
1038
|
log("");
|
|
905
|
-
log(` ${c.cyan}npx cc-workspace init${c.reset} ${c.dim}[path] ["Project Name"]${c.reset}`);
|
|
1039
|
+
log(` ${c.cyan}npx cc-workspace init${c.reset} ${c.dim}[path] ["Project Name"] [--dry-run]${c.reset}`);
|
|
906
1040
|
log(` Setup orchestrator/ in the target workspace.`);
|
|
907
1041
|
log(` Installs global skills/rules/agents if version is newer.`);
|
|
1042
|
+
log(` ${c.dim}--dry-run: preview what would be created without writing files${c.reset}`);
|
|
908
1043
|
log("");
|
|
909
|
-
log(` ${c.cyan}npx cc-workspace update${c.reset} ${c.dim}[--force]${c.reset}`);
|
|
1044
|
+
log(` ${c.cyan}npx cc-workspace update${c.reset} ${c.dim}[--force] [--dry-run]${c.reset}`);
|
|
910
1045
|
log(` Update global components (skills, rules, agents).`);
|
|
911
1046
|
log(` Also updates local orchestrator/ if found (hooks, settings, CLAUDE.md, templates).`);
|
|
912
1047
|
log(` Never overwrites: workspace.md, constitution.md, plans/.`);
|
|
1048
|
+
log(` ${c.dim}--dry-run: preview what would be updated without writing files${c.reset}`);
|
|
913
1049
|
log("");
|
|
914
1050
|
log(` ${c.cyan}npx cc-workspace doctor${c.reset}`);
|
|
915
1051
|
log(` Check all components are installed and consistent.`);
|
|
@@ -936,6 +1072,8 @@ switch (command) {
|
|
|
936
1072
|
log(` ${c.cyan}claude --agent workspace-init${c.reset} ${c.dim}# first time${c.reset}`);
|
|
937
1073
|
log(` ${c.dim} └─ type "go" to start the diagnostic${c.reset}`);
|
|
938
1074
|
log(` ${c.cyan}claude --agent team-lead${c.reset} ${c.dim}# work sessions${c.reset}`);
|
|
1075
|
+
log(` ${c.cyan}claude --agent reviewer${c.reset} ${c.dim}# code review${c.reset}`);
|
|
1076
|
+
log(` ${c.cyan}claude --agent security-auditor${c.reset} ${c.dim}# security audit${c.reset}`);
|
|
939
1077
|
log(` ${c.cyan}claude --agent e2e-validator${c.reset} ${c.dim}# E2E validation (beta)${c.reset}`);
|
|
940
1078
|
log("");
|
|
941
1079
|
break;
|
|
@@ -1033,9 +1171,16 @@ switch (command) {
|
|
|
1033
1171
|
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
1034
1172
|
|
|
1035
1173
|
(async () => {
|
|
1174
|
+
// Check gh CLI availability before PR step
|
|
1175
|
+
let ghAvailable = false;
|
|
1176
|
+
try { execSync("gh --version", { stdio: "pipe" }); ghAvailable = true; } catch {}
|
|
1177
|
+
|
|
1036
1178
|
// Step 1: offer to create PRs
|
|
1179
|
+
if (!ghAvailable) {
|
|
1180
|
+
info(`${c.dim}gh CLI not found — skipping PR creation (install: https://cli.github.com)${c.reset}`);
|
|
1181
|
+
}
|
|
1037
1182
|
for (const [name, repo] of Object.entries(session.repos || {})) {
|
|
1038
|
-
if (!repo.branch_created) continue;
|
|
1183
|
+
if (!repo.branch_created || !ghAvailable) continue;
|
|
1039
1184
|
const repoPath = path.resolve(orchDir, repo.path);
|
|
1040
1185
|
const answer = await ask(
|
|
1041
1186
|
` Create PR ${c.cyan}${repo.session_branch}${c.reset} → ${c.cyan}${repo.source_branch}${c.reset} in ${c.bold}${name}${c.reset}? [y/N] `
|
|
@@ -1053,9 +1198,44 @@ switch (command) {
|
|
|
1053
1198
|
}
|
|
1054
1199
|
}
|
|
1055
1200
|
|
|
1056
|
-
// Step 2: offer to
|
|
1201
|
+
// Step 2: offer to remove worktrees
|
|
1202
|
+
for (const [name, repo] of Object.entries(session.repos || {})) {
|
|
1203
|
+
const worktreePath = repo.worktree_path;
|
|
1204
|
+
if (!worktreePath || !fs.existsSync(worktreePath)) {
|
|
1205
|
+
info(`Worktree ${worktreePath || "unknown"} for ${name} — not found, skipping`);
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const repoPath = path.resolve(orchDir, repo.path);
|
|
1209
|
+
// Check for uncommitted changes
|
|
1210
|
+
let dirty = "";
|
|
1211
|
+
try {
|
|
1212
|
+
dirty = execSync(
|
|
1213
|
+
`git -C "${worktreePath}" status --short 2>/dev/null`,
|
|
1214
|
+
{ encoding: "utf8", timeout: 5000 }
|
|
1215
|
+
).trim();
|
|
1216
|
+
} catch { /* ignore */ }
|
|
1217
|
+
if (dirty) {
|
|
1218
|
+
warn(`${name}: worktree has uncommitted changes`);
|
|
1219
|
+
}
|
|
1220
|
+
const answer = await ask(
|
|
1221
|
+
` Remove worktree ${c.cyan}${worktreePath}${c.reset} for ${c.bold}${name}${c.reset}?${dirty ? ` ${c.red}(has uncommitted changes)${c.reset}` : ""} [y/N] `
|
|
1222
|
+
);
|
|
1223
|
+
if (answer.toLowerCase() === "y") {
|
|
1224
|
+
try {
|
|
1225
|
+
execSync(`git -C "${repoPath}" worktree remove "${worktreePath}" --force`,
|
|
1226
|
+
{ encoding: "utf8", timeout: 10000 });
|
|
1227
|
+
execSync(`git -C "${repoPath}" worktree prune`,
|
|
1228
|
+
{ encoding: "utf8", timeout: 5000 });
|
|
1229
|
+
ok(`Worktree removed for ${name}`);
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
fail(`Worktree removal failed for ${name}: ${e.stderr || e.message}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Step 3: offer to delete session branches
|
|
1057
1237
|
for (const [name, repo] of Object.entries(session.repos || {})) {
|
|
1058
|
-
if (!repo.
|
|
1238
|
+
if (!repo.session_branch) continue;
|
|
1059
1239
|
const repoPath = path.resolve(orchDir, repo.path);
|
|
1060
1240
|
// Check for unpushed commits before offering deletion
|
|
1061
1241
|
let unpushed = "";
|
|
@@ -1073,7 +1253,6 @@ switch (command) {
|
|
|
1073
1253
|
);
|
|
1074
1254
|
if (answer.toLowerCase() === "y") {
|
|
1075
1255
|
try {
|
|
1076
|
-
// Use -D (force) — user already confirmed, branch may not be merged yet
|
|
1077
1256
|
execSync(`git -C "${repoPath}" branch -D "${repo.session_branch}"`,
|
|
1078
1257
|
{ encoding: "utf8", timeout: 5000 });
|
|
1079
1258
|
ok(`Branch deleted in ${name}`);
|