refacil-sdd-ai 5.2.3 → 5.3.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/NOTICE.md +46 -0
- package/README.md +209 -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 +46 -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 +429 -44
- 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 +195 -19
- package/lib/kapso.js +241 -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 +39 -64
- package/skills/archive/SKILL.md +74 -48
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +476 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +31 -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 +74 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +63 -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 +157 -0
- package/skills/test/SKILL.md +35 -10
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +78 -41
- package/templates/compact-guidance.md +10 -0
- package/templates/methodology-guide.md +5 -0
package/lib/bus/broker.js
CHANGED
|
@@ -6,13 +6,17 @@ const crypto = require('crypto');
|
|
|
6
6
|
|
|
7
7
|
const HOME_DIR = path.join(os.homedir(), '.refacil-sdd-ai');
|
|
8
8
|
const BUS_INFO_PATH = path.join(HOME_DIR, 'bus.json');
|
|
9
|
+
const BUS_STARTUP_ERROR_PATH = path.join(HOME_DIR, 'bus-startup-error.json');
|
|
9
10
|
const SESSIONS_PATH = path.join(HOME_DIR, 'sessions.json');
|
|
11
|
+
const BUS_INFO_OWNER = 'refacil-sdd-ai';
|
|
12
|
+
const BUS_INFO_SERVICE = 'refacil-bus';
|
|
10
13
|
|
|
11
14
|
const PORT_CANDIDATES = [7821, 7822, 7823];
|
|
12
15
|
const HOST = '127.0.0.1';
|
|
13
16
|
|
|
14
17
|
const storage = require('./storage');
|
|
15
18
|
const { askHasMatchingReply } = require('./askFulfillment');
|
|
19
|
+
// session.js is lazy-required inside serveReadSpecApi to avoid crashing the CLI on every invocation
|
|
16
20
|
|
|
17
21
|
let WebSocketServer;
|
|
18
22
|
try {
|
|
@@ -27,7 +31,10 @@ function ensureHomeDir() {
|
|
|
27
31
|
|
|
28
32
|
function writeBusInfo(port) {
|
|
29
33
|
ensureHomeDir();
|
|
34
|
+
removeBusStartupError();
|
|
30
35
|
const info = {
|
|
36
|
+
owner: BUS_INFO_OWNER,
|
|
37
|
+
service: BUS_INFO_SERVICE,
|
|
31
38
|
port,
|
|
32
39
|
pid: process.pid,
|
|
33
40
|
startedAt: new Date().toISOString(),
|
|
@@ -36,6 +43,26 @@ function writeBusInfo(port) {
|
|
|
36
43
|
return info;
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
function writeBusStartupError(err) {
|
|
47
|
+
ensureHomeDir();
|
|
48
|
+
const payload = {
|
|
49
|
+
pid: process.pid,
|
|
50
|
+
code: err && err.code ? err.code : null,
|
|
51
|
+
message: err && err.message ? err.message : String(err),
|
|
52
|
+
ts: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
fs.writeFileSync(BUS_STARTUP_ERROR_PATH, JSON.stringify(payload, null, 2) + '\n');
|
|
55
|
+
return payload;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function removeBusStartupError() {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(BUS_STARTUP_ERROR_PATH);
|
|
61
|
+
} catch (_) {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
function removeBusInfo() {
|
|
40
67
|
try {
|
|
41
68
|
fs.unlinkSync(BUS_INFO_PATH);
|
|
@@ -74,6 +101,23 @@ function tryListen(server, port) {
|
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
async function pickPort(server) {
|
|
104
|
+
// If REFACIL_BUS_PORT is set, use it exclusively:
|
|
105
|
+
// - "0" → bind an OS-assigned ephemeral port (port 0)
|
|
106
|
+
// - any other numeric string → use that fixed port
|
|
107
|
+
const envPort = process.env.REFACIL_BUS_PORT;
|
|
108
|
+
if (envPort !== undefined) {
|
|
109
|
+
const portNum = parseInt(envPort, 10);
|
|
110
|
+
if (!Number.isNaN(portNum) && portNum >= 0) {
|
|
111
|
+
await tryListen(server, portNum);
|
|
112
|
+
// When port 0 is requested the OS assigns the actual port; read it back.
|
|
113
|
+
if (portNum === 0) {
|
|
114
|
+
return server.address().port;
|
|
115
|
+
}
|
|
116
|
+
return portNum;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Default: try each candidate in order.
|
|
77
121
|
for (const port of PORT_CANDIDATES) {
|
|
78
122
|
try {
|
|
79
123
|
await tryListen(server, port);
|
|
@@ -83,9 +127,16 @@ async function pickPort(server) {
|
|
|
83
127
|
throw err;
|
|
84
128
|
}
|
|
85
129
|
}
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
|
|
131
|
+
// If all fixed candidates are occupied by external processes, bind an
|
|
132
|
+
// OS-assigned port instead of killing or blaming those processes.
|
|
133
|
+
await tryListen(server, 0);
|
|
134
|
+
const ephemeralPort = server.address().port;
|
|
135
|
+
process.stderr.write(
|
|
136
|
+
`[refacil-bus] All fixed port candidates (${PORT_CANDIDATES.join(', ')}) are occupied by external processes. ` +
|
|
137
|
+
`Broker started on ephemeral port ${ephemeralPort}.\n`,
|
|
88
138
|
);
|
|
139
|
+
return ephemeralPort;
|
|
89
140
|
}
|
|
90
141
|
|
|
91
142
|
function newId() {
|
|
@@ -481,15 +532,60 @@ function onClose(state, ws) {
|
|
|
481
532
|
}
|
|
482
533
|
|
|
483
534
|
const UI_DIR = path.join(__dirname, 'ui');
|
|
535
|
+
const READ_SPEC_UI_DIR = path.join(__dirname, '..', 'spec-reader', 'ui');
|
|
484
536
|
const UI_MIME = {
|
|
485
537
|
'.html': 'text/html; charset=utf-8',
|
|
486
538
|
'.css': 'text/css; charset=utf-8',
|
|
487
539
|
'.js': 'application/javascript; charset=utf-8',
|
|
540
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
488
541
|
'.svg': 'image/svg+xml',
|
|
489
542
|
'.png': 'image/png',
|
|
490
543
|
'.ico': 'image/x-icon',
|
|
491
544
|
};
|
|
492
545
|
|
|
546
|
+
function serveReadSpecApi(sessionId, res) {
|
|
547
|
+
const { readSession, isValidSessionId } = require('../spec-reader/session');
|
|
548
|
+
if (!isValidSessionId(sessionId)) {
|
|
549
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
|
|
550
|
+
res.end(JSON.stringify({ error: 'invalid session id' }));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const payload = readSession(sessionId);
|
|
554
|
+
if (!payload) {
|
|
555
|
+
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
|
|
556
|
+
res.end(JSON.stringify({ error: 'session not found' }));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
560
|
+
res.end(JSON.stringify(payload));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function serveReadSpecUi(req, res) {
|
|
564
|
+
let urlPath = req.url.split('?')[0];
|
|
565
|
+
if (urlPath === '/read-spec' || urlPath === '/read-spec/') {
|
|
566
|
+
urlPath = '/read-spec/index.html';
|
|
567
|
+
}
|
|
568
|
+
const rel = urlPath.replace(/^\/read-spec\/?/, '');
|
|
569
|
+
const safePath = rel.replace(/\.\./g, '');
|
|
570
|
+
const filePath = path.join(READ_SPEC_UI_DIR, safePath || 'index.html');
|
|
571
|
+
if (!filePath.startsWith(READ_SPEC_UI_DIR)) {
|
|
572
|
+
res.writeHead(403);
|
|
573
|
+
res.end('forbidden');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
fs.readFile(filePath, (err, data) => {
|
|
577
|
+
if (err) {
|
|
578
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
579
|
+
res.end('not found');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
583
|
+
const mime = UI_MIME[ext] || 'application/octet-stream';
|
|
584
|
+
res.writeHead(200, { 'content-type': mime });
|
|
585
|
+
res.end(data);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
493
589
|
function serveUi(req, res) {
|
|
494
590
|
let urlPath = req.url.split('?')[0];
|
|
495
591
|
if (urlPath === '/' || urlPath === '') urlPath = '/index.html';
|
|
@@ -515,9 +611,11 @@ function serveUi(req, res) {
|
|
|
515
611
|
|
|
516
612
|
async function start() {
|
|
517
613
|
if (!WebSocketServer) {
|
|
518
|
-
|
|
614
|
+
const err = new Error(
|
|
519
615
|
"Dependencia 'ws' no encontrada. Instala con: npm install -g ws (o npm install ws en el paquete)",
|
|
520
616
|
);
|
|
617
|
+
err.code = 'WS_MISSING';
|
|
618
|
+
throw err;
|
|
521
619
|
}
|
|
522
620
|
|
|
523
621
|
const state = createState();
|
|
@@ -542,6 +640,16 @@ async function start() {
|
|
|
542
640
|
|
|
543
641
|
const server = http.createServer((req, res) => {
|
|
544
642
|
if (req.method === 'GET') {
|
|
643
|
+
const urlPath = req.url.split('?')[0];
|
|
644
|
+
const apiMatch = urlPath.match(/^\/api\/read-spec\/([^/]+)$/);
|
|
645
|
+
if (apiMatch) {
|
|
646
|
+
serveReadSpecApi(apiMatch[1], res);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (urlPath === '/read-spec' || urlPath.startsWith('/read-spec/')) {
|
|
650
|
+
serveReadSpecUi(req, res);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
545
653
|
serveUi(req, res);
|
|
546
654
|
return;
|
|
547
655
|
}
|
|
@@ -578,11 +686,18 @@ async function start() {
|
|
|
578
686
|
|
|
579
687
|
module.exports = {
|
|
580
688
|
start,
|
|
689
|
+
serveReadSpecApi,
|
|
690
|
+
serveReadSpecUi,
|
|
581
691
|
BUS_INFO_PATH,
|
|
692
|
+
BUS_STARTUP_ERROR_PATH,
|
|
693
|
+
BUS_INFO_OWNER,
|
|
694
|
+
BUS_INFO_SERVICE,
|
|
582
695
|
SESSIONS_PATH,
|
|
583
696
|
HOME_DIR,
|
|
584
697
|
PORT_CANDIDATES,
|
|
585
698
|
HOST,
|
|
699
|
+
writeBusStartupError,
|
|
700
|
+
removeBusStartupError,
|
|
586
701
|
};
|
|
587
702
|
|
|
588
703
|
if (require.main === module) {
|
|
@@ -593,6 +708,9 @@ if (require.main === module) {
|
|
|
593
708
|
);
|
|
594
709
|
})
|
|
595
710
|
.catch((err) => {
|
|
711
|
+
try {
|
|
712
|
+
writeBusStartupError(err);
|
|
713
|
+
} catch (_) {}
|
|
596
714
|
process.stderr.write(`Error arrancando broker: ${err.message}\n`);
|
|
597
715
|
process.exit(1);
|
|
598
716
|
});
|
package/lib/bus/spawn.js
CHANGED
|
@@ -1,121 +1,189 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const net = require('net');
|
|
4
|
-
const { spawn } = require('child_process');
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const {
|
|
6
|
+
BUS_INFO_PATH,
|
|
7
|
+
BUS_STARTUP_ERROR_PATH,
|
|
8
|
+
BUS_INFO_OWNER,
|
|
9
|
+
BUS_INFO_SERVICE,
|
|
10
|
+
HOST,
|
|
11
|
+
} = require('./broker');
|
|
12
|
+
|
|
13
|
+
function readBusInfo() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(BUS_INFO_PATH, 'utf8');
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
} catch (_) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readStartupError() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = fs.readFileSync(BUS_STARTUP_ERROR_PATH, 'utf8');
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
} catch (_) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isRefacilBrokerInfo(info) {
|
|
32
|
+
return !!(
|
|
33
|
+
info &&
|
|
34
|
+
info.owner === BUS_INFO_OWNER &&
|
|
35
|
+
info.service === BUS_INFO_SERVICE
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cleanStartupError() {
|
|
40
|
+
try {
|
|
41
|
+
fs.unlinkSync(BUS_STARTUP_ERROR_PATH);
|
|
42
|
+
} catch (_) {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isProcessAlive(pid) {
|
|
46
|
+
if (!pid) return false;
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return err.code === 'EPERM';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkPort(port, timeoutMs = 500) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const socket = new net.Socket();
|
|
58
|
+
let done = false;
|
|
59
|
+
const finish = (ok) => {
|
|
60
|
+
if (done) return;
|
|
61
|
+
done = true;
|
|
62
|
+
try { socket.destroy(); } catch (_) {}
|
|
63
|
+
resolve(ok);
|
|
64
|
+
};
|
|
65
|
+
socket.setTimeout(timeoutMs);
|
|
66
|
+
socket.once('connect', () => finish(true));
|
|
67
|
+
socket.once('timeout', () => finish(false));
|
|
68
|
+
socket.once('error', () => finish(false));
|
|
69
|
+
socket.connect(port, HOST);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function isBrokerAlive() {
|
|
74
|
+
const info = readBusInfo();
|
|
75
|
+
if (!info) return { alive: false };
|
|
76
|
+
if (!isRefacilBrokerInfo(info)) return { alive: false, staleInfo: info };
|
|
77
|
+
if (!isProcessAlive(info.pid)) return { alive: false, staleInfo: info };
|
|
78
|
+
const portOk = await checkPort(info.port);
|
|
79
|
+
if (!portOk) return { alive: false, staleInfo: info };
|
|
80
|
+
return { alive: true, info };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sleep(ms) {
|
|
84
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function waitForBroker(maxMs = 3000) {
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
while (Date.now() - start < maxMs) {
|
|
90
|
+
const status = await isBrokerAlive();
|
|
91
|
+
if (status.alive) return status.info;
|
|
92
|
+
await sleep(100);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cleanStaleInfo() {
|
|
98
|
+
try {
|
|
99
|
+
fs.unlinkSync(BUS_INFO_PATH);
|
|
100
|
+
} catch (_) {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hasWsDependency(packageRoot) {
|
|
104
|
+
try {
|
|
105
|
+
require.resolve('ws', { paths: [packageRoot] });
|
|
106
|
+
return true;
|
|
107
|
+
} catch (_) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildBrokerStartError(packageRoot, startupError) {
|
|
113
|
+
if (!hasWsDependency(packageRoot) || (startupError && startupError.code === 'WS_MISSING')) {
|
|
114
|
+
return new Error(
|
|
115
|
+
'No se pudo iniciar el broker porque falta la dependencia "ws". Instala las dependencias del paquete refacil-sdd-ai.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (startupError && startupError.code === 'EADDRINUSE') {
|
|
120
|
+
return new Error(
|
|
121
|
+
`No se pudo iniciar el broker porque el puerto solicitado está ocupado: ${startupError.message}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (startupError && startupError.message) {
|
|
126
|
+
return new Error(`No se pudo iniciar el broker: ${startupError.message}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return new Error(
|
|
130
|
+
'No se pudo iniciar el broker: el proceso no publicó bus.json ni reportó una causa de arranque.',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function ensureBroker(packageRoot) {
|
|
135
|
+
const status = await isBrokerAlive();
|
|
136
|
+
if (status.alive) return { info: status.info, started: false };
|
|
137
|
+
|
|
138
|
+
if (status.staleInfo) cleanStaleInfo();
|
|
139
|
+
cleanStartupError();
|
|
140
|
+
|
|
141
|
+
const cliPath = path.join(packageRoot, 'bin', 'cli.js');
|
|
142
|
+
const child = spawn(process.execPath, [cliPath, 'bus', 'serve'], {
|
|
143
|
+
detached: true,
|
|
144
|
+
stdio: 'ignore',
|
|
145
|
+
windowsHide: true,
|
|
146
|
+
});
|
|
147
|
+
child.unref();
|
|
148
|
+
|
|
149
|
+
const info = await waitForBroker(5000);
|
|
150
|
+
if (!info) {
|
|
151
|
+
throw buildBrokerStartError(packageRoot, readStartupError());
|
|
152
|
+
}
|
|
153
|
+
return { info, started: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function stopBroker() {
|
|
157
|
+
const info = readBusInfo();
|
|
158
|
+
if (!info) return { stopped: false, reason: 'no-info' };
|
|
159
|
+
if (!isRefacilBrokerInfo(info)) {
|
|
160
|
+
cleanStaleInfo();
|
|
161
|
+
return { stopped: false, reason: 'foreign-info', info };
|
|
162
|
+
}
|
|
163
|
+
if (!isProcessAlive(info.pid)) {
|
|
164
|
+
cleanStaleInfo();
|
|
165
|
+
return { stopped: false, reason: 'not-alive', info };
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
process.kill(info.pid, 'SIGTERM');
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return { stopped: false, reason: err.message, info };
|
|
171
|
+
}
|
|
172
|
+
// En Windows, SIGTERM termina sin ejecutar handlers del broker, así que
|
|
173
|
+
// limpiamos el bus.json desde aquí para dejar el estado consistente.
|
|
174
|
+
cleanStaleInfo();
|
|
175
|
+
return { stopped: true, info };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
readBusInfo,
|
|
180
|
+
isBrokerAlive,
|
|
181
|
+
ensureBroker,
|
|
182
|
+
stopBroker,
|
|
183
|
+
waitForBroker,
|
|
184
|
+
cleanStaleInfo,
|
|
185
|
+
readStartupError,
|
|
186
|
+
cleanStartupError,
|
|
187
|
+
isRefacilBrokerInfo,
|
|
188
|
+
buildBrokerStartError,
|
|
189
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Active changes directory: refacil-sdd/changes, or openspec/changes as legacy fallback.
|
|
8
|
+
* @param {string} projectRoot
|
|
9
|
+
* @returns {string|null}
|
|
10
|
+
*/
|
|
11
|
+
function resolveChangesDir(projectRoot) {
|
|
12
|
+
const sddChangesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
|
|
13
|
+
const legacyChangesDir = path.join(projectRoot, 'openspec', 'changes');
|
|
14
|
+
if (fs.existsSync(sddChangesDir)) return sddChangesDir;
|
|
15
|
+
if (fs.existsSync(legacyChangesDir)) return legacyChangesDir;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} changesDir
|
|
21
|
+
* @param {string} changeName
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function changeNeedsReview(changesDir, changeName) {
|
|
25
|
+
const changePath = path.join(changesDir, changeName);
|
|
26
|
+
if (fs.existsSync(path.join(changePath, '.review-passed'))) return false;
|
|
27
|
+
|
|
28
|
+
// Only block if implementation has started (at least one task done).
|
|
29
|
+
// Pure proposals (all tasks pending) and bug fixes without tasks.md do not block push.
|
|
30
|
+
const tasksPath = path.join(changePath, 'tasks.md');
|
|
31
|
+
if (!fs.existsSync(tasksPath)) return false;
|
|
32
|
+
|
|
33
|
+
const tasksContent = fs.readFileSync(tasksPath, 'utf8');
|
|
34
|
+
const done = (tasksContent.match(/^- \[x\]/gm) || []).length;
|
|
35
|
+
return done > 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} projectRoot
|
|
40
|
+
* @returns {string[]}
|
|
41
|
+
*/
|
|
42
|
+
function listActiveChangesMissingReview(projectRoot) {
|
|
43
|
+
const changesDir = resolveChangesDir(projectRoot);
|
|
44
|
+
if (!changesDir || !fs.existsSync(changesDir)) return [];
|
|
45
|
+
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const active = entries.filter((e) => e.isDirectory() && e.name !== 'archive');
|
|
54
|
+
return active.filter((e) => changeNeedsReview(changesDir, e.name)).map((e) => e.name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string[]} names
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function buildReviewBlockReason(names) {
|
|
62
|
+
if (names.length === 0) return '';
|
|
63
|
+
|
|
64
|
+
const joined = names.join(', ');
|
|
65
|
+
if (names.length === 1) {
|
|
66
|
+
return (
|
|
67
|
+
`[refacil-sdd-ai] Review pending for: ${joined}. ` +
|
|
68
|
+
'Stop the push and run /refacil:review on that change before pushing code. ' +
|
|
69
|
+
'If the review passes, retry the git push. ' +
|
|
70
|
+
'If the review requires corrections, report the findings to the user and DO NOT retry the push.'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
`[refacil-sdd-ai] Multiple changes without approved review: ${joined}. ` +
|
|
76
|
+
'Stop the push and ask the user to explicitly select which change they want to push. ' +
|
|
77
|
+
'Then run /refacil:review <change-name> for that specific change and retry the push. ' +
|
|
78
|
+
'Do not run automatic review without explicit selection when there is more than one pending change.'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} command
|
|
84
|
+
* @param {string} projectRoot
|
|
85
|
+
* @returns {{ decision: 'block', reason: string }|null}
|
|
86
|
+
*/
|
|
87
|
+
function evaluateGitPushReview(command, projectRoot) {
|
|
88
|
+
if (!command || !/git\s+push/.test(command)) return null;
|
|
89
|
+
|
|
90
|
+
const missing = listActiveChangesMissingReview(projectRoot);
|
|
91
|
+
if (missing.length === 0) return null;
|
|
92
|
+
|
|
93
|
+
return { decision: 'block', reason: buildReviewBlockReason(missing) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
resolveChangesDir,
|
|
98
|
+
changeNeedsReview,
|
|
99
|
+
listActiveChangesMissingReview,
|
|
100
|
+
buildReviewBlockReason,
|
|
101
|
+
evaluateGitPushReview,
|
|
102
|
+
};
|