refacil-sdd-ai 4.5.7 → 5.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/README.md CHANGED
@@ -29,6 +29,8 @@ refacil-sdd-ai init
29
29
  # whose folder already exists. Use --all to install for all three without prompting.
30
30
  # Copies skills and sub-agents to the selected IDEs, configures hooks,
31
31
  # and creates/updates .claudeignore, .cursorignore and .opencodeignore.
32
+ # Also prompts for global branch config (baseBranch, protectedBranches) pre-filled
33
+ # from ~/.refacil-sdd-ai/config.yaml. Skipped with --yes or --defaults.
32
34
 
33
35
  # 3. Restart your IDE session
34
36
  # (new skills are not detected until you restart)
@@ -100,6 +102,8 @@ Native CLI for **`refacil-sdd/`** (no separate OpenSpec skill layer). Used by sk
100
102
  | `refacil-sdd-ai sdd tasks-update <name>` | Mark a task done (`--task N --done`) |
101
103
  | `refacil-sdd-ai sdd archive <name>` | Move a regular change to `refacil-sdd/changes/archive/` |
102
104
  | `refacil-sdd-ai sdd validate-name <name>` | Validate change folder name (must start with a letter) |
105
+ | `refacil-sdd-ai sdd config [--json]` | Show effective branch configuration (protectedBranches, baseBranch) after cascade: project `refacil-sdd/config.yaml` → global `~/.refacil-sdd-ai/config.yaml` → built-in defaults |
106
+ | `refacil-sdd-ai sdd write-config [--global] [--base-branch <v>] [--protected-branches <csv>]` | Write or merge branch config into `refacil-sdd/config.yaml` (project) or `~/.refacil-sdd-ai/config.yaml` (`--global`). Performs a semantic no-op check — skips rewrite if values are already set. Directory is auto-created if absent. |
103
107
 
104
108
  Run **`refacil-sdd-ai help`** for the full list including `bus` and `compact` subcommands.
105
109
 
@@ -372,7 +376,7 @@ The SDD-AI methodology generates a lot of context (artifacts, specs, prompts). T
372
376
  Defined in `skills/prereqs/METHODOLOGY-CONTRACT.md`:
373
377
 
374
378
  - **Flow states**: `READY_FOR_APPLY` / `VERIFY` / `REVIEW` / `ARCHIVE` / `MERGE` — each transition validates prerequisites.
375
- - **Branch policy**: every new branch (`feature/*`, `fix/*`, etc.) is created from an up-to-date `develop`/`dev`. Integration to protected branches (`testing`, `develop`, `dev`, `main`, `master`, `qa`) always via PR. **Never** direct commits to `master`/`main`. Single exception: repos without `develop`/`dev`, where `main` or `master` may be used temporarily as a base.
379
+ - **Branch policy**: every new branch (`feature/*`, `fix/*`, etc.) is created from the `baseBranch` returned by `refacil-sdd-ai sdd config --json`. Integration to protected branches (as listed by `sdd config --json`) always via PR **never** direct commits to a protected branch. Branch rules are resolved via a two-level cascade: project (`refacil-sdd/config.yaml`) → global (`~/.refacil-sdd-ai/config.yaml`) → built-in defaults (`master`, `main`, `develop`, `dev`, `testing`, `qa`). Use `sdd write-config` to set project- or team-level overrides. The global config at `~/.refacil-sdd-ai/config.yaml` is preserved across package updates and can be used to set team-wide defaults without per-repo configuration.
376
380
  - **Multi-stack tests**: detects the real test command (does not hardcode `npm test`).
377
381
  - **`AGENTS.md` by profile** (`sdd` vs `agents`): the methodology respects both.
378
382
  - **Output mode**: concise by default, detailed on demand.
package/bin/cli.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const os = require('os');
7
8
  const {
8
9
  syncCompactGuidance,
9
10
  removeCompactGuidance,
@@ -27,7 +28,7 @@ const {
27
28
  const { installHooks, uninstallHooks, cleanLegacySettingsHooks, installOpenCodePlugin, uninstallOpenCodePlugin } = require('../lib/hooks');
28
29
  const { handleCompact } = require('../lib/commands/compact');
29
30
  const { handleBus } = require('../lib/commands/bus');
30
- const { handleSdd, autoMigrateOpenspec, findProjectRoot } = require('../lib/commands/sdd');
31
+ const { handleSdd, autoMigrateOpenspec, findProjectRoot, cmdWriteConfig } = require('../lib/commands/sdd');
31
32
  const { syncIgnoreFiles } = require('../lib/ignore-files');
32
33
  const { methodologyMigrationPending } = require('../lib/methodology-migration-pending');
33
34
 
@@ -396,6 +397,114 @@ function checkReview() {
396
397
  }
397
398
  }
398
399
 
400
+ // --- Branch config prompt (used by init) ---
401
+
402
+ /**
403
+ * Prompt the user for global branch configuration interactively.
404
+ * Skips if --yes or --defaults flag is present, or if stdout is not a TTY.
405
+ * Pre-fills from existing global config values.
406
+ * On confirmation, writes the global config via cmdWriteConfig.
407
+ */
408
+ /** Normalize a comma-separated branch string into a trimmed, non-empty array. Falls back to `fallback` if empty. */
409
+ function parseBranchList(raw, fallback) {
410
+ const parsed = raw.split(',').map((s) => s.trim()).filter(Boolean);
411
+ return parsed.length > 0 ? parsed : fallback;
412
+ }
413
+
414
+ async function promptBranchConfig() {
415
+ const skipFlags = ['--yes', '--defaults'];
416
+ if (skipFlags.some((f) => process.argv.includes(f))) return;
417
+ if (!process.stdout.isTTY) return;
418
+
419
+ const { readConfigFile, DEFAULT_PROTECTED_BRANCHES, DEFAULT_BASE_BRANCH } = require('../lib/config');
420
+ const globalConfigPath = path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml');
421
+ const globalConfig = readConfigFile(globalConfigPath) || {};
422
+ const currentBaseBranch = (typeof globalConfig.baseBranch === 'string' && globalConfig.baseBranch.trim()) ? globalConfig.baseBranch.trim() : DEFAULT_BASE_BRANCH;
423
+ const currentProtected = (Array.isArray(globalConfig.protectedBranches) && globalConfig.protectedBranches.length > 0) ? globalConfig.protectedBranches : DEFAULT_PROTECTED_BRANCHES;
424
+
425
+ console.log('\n Branch configuration (global, stored in ~/.refacil-sdd-ai/config.yaml)');
426
+ console.log(` Current base branch: ${currentBaseBranch}`);
427
+ console.log(` Current protected branches: ${currentProtected.join(', ')}`);
428
+ console.log(' Press Enter to keep current values, or type new ones.\n');
429
+
430
+ let baseBranch;
431
+ let protectedBranches;
432
+
433
+ try {
434
+ const clack = require('@clack/prompts');
435
+
436
+ const bbResult = await clack.text({
437
+ message: `Base branch (current: ${currentBaseBranch}):`,
438
+ placeholder: currentBaseBranch,
439
+ validate: () => undefined,
440
+ });
441
+ if (clack.isCancel(bbResult)) {
442
+ console.log(' Branch config prompt cancelled. Keeping existing values.\n');
443
+ return;
444
+ }
445
+ baseBranch = (bbResult && bbResult.trim()) ? bbResult.trim() : currentBaseBranch;
446
+
447
+ const pbResult = await clack.text({
448
+ message: `Protected branches, comma-separated (current: ${currentProtected.join(', ')}):`,
449
+ placeholder: currentProtected.join(', '),
450
+ validate: () => undefined,
451
+ });
452
+ if (clack.isCancel(pbResult)) {
453
+ console.log(' Branch config prompt cancelled. Keeping existing values.\n');
454
+ return;
455
+ }
456
+ protectedBranches = parseBranchList((pbResult && pbResult.trim()) ? pbResult.trim() : currentProtected.join(', '), currentProtected);
457
+
458
+ const confirm = await clack.confirm({
459
+ message: `Save global config — base: "${baseBranch}", protected: [${protectedBranches.join(', ')}]?`,
460
+ initialValue: true,
461
+ });
462
+ if (clack.isCancel(confirm) || !confirm) {
463
+ console.log(' Branch config not saved.\n');
464
+ return;
465
+ }
466
+ } catch (_) {
467
+ // @clack/prompts not available — use inline readline fallback
468
+ const readline = require('readline');
469
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
470
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
471
+
472
+ const bbAnswer = await ask(` Base branch [${currentBaseBranch}]: `);
473
+ baseBranch = (bbAnswer && bbAnswer.trim()) ? bbAnswer.trim() : currentBaseBranch;
474
+
475
+ const pbAnswer = await ask(` Protected branches [${currentProtected.join(', ')}]: `);
476
+ protectedBranches = parseBranchList((pbAnswer && pbAnswer.trim()) ? pbAnswer.trim() : currentProtected.join(', '), currentProtected);
477
+
478
+ const confirmAnswer = await ask(` Save global config — base: "${baseBranch}", protected: [${protectedBranches.join(', ')}]? (Y/n): `);
479
+ rl.close();
480
+ if (confirmAnswer.trim().toLowerCase() === 'n') {
481
+ console.log(' Branch config not saved.\n');
482
+ return;
483
+ }
484
+ }
485
+
486
+ // Fix 2: pre-check to avoid process.exit(0) from cmdWriteConfig no-op path when called programmatically
487
+ const valuesUnchanged = baseBranch === currentBaseBranch &&
488
+ JSON.stringify(protectedBranches.slice().sort()) === JSON.stringify(currentProtected.slice().sort());
489
+ if (valuesUnchanged) {
490
+ console.log(' Branch config unchanged. Keeping existing values.\n');
491
+ return;
492
+ }
493
+
494
+ // Build argv-style array and call cmdWriteConfig directly
495
+ const writeArgv = [
496
+ '--global',
497
+ '--base-branch', baseBranch,
498
+ '--protected-branches', protectedBranches.join(','),
499
+ ];
500
+ try {
501
+ cmdWriteConfig(writeArgv, projectRoot);
502
+ } catch (err) {
503
+ console.error(` Warning: could not write global branch config: ${err.message}`);
504
+ }
505
+ console.log('');
506
+ }
507
+
399
508
  // --- High-level commands ---
400
509
 
401
510
  async function init() {
@@ -417,6 +526,9 @@ async function init() {
417
526
  // Select target IDEs (interactive selector or --all / non-TTY)
418
527
  const selectedIDEs = await selectIDEs();
419
528
 
529
+ // Prompt for global branch configuration (skipped with --yes/--defaults or non-TTY)
530
+ await promptBranchConfig();
531
+
420
532
  if (selectedIDEs.length === 0) {
421
533
  console.log('\n No IDEs selected. Nothing installed.\n');
422
534
  console.log(' Re-run with: refacil-sdd-ai init --all to install for all IDEs');
@@ -644,6 +756,7 @@ function help() {
644
756
  Commands:
645
757
  init Install skills in .claude/, .cursor/ and/or .opencode/ (interactive IDE selector).
646
758
  Use --all to install for all three IDEs without prompting.
759
+ Use --yes or --defaults to skip interactive branch config prompts.
647
760
  Creates CLAUDE.md, .cursorrules and .opencode/opencode.json as appropriate.
648
761
  update Re-copy skills for all detected IDEs (.claude/, .cursor/, .opencode/)
649
762
  migration-pending Same validation as hooks/notify-update: list migrations (exit 1 if any; --json)
@@ -680,6 +793,11 @@ function help() {
680
793
  sdd mark-reviewed <name> Write .review-passed (requires --verdict and --summary)
681
794
  sdd tasks-update <name> Mark task N as completed (--task N --done)
682
795
  sdd validate-name <name> Validate change name format
796
+ sdd config [--json] Show effective branch config (project > global > defaults)
797
+ sdd write-config Write branch config to project or global config file
798
+ [--global] Write to ~/.refacil-sdd-ai/config.yaml (global level)
799
+ [--base-branch <branch>] Base branch for new changes
800
+ [--protected-branches <csv>] Protected branches (comma-separated)
683
801
  clean Remove skills and SDD-AI hooks from all detected IDEs
684
802
  (.claude/settings.json, .cursor/hooks.json, .opencode/plugins/)
685
803
  help Show this help
@@ -2,6 +2,8 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const os = require('os');
6
+ const { loadBranchConfigWithSources, parseYaml, readConfigFile } = require('../config');
5
7
 
6
8
  function findProjectRoot() {
7
9
  let dir = process.cwd();
@@ -62,6 +64,47 @@ function validateChangeName(name) {
62
64
  return { valid: true };
63
65
  }
64
66
 
67
+ function resolveExistingChangeName(projectRoot, inputName) {
68
+ if (!inputName || typeof inputName !== 'string') {
69
+ return { ok: false, reason: 'El nombre del cambio no puede estar vacío.' };
70
+ }
71
+
72
+ const normalizedInput = inputName.trim();
73
+ const lowerInput = normalizedInput.toLowerCase();
74
+ const changesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
75
+
76
+ // Keep backward-compatible behavior when directory doesn't exist yet.
77
+ if (!fs.existsSync(changesDir)) {
78
+ return { ok: true, name: lowerInput };
79
+ }
80
+
81
+ const entries = fs.readdirSync(changesDir, { withFileTypes: true })
82
+ .filter((e) => e.isDirectory() && e.name !== 'archive')
83
+ .map((e) => e.name);
84
+
85
+ if (entries.includes(normalizedInput)) {
86
+ return { ok: true, name: normalizedInput };
87
+ }
88
+
89
+ if (entries.includes(lowerInput)) {
90
+ return { ok: true, name: lowerInput };
91
+ }
92
+
93
+ const ciMatches = entries.filter((n) => n.toLowerCase() === lowerInput);
94
+ if (ciMatches.length === 1) {
95
+ return { ok: true, name: ciMatches[0] };
96
+ }
97
+
98
+ if (ciMatches.length > 1) {
99
+ return {
100
+ ok: false,
101
+ reason: `Nombre de cambio ambiguo: '${inputName}'. Coincidencias: ${ciMatches.join(', ')}`,
102
+ };
103
+ }
104
+
105
+ return { ok: true, name: lowerInput };
106
+ }
107
+
65
108
  function autoMigrateOpenspec(root) {
66
109
  const oldDir = path.join(root, 'openspec');
67
110
  const newDir = path.join(root, 'refacil-sdd');
@@ -77,49 +120,7 @@ function autoMigrateOpenspec(root) {
77
120
  // Si ambos existen o ninguno existe → no hacer nada
78
121
  }
79
122
 
80
- // --- Minimal YAML parser/serializer for memory.yaml ---
81
- // Supports: string values and string-array values only.
82
-
83
- function parseMemoryYaml(content) {
84
- const result = {};
85
- const lines = content.split('\n');
86
- let currentKey = null;
87
- let currentList = null;
88
-
89
- for (const line of lines) {
90
- if (!line.trim() || line.trim().startsWith('#')) continue;
91
-
92
- // List item: " - value"
93
- if (/^\s{2,}- /.test(line)) {
94
- const value = line.replace(/^\s*- /, '').trim();
95
- if (currentKey && currentList !== null) {
96
- currentList.push(value);
97
- }
98
- continue;
99
- }
100
-
101
- // Key-value: "key: value" or "key:" (empty/start of list)
102
- const kvMatch = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)/);
103
- if (kvMatch) {
104
- currentKey = kvMatch[1];
105
- const val = kvMatch[2].trim();
106
- if (val === '') {
107
- // Could be a list
108
- currentList = [];
109
- result[currentKey] = currentList;
110
- } else {
111
- currentList = null;
112
- result[currentKey] = val;
113
- }
114
- continue;
115
- }
116
-
117
- currentKey = null;
118
- currentList = null;
119
- }
120
-
121
- return result;
122
- }
123
+ // parseYaml is imported from lib/config.js (shared parser, no duplication)
123
124
 
124
125
  function serializeMemoryYaml(obj) {
125
126
  const lines = [];
@@ -180,7 +181,15 @@ function cmdNewChange(argv, projectRoot) {
180
181
 
181
182
  function cmdArchive(argv, projectRoot) {
182
183
  const args = parseArgs(argv);
183
- const name = args._positional[0];
184
+ const rawName = args._positional[0];
185
+
186
+ autoMigrateOpenspec(projectRoot);
187
+ const resolved = resolveExistingChangeName(projectRoot, rawName);
188
+ if (!resolved.ok) {
189
+ console.error(resolved.reason);
190
+ process.exit(1);
191
+ }
192
+ const name = resolved.name;
184
193
 
185
194
  const validation = validateChangeName(name);
186
195
  if (!validation.valid) {
@@ -188,8 +197,6 @@ function cmdArchive(argv, projectRoot) {
188
197
  process.exit(1);
189
198
  }
190
199
 
191
- autoMigrateOpenspec(projectRoot);
192
-
193
200
  const sourceDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
194
201
  if (!fs.existsSync(sourceDir)) {
195
202
  console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/`);
@@ -222,14 +229,21 @@ function cmdArchive(argv, projectRoot) {
222
229
 
223
230
  function cmdSetMemory(argv, projectRoot) {
224
231
  const args = parseArgs(argv);
225
- const name = args._positional[0];
232
+ const rawName = args._positional[0];
226
233
 
227
- if (!name) {
234
+ if (!rawName) {
228
235
  console.error('Uso: refacil-sdd-ai sdd set-memory <nombre-cambio> [--last-step <value>] [--stack-detected <value>] [--touched-files <csv>] [--commands-run <value>] [--criteria-run <csv>]');
229
236
  process.exit(1);
230
237
  }
231
238
 
232
239
  const root = projectRoot;
240
+ autoMigrateOpenspec(root);
241
+ const resolved = resolveExistingChangeName(root, rawName);
242
+ if (!resolved.ok) {
243
+ console.error(resolved.reason);
244
+ process.exit(1);
245
+ }
246
+ const name = resolved.name;
233
247
 
234
248
  // Guard: ensure the change directory exists before any file operation
235
249
  const changeDir = path.join(root, 'refacil-sdd', 'changes', name);
@@ -251,7 +265,7 @@ function cmdSetMemory(argv, projectRoot) {
251
265
  let existing = {};
252
266
  if (fs.existsSync(memoryPath)) {
253
267
  try {
254
- existing = parseMemoryYaml(fs.readFileSync(memoryPath, 'utf8'));
268
+ existing = parseYaml(fs.readFileSync(memoryPath, 'utf8'));
255
269
  } catch (_) {
256
270
  existing = {};
257
271
  }
@@ -274,15 +288,22 @@ function cmdSetMemory(argv, projectRoot) {
274
288
 
275
289
  function cmdGetMemory(argv, projectRoot) {
276
290
  const args = parseArgs(argv);
277
- const name = args._positional[0];
291
+ const rawName = args._positional[0];
278
292
  const wantJson = args.json === true;
279
293
 
280
- if (!name) {
294
+ if (!rawName) {
281
295
  console.error('Uso: refacil-sdd-ai sdd get-memory <nombre-cambio> [--json]');
282
296
  process.exit(1);
283
297
  }
284
298
 
285
299
  const root = projectRoot;
300
+ autoMigrateOpenspec(root);
301
+ const resolved = resolveExistingChangeName(root, rawName);
302
+ if (!resolved.ok) {
303
+ console.error(resolved.reason);
304
+ process.exit(1);
305
+ }
306
+ const name = resolved.name;
286
307
  const memoryPath = path.join(root, 'refacil-sdd', 'changes', name, 'memory.yaml');
287
308
 
288
309
  if (!fs.existsSync(memoryPath)) {
@@ -297,7 +318,7 @@ function cmdGetMemory(argv, projectRoot) {
297
318
  if (wantJson) {
298
319
  let parsed = {};
299
320
  try {
300
- parsed = parseMemoryYaml(content);
321
+ parsed = parseYaml(content);
301
322
  } catch (_) {
302
323
  parsed = {};
303
324
  }
@@ -309,14 +330,21 @@ function cmdGetMemory(argv, projectRoot) {
309
330
 
310
331
  function cmdSetReviewFails(argv, projectRoot) {
311
332
  const args = parseArgs(argv);
312
- const name = args._positional[0];
333
+ const rawName = args._positional[0];
313
334
 
314
- if (!name) {
335
+ if (!rawName) {
315
336
  console.error('Uso: refacil-sdd-ai sdd set-review-fails <nombre-cambio> --files <csv>');
316
337
  process.exit(1);
317
338
  }
318
339
 
319
340
  const root = projectRoot;
341
+ autoMigrateOpenspec(root);
342
+ const resolved = resolveExistingChangeName(root, rawName);
343
+ if (!resolved.ok) {
344
+ console.error(resolved.reason);
345
+ process.exit(1);
346
+ }
347
+ const name = resolved.name;
320
348
  const changeDir = path.join(root, 'refacil-sdd', 'changes', name);
321
349
  if (!fs.existsSync(changeDir)) {
322
350
  console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/`);
@@ -334,14 +362,21 @@ function cmdSetReviewFails(argv, projectRoot) {
334
362
 
335
363
  function cmdClearReviewFails(argv, projectRoot) {
336
364
  const args = parseArgs(argv);
337
- const name = args._positional[0];
365
+ const rawName = args._positional[0];
338
366
 
339
- if (!name) {
367
+ if (!rawName) {
340
368
  console.error('Uso: refacil-sdd-ai sdd clear-review-fails <nombre-cambio>');
341
369
  process.exit(1);
342
370
  }
343
371
 
344
372
  const root = projectRoot;
373
+ autoMigrateOpenspec(root);
374
+ const resolved = resolveExistingChangeName(root, rawName);
375
+ if (!resolved.ok) {
376
+ console.error(resolved.reason);
377
+ process.exit(1);
378
+ }
379
+ const name = resolved.name;
345
380
  const reviewFailsPath = path.join(root, 'refacil-sdd', 'changes', name, '.review-last-fails.json');
346
381
 
347
382
  if (fs.existsSync(reviewFailsPath)) {
@@ -392,15 +427,21 @@ function cmdList(argv, projectRoot) {
392
427
 
393
428
  function cmdStatus(argv, projectRoot) {
394
429
  const args = parseArgs(argv);
395
- const name = args._positional[0];
430
+ const rawName = args._positional[0];
396
431
  const wantJson = args.json === true;
397
432
 
398
- if (!name) {
433
+ if (!rawName) {
399
434
  console.error('Uso: refacil-sdd-ai sdd status <nombre-cambio> [--json]');
400
435
  process.exit(1);
401
436
  }
402
437
 
403
438
  autoMigrateOpenspec(projectRoot);
439
+ const resolved = resolveExistingChangeName(projectRoot, rawName);
440
+ if (!resolved.ok) {
441
+ console.error(resolved.reason);
442
+ process.exit(1);
443
+ }
444
+ const name = resolved.name;
404
445
 
405
446
  const changeDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
406
447
  if (!fs.existsSync(changeDir)) {
@@ -481,9 +522,9 @@ function cmdStatus(argv, projectRoot) {
481
522
 
482
523
  function cmdMarkReviewed(argv, projectRoot) {
483
524
  const args = parseArgs(argv);
484
- const name = args._positional[0];
525
+ const rawName = args._positional[0];
485
526
 
486
- if (!name) {
527
+ if (!rawName) {
487
528
  console.error('Uso: refacil-sdd-ai sdd mark-reviewed <nombre-cambio> --verdict <verdict> --summary "<resumen>" [--fail-count N] [--preexisting-count N] [--blockers]');
488
529
  process.exit(1);
489
530
  }
@@ -499,6 +540,12 @@ function cmdMarkReviewed(argv, projectRoot) {
499
540
  }
500
541
 
501
542
  autoMigrateOpenspec(projectRoot);
543
+ const resolved = resolveExistingChangeName(projectRoot, rawName);
544
+ if (!resolved.ok) {
545
+ console.error(resolved.reason);
546
+ process.exit(1);
547
+ }
548
+ const name = resolved.name;
502
549
 
503
550
  const changeDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
504
551
  if (!fs.existsSync(changeDir)) {
@@ -522,9 +569,9 @@ function cmdMarkReviewed(argv, projectRoot) {
522
569
 
523
570
  function cmdTasksUpdate(argv, projectRoot) {
524
571
  const args = parseArgs(argv);
525
- const name = args._positional[0];
572
+ const rawName = args._positional[0];
526
573
 
527
- if (!name) {
574
+ if (!rawName) {
528
575
  console.error('Uso: refacil-sdd-ai sdd tasks-update <nombre-cambio> --task N --done');
529
576
  process.exit(1);
530
577
  }
@@ -541,6 +588,12 @@ function cmdTasksUpdate(argv, projectRoot) {
541
588
  }
542
589
 
543
590
  autoMigrateOpenspec(projectRoot);
591
+ const resolved = resolveExistingChangeName(projectRoot, rawName);
592
+ if (!resolved.ok) {
593
+ console.error(resolved.reason);
594
+ process.exit(1);
595
+ }
596
+ const name = resolved.name;
544
597
 
545
598
  const tasksFile = path.join(projectRoot, 'refacil-sdd', 'changes', name, 'tasks.md');
546
599
  if (!fs.existsSync(tasksFile)) {
@@ -577,6 +630,88 @@ function cmdTasksUpdate(argv, projectRoot) {
577
630
  console.log(`Task ${taskN} de '${name}' marcada como completada.`);
578
631
  }
579
632
 
633
+ function cmdConfig(argv, projectRoot) {
634
+ const args = parseArgs(argv);
635
+ const wantJson = args.json === true;
636
+
637
+ const { protectedBranches, baseBranch, sources } = loadBranchConfigWithSources(projectRoot);
638
+
639
+ if (wantJson) {
640
+ process.stdout.write(JSON.stringify({ protectedBranches, baseBranch }) + '\n');
641
+ } else {
642
+ console.log(`protectedBranches [${sources.protectedBranches}]: ${protectedBranches.join(', ')}`);
643
+ console.log(`baseBranch [${sources.baseBranch}]: ${baseBranch}`);
644
+ }
645
+ }
646
+
647
+
648
+ function cmdWriteConfig(argv, projectRoot) {
649
+ const args = parseArgs(argv);
650
+
651
+ const isGlobal = args.global === true;
652
+ const rawBaseBranch = args['base-branch'];
653
+ const rawProtectedBranches = args['protected-branches'];
654
+
655
+ // CR-03: no flags provided
656
+ if (rawBaseBranch === undefined && rawProtectedBranches === undefined) {
657
+ console.error('Uso: refacil-sdd-ai sdd write-config [--global] [--base-branch <branch>] [--protected-branches <csv>]');
658
+ console.error('Debe especificar al menos --base-branch o --protected-branches.');
659
+ process.exit(1);
660
+ }
661
+
662
+ // CR-01: empty --base-branch after trim
663
+ if (rawBaseBranch !== undefined && (typeof rawBaseBranch !== 'string' || rawBaseBranch.trim() === '')) {
664
+ console.error('Error: --base-branch no puede estar vacío.');
665
+ process.exit(1);
666
+ }
667
+
668
+ // CR-02: --protected-branches: split, trim, filter; error if empty result
669
+ let protectedBranchesList;
670
+ if (rawProtectedBranches !== undefined) {
671
+ protectedBranchesList = String(rawProtectedBranches).split(',').map((s) => s.trim()).filter(Boolean);
672
+ if (protectedBranchesList.length === 0) {
673
+ console.error('Error: --protected-branches no puede resultar en una lista vacía.');
674
+ process.exit(1);
675
+ }
676
+ }
677
+
678
+ const targetPath = isGlobal
679
+ ? path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml')
680
+ : path.join(projectRoot, 'refacil-sdd', 'config.yaml');
681
+
682
+ // CR-04: read existing file; null if absent or corrupt
683
+ const existing = readConfigFile(targetPath) || {};
684
+
685
+ // Merge: start from existing, overwrite only provided keys
686
+ const merged = Object.assign({}, existing);
687
+ if (rawBaseBranch !== undefined) {
688
+ merged.baseBranch = rawBaseBranch.trim();
689
+ }
690
+ if (protectedBranchesList !== undefined) {
691
+ merged.protectedBranches = protectedBranchesList;
692
+ }
693
+
694
+ // CA-03: no-op when all provided keys already match existing config (semantic comparison)
695
+ const isNoOp = Object.keys(existing).length > 0 &&
696
+ (rawBaseBranch === undefined || existing.baseBranch === rawBaseBranch.trim()) &&
697
+ (protectedBranchesList === undefined ||
698
+ (Array.isArray(existing.protectedBranches) &&
699
+ JSON.stringify(existing.protectedBranches.slice().sort()) === JSON.stringify(protectedBranchesList.slice().sort())));
700
+ if (isNoOp) {
701
+ console.log(`Sin cambios: ${targetPath} ya tiene los valores indicados.`);
702
+ process.exit(0);
703
+ }
704
+ const proposed = serializeMemoryYaml(merged);
705
+
706
+ // Create directory if absent
707
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
708
+
709
+ fs.writeFileSync(targetPath, proposed, 'utf8');
710
+
711
+ const level = isGlobal ? 'global' : 'proyecto';
712
+ console.log(`Configuración de ramas escrita en ${targetPath} (nivel: ${level})`);
713
+ }
714
+
580
715
  function sddHelp() {
581
716
  console.log(`
582
717
  refacil-sdd-ai sdd — Gestión de artefactos SDD-AI
@@ -607,6 +742,14 @@ function sddHelp() {
607
742
  sdd set-review-fails <nombre> Escribe .review-last-fails.json con archivos fallidos
608
743
  --files <csv> Archivos con fallos (separados por coma)
609
744
  sdd clear-review-fails <nombre> Elimina .review-last-fails.json del cambio
745
+ sdd config [--json] Muestra la configuración efectiva de ramas
746
+ (project > global > defaults)
747
+ [--json] Salida en JSON (útil para agentes)
748
+ sdd write-config Escribe la configuración de ramas en el archivo de config
749
+ [--global] Escribe en ~/.refacil-sdd-ai/config.yaml (global)
750
+ Sin --global: escribe en refacil-sdd/config.yaml (proyecto)
751
+ [--base-branch <branch>] Rama base para nuevos cambios
752
+ [--protected-branches <csv>] Ramas protegidas (separadas por coma)
610
753
 
611
754
  Notas:
612
755
  - Los nombres de cambio deben empezar con minúscula y usar solo [a-z0-9-]
@@ -655,10 +798,16 @@ function handleSdd(sub, argv, projectRoot) {
655
798
  case 'clear-review-fails':
656
799
  cmdClearReviewFails(args, root);
657
800
  break;
801
+ case 'config':
802
+ cmdConfig(args, root);
803
+ break;
804
+ case 'write-config':
805
+ cmdWriteConfig(args, root);
806
+ break;
658
807
  default:
659
808
  sddHelp();
660
809
  process.exit(1);
661
810
  }
662
811
  }
663
812
 
664
- module.exports = { handleSdd, parseArgs, autoMigrateOpenspec, validateChangeName, findProjectRoot };
813
+ module.exports = { handleSdd, parseArgs, autoMigrateOpenspec, validateChangeName, resolveExistingChangeName, findProjectRoot, cmdWriteConfig };
package/lib/config.js ADDED
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const DEFAULT_PROTECTED_BRANCHES = ['master', 'main', 'develop', 'dev', 'testing', 'qa'];
8
+ const DEFAULT_BASE_BRANCH = 'develop';
9
+
10
+ // Minimal YAML parser — supports string scalars and string-array values only.
11
+ function parseYaml(content) {
12
+ const result = {};
13
+ const lines = content.split('\n');
14
+ let currentKey = null;
15
+ let currentList = null;
16
+
17
+ for (const line of lines) {
18
+ if (!line.trim() || line.trim().startsWith('#')) continue;
19
+
20
+ // List item: " - value"
21
+ if (/^\s{2,}- /.test(line)) {
22
+ const value = line.replace(/^\s*- /, '').trim();
23
+ if (currentKey && currentList !== null) {
24
+ currentList.push(value);
25
+ }
26
+ continue;
27
+ }
28
+
29
+ // Key-value: "key: value" or "key:" (empty / start of list)
30
+ const kvMatch = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)/);
31
+ if (kvMatch) {
32
+ currentKey = kvMatch[1];
33
+ const val = kvMatch[2].trim();
34
+ if (val === '') {
35
+ currentList = [];
36
+ result[currentKey] = currentList;
37
+ } else {
38
+ currentList = null;
39
+ result[currentKey] = val;
40
+ }
41
+ continue;
42
+ }
43
+
44
+ currentKey = null;
45
+ currentList = null;
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Try to read and parse a YAML config file.
53
+ * Returns the parsed object on success, or null if the file does not exist or cannot be parsed.
54
+ * File-not-found is silent (expected). Read/parse errors emit a warning to stderr.
55
+ */
56
+ function readConfigFile(filePath) {
57
+ try {
58
+ if (!fs.existsSync(filePath)) return null;
59
+ const content = fs.readFileSync(filePath, 'utf8');
60
+ const parsed = parseYaml(content);
61
+ if (Object.keys(parsed).length === 0 && content.trim().length > 0) {
62
+ process.stderr.write(`[refacil-sdd-ai] warning: could not parse config file at ${filePath} — treating as empty.\n`);
63
+ }
64
+ return parsed;
65
+ } catch (_) {
66
+ process.stderr.write(`[refacil-sdd-ai] warning: could not read config file at ${filePath} — ignoring.\n`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Validate `protectedBranches` from a parsed config object.
73
+ * Returns the value if valid, or null + emits a warning if invalid.
74
+ * @param {object} cfg — parsed YAML object
75
+ * @param {string} src — source label for the warning ('project' | 'global')
76
+ */
77
+ function extractProtectedBranches(cfg, src) {
78
+ if (!('protectedBranches' in cfg)) return null;
79
+ const val = cfg.protectedBranches;
80
+ if (!Array.isArray(val)) {
81
+ process.stderr.write(
82
+ `[refacil-sdd-ai] warning: protectedBranches in ${src} config must be a list — ignoring.\n`,
83
+ );
84
+ return null;
85
+ }
86
+ return val;
87
+ }
88
+
89
+ /**
90
+ * Validate `baseBranch` from a parsed config object.
91
+ * Returns the value if valid, or null + emits a warning if invalid.
92
+ * @param {object} cfg — parsed YAML object
93
+ * @param {string} src — source label for the warning ('project' | 'global')
94
+ */
95
+ function extractBaseBranch(cfg, src) {
96
+ if (!('baseBranch' in cfg)) return null;
97
+ const val = cfg.baseBranch;
98
+ if (typeof val !== 'string' || val.trim() === '') {
99
+ process.stderr.write(
100
+ `[refacil-sdd-ai] warning: baseBranch in ${src} config must be a non-empty string — ignoring.\n`,
101
+ );
102
+ return null;
103
+ }
104
+ return val.trim();
105
+ }
106
+
107
+ /**
108
+ * Load branch configuration with cascade (project > global > defaults) and source tracking.
109
+ *
110
+ * Returns:
111
+ * ```
112
+ * {
113
+ * protectedBranches: string[],
114
+ * baseBranch: string,
115
+ * sources: {
116
+ * protectedBranches: 'project' | 'global' | 'default',
117
+ * baseBranch: 'project' | 'global' | 'default',
118
+ * }
119
+ * }
120
+ * ```
121
+ *
122
+ * Never throws — all errors are handled internally.
123
+ *
124
+ * @param {string} projectRoot — absolute path to the project root
125
+ */
126
+ function loadBranchConfigWithSources(projectRoot) {
127
+ const projectConfigPath = path.join(projectRoot, 'refacil-sdd', 'config.yaml');
128
+ const globalConfigPath = path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml');
129
+
130
+ const projectCfg = readConfigFile(projectConfigPath);
131
+ const globalCfg = readConfigFile(globalConfigPath);
132
+
133
+ // --- protectedBranches ---
134
+ let protectedBranches = null;
135
+ let protectedBranchesSource = 'default';
136
+
137
+ if (projectCfg !== null) {
138
+ const val = extractProtectedBranches(projectCfg, 'project');
139
+ if (val !== null) {
140
+ protectedBranches = val;
141
+ protectedBranchesSource = 'project';
142
+ }
143
+ }
144
+
145
+ if (protectedBranches === null && globalCfg !== null) {
146
+ const val = extractProtectedBranches(globalCfg, 'global');
147
+ if (val !== null) {
148
+ protectedBranches = val;
149
+ protectedBranchesSource = 'global';
150
+ }
151
+ }
152
+
153
+ if (protectedBranches === null) {
154
+ protectedBranches = DEFAULT_PROTECTED_BRANCHES.slice();
155
+ protectedBranchesSource = 'default';
156
+ }
157
+
158
+ if (protectedBranches.length === 0) {
159
+ process.stderr.write('[refacil-sdd-ai] warning: protectedBranches is empty — no branches will be protected.\n');
160
+ }
161
+
162
+ // --- baseBranch ---
163
+ let baseBranch = null;
164
+ let baseBranchSource = 'default';
165
+
166
+ if (projectCfg !== null) {
167
+ const val = extractBaseBranch(projectCfg, 'project');
168
+ if (val !== null) {
169
+ baseBranch = val;
170
+ baseBranchSource = 'project';
171
+ }
172
+ }
173
+
174
+ if (baseBranch === null && globalCfg !== null) {
175
+ const val = extractBaseBranch(globalCfg, 'global');
176
+ if (val !== null) {
177
+ baseBranch = val;
178
+ baseBranchSource = 'global';
179
+ }
180
+ }
181
+
182
+ if (baseBranch === null) {
183
+ baseBranch = DEFAULT_BASE_BRANCH;
184
+ baseBranchSource = 'default';
185
+ }
186
+
187
+ return {
188
+ protectedBranches,
189
+ baseBranch,
190
+ sources: {
191
+ protectedBranches: protectedBranchesSource,
192
+ baseBranch: baseBranchSource,
193
+ },
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Load branch configuration (no source tracking).
199
+ * Returns { protectedBranches, baseBranch }.
200
+ * Never throws.
201
+ *
202
+ * @param {string} projectRoot — absolute path to the project root
203
+ */
204
+ function loadBranchConfig(projectRoot) {
205
+ const { protectedBranches, baseBranch } = loadBranchConfigWithSources(projectRoot);
206
+ return { protectedBranches, baseBranch };
207
+ }
208
+
209
+ module.exports = {
210
+ parseYaml,
211
+ readConfigFile,
212
+ loadBranchConfig,
213
+ loadBranchConfigWithSources,
214
+ DEFAULT_PROTECTED_BRANCHES,
215
+ DEFAULT_BASE_BRANCH,
216
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "refacil-sdd-ai",
3
- "version": "4.5.7",
3
+ "version": "5.0.0",
4
4
  "description": "SDD-AI: Specification-Driven Development with AI — development methodology using AI with Claude Code, Cursor and OpenCode",
5
5
  "bin": {
6
6
  "refacil-sdd-ai": "./bin/cli.js"
@@ -31,13 +31,13 @@
31
31
  "license": "MIT",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": ""
34
+ "url": "https://github.com/Erikole21/refacil-sdd-ai"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=20.0.0"
38
38
  },
39
39
  "scripts": {
40
- "test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js test/methodology-migration-pending.test.js test/sdd.test.js test/refactor-integrar-openspec-nativo.test.js test/refactor-rutas-refacil-sdd.test.js test/refactor-agents-english.test.js test/remove-openspec-legacy.test.js test/find-project-root.test.js test/opencode-installer.test.js test/opencode-plugin.test.js"
40
+ "test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js test/methodology-migration-pending.test.js test/sdd.test.js test/config.test.js test/refactor-integrar-openspec-nativo.test.js test/refactor-rutas-refacil-sdd.test.js test/refactor-agents-english.test.js test/remove-openspec-legacy.test.js test/find-project-root.test.js test/opencode-installer.test.js test/opencode-plugin.test.js"
41
41
  },
42
42
  "dependencies": {
43
43
  "ws": "^8.18.0"
@@ -50,12 +50,59 @@ Note: `specs` is `true` if `specs.md` exists in the root **OR** at least one `.m
50
50
 
51
51
  Run `git branch --show-current` to get the current branch.
52
52
 
53
- Apply the **Protected branch policy and branch creation** defined in `refacil-prereqs/METHODOLOGY-CONTRACT.md`.
53
+ If the current branch is already a working branch (`feature/*`, `fix/*`, `hotfix/*`, `refactor/*`, etc.), continue without interruption to Step 1.5.
54
54
 
55
- - **If the current branch is protected**: execute the full branch creation protocol **before continuing**. Do not delegate to the sub-agent until on a working branch.
56
- - For `refacil:apply`, the suggested branch prefix is `feature/`.
57
- - If the current branch is already a working branch (`feature/*`, `fix/*`, `hotfix/*`, `refactor/*`, etc.), continue without interruption.
58
- - If the user does not approve the branch creation/change, **stop**. Do not continue with implementation.
55
+ If the current branch is protected, execute the 3-gate protocol below strictly. Each gate is a hard stop — do not proceed to the next gate until the user has replied.
56
+
57
+ ---
58
+
59
+ **[GATE 1 — STOP AND WAIT: ask for task identifier]**
60
+
61
+ Ask the user exactly this question and then STOP. Do NOT run any git command. Do NOT propose a branch name. Do NOT continue to Gate 2 until the user replies:
62
+
63
+ > "What is the task number or identifier for this branch? (e.g. SEGINF-20, REF-123, or a short descriptive name)"
64
+
65
+ If the user says they have no ID, note that and proceed to Gate 2 with `<ID> = none`.
66
+
67
+ ---
68
+
69
+ **[GATE 2 — STOP AND WAIT: propose branch name and ask for approval]**
70
+
71
+ Only after receiving the user's reply to Gate 1:
72
+
73
+ 1. Verify clean working directory (`git status --porcelain`).
74
+ 2. If there are uncommitted changes, ask for approval to stash them (`git stash push -m "auto-stash-refacil"`). Do NOT stash without approval.
75
+ 3. Detect the effective configuration by running:
76
+ ```
77
+ refacil-sdd-ai sdd config --json
78
+ ```
79
+ Parse `baseBranch` and `protectedBranches` from the JSON output.
80
+ If the command fails or exits non-zero, fall back to:
81
+ - `protectedBranches` = [master, main]
82
+ - `baseBranch` = main (or master if main does not exist in the repo)
83
+ 4. Determine the base branch:
84
+ - Use the `baseBranch` value from the config (or the fallback).
85
+ - Only if that branch does not exist in the repo (new repo), use `main` or `master` as a temporary exception and recommend adopting the standard flow.
86
+ 5. Compose the branch name with `feature/` prefix:
87
+ - Feature: `feature/<ID>` (e.g. `feature/SEGINF-20`)
88
+ - Without ID: propose a short descriptive name (e.g. `feature/add-configurable-branches`)
89
+ 6. Present the proposed name and ask for approval. Then STOP. Do NOT run `git checkout` or `git switch`. Do NOT create the branch yet. Wait for the user's explicit confirmation:
90
+
91
+ > "I'll create branch `<proposed-name>` from `<base-branch>`. Shall I proceed?"
92
+
93
+ ---
94
+
95
+ **[GATE 3 — execute only after explicit approval from Gate 2]**
96
+
97
+ Only after the user explicitly confirms (e.g. "yes", "go", "ok", "proceed"):
98
+
99
+ 1. Switch to the base branch and update it (`git checkout <base>` + `git pull origin <base>`).
100
+ 2. Create the working branch (`git checkout -b <branch-name>`).
101
+ 3. If a stash was approved in Gate 2, restore it (`git stash pop`).
102
+
103
+ If the user does not approve at Gate 2, stop entirely. Do not create any branch. Do not continue with implementation.
104
+
105
+ ---
59
106
 
60
107
  ### Step 1.5: Build structured briefing (reduces sub-agent tool calls)
61
108
 
@@ -22,7 +22,7 @@ Verify the change is truly complete:
22
22
 
23
23
  2. **Tests pass**: Resolve and run the test command according to `refacil-prereqs/METHODOLOGY-CONTRACT.md`. If there are failing tests, inform and ask if they want to continue.
24
24
 
25
- 3. **No pending files**: Run `git status` and verify if there are uncommitted changes related to the feature. If there are, suggest committing before archiving.
25
+ 3. **Working tree scope hygiene**: Run `git status` and check whether there are files unrelated to the current change scope. It is expected to have uncommitted changes in this step. If unrelated files are detected, warn the user and ask whether to continue archiving anyway. Do not suggest commit in this step; commit/push decisions are handled in `refacil:up-code`.
26
26
 
27
27
  4. **Review approved (blocking)**: Verify that the `.review-passed` file exists in the change folder (`refacil-sdd/changes/[change-name]/.review-passed`) following **`METHODOLOGY-CONTRACT.md` §8** (dotfile; do not conclude by listings without dotfiles). If it does NOT exist, **stop the archiving** and inform the user:
28
28
  ```
@@ -126,6 +126,8 @@ Bug fixes only contain `summary.md` (and optionally `.review-passed`). The CLI `
126
126
 
127
127
  The spec and review evidence are written **before** running the CLI archive command, while the artifacts are still at their original paths. The CLI only moves the folder — it never syncs specs.
128
128
 
129
+ `refacil-sdd-ai sdd archive` normalizes the provided `changeName` to lowercase before validation/path resolution. Prefer lowercase names for consistency across commands and records.
130
+
129
131
  1. **Sync spec to `refacil-sdd/specs/` (before archiving)**:
130
132
  - Read `refacil-sdd/changes/<changeName>/specs.md` (and all `.md` under `specs/` if that subfolder exists).
131
133
  - Determine the spec folder name: use `<changeName>` unless a more descriptive name is clearly better.
@@ -75,11 +75,59 @@ If the sub-agent reported `crossRepo: true` in any hypothesis: before implementi
75
75
 
76
76
  Run `git branch --show-current` to get the current branch.
77
77
 
78
- Apply the **Protected branch policy and branch creation** defined in `refacil-prereqs/METHODOLOGY-CONTRACT.md`.
78
+ If the current branch is already a working branch (`feature/*`, `fix/*`, `hotfix/*`, `refactor/*`, etc.), continue without interruption to Step 5.
79
79
 
80
- - If the current branch is protected, execute the full protocol before continuing.
81
- - For `refacil:bug`, the suggested branch prefix is `fix/`.
82
- - If the current branch is already a working branch (`feature/*`, `fix/*`, `hotfix/*`, `refactor/*`, etc.), continue without interruption.
80
+ If the current branch is protected, execute the 3-gate protocol below strictly. Each gate is a hard stop — do not proceed to the next gate until the user has replied.
81
+
82
+ ---
83
+
84
+ **[GATE 1 — STOP AND WAIT: ask for task identifier]**
85
+
86
+ Ask the user exactly this question and then STOP. Do NOT run any git command. Do NOT propose a branch name. Do NOT continue to Gate 2 until the user replies:
87
+
88
+ > "What is the task number or identifier for this branch? (e.g. SEGINF-20, REF-123, or a short descriptive name)"
89
+
90
+ If the user says they have no ID, note that and proceed to Gate 2 with `<ID> = none`.
91
+
92
+ ---
93
+
94
+ **[GATE 2 — STOP AND WAIT: propose branch name and ask for approval]**
95
+
96
+ Only after receiving the user's reply to Gate 1:
97
+
98
+ 1. Verify clean working directory (`git status --porcelain`).
99
+ 2. If there are uncommitted changes, ask for approval to stash them (`git stash push -m "auto-stash-refacil"`). Do NOT stash without approval.
100
+ 3. Detect the effective configuration by running:
101
+ ```
102
+ refacil-sdd-ai sdd config --json
103
+ ```
104
+ Parse `baseBranch` and `protectedBranches` from the JSON output.
105
+ If the command fails or exits non-zero, fall back to:
106
+ - `protectedBranches` = [master, main]
107
+ - `baseBranch` = main (or master if main does not exist in the repo)
108
+ 4. Determine the base branch:
109
+ - Use the `baseBranch` value from the config (or the fallback).
110
+ - Only if that branch does not exist in the repo (new repo), use `main` or `master` as a temporary exception and recommend adopting the standard flow.
111
+ 5. Compose the branch name with `fix/` prefix:
112
+ - Bugfix: `fix/<ID>` (e.g. `fix/SEGINF-20`)
113
+ - Without ID: propose a short descriptive name (e.g. `fix/session-timeout-redis`)
114
+ 6. Present the proposed name and ask for approval. Then STOP. Do NOT run `git checkout` or `git switch`. Do NOT create the branch yet. Wait for the user's explicit confirmation:
115
+
116
+ > "I'll create branch `<proposed-name>` from `<base-branch>`. Shall I proceed?"
117
+
118
+ ---
119
+
120
+ **[GATE 3 — execute only after explicit approval from Gate 2]**
121
+
122
+ Only after the user explicitly confirms (e.g. "yes", "go", "ok", "proceed"):
123
+
124
+ 1. Switch to the base branch and update it (`git checkout <base>` + `git pull origin <base>`).
125
+ 2. Create the working branch (`git checkout -b <branch-name>`).
126
+ 3. If a stash was approved in Gate 2, restore it (`git stash pop`).
127
+
128
+ If the user does not approve at Gate 2, stop entirely. Do not create any branch. Do not continue with implementation.
129
+
130
+ ---
83
131
 
84
132
  ### Step 5: Delegate implementation to the refacil-debugger sub-agent (mode: fix)
85
133
 
@@ -34,24 +34,28 @@ Coverage (if applicable): detect the project command (`test:cov`, `coverage`, `p
34
34
 
35
35
  ## §4 — Protected branch policy and branch creation
36
36
 
37
- Protected branches (never develop directly on them): `master`, `main`, `develop`, `dev`, `testing`, `qa`.
37
+ > **Dynamic config**: before applying any branch rule, run `refacil-sdd-ai sdd config --json`
38
+ > to obtain the effective `protectedBranches` and `baseBranch` for this project.
39
+ > The values below are the built-in defaults and serve as the fallback if the command is unavailable.
40
+
41
+ Protected branches built-in defaults (authoritative list: `refacil-sdd-ai sdd config --json`): `master`, `main`, `develop`, `dev`, `testing`, `qa`. These are the fallback when no config file is present. When `sdd config --json` is unavailable, treat at minimum `master` and `main` as protected — they are the universally protected branches across all projects.
38
42
 
39
43
  Critical rule:
40
44
  - **NEVER** make direct changes on protected branches.
41
- - All integration to protected branches is done via PR, without exceptions (including `testing`).
45
+ - All integration to protected branches is done via PR.
42
46
 
43
47
  ### Working branch creation
44
48
 
45
- - General rule: every new working branch (`feature/*`, `fix/*`, `hotfix/*`, `refactor/*`, etc.) must be created from an updated `develop` or `dev`.
46
- - Exception for new repos: if neither `develop` nor `dev` exists yet, creating temporarily from `main` or `master` is allowed.
47
- - If the exception is used, recommend creating `develop`/`dev` and adopting that flow as the repo standard.
48
- - **NEVER** create working branches from `testing`, `qa`, or from other feature/bug branches.
49
+ - General rule: every new working branch (`feature/*`, `fix/*`, `hotfix/*`, `refactor/*`, etc.) must be created from the `baseBranch` returned by `sdd config --json`.
50
+ - Exception for new repos: if the configured `baseBranch` does not exist yet, creating temporarily from `main` or `master` is allowed.
51
+ - If the exception is used, recommend creating the configured `baseBranch` and adopting that flow as the repo standard.
52
+ - **NEVER** create working branches from any other protected branch (as listed by `sdd config --json`), or from other feature/bug branches.
49
53
 
50
54
  ### Integration
51
55
 
52
56
  - All integration to any protected branch requires a **PR**.
53
- - No exceptions: `testing`, `develop`, `main`, `master`, `qa`, `release/*` — all require PR.
54
- - Recommend the user create a PR to `testing` so the changes are available in the test environment.
57
+ - No exceptions: all protected branches (as returned by `sdd config --json`), plus `release/*` patterns — all require PR.
58
+ - Recommend the user create a PR to one of the protected branches listed by `sdd config --json` to make the changes available for integration.
55
59
 
56
60
  ### Protocol when the current branch is protected
57
61
 
@@ -76,8 +80,8 @@ Only after receiving the user's reply to Gate 1:
76
80
  1. Verify clean working directory (`git status --porcelain`).
77
81
  2. If there are uncommitted changes, ask for approval to stash them (`git stash push -m "auto-stash-refacil"`). Do NOT stash without approval.
78
82
  3. Detect the base branch:
79
- - Prefer `develop`, then `dev`.
80
- - Only if neither exists (new repo), use `main` or `master` as a temporary exception.
83
+ - Use the `baseBranch` from `sdd config --json`.
84
+ - Only if that branch does not exist (new repo), use `main` or `master` as a temporary exception.
81
85
  4. Compose the branch name:
82
86
  - Feature: `feature/<ID>` (e.g. `feature/SEGINF-20`)
83
87
  - Bugfix: `fix/<ID>` (e.g. `fix/SEGINF-20`)
@@ -40,6 +40,45 @@ mkdir -p refacil-sdd/changes
40
40
 
41
41
  Inform the user that SDD artifacts will be stored in `refacil-sdd/changes/<change-name>/`.
42
42
 
43
+ ### Step 3b: Branch configuration (project-level)
44
+
45
+ Check and optionally set project-specific branch configuration that overrides the global config.
46
+
47
+ **3b.1 Show inherited values** — run:
48
+
49
+ ```bash
50
+ refacil-sdd-ai sdd config --json
51
+ ```
52
+
53
+ Parse the JSON output and display the effective values with their source:
54
+
55
+ ```
56
+ baseBranch [<source>]: <value>
57
+ protectedBranches [<source>]: <value>
58
+ ```
59
+
60
+ Where `<source>` is one of `project`, `global`, or `default`.
61
+
62
+ **3b.2 Check for existing project config** — if `refacil-sdd/config.yaml` already exists, show its current values and ask the user if they want to update them. If the user declines, skip to Step 4.
63
+
64
+ **3b.3 Ask for project-level overrides** — prompt the user:
65
+
66
+ ```
67
+ Do you want to set project-specific branch configuration?
68
+ baseBranch (inherited: <value> from <source>):
69
+ protectedBranches (inherited: <value> from <source>):
70
+ Press Enter to skip and inherit the values shown above.
71
+ ```
72
+
73
+ - If the user provides values: run `refacil-sdd-ai sdd write-config --base-branch <v> --protected-branches <csv>` (no `--global` flag — this writes to `refacil-sdd/config.yaml`).
74
+ - If the user skips (presses Enter or provides no values): do **not** write any file. Project will inherit from global or defaults.
75
+
76
+ **3b.4 Confirm the result** — after writing (or skipping), show the new effective config:
77
+
78
+ ```bash
79
+ refacil-sdd-ai sdd config
80
+ ```
81
+
43
82
  ### Step 4: Generate `.agents/` and `AGENTS.md`
44
83
 
45
84
  Analyze the repo and generate the documentation structure. If they already exist, ask whether to regenerate.
@@ -139,7 +178,7 @@ If the user wants to customize additional exclusions, they can edit them directl
139
178
 
140
179
  ```
141
180
  === refacil:setup completed ===
142
- Node.js / refacil-sdd-ai / refacil-sdd/changes/ / AGENTS.md / CLAUDE.md / .cursorrules / .claudeignore / .cursorignore / skills OK
181
+ Node.js / refacil-sdd-ai / refacil-sdd/changes/ / branch config / AGENTS.md / CLAUDE.md / .cursorrules / .claudeignore / .cursorignore / skills OK
143
182
 
144
183
  Restart Claude Code or Cursor session if this is the first skills installation.
145
184
  The next step is to review the available flow.
@@ -17,7 +17,10 @@ Applies the branch and integration policy defined in `refacil-prereqs/METHODOLOG
17
17
 
18
18
  Run `git branch --show-current` to get the branch name.
19
19
 
20
- - If the current branch is protected (according to `METHODOLOGY-CONTRACT.md`), **stop** and inform the user:
20
+ Run `refacil-sdd-ai sdd config --json` to obtain the effective `protectedBranches` list for this project.
21
+ If the command fails or exits non-zero, use the default list: master, main.
22
+
23
+ - If the current branch is in the `protectedBranches` list, **stop** and inform the user:
21
24
  ```
22
25
  Cannot push code from a protected branch ([name]).
23
26
  Branch validation is done in /refacil:apply or /refacil:bug before writing code.
@@ -78,12 +81,13 @@ Run `git push -u origin [current-branch]` to push the changes.
78
81
  Remote: origin/[branch-name]
79
82
  ```
80
83
 
81
- 2. **Ask the user** which branch they want to create the PR to. Suggest `testing` as the default target:
84
+ 2. **Ask the user** which branch they want to create the PR to. Show the list of protected branches obtained from `sdd config --json` in Step 1 so the user can pick one:
82
85
  ```
83
- Which branch do you want to create the PR to? (recommended: testing)
86
+ Which branch do you want to create the PR to?
87
+ Protected branches available: [list from sdd config --json]
84
88
  ```
85
89
 
86
- If the user indicates a different branch than `testing`, verify it exists on the remote by inspecting `git branch -r` output before generating the link. If it does not exist, inform the user and ask them to confirm or correct the name.
90
+ Verify the chosen branch exists on the remote by inspecting `git branch -r` output before generating the link. If it does not exist, inform the user and ask them to confirm or correct the name. If the user indicates a branch not in the protected branches list, warn them before proceeding.
87
91
 
88
92
  3. Get the remote repository URL with `git remote get-url origin` and detect the VCS hosting used by this repository to generate the correct PR/MR link:
89
93
  - **GitHub** (url contains `github.com`): `https://github.com/[owner]/[repo]/compare/[target-branch]...[current-branch]?expand=1`
@@ -93,12 +97,12 @@ Run `git push -u origin [current-branch]` to push the changes.
93
97
  - For SSH remotes (`git@host:group/repo.git`), extract host/namespace/repo from the segment after `:`.
94
98
  - If hosting cannot be determined, do not assume a provider: show the detected remote URL and ask the user which platform is used before generating the final PR/MR link.
95
99
 
96
- 4. Show the generated link (provider-specific) to the user and recommend PR to `testing`:
100
+ 4. Show the generated link (provider-specific) to the user:
97
101
  ```
98
102
  Create your PR here: [link]
99
103
 
100
- Tip: PR to testing is recommended to enable integrated testing
101
- before promoting to other protected branches.
104
+ Tip: PRing to a protected branch (e.g. one of those listed by `sdd config --json`) is recommended
105
+ before promoting to main/master.
102
106
  ```
103
107
 
104
108
  **This is the terminal step of the SDD flow.** Do not ask for a next skill — the cycle closes here. Apply the terminal step rule from `METHODOLOGY-CONTRACT.md §5`.