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.
Files changed (35) hide show
  1. package/CHANGELOG.md +291 -0
  2. package/README.md +123 -41
  3. package/bin/cli.js +313 -134
  4. package/global-skills/agents/e2e-validator.md +151 -32
  5. package/global-skills/agents/implementer.md +77 -71
  6. package/global-skills/agents/reviewer.md +192 -0
  7. package/global-skills/agents/security-auditor.md +345 -0
  8. package/global-skills/agents/team-lead.md +93 -101
  9. package/global-skills/agents/workspace-init.md +16 -5
  10. package/global-skills/bootstrap-repo/SKILL.md +1 -0
  11. package/global-skills/cleanup/SKILL.md +35 -25
  12. package/global-skills/cross-service-check/SKILL.md +1 -0
  13. package/global-skills/cycle-retrospective/SKILL.md +6 -4
  14. package/global-skills/dispatch-feature/SKILL.md +225 -173
  15. package/global-skills/dispatch-feature/references/anti-patterns.md +52 -35
  16. package/global-skills/dispatch-feature/references/spawn-templates.md +140 -97
  17. package/global-skills/doctor/SKILL.md +124 -25
  18. package/global-skills/e2e-validator/references/container-strategies.md +55 -23
  19. package/global-skills/hooks/orphan-cleanup.sh +60 -0
  20. package/global-skills/hooks/permission-auto-approve.sh +61 -4
  21. package/global-skills/hooks/session-start-context.sh +10 -47
  22. package/global-skills/hooks/test_hooks.sh +242 -0
  23. package/global-skills/hooks/user-prompt-guard.sh +6 -6
  24. package/global-skills/hooks/validate-spawn-prompt.sh +40 -30
  25. package/global-skills/incident-debug/SKILL.md +1 -0
  26. package/global-skills/merge-prep/SKILL.md +1 -0
  27. package/global-skills/metrics/SKILL.md +139 -0
  28. package/global-skills/plan-review/SKILL.md +2 -1
  29. package/global-skills/qa-ruthless/SKILL.md +2 -0
  30. package/global-skills/refresh-profiles/SKILL.md +1 -0
  31. package/global-skills/rules/context-hygiene.md +4 -19
  32. package/global-skills/rules/model-routing.md +31 -18
  33. package/global-skills/session/SKILL.md +41 -20
  34. package/global-skills/templates/workspace.template.md +1 -1
  35. 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
- function mkdirp(dir) { fs.mkdirSync(dir, { recursive: true }); }
62
+ let DRY_RUN = false;
62
63
 
63
- function copyFile(src, dest) { fs.copyFileSync(src, dest); }
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
- mkdirp(dest);
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) : copyFile(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 pkgHas = (kw) => {
106
- try { return fs.readFileSync(path.join(dir, "package.json"), "utf8").includes(kw); }
107
- catch { return false; }
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
- 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";
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
- // 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
+ // 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
- fs.writeFileSync(path.join(orchDir, ".claude", "settings.json"), JSON.stringify(settings, null, 2) + "\n");
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 — you can write in orchestrator/.
272
- You clarify, plan, delegate, track.
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
- - \`disallowedTools: Bash\` no direct shell
276
- - \`tools\` : Read, Write, Edit, Glob, Grep, Task(implementer, Explore), Teammate, SendMessage
277
- - Hook \`PreToolUse\` path-aware: allows orchestrator/, blocks sibling repos
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 in waves → QA |
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 → wavescollect → verify
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 (haiku)
368
+ - **plan-review**: plan sanity check (sonnet — constitution compliance)
318
369
  - **merge-prep**: pre-merge, conflicts, PR summaries
319
- - **cycle-retrospective**: post-cycle learning (haiku)
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. Clarify ambiguities BEFORE planning (except mode C)
331
- 4. All plans in markdown in \`./plans/\`
332
- 5. Dispatch via Agent Teams (Teammate tool) in waves
333
- 6. Full constitution (all rules from constitution.md) in every spawn prompt
334
- 7. UX standards injected for frontend teammates
335
- 8. Each teammate detects dead code
336
- 9. Escalate arch decisions not covered by the plan
337
- 10. Ruthless QA UX violations = blocking
338
- 11. Compact after each cycle
339
- 12. Hooks are warning-only never blocking
340
- 13. Retrospective cycle after each completed feature
341
- 14. Session branches for parallel isolation teammates use session/{name}, never create own branches
342
- 15. Never \`git checkout -b\` in reposuse \`git branch\` (no checkout) to avoid disrupting parallel sessions
343
- 16. E2E validation via \`claude --agent e2e-validator\` after plans are complete
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 QAUX 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)) fs.unlinkSync(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
- fs.writeFileSync(claudeMd, claudeMdContent());
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
- fs.writeFileSync(planTpl, planTemplateContent());
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 fs.writeFileSync(wsMd, `# Workspace: ${projectName}\n\n## Projet\n[UNCONFIGURED]\n`);
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 fs.writeFileSync(constMd, [
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
- fs.writeFileSync(claudeMd, claudeMdContent());
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
- fs.writeFileSync(planTpl, planTemplateContent());
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
- fs.writeFileSync(gi, [
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
- fs.writeFileSync(path.join(orchDir, "plans", "service-profiles.md"), profileLines.join("\n"));
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} 13 ${c.dim}(~/.claude/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 dirs
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
- if (inOrch) {
732
- step("Local workspace (inside orchestrator/)");
733
- check("workspace.md", true, "");
734
- check("constitution.md", fs.existsSync(path.join(cwd, "constitution.md")), "missing");
735
- check("plans/", fs.existsSync(path.join(cwd, "plans")), "missing");
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
- const workspace = args[1] || ".";
799
- const name = args[2] || "My Project";
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 (globals + local orchestrator/).${c.reset}\n`);
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 (globals only — no local orchestrator/ found).${c.reset}\n`);
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
- // Skills
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
- // Rules
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("Rules removed");
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 delete session branches
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.branch_created) continue;
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}`);