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