refacil-sdd-ai 5.2.3 → 5.3.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/NOTICE.md +46 -0
- package/README.md +210 -42
- package/agents/auditor.md +46 -0
- package/agents/debugger.md +41 -1
- package/agents/implementer.md +76 -10
- package/agents/investigator.md +36 -0
- package/agents/proposer.md +56 -2
- package/agents/tester.md +45 -8
- package/agents/validator.md +67 -13
- package/bin/cli.js +396 -84
- package/lib/bus/broker.js +121 -3
- package/lib/bus/spawn.js +189 -121
- package/lib/check-review.js +102 -0
- package/lib/codegraph-telemetry.js +135 -0
- package/lib/codegraph.js +273 -0
- package/lib/commands/autopilot.js +120 -0
- package/lib/commands/bus.js +29 -36
- package/lib/commands/compact.js +185 -46
- package/lib/commands/read-spec.js +352 -0
- package/lib/commands/sdd.js +600 -47
- package/lib/compact-guidance.js +122 -77
- package/lib/config.js +136 -0
- package/lib/global-paths.js +56 -20
- package/lib/hooks.js +26 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +196 -19
- package/lib/kapso.js +308 -0
- package/lib/methodology-migration-pending.js +13 -0
- package/lib/open-browser.js +32 -0
- package/lib/opencode-migrate.js +148 -0
- package/lib/opencode-plugin/index.js +84 -104
- package/lib/opencode-plugin/rules.js +236 -0
- package/lib/project-root.js +154 -0
- package/lib/repo-ide-sync.js +5 -0
- package/lib/spec-reader/lang.js +72 -0
- package/lib/spec-reader/md-parser.js +299 -0
- package/lib/spec-reader/session.js +139 -0
- package/lib/spec-reader/ui/app.js +685 -0
- package/lib/spec-reader/ui/index.html +59 -0
- package/lib/spec-reader/ui/mixed-lang.js +200 -0
- package/lib/spec-reader/ui/model-cache.js +117 -0
- package/lib/spec-reader/ui/style.css +294 -0
- package/lib/spec-reader/ui/supertonic-helper.js +565 -0
- package/lib/spec-sync.js +258 -0
- package/lib/test-scope.js +713 -0
- package/lib/testing-policy-sync.js +14 -2
- package/package.json +5 -3
- package/skills/apply/SKILL.md +50 -65
- package/skills/archive/SKILL.md +84 -50
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +505 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +35 -13
- package/skills/inbox/SKILL.md +9 -0
- package/skills/join/SKILL.md +1 -1
- package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
- package/skills/prereqs/SKILL.md +1 -1
- package/skills/propose/SKILL.md +82 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +71 -25
- package/skills/review/checklist.md +2 -2
- package/skills/say/SKILL.md +40 -4
- package/skills/setup/SKILL.md +59 -5
- package/skills/setup/troubleshooting.md +11 -3
- package/skills/stats/SKILL.md +160 -0
- package/skills/status/SKILL.md +116 -0
- package/skills/test/SKILL.md +38 -11
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +85 -40
- package/templates/compact-guidance.md +10 -0
- package/templates/methodology-guide.md +5 -0
package/lib/installer.js
CHANGED
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
globalClaudeDir,
|
|
8
|
+
globalCursorDir,
|
|
9
|
+
globalOpenCodeDir,
|
|
10
|
+
globalCodexDir,
|
|
11
|
+
globalSddVersionPath,
|
|
12
|
+
validateOpenCodeConfigDir,
|
|
13
|
+
} = require('./global-paths');
|
|
14
|
+
const { migrateOpenCodeLegacyArtifacts } = require('./opencode-migrate');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_IDE_DIRS = ['claude', 'cursor', 'opencode', 'codex'];
|
|
7
17
|
const { convertAgentToToml } = require('./toml-converter');
|
|
8
18
|
|
|
9
19
|
const SKILLS = [
|
|
@@ -19,6 +29,8 @@ const SKILLS = [
|
|
|
19
29
|
'archive',
|
|
20
30
|
'bug',
|
|
21
31
|
'up-code',
|
|
32
|
+
'autopilot',
|
|
33
|
+
'read-spec',
|
|
22
34
|
'join',
|
|
23
35
|
'say',
|
|
24
36
|
'ask',
|
|
@@ -26,6 +38,8 @@ const SKILLS = [
|
|
|
26
38
|
'inbox',
|
|
27
39
|
'attend',
|
|
28
40
|
'update',
|
|
41
|
+
'stats',
|
|
42
|
+
'status',
|
|
29
43
|
];
|
|
30
44
|
|
|
31
45
|
const AGENTS = [
|
|
@@ -70,12 +84,17 @@ function copyDir(src, dest) {
|
|
|
70
84
|
* Install skills into global IDE directories.
|
|
71
85
|
* @param {string} packageRoot - path to the refacil-sdd-ai package
|
|
72
86
|
* @param {string} homeDir - user home directory (injectable for testing; default: os.homedir())
|
|
73
|
-
* @param {string[]} [ideDirs] - which IDEs to install for (
|
|
87
|
+
* @param {string[]} [ideDirs] - which IDEs to install for (default: all four IDEs)
|
|
74
88
|
* @returns {number} number of skills installed
|
|
75
89
|
*/
|
|
76
90
|
function installSkills(packageRoot, homeDir, ideDirs) {
|
|
77
91
|
const resolvedHome = homeDir || os.homedir();
|
|
78
|
-
const dirs = ideDirs ||
|
|
92
|
+
const dirs = ideDirs || DEFAULT_IDE_DIRS;
|
|
93
|
+
const installOpenCode =
|
|
94
|
+
(dirs.includes('opencode') || dirs.includes('.opencode')) && validateOpenCodeConfigDir();
|
|
95
|
+
if (installOpenCode) {
|
|
96
|
+
migrateOpenCodeLegacyArtifacts(resolvedHome);
|
|
97
|
+
}
|
|
79
98
|
let installed = 0;
|
|
80
99
|
|
|
81
100
|
for (const skill of SKILLS) {
|
|
@@ -88,7 +107,7 @@ function installSkills(packageRoot, homeDir, ideDirs) {
|
|
|
88
107
|
if (dirs.includes('cursor') || dirs.includes('.cursor')) {
|
|
89
108
|
copyDir(srcDir, path.join(globalCursorDir(resolvedHome), 'skills', `refacil-${skill}`));
|
|
90
109
|
}
|
|
91
|
-
if (
|
|
110
|
+
if (installOpenCode) {
|
|
92
111
|
// OpenCode: byte-for-byte copy (same as Claude Code — no transformation needed)
|
|
93
112
|
copyDir(srcDir, path.join(globalOpenCodeDir(resolvedHome), 'skills', `refacil-${skill}`));
|
|
94
113
|
}
|
|
@@ -129,7 +148,17 @@ function installOpenCodeJson(projectRoot) {
|
|
|
129
148
|
merged['$schema'] = sddKeys['$schema'];
|
|
130
149
|
}
|
|
131
150
|
|
|
132
|
-
|
|
151
|
+
// CA-16: idempotency guard — skip write if serialized content is identical to what is on disk.
|
|
152
|
+
const proposed = JSON.stringify(merged, null, 2) + '\n';
|
|
153
|
+
if (fs.existsSync(ocJsonPath)) {
|
|
154
|
+
try {
|
|
155
|
+
const onDisk = fs.readFileSync(ocJsonPath, 'utf8');
|
|
156
|
+
if (onDisk === proposed) return;
|
|
157
|
+
} catch (_) {
|
|
158
|
+
// CR-02 pattern: if read fails, proceed to write.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
fs.writeFileSync(ocJsonPath, proposed);
|
|
133
162
|
}
|
|
134
163
|
|
|
135
164
|
// Claude Code: tools allowlist granular, model: sonnet|opus|haiku
|
|
@@ -239,12 +268,17 @@ function transformFrontmatterForOpenCode(content) {
|
|
|
239
268
|
* Install agents into global IDE directories.
|
|
240
269
|
* @param {string} packageRoot - path to the refacil-sdd-ai package
|
|
241
270
|
* @param {string} homeDir - user home directory (injectable for testing; default: os.homedir())
|
|
242
|
-
* @param {string[]} [ideDirs] - which IDEs to install for (
|
|
271
|
+
* @param {string[]} [ideDirs] - which IDEs to install for (default: all four IDEs)
|
|
243
272
|
* @returns {number} number of agents installed
|
|
244
273
|
*/
|
|
245
274
|
function installAgents(packageRoot, homeDir, ideDirs) {
|
|
246
275
|
const resolvedHome = homeDir || os.homedir();
|
|
247
|
-
const dirs = ideDirs ||
|
|
276
|
+
const dirs = ideDirs || DEFAULT_IDE_DIRS;
|
|
277
|
+
const installOpenCode =
|
|
278
|
+
(dirs.includes('opencode') || dirs.includes('.opencode')) && validateOpenCodeConfigDir();
|
|
279
|
+
if (installOpenCode) {
|
|
280
|
+
migrateOpenCodeLegacyArtifacts(resolvedHome);
|
|
281
|
+
}
|
|
248
282
|
|
|
249
283
|
const claudeAgentsDir = path.join(globalClaudeDir(resolvedHome), 'agents');
|
|
250
284
|
const cursorAgentsDir = path.join(globalCursorDir(resolvedHome), 'agents');
|
|
@@ -253,7 +287,7 @@ function installAgents(packageRoot, homeDir, ideDirs) {
|
|
|
253
287
|
|
|
254
288
|
if (dirs.includes('claude') || dirs.includes('.claude')) fs.mkdirSync(claudeAgentsDir, { recursive: true });
|
|
255
289
|
if (dirs.includes('cursor') || dirs.includes('.cursor')) fs.mkdirSync(cursorAgentsDir, { recursive: true });
|
|
256
|
-
if (
|
|
290
|
+
if (installOpenCode) fs.mkdirSync(openCodeAgentsDir, { recursive: true });
|
|
257
291
|
if (dirs.includes('codex') || dirs.includes('.codex')) fs.mkdirSync(codexAgentsDir, { recursive: true });
|
|
258
292
|
|
|
259
293
|
let installed = 0;
|
|
@@ -273,7 +307,7 @@ function installAgents(packageRoot, homeDir, ideDirs) {
|
|
|
273
307
|
transformFrontmatterForCursor(content),
|
|
274
308
|
);
|
|
275
309
|
}
|
|
276
|
-
if (
|
|
310
|
+
if (installOpenCode) {
|
|
277
311
|
fs.writeFileSync(
|
|
278
312
|
path.join(openCodeAgentsDir, `refacil-${agent}.md`),
|
|
279
313
|
transformFrontmatterForOpenCode(content),
|
|
@@ -365,10 +399,12 @@ function removeProjectLevelArtifacts(projectRoot) {
|
|
|
365
399
|
} catch (_) {}
|
|
366
400
|
}
|
|
367
401
|
|
|
368
|
-
// Remove .opencode/plugins/refacil-hooks.js if present
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
402
|
+
// Remove .opencode/plugins/refacil-hooks.js and refacil-check-review.js if present
|
|
403
|
+
for (const name of ['refacil-hooks.js', 'refacil-check-review.js']) {
|
|
404
|
+
const ocPlugin = path.join(projectRoot, '.opencode', 'plugins', name);
|
|
405
|
+
if (fs.existsSync(ocPlugin)) {
|
|
406
|
+
try { fs.unlinkSync(ocPlugin); removed++; } catch (_) {}
|
|
407
|
+
}
|
|
372
408
|
}
|
|
373
409
|
// Remove .opencode/plugins/ if now empty
|
|
374
410
|
const ocPluginsDir = path.join(projectRoot, '.opencode', 'plugins');
|
|
@@ -412,10 +448,12 @@ function removeOpenCodeArtifacts(projectRoot) {
|
|
|
412
448
|
} catch (_) {}
|
|
413
449
|
}
|
|
414
450
|
|
|
415
|
-
// Remove .opencode/plugins/refacil-hooks.js
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
451
|
+
// Remove .opencode/plugins/refacil-hooks.js and refacil-check-review.js
|
|
452
|
+
for (const name of ['refacil-hooks.js', 'refacil-check-review.js']) {
|
|
453
|
+
const pluginFile = path.join(projectRoot, '.opencode', 'plugins', name);
|
|
454
|
+
if (fs.existsSync(pluginFile)) {
|
|
455
|
+
try { fs.unlinkSync(pluginFile); } catch (_) {}
|
|
456
|
+
}
|
|
419
457
|
}
|
|
420
458
|
|
|
421
459
|
// Revert SDD-AI keys from .opencode/opencode.json (currently only $schema key, leave file if other keys remain)
|
|
@@ -439,6 +477,23 @@ function writeGuideFile(destPath, header, label) {
|
|
|
439
477
|
`# ${header}\n\n` +
|
|
440
478
|
'Contexto completo del proyecto: ver `AGENTS.md`.\n' +
|
|
441
479
|
'Si no existe, ejecuta `/refacil:setup`.\n';
|
|
480
|
+
|
|
481
|
+
// Idempotency guard: skip write if normalized content is identical (CA-01, CA-02, CR-05).
|
|
482
|
+
// Normalization is comparison-only — the content written to disk is never altered.
|
|
483
|
+
if (fs.existsSync(destPath)) {
|
|
484
|
+
try {
|
|
485
|
+
const existing = fs.readFileSync(destPath, 'utf8');
|
|
486
|
+
const normalizedExisting = existing.replace(/\r\n/g, '\n');
|
|
487
|
+
const normalizedNew = content.replace(/\r\n/g, '\n');
|
|
488
|
+
if (normalizedExisting === normalizedNew) {
|
|
489
|
+
// Content is identical (or differs only in CRLF vs LF) — skip write.
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
} catch (_) {
|
|
493
|
+
// CR-02: if read fails, treat as non-identical and proceed to write.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
442
497
|
fs.writeFileSync(destPath, content);
|
|
443
498
|
console.log(` ${label} generado.`);
|
|
444
499
|
return true;
|
|
@@ -573,12 +628,12 @@ function removeSkills(projectRoot) {
|
|
|
573
628
|
/**
|
|
574
629
|
* Remove skills from global IDE directories.
|
|
575
630
|
* @param {string} homeDir - user home directory (injectable for testing)
|
|
576
|
-
* @param {string[]} [ideDirs] - which IDEs to remove from (
|
|
631
|
+
* @param {string[]} [ideDirs] - which IDEs to remove from (default: all four IDEs)
|
|
577
632
|
* @returns {number} number of skill directories removed
|
|
578
633
|
*/
|
|
579
634
|
function removeGlobalSkills(homeDir, ideDirs) {
|
|
580
635
|
const resolvedHome = homeDir || os.homedir();
|
|
581
|
-
const dirs = ideDirs ||
|
|
636
|
+
const dirs = ideDirs || DEFAULT_IDE_DIRS;
|
|
582
637
|
let removed = 0;
|
|
583
638
|
|
|
584
639
|
for (const skill of SKILLS) {
|
|
@@ -610,6 +665,42 @@ function removeGlobalSkills(homeDir, ideDirs) {
|
|
|
610
665
|
return removed;
|
|
611
666
|
}
|
|
612
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Remove all refacil-* OpenCode artifacts from the global OpenCode config directory.
|
|
670
|
+
* Tolerant: each removal is wrapped in try/catch.
|
|
671
|
+
* @param {string} [homeDir] - user home directory (injectable for testing)
|
|
672
|
+
*/
|
|
673
|
+
function removeOpenCodeGlobalArtifacts(homeDir) {
|
|
674
|
+
const resolvedHome = homeDir || os.homedir();
|
|
675
|
+
const ocDir = globalOpenCodeDir(resolvedHome);
|
|
676
|
+
|
|
677
|
+
for (const skill of SKILLS) {
|
|
678
|
+
const skillDir = path.join(ocDir, 'skills', `refacil-${skill}`);
|
|
679
|
+
if (fs.existsSync(skillDir)) {
|
|
680
|
+
try { fs.rmSync(skillDir, { recursive: true }); } catch (_) {}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const agentsDir = path.join(ocDir, 'agents');
|
|
685
|
+
if (fs.existsSync(agentsDir)) {
|
|
686
|
+
try {
|
|
687
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
688
|
+
for (const entry of entries) {
|
|
689
|
+
if (entry.isFile() && entry.name.startsWith('refacil-') && entry.name.endsWith('.md')) {
|
|
690
|
+
try { fs.unlinkSync(path.join(agentsDir, entry.name)); } catch (_) {}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (_) {}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
for (const name of ['refacil-hooks.js', 'refacil-check-review.js', 'rules.js']) {
|
|
697
|
+
const pluginFile = path.join(ocDir, 'plugins', name);
|
|
698
|
+
if (fs.existsSync(pluginFile)) {
|
|
699
|
+
try { fs.unlinkSync(pluginFile); } catch (_) {}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
613
704
|
/**
|
|
614
705
|
* Remove all refacil-* Codex artifacts from the global ~/.codex directory.
|
|
615
706
|
* Tolerant: each removal is wrapped in try/catch.
|
|
@@ -730,6 +821,89 @@ function checkNodeVersion() {
|
|
|
730
821
|
return true;
|
|
731
822
|
}
|
|
732
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Prompt the user for their preferred CodeGraph integration mode.
|
|
826
|
+
* Skippable with --yes / --defaults flags or when stdout is not a TTY.
|
|
827
|
+
* Persists the answer via writeConfigValue from lib/config.js.
|
|
828
|
+
*
|
|
829
|
+
* @param {string} homeDir - user home directory
|
|
830
|
+
* @returns {Promise<void>}
|
|
831
|
+
*/
|
|
832
|
+
async function promptCodegraphMode(homeDir) {
|
|
833
|
+
const { writeConfigValue, CODEGRAPH_MODES, DEFAULT_CODEGRAPH_MODE } = require('./config');
|
|
834
|
+
const resolvedHome = homeDir || os.homedir();
|
|
835
|
+
|
|
836
|
+
const skipFlags = ['--yes', '--defaults'];
|
|
837
|
+
if (skipFlags.some((f) => process.argv.includes(f)) || !process.stdout.isTTY) {
|
|
838
|
+
writeConfigValue('codegraphMode', DEFAULT_CODEGRAPH_MODE, resolvedHome);
|
|
839
|
+
if (DEFAULT_CODEGRAPH_MODE !== 'disabled') {
|
|
840
|
+
try {
|
|
841
|
+
const cg = require('./codegraph');
|
|
842
|
+
const { readSelectedIDEs } = require('./global-paths');
|
|
843
|
+
cg.registerMcp(readSelectedIDEs(resolvedHome) || ['.claude', '.cursor', '.opencode', '.codex'], resolvedHome);
|
|
844
|
+
if (DEFAULT_CODEGRAPH_MODE === 'enabled' && cg.isInstalled()) {
|
|
845
|
+
cg.init(require('path').resolve('.'));
|
|
846
|
+
}
|
|
847
|
+
} catch (_) {}
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
console.log('\n CodeGraph integration (optional — https://github.com/colbymchenry/codegraph)');
|
|
853
|
+
console.log(' When enabled, exploratory sub-agents (investigate, propose, debug) will');
|
|
854
|
+
console.log(' prefer CodeGraph symbol queries over raw file reads, reducing token usage ~71%.');
|
|
855
|
+
console.log(` Mode options: ${CODEGRAPH_MODES.join(' | ')} (default: ${DEFAULT_CODEGRAPH_MODE})\n`);
|
|
856
|
+
|
|
857
|
+
let selectedMode;
|
|
858
|
+
|
|
859
|
+
try {
|
|
860
|
+
const clack = require('@clack/prompts');
|
|
861
|
+
const result = await clack.select({
|
|
862
|
+
message: 'CodeGraph integration mode:',
|
|
863
|
+
options: [
|
|
864
|
+
{ value: 'enabled', label: 'enabled — auto-index every repo on setup (recommended)' },
|
|
865
|
+
{ value: 'per-repo', label: 'per-repo — ask once per project (set via /refacil:setup)' },
|
|
866
|
+
{ value: 'disabled', label: 'disabled — never use CodeGraph' },
|
|
867
|
+
],
|
|
868
|
+
initialValue: DEFAULT_CODEGRAPH_MODE,
|
|
869
|
+
});
|
|
870
|
+
if (clack.isCancel(result)) {
|
|
871
|
+
console.log(' CodeGraph config prompt cancelled. Using default (enabled).\n');
|
|
872
|
+
selectedMode = DEFAULT_CODEGRAPH_MODE;
|
|
873
|
+
} else {
|
|
874
|
+
selectedMode = result;
|
|
875
|
+
}
|
|
876
|
+
} catch (_) {
|
|
877
|
+
// @clack/prompts not available — use inline readline fallback
|
|
878
|
+
const readline = require('readline');
|
|
879
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
880
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
881
|
+
|
|
882
|
+
const answer = await ask(
|
|
883
|
+
` CodeGraph mode [${DEFAULT_CODEGRAPH_MODE}] (${CODEGRAPH_MODES.join('/')}): `,
|
|
884
|
+
);
|
|
885
|
+
rl.close();
|
|
886
|
+
const trimmed = answer.trim().toLowerCase();
|
|
887
|
+
selectedMode = CODEGRAPH_MODES.includes(trimmed) ? trimmed : DEFAULT_CODEGRAPH_MODE;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
writeConfigValue('codegraphMode', selectedMode, resolvedHome);
|
|
891
|
+
if (selectedMode !== 'disabled') {
|
|
892
|
+
try {
|
|
893
|
+
const cg = require('./codegraph');
|
|
894
|
+
const { readSelectedIDEs } = require('./global-paths');
|
|
895
|
+
cg.registerMcp(readSelectedIDEs(resolvedHome) || ['.claude', '.cursor', '.opencode', '.codex'], resolvedHome);
|
|
896
|
+
// Auto-index current repo in background when mode is enabled and CLI is installed
|
|
897
|
+
if (selectedMode === 'enabled' && cg.isInstalled()) {
|
|
898
|
+
const projectRoot = require('path').resolve('.');
|
|
899
|
+
cg.init(projectRoot);
|
|
900
|
+
console.log(' CodeGraph: indexing current repo in background (~30s).');
|
|
901
|
+
}
|
|
902
|
+
} catch (_) {}
|
|
903
|
+
}
|
|
904
|
+
console.log(` CodeGraph mode set to: ${selectedMode}\n`);
|
|
905
|
+
}
|
|
906
|
+
|
|
733
907
|
module.exports = {
|
|
734
908
|
SKILLS,
|
|
735
909
|
AGENTS,
|
|
@@ -741,10 +915,12 @@ module.exports = {
|
|
|
741
915
|
transformFrontmatterForOpenCode,
|
|
742
916
|
installAgents,
|
|
743
917
|
removeOpenCodeArtifacts,
|
|
918
|
+
removeOpenCodeGlobalArtifacts,
|
|
744
919
|
removeCodexArtifacts,
|
|
745
920
|
removeProjectLevelArtifacts,
|
|
746
921
|
createClaudeMd,
|
|
747
922
|
createCursorRules,
|
|
923
|
+
writeGuideFile,
|
|
748
924
|
readRepoVersion,
|
|
749
925
|
writeRepoVersion,
|
|
750
926
|
readGlobalVersion,
|
|
@@ -755,4 +931,5 @@ module.exports = {
|
|
|
755
931
|
removeOpenspecLegacyAssets,
|
|
756
932
|
checkClaudeCodeVersion,
|
|
757
933
|
checkNodeVersion,
|
|
934
|
+
promptCodegraphMode,
|
|
758
935
|
};
|
package/lib/kapso.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const readline = require('readline/promises');
|
|
8
|
+
|
|
9
|
+
const KAPSO_ENV_DIR = path.join(os.homedir(), '.refacil-sdd-ai');
|
|
10
|
+
const KAPSO_ENV_FILE = path.join(KAPSO_ENV_DIR, 'kapso.env');
|
|
11
|
+
const KAPSO_LOG_FILE = path.join(KAPSO_ENV_DIR, 'autopilot.log');
|
|
12
|
+
const KAPSO_API_BASE = 'https://api.kapso.ai/meta/whatsapp/v24.0';
|
|
13
|
+
|
|
14
|
+
const PHONE_REGEX = /^\+\d{7,15}$/;
|
|
15
|
+
|
|
16
|
+
// ─── credentials ─────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function readCredentials() {
|
|
19
|
+
if (!fs.existsSync(KAPSO_ENV_FILE)) return null;
|
|
20
|
+
const creds = {};
|
|
21
|
+
for (const line of fs.readFileSync(KAPSO_ENV_FILE, 'utf8').split('\n')) {
|
|
22
|
+
const m = line.match(/^([^#][^=]+)=(.+)$/);
|
|
23
|
+
if (m) creds[m[1].trim()] = m[2].trim();
|
|
24
|
+
}
|
|
25
|
+
const { KAPSO_API_KEY, KAPSO_PHONE_NUMBER_ID, NOTIFY_PHONE } = creds;
|
|
26
|
+
if (!KAPSO_API_KEY || !KAPSO_PHONE_NUMBER_ID || !NOTIFY_PHONE) return null;
|
|
27
|
+
return { apiKey: KAPSO_API_KEY, phoneNumberId: KAPSO_PHONE_NUMBER_ID, notifyPhone: NOTIFY_PHONE };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── http ─────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function postMessage(creds, bodyText) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const payload = JSON.stringify({
|
|
35
|
+
messaging_product: 'whatsapp',
|
|
36
|
+
recipient_type: 'individual',
|
|
37
|
+
to: creds.notifyPhone,
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: { body: bodyText },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const urlPath = `${KAPSO_API_BASE}/${creds.phoneNumberId}/messages`;
|
|
43
|
+
const parsed = new URL(urlPath);
|
|
44
|
+
|
|
45
|
+
const req = https.request(
|
|
46
|
+
{
|
|
47
|
+
hostname: parsed.hostname,
|
|
48
|
+
path: parsed.pathname,
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
'X-API-Key': creds.apiKey,
|
|
53
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
(res) => {
|
|
57
|
+
let data = '';
|
|
58
|
+
res.on('data', (chunk) => (data += chunk));
|
|
59
|
+
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
req.on('error', reject);
|
|
64
|
+
req.write(payload);
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── validation ────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
// Unfilled template token left in the command, e.g. "<changeName>" or "<repoSlug>".
|
|
72
|
+
const PLACEHOLDER_REGEX = /<[^>]+>/;
|
|
73
|
+
|
|
74
|
+
function isBlankOrPlaceholder(value) {
|
|
75
|
+
if (value === null || value === undefined) return true;
|
|
76
|
+
const s = String(value).trim();
|
|
77
|
+
if (s === '') return true;
|
|
78
|
+
if (PLACEHOLDER_REGEX.test(s)) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Returns a list of human-readable problems with the notify payload.
|
|
83
|
+
// Empty list ⇒ the payload is complete enough to send.
|
|
84
|
+
//
|
|
85
|
+
// This is the guard against the autopilot failure mode where the skill fires
|
|
86
|
+
// `kapso notify` once with unfilled <placeholders>/empty flags (sending a junk
|
|
87
|
+
// WhatsApp message) and again with the real values. We reject the incomplete
|
|
88
|
+
// call so it never reaches postMessage().
|
|
89
|
+
function validateNotifyOpts(type, opts) {
|
|
90
|
+
const problems = [];
|
|
91
|
+
const required =
|
|
92
|
+
type === 'success'
|
|
93
|
+
? ['repo', 'change', 'branch', 'tasks']
|
|
94
|
+
: ['repo', 'change', 'branch', 'phase'];
|
|
95
|
+
|
|
96
|
+
for (const field of required) {
|
|
97
|
+
if (isBlankOrPlaceholder(opts[field])) {
|
|
98
|
+
problems.push(`--${field} faltante, vacío o sin rellenar`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// tasks must look like "done/total" (e.g. "5/5"), not a placeholder or prose.
|
|
103
|
+
if (
|
|
104
|
+
type === 'success' &&
|
|
105
|
+
!isBlankOrPlaceholder(opts.tasks) &&
|
|
106
|
+
!/^\d+\/\d+$/.test(String(opts.tasks).trim())
|
|
107
|
+
) {
|
|
108
|
+
problems.push(`--tasks con formato inválido (esperado "done/total"): "${opts.tasks}"`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return problems;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── log ─────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function appendLog(msg) {
|
|
117
|
+
try {
|
|
118
|
+
fs.mkdirSync(KAPSO_ENV_DIR, { recursive: true });
|
|
119
|
+
fs.appendFileSync(KAPSO_LOG_FILE, `${new Date().toISOString()} ${msg}\n`, 'utf8');
|
|
120
|
+
} catch (_) {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── public commands ──────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
async function setup() {
|
|
126
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
console.log('\n Kapso setup — WhatsApp notification credentials\n');
|
|
130
|
+
|
|
131
|
+
let apiKey = '';
|
|
132
|
+
while (!apiKey.trim()) {
|
|
133
|
+
apiKey = await rl.question(' KAPSO_API_KEY: ');
|
|
134
|
+
if (!apiKey.trim()) console.log(' KAPSO_API_KEY cannot be empty. Try again.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let phoneNumberId = '';
|
|
138
|
+
while (!phoneNumberId.trim()) {
|
|
139
|
+
phoneNumberId = await rl.question(' KAPSO_PHONE_NUMBER_ID: ');
|
|
140
|
+
if (!phoneNumberId.trim()) console.log(' KAPSO_PHONE_NUMBER_ID cannot be empty. Try again.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let notifyPhone = '';
|
|
144
|
+
while (true) {
|
|
145
|
+
notifyPhone = await rl.question(
|
|
146
|
+
" NOTIFY_PHONE (E.164 format, e.g. +5731XXXXXXXX, or 'cancelar' to abort): ",
|
|
147
|
+
);
|
|
148
|
+
if (notifyPhone.trim().toLowerCase() === 'cancelar') {
|
|
149
|
+
console.log('\n Setup cancelled.\n');
|
|
150
|
+
rl.close();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (PHONE_REGEX.test(notifyPhone.trim())) break;
|
|
154
|
+
console.log(' Invalid format. Must start with + followed by 7-15 digits (E.164). Try again.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fs.mkdirSync(KAPSO_ENV_DIR, { recursive: true });
|
|
158
|
+
|
|
159
|
+
const content =
|
|
160
|
+
[`KAPSO_API_KEY=${apiKey.trim()}`, `KAPSO_PHONE_NUMBER_ID=${phoneNumberId.trim()}`, `NOTIFY_PHONE=${notifyPhone.trim()}`].join('\n') +
|
|
161
|
+
'\n';
|
|
162
|
+
|
|
163
|
+
fs.writeFileSync(KAPSO_ENV_FILE, content, { encoding: 'utf8' });
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
fs.chmodSync(KAPSO_ENV_FILE, 0o600);
|
|
167
|
+
} catch (_) {}
|
|
168
|
+
|
|
169
|
+
console.log('\n ✅ Kapso configurado en ~/.refacil-sdd-ai/kapso.env\n');
|
|
170
|
+
} finally {
|
|
171
|
+
rl.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// kapso preflight
|
|
176
|
+
// Returns credential status and a 24h window notice (sync, always exits 0).
|
|
177
|
+
// sync — no network I/O
|
|
178
|
+
function preflight() {
|
|
179
|
+
const creds = readCredentials();
|
|
180
|
+
if (!creds) {
|
|
181
|
+
return { kapsoEnabled: false };
|
|
182
|
+
}
|
|
183
|
+
const preflightMessage =
|
|
184
|
+
'WhatsApp solo entrega mensajes de texto libre dentro de las **24 horas** de tu último mensaje **entrante** al número de notificación. ' +
|
|
185
|
+
'Si han pasado más de 24 horas desde que enviaste un mensaje a ese número, envía cualquier mensaje (p.ej. `ok`) antes de una ejecución larga. ' +
|
|
186
|
+
'Sin eso, el pipeline igual corre pero la alerta final de WhatsApp puede no llegar.';
|
|
187
|
+
return { kapsoEnabled: true, notifyPhone: creds.notifyPhone, preflightMessage };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// kapso notify success --repo <slug> --change <name> --branch <branch>
|
|
191
|
+
// --tasks <done>/<total> --duration <min>
|
|
192
|
+
// [--pr <url>] [--improvements <n>] [--apply <n>] [--test <n>] [--review <n>]
|
|
193
|
+
// [--warnings "<w1>|<w2>"]
|
|
194
|
+
//
|
|
195
|
+
// kapso notify failure --repo <slug> --change <name> --branch <branch>
|
|
196
|
+
// --phase <phase> --last-commit "<commit>" --error "<summary>"
|
|
197
|
+
async function notify(type, opts) {
|
|
198
|
+
if (type !== 'success' && type !== 'failure') {
|
|
199
|
+
console.error(` Unknown notify type: "${type}". Use "success" or "failure".`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Reject incomplete/placeholder payloads BEFORE doing anything else so a junk
|
|
204
|
+
// notification is never sent. Throwing surfaces a clear, actionable error to
|
|
205
|
+
// the caller (autopilot) without ever hitting the WhatsApp API.
|
|
206
|
+
const problems = validateNotifyOpts(type, opts);
|
|
207
|
+
if (problems.length > 0) {
|
|
208
|
+
appendLog(`kapso notify ${type} REJECTED (datos inválidos): ${problems.join('; ')}`);
|
|
209
|
+
throw new Error(
|
|
210
|
+
`kapso notify ${type}: datos inválidos o incompletos — ${problems.join('; ')}. ` +
|
|
211
|
+
'No se envió la notificación. Reintenta con todos los flags requeridos rellenados con valores reales.',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const creds = readCredentials();
|
|
216
|
+
if (!creds) {
|
|
217
|
+
appendLog('kapso notify skipped: no credentials');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let message;
|
|
222
|
+
|
|
223
|
+
if (type === 'success') {
|
|
224
|
+
const {
|
|
225
|
+
repo = '',
|
|
226
|
+
change = '',
|
|
227
|
+
branch = '',
|
|
228
|
+
tasks = '0/0',
|
|
229
|
+
duration = '0',
|
|
230
|
+
pr = null,
|
|
231
|
+
apply = 0,
|
|
232
|
+
test = 0,
|
|
233
|
+
review = 0,
|
|
234
|
+
warnings = '',
|
|
235
|
+
} = opts;
|
|
236
|
+
|
|
237
|
+
const totalImprovements = Number(apply) + Number(test) + Number(review);
|
|
238
|
+
const warningList = warnings ? warnings.split('|').map((w) => w.trim()).filter(Boolean) : [];
|
|
239
|
+
|
|
240
|
+
let msg = [
|
|
241
|
+
'✅ SDD Autopilot completado',
|
|
242
|
+
'',
|
|
243
|
+
`Repo: ${repo}`,
|
|
244
|
+
`Change: ${change}`,
|
|
245
|
+
`Branch: ${branch}`,
|
|
246
|
+
`Tareas: ${tasks}`,
|
|
247
|
+
'Tests: pass',
|
|
248
|
+
'Review: ✓',
|
|
249
|
+
`Mejoras aplicadas: ${totalImprovements} (apply: ${apply}, test: ${test}, review: ${review})`,
|
|
250
|
+
`Duración: ${duration} min`,
|
|
251
|
+
].join('\n');
|
|
252
|
+
|
|
253
|
+
if (warningList.length > 0) {
|
|
254
|
+
msg += `\n\n⚠️ Warnings (${warningList.length}):\n` + warningList.slice(0, 3).join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
msg += `\n\nPR: ${pr || 'sin PR (solo push)'}`;
|
|
258
|
+
message = msg;
|
|
259
|
+
} else if (type === 'failure') {
|
|
260
|
+
const {
|
|
261
|
+
repo = '',
|
|
262
|
+
change = '',
|
|
263
|
+
branch = '',
|
|
264
|
+
phase = '',
|
|
265
|
+
lastCommit = '',
|
|
266
|
+
error = '',
|
|
267
|
+
} = opts;
|
|
268
|
+
|
|
269
|
+
message = [
|
|
270
|
+
'❌ SDD Autopilot falló',
|
|
271
|
+
'',
|
|
272
|
+
`Repo: ${repo}`,
|
|
273
|
+
`Change: ${change}`,
|
|
274
|
+
`Fase fallida: ${phase}`,
|
|
275
|
+
`Branch: ${branch}`,
|
|
276
|
+
`Último commit: ${lastCommit}`,
|
|
277
|
+
'',
|
|
278
|
+
'Detalle:',
|
|
279
|
+
error,
|
|
280
|
+
'',
|
|
281
|
+
'El árbol de trabajo quedó en su estado actual para revisión.',
|
|
282
|
+
].join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const result = await postMessage(creds, message);
|
|
287
|
+
if (result.status >= 200 && result.status < 300) {
|
|
288
|
+
appendLog(`kapso notify ${type} sent OK`);
|
|
289
|
+
console.log(` ✅ Kapso: ${type} notification sent`);
|
|
290
|
+
} else {
|
|
291
|
+
appendLog(`kapso notify ${type} FAILED (HTTP ${result.status}): ${result.body}`);
|
|
292
|
+
console.warn(` ⚠️ Kapso API error (HTTP ${result.status}): ${result.body}`);
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
appendLog(`kapso notify ${type} NETWORK ERROR: ${err.message}`);
|
|
296
|
+
console.warn(` ⚠️ Kapso network error: ${err.message}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
setup,
|
|
302
|
+
preflight,
|
|
303
|
+
notify,
|
|
304
|
+
readCredentials,
|
|
305
|
+
validateNotifyOpts,
|
|
306
|
+
isBlankOrPlaceholder,
|
|
307
|
+
PHONE_REGEX,
|
|
308
|
+
};
|
|
@@ -95,6 +95,19 @@ function methodologyMigrationPending(root) {
|
|
|
95
95
|
reasons.push(`commands opsx sobrantes: ${extraOpsx.join(', ')}`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// CodeGraph: CLI not installed when user has opted in (enabled or per-repo)
|
|
99
|
+
try {
|
|
100
|
+
const { loadBranchConfigWithSources } = require('./config');
|
|
101
|
+
const { isInstalled: cgIsInstalled } = require('./codegraph');
|
|
102
|
+
const cfgInfo = loadBranchConfigWithSources(root);
|
|
103
|
+
const cgMode = cfgInfo.codegraphMode;
|
|
104
|
+
if ((cgMode === 'enabled' || cgMode === 'per-repo') && !cgIsInstalled()) {
|
|
105
|
+
reasons.push(`CodeGraph CLI no instalado (modo: ${cgMode}) — ejecuta /refacil:update para instalarlo`);
|
|
106
|
+
}
|
|
107
|
+
} catch (_) {
|
|
108
|
+
// Tolerant — CodeGraph check must not break migration detection
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
return { pending: reasons.length > 0, reasons };
|
|
99
112
|
}
|
|
100
113
|
|