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 +38 -2
- package/agents/proposer.md +12 -0
- package/bin/cli.js +142 -1
- package/lib/commands/sdd.js +121 -46
- package/lib/config.js +273 -0
- package/package.json +3 -3
- package/skills/apply/SKILL.md +52 -5
- package/skills/bug/SKILL.md +52 -4
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +14 -10
- package/skills/propose/SKILL.md +5 -0
- package/skills/setup/SKILL.md +40 -1
- package/skills/up-code/SKILL.md +11 -7
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
|
|
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
|
|
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
|
|
package/agents/proposer.md
CHANGED
|
@@ -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
|
package/lib/commands/sdd.js
CHANGED
|
@@ -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
|
-
//
|
|
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 =
|
|
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 =
|
|
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": "
|
|
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"
|
package/skills/apply/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
package/skills/bug/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
78
|
+
If the current branch is already a working branch (`feature/*`, `fix/*`, `hotfix/*`, `refactor/*`, etc.), continue without interruption to Step 5.
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
46
|
-
- Exception for new repos: if
|
|
47
|
-
- If the exception is used, recommend creating `
|
|
48
|
-
- **NEVER** create working branches from `
|
|
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:
|
|
54
|
-
- Recommend the user create a PR to `
|
|
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
|
-
-
|
|
80
|
-
- Only if
|
|
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`)
|
package/skills/propose/SKILL.md
CHANGED
|
@@ -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.
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -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.
|
package/skills/up-code/SKILL.md
CHANGED
|
@@ -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
|
-
-
|
|
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.
|
|
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?
|
|
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
|
-
|
|
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
|
|
100
|
+
4. Show the generated link (provider-specific) to the user:
|
|
97
101
|
```
|
|
98
102
|
Create your PR here: [link]
|
|
99
103
|
|
|
100
|
-
Tip:
|
|
101
|
-
before promoting to
|
|
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`.
|