refacil-sdd-ai 4.5.8 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, artifactLanguage)
33
+ # pre-filled 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,9 +102,43 @@ 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 configuration (protectedBranches, baseBranch, artifactLanguage) after cascade: project `refacil-sdd/config.yaml` → global `~/.refacil-sdd-ai/config.yaml` → built-in defaults. `--json` also includes a `sources` field indicating the resolution level for each value (`project`, `global`, or `default`). |
106
+ | `refacil-sdd-ai sdd write-config [--global] [--base-branch <v>] [--protected-branches <csv>] [--artifact-language <lang>]` | Write or merge 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
 
110
+ ### Artifact Language
111
+
112
+ By default, `/refacil:propose` generates proposal, specs, design, and tasks in **English**. Set `artifactLanguage` to have the artifacts produced in your team's preferred language so developers can review them in their natural language.
113
+
114
+ **Supported values**: `english` (default) · `spanish`
115
+
116
+ **Configure globally** — applies to all repos for this user:
117
+
118
+ ```bash
119
+ refacil-sdd-ai sdd write-config --global --artifact-language spanish
120
+ ```
121
+
122
+ **Configure per project** — overrides the global value (commit `refacil-sdd/config.yaml` for team-wide effect):
123
+
124
+ ```bash
125
+ refacil-sdd-ai sdd write-config --artifact-language spanish
126
+ ```
127
+
128
+ **Check the active value**:
129
+
130
+ ```bash
131
+ refacil-sdd-ai sdd config
132
+ # artifactLanguage [global]: spanish
133
+
134
+ refacil-sdd-ai sdd config --json
135
+ # { ..., "artifactLanguage": "spanish", "sources": { "artifactLanguage": "global" } }
136
+ ```
137
+
138
+ **Cascade**: project `refacil-sdd/config.yaml` → global `~/.refacil-sdd-ai/config.yaml` → default `english`.
139
+
140
+ `refacil-sdd-ai init` also prompts for this preference and writes to the global config. Skip with `--yes` to keep the current value.
141
+
106
142
  ### Command rewrite control (`compact-bash`)
107
143
 
108
144
  | Command | Description |
@@ -372,11 +408,11 @@ The SDD-AI methodology generates a lot of context (artifacts, specs, prompts). T
372
408
  Defined in `skills/prereqs/METHODOLOGY-CONTRACT.md`:
373
409
 
374
410
  - **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.
411
+ - **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
412
  - **Multi-stack tests**: detects the real test command (does not hardcode `npm test`).
377
413
  - **`AGENTS.md` by profile** (`sdd` vs `agents`): the methodology respects both.
378
414
  - **Output mode**: concise by default, detailed on demand.
379
- - **Language policy**: internal agent and skill instructions are in **English**. Responses to the user are in the **user's language** (default: Spanish). SDD artifacts are in the **team's agreed language**.
415
+ - **Language policy**: internal agent and skill instructions are in **English**. Responses to the user are in the **user's language** (default: Spanish). SDD artifact language (proposal, specs, design, tasks) defaults to **English** and is configurable via `artifactLanguage` — see [Artifact Language](#artifact-language).
380
416
 
381
417
  ---
382
418
 
@@ -153,6 +153,18 @@ Effort estimate: **S** (< 1h), **M** (1-4h), **L** (> 4h).
153
153
 
154
154
  ### Step 1: Explore the codebase
155
155
 
156
+ #### Step 1a: Language resolution (run FIRST, before any exploration)
157
+
158
+ Run: `refacil-sdd-ai sdd config --json`
159
+
160
+ Read the `artifactLanguage` field from the JSON output. Prepend the following instruction to your working context for this session:
161
+
162
+ > Generate ALL artifact content (proposal.md, specs.md, design.md, tasks.md) in **<artifactLanguage>** language. This applies to all prose, comments, labels, and descriptions inside the artifact files.
163
+
164
+ Fallback rule: if the command fails, produces invalid JSON, or returns an unknown/missing `artifactLanguage` value, use `english` and continue without interruption.
165
+
166
+ #### Step 1b: Codebase exploration
167
+
156
168
  Before generating artifacts, explore the project so that `design.md` is realistic and not invented:
157
169
  - Read `AGENTS.md` to understand the current architecture.
158
170
  - Identify files and modules relevant to the described change.
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,136 @@ 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, SUPPORTED_LANGUAGES, DEFAULT_ARTIFACT_LANGUAGE } = 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
+ const currentArtifactLanguage = (typeof globalConfig.artifactLanguage === 'string' && SUPPORTED_LANGUAGES.includes(globalConfig.artifactLanguage.trim())) ? globalConfig.artifactLanguage.trim() : DEFAULT_ARTIFACT_LANGUAGE;
425
+
426
+ console.log('\n Branch configuration (global, stored in ~/.refacil-sdd-ai/config.yaml)');
427
+ console.log(` Current base branch: ${currentBaseBranch}`);
428
+ console.log(` Current protected branches: ${currentProtected.join(', ')}`);
429
+ console.log(` Current artifact language: ${currentArtifactLanguage}`);
430
+ console.log(' Press Enter to keep current values, or type new ones.\n');
431
+
432
+ let baseBranch;
433
+ let protectedBranches;
434
+ let artifactLanguage;
435
+
436
+ try {
437
+ const clack = require('@clack/prompts');
438
+
439
+ const bbResult = await clack.text({
440
+ message: `Base branch (current: ${currentBaseBranch}):`,
441
+ placeholder: currentBaseBranch,
442
+ validate: () => undefined,
443
+ });
444
+ if (clack.isCancel(bbResult)) {
445
+ console.log(' Branch config prompt cancelled. Keeping existing values.\n');
446
+ return;
447
+ }
448
+ baseBranch = (bbResult && bbResult.trim()) ? bbResult.trim() : currentBaseBranch;
449
+
450
+ const pbResult = await clack.text({
451
+ message: `Protected branches, comma-separated (current: ${currentProtected.join(', ')}):`,
452
+ placeholder: currentProtected.join(', '),
453
+ validate: () => undefined,
454
+ });
455
+ if (clack.isCancel(pbResult)) {
456
+ console.log(' Branch config prompt cancelled. Keeping existing values.\n');
457
+ return;
458
+ }
459
+ protectedBranches = parseBranchList((pbResult && pbResult.trim()) ? pbResult.trim() : currentProtected.join(', '), currentProtected);
460
+
461
+ const alResult = await clack.text({
462
+ message: `Artifact language — ${SUPPORTED_LANGUAGES.join(' | ')} (current: ${currentArtifactLanguage}):`,
463
+ placeholder: currentArtifactLanguage,
464
+ validate: () => undefined,
465
+ });
466
+ if (clack.isCancel(alResult)) {
467
+ console.log(' Branch config prompt cancelled. Keeping existing values.\n');
468
+ return;
469
+ }
470
+ const alRaw = (alResult && alResult.trim()) ? alResult.trim() : currentArtifactLanguage;
471
+ artifactLanguage = SUPPORTED_LANGUAGES.includes(alRaw) ? alRaw : currentArtifactLanguage;
472
+
473
+ const confirm = await clack.confirm({
474
+ message: `Save global config — base: "${baseBranch}", protected: [${protectedBranches.join(', ')}], language: ${artifactLanguage}?`,
475
+ initialValue: true,
476
+ });
477
+ if (clack.isCancel(confirm) || !confirm) {
478
+ console.log(' Branch config not saved.\n');
479
+ return;
480
+ }
481
+ } catch (_) {
482
+ // @clack/prompts not available — use inline readline fallback
483
+ const readline = require('readline');
484
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
485
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
486
+
487
+ const bbAnswer = await ask(` Base branch [${currentBaseBranch}]: `);
488
+ baseBranch = (bbAnswer && bbAnswer.trim()) ? bbAnswer.trim() : currentBaseBranch;
489
+
490
+ const pbAnswer = await ask(` Protected branches [${currentProtected.join(', ')}]: `);
491
+ protectedBranches = parseBranchList((pbAnswer && pbAnswer.trim()) ? pbAnswer.trim() : currentProtected.join(', '), currentProtected);
492
+
493
+ const alAnswer = await ask(` Artifact language [${currentArtifactLanguage}] (${SUPPORTED_LANGUAGES.join('/')}): `);
494
+ const alRaw = (alAnswer && alAnswer.trim()) ? alAnswer.trim() : currentArtifactLanguage;
495
+ artifactLanguage = SUPPORTED_LANGUAGES.includes(alRaw) ? alRaw : currentArtifactLanguage;
496
+
497
+ const confirmAnswer = await ask(` Save global config — base: "${baseBranch}", protected: [${protectedBranches.join(', ')}], language: ${artifactLanguage}? (Y/n): `);
498
+ rl.close();
499
+ if (confirmAnswer.trim().toLowerCase() === 'n') {
500
+ console.log(' Branch config not saved.\n');
501
+ return;
502
+ }
503
+ }
504
+
505
+ // Pre-check to avoid process.exit(0) from cmdWriteConfig no-op path when called programmatically
506
+ const valuesUnchanged = baseBranch === currentBaseBranch &&
507
+ JSON.stringify(protectedBranches.slice().sort()) === JSON.stringify(currentProtected.slice().sort()) &&
508
+ artifactLanguage === currentArtifactLanguage;
509
+ if (valuesUnchanged) {
510
+ console.log(' Branch config unchanged. Keeping existing values.\n');
511
+ return;
512
+ }
513
+
514
+ // Build argv-style array and call cmdWriteConfig directly
515
+ // Only write artifactLanguage if user chose a non-default value
516
+ const writeArgv = [
517
+ '--global',
518
+ '--base-branch', baseBranch,
519
+ '--protected-branches', protectedBranches.join(','),
520
+ '--artifact-language', artifactLanguage,
521
+ ];
522
+ try {
523
+ cmdWriteConfig(writeArgv, projectRoot);
524
+ } catch (err) {
525
+ console.error(` Warning: could not write global branch config: ${err.message}`);
526
+ }
527
+ console.log('');
528
+ }
529
+
399
530
  // --- High-level commands ---
400
531
 
401
532
  async function init() {
@@ -417,6 +548,9 @@ async function init() {
417
548
  // Select target IDEs (interactive selector or --all / non-TTY)
418
549
  const selectedIDEs = await selectIDEs();
419
550
 
551
+ // Prompt for global branch configuration (skipped with --yes/--defaults or non-TTY)
552
+ await promptBranchConfig();
553
+
420
554
  if (selectedIDEs.length === 0) {
421
555
  console.log('\n No IDEs selected. Nothing installed.\n');
422
556
  console.log(' Re-run with: refacil-sdd-ai init --all to install for all IDEs');
@@ -644,6 +778,7 @@ function help() {
644
778
  Commands:
645
779
  init Install skills in .claude/, .cursor/ and/or .opencode/ (interactive IDE selector).
646
780
  Use --all to install for all three IDEs without prompting.
781
+ Use --yes or --defaults to skip interactive branch config prompts.
647
782
  Creates CLAUDE.md, .cursorrules and .opencode/opencode.json as appropriate.
648
783
  update Re-copy skills for all detected IDEs (.claude/, .cursor/, .opencode/)
649
784
  migration-pending Same validation as hooks/notify-update: list migrations (exit 1 if any; --json)
@@ -680,6 +815,12 @@ function help() {
680
815
  sdd mark-reviewed <name> Write .review-passed (requires --verdict and --summary)
681
816
  sdd tasks-update <name> Mark task N as completed (--task N --done)
682
817
  sdd validate-name <name> Validate change name format
818
+ sdd config [--json] Show effective branch config (project > global > defaults)
819
+ sdd write-config Write branch config to project or global config file
820
+ [--global] Write to ~/.refacil-sdd-ai/config.yaml (global level)
821
+ [--base-branch <branch>] Base branch for new changes
822
+ [--protected-branches <csv>] Protected branches (comma-separated)
823
+ [--artifact-language <lang>] Artifact language: english (default) or spanish
683
824
  clean Remove skills and SDD-AI hooks from all detected IDEs
684
825
  (.claude/settings.json, .cursor/hooks.json, .opencode/plugins/)
685
826
  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, SUPPORTED_LANGUAGES } = require('../config');
5
7
 
6
8
  function findProjectRoot() {
7
9
  let dir = process.cwd();
@@ -118,49 +120,7 @@ function autoMigrateOpenspec(root) {
118
120
  // Si ambos existen o ninguno existe → no hacer nada
119
121
  }
120
122
 
121
- // --- Minimal YAML parser/serializer for memory.yaml ---
122
- // Supports: string values and string-array values only.
123
-
124
- function parseMemoryYaml(content) {
125
- const result = {};
126
- const lines = content.split('\n');
127
- let currentKey = null;
128
- let currentList = null;
129
-
130
- for (const line of lines) {
131
- if (!line.trim() || line.trim().startsWith('#')) continue;
132
-
133
- // List item: " - value"
134
- if (/^\s{2,}- /.test(line)) {
135
- const value = line.replace(/^\s*- /, '').trim();
136
- if (currentKey && currentList !== null) {
137
- currentList.push(value);
138
- }
139
- continue;
140
- }
141
-
142
- // Key-value: "key: value" or "key:" (empty/start of list)
143
- const kvMatch = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)/);
144
- if (kvMatch) {
145
- currentKey = kvMatch[1];
146
- const val = kvMatch[2].trim();
147
- if (val === '') {
148
- // Could be a list
149
- currentList = [];
150
- result[currentKey] = currentList;
151
- } else {
152
- currentList = null;
153
- result[currentKey] = val;
154
- }
155
- continue;
156
- }
157
-
158
- currentKey = null;
159
- currentList = null;
160
- }
161
-
162
- return result;
163
- }
123
+ // parseYaml is imported from lib/config.js (shared parser, no duplication)
164
124
 
165
125
  function serializeMemoryYaml(obj) {
166
126
  const lines = [];
@@ -305,7 +265,7 @@ function cmdSetMemory(argv, projectRoot) {
305
265
  let existing = {};
306
266
  if (fs.existsSync(memoryPath)) {
307
267
  try {
308
- existing = parseMemoryYaml(fs.readFileSync(memoryPath, 'utf8'));
268
+ existing = parseYaml(fs.readFileSync(memoryPath, 'utf8'));
309
269
  } catch (_) {
310
270
  existing = {};
311
271
  }
@@ -358,7 +318,7 @@ function cmdGetMemory(argv, projectRoot) {
358
318
  if (wantJson) {
359
319
  let parsed = {};
360
320
  try {
361
- parsed = parseMemoryYaml(content);
321
+ parsed = parseYaml(content);
362
322
  } catch (_) {
363
323
  parsed = {};
364
324
  }
@@ -670,6 +630,106 @@ function cmdTasksUpdate(argv, projectRoot) {
670
630
  console.log(`Task ${taskN} de '${name}' marcada como completada.`);
671
631
  }
672
632
 
633
+ function cmdConfig(argv, projectRoot) {
634
+ const args = parseArgs(argv);
635
+ const wantJson = args.json === true;
636
+
637
+ const { protectedBranches, baseBranch, artifactLanguage, sources } = loadBranchConfigWithSources(projectRoot);
638
+
639
+ if (wantJson) {
640
+ process.stdout.write(JSON.stringify({ protectedBranches, baseBranch, artifactLanguage, sources }) + '\n');
641
+ } else {
642
+ console.log(`protectedBranches [${sources.protectedBranches}]: ${protectedBranches.join(', ')}`);
643
+ console.log(`baseBranch [${sources.baseBranch}]: ${baseBranch}`);
644
+ console.log(`artifactLanguage [${sources.artifactLanguage}]: ${artifactLanguage}`);
645
+ }
646
+ }
647
+
648
+
649
+ function cmdWriteConfig(argv, projectRoot) {
650
+ const args = parseArgs(argv);
651
+
652
+ const isGlobal = args.global === true;
653
+ const rawBaseBranch = args['base-branch'];
654
+ const rawProtectedBranches = args['protected-branches'];
655
+ const rawArtifactLanguage = args['artifact-language'];
656
+
657
+ // CR-03: no flags provided
658
+ if (rawBaseBranch === undefined && rawProtectedBranches === undefined && rawArtifactLanguage === undefined) {
659
+ console.error('Uso: refacil-sdd-ai sdd write-config [--global] [--base-branch <branch>] [--protected-branches <csv>] [--artifact-language <language>]');
660
+ console.error('Debe especificar al menos --base-branch, --protected-branches o --artifact-language.');
661
+ process.exit(1);
662
+ }
663
+
664
+ // CR-01: empty --base-branch after trim
665
+ if (rawBaseBranch !== undefined && (typeof rawBaseBranch !== 'string' || rawBaseBranch.trim() === '')) {
666
+ console.error('Error: --base-branch no puede estar vacío.');
667
+ process.exit(1);
668
+ }
669
+
670
+ // CR-02: --protected-branches: split, trim, filter; error if empty result
671
+ let protectedBranchesList;
672
+ if (rawProtectedBranches !== undefined) {
673
+ protectedBranchesList = String(rawProtectedBranches).split(',').map((s) => s.trim()).filter(Boolean);
674
+ if (protectedBranchesList.length === 0) {
675
+ console.error('Error: --protected-branches no puede resultar en una lista vacía.');
676
+ process.exit(1);
677
+ }
678
+ }
679
+
680
+ // --artifact-language: must be non-empty and in SUPPORTED_LANGUAGES
681
+ if (rawArtifactLanguage !== undefined) {
682
+ if (typeof rawArtifactLanguage !== 'string' || rawArtifactLanguage.trim() === '') {
683
+ console.error('Error: --artifact-language no puede estar vacío.');
684
+ process.exit(1);
685
+ }
686
+ if (!SUPPORTED_LANGUAGES.includes(rawArtifactLanguage.trim())) {
687
+ console.error(`Error: --artifact-language "${rawArtifactLanguage.trim()}" no es un lenguaje soportado. Valores válidos: ${SUPPORTED_LANGUAGES.join(', ')}.`);
688
+ process.exit(1);
689
+ }
690
+ }
691
+
692
+ const targetPath = isGlobal
693
+ ? path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml')
694
+ : path.join(projectRoot, 'refacil-sdd', 'config.yaml');
695
+
696
+ // CR-04: read existing file; null if absent or corrupt
697
+ const existing = readConfigFile(targetPath) || {};
698
+
699
+ // Merge: start from existing, overwrite only provided keys
700
+ const merged = Object.assign({}, existing);
701
+ if (rawBaseBranch !== undefined) {
702
+ merged.baseBranch = rawBaseBranch.trim();
703
+ }
704
+ if (protectedBranchesList !== undefined) {
705
+ merged.protectedBranches = protectedBranchesList;
706
+ }
707
+ if (rawArtifactLanguage !== undefined) {
708
+ merged.artifactLanguage = rawArtifactLanguage.trim();
709
+ }
710
+
711
+ // CA-03: no-op when all provided keys already match existing config (semantic comparison)
712
+ const isNoOp = Object.keys(existing).length > 0 &&
713
+ (rawBaseBranch === undefined || existing.baseBranch === rawBaseBranch.trim()) &&
714
+ (protectedBranchesList === undefined ||
715
+ (Array.isArray(existing.protectedBranches) &&
716
+ JSON.stringify(existing.protectedBranches.slice().sort()) === JSON.stringify(protectedBranchesList.slice().sort()))) &&
717
+ (rawArtifactLanguage === undefined || existing.artifactLanguage === rawArtifactLanguage.trim());
718
+ if (isNoOp) {
719
+ console.log(`Sin cambios: ${targetPath} ya tiene los valores indicados.`);
720
+ process.exit(0);
721
+ }
722
+ const proposed = serializeMemoryYaml(merged);
723
+
724
+ // Create directory if absent
725
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
726
+
727
+ fs.writeFileSync(targetPath, proposed, 'utf8');
728
+
729
+ const level = isGlobal ? 'global' : 'proyecto';
730
+ console.log(`Configuración escrita en ${targetPath} (nivel: ${level})`);
731
+ }
732
+
673
733
  function sddHelp() {
674
734
  console.log(`
675
735
  refacil-sdd-ai sdd — Gestión de artefactos SDD-AI
@@ -700,6 +760,15 @@ function sddHelp() {
700
760
  sdd set-review-fails <nombre> Escribe .review-last-fails.json con archivos fallidos
701
761
  --files <csv> Archivos con fallos (separados por coma)
702
762
  sdd clear-review-fails <nombre> Elimina .review-last-fails.json del cambio
763
+ sdd config [--json] Muestra la configuración efectiva de ramas
764
+ (project > global > defaults)
765
+ [--json] Salida en JSON (útil para agentes)
766
+ sdd write-config Escribe la configuración en el archivo de config
767
+ [--global] Escribe en ~/.refacil-sdd-ai/config.yaml (global)
768
+ Sin --global: escribe en refacil-sdd/config.yaml (proyecto)
769
+ [--base-branch <branch>] Rama base para nuevos cambios
770
+ [--protected-branches <csv>] Ramas protegidas (separadas por coma)
771
+ [--artifact-language <language>] Idioma para los artefactos SDD generados (english, spanish)
703
772
 
704
773
  Notas:
705
774
  - Los nombres de cambio deben empezar con minúscula y usar solo [a-z0-9-]
@@ -748,10 +817,16 @@ function handleSdd(sub, argv, projectRoot) {
748
817
  case 'clear-review-fails':
749
818
  cmdClearReviewFails(args, root);
750
819
  break;
820
+ case 'config':
821
+ cmdConfig(args, root);
822
+ break;
823
+ case 'write-config':
824
+ cmdWriteConfig(args, root);
825
+ break;
751
826
  default:
752
827
  sddHelp();
753
828
  process.exit(1);
754
829
  }
755
830
  }
756
831
 
757
- module.exports = { handleSdd, parseArgs, autoMigrateOpenspec, validateChangeName, resolveExistingChangeName, findProjectRoot };
832
+ module.exports = { handleSdd, parseArgs, autoMigrateOpenspec, validateChangeName, resolveExistingChangeName, findProjectRoot, cmdWriteConfig };
package/lib/config.js ADDED
@@ -0,0 +1,273 @@
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
+ const SUPPORTED_LANGUAGES = ['english', 'spanish'];
10
+ const DEFAULT_ARTIFACT_LANGUAGE = 'english';
11
+
12
+ // Minimal YAML parser — supports string scalars and string-array values only.
13
+ function parseYaml(content) {
14
+ const result = {};
15
+ const lines = content.split('\n');
16
+ let currentKey = null;
17
+ let currentList = null;
18
+
19
+ for (const line of lines) {
20
+ if (!line.trim() || line.trim().startsWith('#')) continue;
21
+
22
+ // List item: " - value"
23
+ if (/^\s{2,}- /.test(line)) {
24
+ const value = line.replace(/^\s*- /, '').trim();
25
+ if (currentKey && currentList !== null) {
26
+ currentList.push(value);
27
+ }
28
+ continue;
29
+ }
30
+
31
+ // Key-value: "key: value" or "key:" (empty / start of list)
32
+ const kvMatch = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)/);
33
+ if (kvMatch) {
34
+ currentKey = kvMatch[1];
35
+ const val = kvMatch[2].trim();
36
+ if (val === '') {
37
+ currentList = [];
38
+ result[currentKey] = currentList;
39
+ } else {
40
+ currentList = null;
41
+ result[currentKey] = val;
42
+ }
43
+ continue;
44
+ }
45
+
46
+ currentKey = null;
47
+ currentList = null;
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ /**
54
+ * Try to read and parse a YAML config file.
55
+ * Returns the parsed object on success, or null if the file does not exist or cannot be parsed.
56
+ * File-not-found is silent (expected). Read/parse errors emit a warning to stderr.
57
+ */
58
+ function readConfigFile(filePath) {
59
+ try {
60
+ if (!fs.existsSync(filePath)) return null;
61
+ const content = fs.readFileSync(filePath, 'utf8');
62
+ const parsed = parseYaml(content);
63
+ if (Object.keys(parsed).length === 0 && content.trim().length > 0) {
64
+ process.stderr.write(`[refacil-sdd-ai] warning: could not parse config file at ${filePath} — treating as empty.\n`);
65
+ }
66
+ return parsed;
67
+ } catch (_) {
68
+ process.stderr.write(`[refacil-sdd-ai] warning: could not read config file at ${filePath} — ignoring.\n`);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Validate `protectedBranches` from a parsed config object.
75
+ * Returns the value if valid, or null + emits a warning if invalid.
76
+ * @param {object} cfg — parsed YAML object
77
+ * @param {string} src — source label for the warning ('project' | 'global')
78
+ */
79
+ function extractProtectedBranches(cfg, src) {
80
+ if (!('protectedBranches' in cfg)) return null;
81
+ const val = cfg.protectedBranches;
82
+ if (!Array.isArray(val)) {
83
+ process.stderr.write(
84
+ `[refacil-sdd-ai] warning: protectedBranches in ${src} config must be a list — ignoring.\n`,
85
+ );
86
+ return null;
87
+ }
88
+ return val;
89
+ }
90
+
91
+ /**
92
+ * Validate `artifactLanguage` from a parsed config object.
93
+ * Returns the value if valid, or null + emits a warning if unknown.
94
+ * @param {object} cfg — parsed YAML object
95
+ * @param {string} src — source label for the warning ('project' | 'global')
96
+ */
97
+ function extractArtifactLanguage(cfg, src) {
98
+ if (!('artifactLanguage' in cfg)) return null;
99
+ const val = cfg.artifactLanguage;
100
+ if (typeof val !== 'string' || val.trim() === '') {
101
+ process.stderr.write(
102
+ `[refacil-sdd-ai] warning: artifactLanguage in ${src} config must be a non-empty string — ignoring.\n`,
103
+ );
104
+ return null;
105
+ }
106
+ const trimmed = val.trim();
107
+ if (!SUPPORTED_LANGUAGES.includes(trimmed)) {
108
+ process.stderr.write(
109
+ `[refacil-sdd-ai] warning: artifactLanguage "${trimmed}" in ${src} config is not a supported language (${SUPPORTED_LANGUAGES.join(', ')}) — ignoring.\n`,
110
+ );
111
+ return null;
112
+ }
113
+ return trimmed;
114
+ }
115
+
116
+ /**
117
+ * Validate `baseBranch` from a parsed config object.
118
+ * Returns the value if valid, or null + emits a warning if invalid.
119
+ * @param {object} cfg — parsed YAML object
120
+ * @param {string} src — source label for the warning ('project' | 'global')
121
+ */
122
+ function extractBaseBranch(cfg, src) {
123
+ if (!('baseBranch' in cfg)) return null;
124
+ const val = cfg.baseBranch;
125
+ if (typeof val !== 'string' || val.trim() === '') {
126
+ process.stderr.write(
127
+ `[refacil-sdd-ai] warning: baseBranch in ${src} config must be a non-empty string — ignoring.\n`,
128
+ );
129
+ return null;
130
+ }
131
+ return val.trim();
132
+ }
133
+
134
+ /**
135
+ * Load branch configuration with cascade (project > global > defaults) and source tracking.
136
+ *
137
+ * Returns:
138
+ * ```
139
+ * {
140
+ * protectedBranches: string[],
141
+ * baseBranch: string,
142
+ * sources: {
143
+ * protectedBranches: 'project' | 'global' | 'default',
144
+ * baseBranch: 'project' | 'global' | 'default',
145
+ * }
146
+ * }
147
+ * ```
148
+ *
149
+ * Never throws — all errors are handled internally.
150
+ *
151
+ * @param {string} projectRoot — absolute path to the project root
152
+ */
153
+ function loadBranchConfigWithSources(projectRoot) {
154
+ const projectConfigPath = path.join(projectRoot, 'refacil-sdd', 'config.yaml');
155
+ const globalConfigPath = path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml');
156
+
157
+ const projectCfg = readConfigFile(projectConfigPath);
158
+ const globalCfg = readConfigFile(globalConfigPath);
159
+
160
+ // --- protectedBranches ---
161
+ let protectedBranches = null;
162
+ let protectedBranchesSource = 'default';
163
+
164
+ if (projectCfg !== null) {
165
+ const val = extractProtectedBranches(projectCfg, 'project');
166
+ if (val !== null) {
167
+ protectedBranches = val;
168
+ protectedBranchesSource = 'project';
169
+ }
170
+ }
171
+
172
+ if (protectedBranches === null && globalCfg !== null) {
173
+ const val = extractProtectedBranches(globalCfg, 'global');
174
+ if (val !== null) {
175
+ protectedBranches = val;
176
+ protectedBranchesSource = 'global';
177
+ }
178
+ }
179
+
180
+ if (protectedBranches === null) {
181
+ protectedBranches = DEFAULT_PROTECTED_BRANCHES.slice();
182
+ protectedBranchesSource = 'default';
183
+ }
184
+
185
+ if (protectedBranches.length === 0) {
186
+ process.stderr.write('[refacil-sdd-ai] warning: protectedBranches is empty — no branches will be protected.\n');
187
+ }
188
+
189
+ // --- baseBranch ---
190
+ let baseBranch = null;
191
+ let baseBranchSource = 'default';
192
+
193
+ if (projectCfg !== null) {
194
+ const val = extractBaseBranch(projectCfg, 'project');
195
+ if (val !== null) {
196
+ baseBranch = val;
197
+ baseBranchSource = 'project';
198
+ }
199
+ }
200
+
201
+ if (baseBranch === null && globalCfg !== null) {
202
+ const val = extractBaseBranch(globalCfg, 'global');
203
+ if (val !== null) {
204
+ baseBranch = val;
205
+ baseBranchSource = 'global';
206
+ }
207
+ }
208
+
209
+ if (baseBranch === null) {
210
+ baseBranch = DEFAULT_BASE_BRANCH;
211
+ baseBranchSource = 'default';
212
+ }
213
+
214
+ // --- artifactLanguage ---
215
+ let artifactLanguage = null;
216
+ let artifactLanguageSource = 'default';
217
+
218
+ if (projectCfg !== null) {
219
+ const val = extractArtifactLanguage(projectCfg, 'project');
220
+ if (val !== null) {
221
+ artifactLanguage = val;
222
+ artifactLanguageSource = 'project';
223
+ }
224
+ }
225
+
226
+ if (artifactLanguage === null && globalCfg !== null) {
227
+ const val = extractArtifactLanguage(globalCfg, 'global');
228
+ if (val !== null) {
229
+ artifactLanguage = val;
230
+ artifactLanguageSource = 'global';
231
+ }
232
+ }
233
+
234
+ if (artifactLanguage === null) {
235
+ artifactLanguage = DEFAULT_ARTIFACT_LANGUAGE;
236
+ artifactLanguageSource = 'default';
237
+ }
238
+
239
+ return {
240
+ protectedBranches,
241
+ baseBranch,
242
+ artifactLanguage,
243
+ sources: {
244
+ protectedBranches: protectedBranchesSource,
245
+ baseBranch: baseBranchSource,
246
+ artifactLanguage: artifactLanguageSource,
247
+ },
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Load branch configuration (no source tracking).
253
+ * Returns { protectedBranches, baseBranch }.
254
+ * Never throws.
255
+ *
256
+ * @param {string} projectRoot — absolute path to the project root
257
+ */
258
+ function loadBranchConfig(projectRoot) {
259
+ const { protectedBranches, baseBranch, artifactLanguage } = loadBranchConfigWithSources(projectRoot);
260
+ return { protectedBranches, baseBranch, artifactLanguage };
261
+ }
262
+
263
+ module.exports = {
264
+ parseYaml,
265
+ readConfigFile,
266
+ loadBranchConfig,
267
+ loadBranchConfigWithSources,
268
+ extractArtifactLanguage,
269
+ DEFAULT_PROTECTED_BRANCHES,
270
+ DEFAULT_BASE_BRANCH,
271
+ SUPPORTED_LANGUAGES,
272
+ DEFAULT_ARTIFACT_LANGUAGE,
273
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "refacil-sdd-ai",
3
- "version": "4.5.8",
3
+ "version": "5.0.1",
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
 
@@ -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`)
@@ -45,9 +45,14 @@ Communicate the final agreed slug to the user before generating.
45
45
 
46
46
  ### Step 2: Delegate to the refacil-proposer sub-agent
47
47
 
48
+ Before delegating, resolve the artifact language:
49
+
50
+ Run `refacil-sdd-ai sdd config --json` and read the `artifactLanguage` field. If the command fails or the field is missing/unknown, use `english`.
51
+
48
52
  Invoke the `refacil-proposer` sub-agent passing it:
49
53
  - `changeName`: the valid slug agreed in Step 1.5.
50
54
  - `description`: complete description of the change (from Step 1 or from `$ARGUMENTS`).
55
+ - `artifactLanguage`: the resolved language (e.g. `english` or `spanish`). Pass it explicitly so the sub-agent uses it immediately, before it reads AGENTS.md.
51
56
 
52
57
  The sub-agent:
53
58
  - Explores the codebase (reads `AGENTS.md`, detects relevant files and conventions) before generating.
@@ -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`.