refacil-sdd-ai 5.2.2 → 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.
Files changed (76) hide show
  1. package/NOTICE.md +46 -0
  2. package/README.md +209 -42
  3. package/agents/auditor.md +46 -0
  4. package/agents/debugger.md +41 -1
  5. package/agents/implementer.md +76 -10
  6. package/agents/investigator.md +36 -0
  7. package/agents/proposer.md +46 -2
  8. package/agents/tester.md +45 -8
  9. package/agents/validator.md +67 -13
  10. package/bin/cli.js +428 -83
  11. package/bin/postinstall.js +20 -0
  12. package/lib/bus/broker.js +121 -3
  13. package/lib/bus/spawn.js +189 -121
  14. package/lib/check-review.js +102 -0
  15. package/lib/codegraph-telemetry.js +135 -0
  16. package/lib/codegraph.js +273 -0
  17. package/lib/commands/autopilot.js +120 -0
  18. package/lib/commands/bus.js +29 -36
  19. package/lib/commands/compact.js +185 -46
  20. package/lib/commands/read-spec.js +352 -0
  21. package/lib/commands/sdd.js +429 -44
  22. package/lib/compact-guidance.js +122 -77
  23. package/lib/config.js +136 -0
  24. package/lib/global-paths.js +56 -20
  25. package/lib/hooks.js +32 -4
  26. package/lib/ide-detection.js +1 -1
  27. package/lib/ignore-files.js +5 -1
  28. package/lib/installer.js +202 -19
  29. package/lib/kapso.js +241 -0
  30. package/lib/methodology-migration-pending.js +13 -0
  31. package/lib/open-browser.js +32 -0
  32. package/lib/opencode-migrate.js +148 -0
  33. package/lib/opencode-plugin/index.js +84 -104
  34. package/lib/opencode-plugin/rules.js +236 -0
  35. package/lib/project-root.js +154 -0
  36. package/lib/repo-ide-sync.js +5 -0
  37. package/lib/spec-reader/lang.js +72 -0
  38. package/lib/spec-reader/md-parser.js +299 -0
  39. package/lib/spec-reader/session.js +139 -0
  40. package/lib/spec-reader/ui/app.js +685 -0
  41. package/lib/spec-reader/ui/index.html +59 -0
  42. package/lib/spec-reader/ui/mixed-lang.js +200 -0
  43. package/lib/spec-reader/ui/model-cache.js +117 -0
  44. package/lib/spec-reader/ui/style.css +294 -0
  45. package/lib/spec-reader/ui/supertonic-helper.js +565 -0
  46. package/lib/spec-sync.js +258 -0
  47. package/lib/test-scope.js +713 -0
  48. package/lib/testing-policy-sync.js +14 -2
  49. package/package.json +6 -3
  50. package/skills/apply/SKILL.md +39 -64
  51. package/skills/archive/SKILL.md +74 -48
  52. package/skills/ask/SKILL.md +43 -8
  53. package/skills/autopilot/SKILL.md +476 -0
  54. package/skills/bug/SKILL.md +52 -53
  55. package/skills/explore/SKILL.md +48 -1
  56. package/skills/guide/SKILL.md +31 -13
  57. package/skills/inbox/SKILL.md +9 -0
  58. package/skills/join/SKILL.md +1 -1
  59. package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
  60. package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
  61. package/skills/prereqs/SKILL.md +1 -1
  62. package/skills/propose/SKILL.md +74 -19
  63. package/skills/read-spec/SKILL.md +76 -0
  64. package/skills/reply/SKILL.md +42 -9
  65. package/skills/review/SKILL.md +63 -25
  66. package/skills/review/checklist.md +2 -2
  67. package/skills/say/SKILL.md +40 -4
  68. package/skills/setup/SKILL.md +59 -5
  69. package/skills/setup/troubleshooting.md +11 -3
  70. package/skills/stats/SKILL.md +157 -0
  71. package/skills/test/SKILL.md +35 -10
  72. package/skills/up-code/SKILL.md +20 -13
  73. package/skills/update/SKILL.md +32 -1
  74. package/skills/verify/SKILL.md +78 -41
  75. package/templates/compact-guidance.md +10 -0
  76. package/templates/methodology-guide.md +5 -0
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CodeGraph telemetry — analogous to lib/compact/telemetry.js.
5
+ *
6
+ * Logs events to ~/.refacil-sdd-ai/codegraph.log (JSONL).
7
+ * Uses the same try/catch-silent pattern and ensureDir() approach
8
+ * as compact telemetry. Never throws to the caller.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const HOME_DIR = path.join(os.homedir(), '.refacil-sdd-ai');
16
+ const LOG_PATH = path.join(HOME_DIR, 'codegraph.log');
17
+
18
+ function ensureDir() {
19
+ try {
20
+ fs.mkdirSync(HOME_DIR, { recursive: true });
21
+ } catch (_) {}
22
+ }
23
+
24
+ /**
25
+ * Log a CodeGraph usage event.
26
+ * @param {string} skillId — e.g. 'investigator' | 'proposer' | 'debugger'
27
+ * @param {boolean} hasGraph — whether .codegraph/ was present in the repo
28
+ * @param {number} toolCallsCount — number of CodeGraph tool calls made this session
29
+ * @param {number} estimatedTokensSaved — estimated tokens saved vs reading files directly
30
+ */
31
+ function logEvent(skillId, hasGraph, toolCallsCount, estimatedTokensSaved) {
32
+ try {
33
+ ensureDir();
34
+ const line =
35
+ JSON.stringify({
36
+ ts: new Date().toISOString(),
37
+ skillId: skillId || 'unknown',
38
+ hasGraph: Boolean(hasGraph),
39
+ toolCallsCount: toolCallsCount || 0,
40
+ estimatedTokensSaved: estimatedTokensSaved || 0,
41
+ }) + '\n';
42
+ fs.appendFileSync(LOG_PATH, line);
43
+ } catch (_) {
44
+ // Telemetry must never break the caller
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Read all log entries as an array of parsed objects.
50
+ * Returns [] if the log file does not exist or cannot be read.
51
+ * @returns {object[]}
52
+ */
53
+ function readLog() {
54
+ try {
55
+ const content = fs.readFileSync(LOG_PATH, 'utf8');
56
+ return content
57
+ .split('\n')
58
+ .filter(Boolean)
59
+ .map((line) => {
60
+ try {
61
+ return JSON.parse(line);
62
+ } catch (_) {
63
+ return null;
64
+ }
65
+ })
66
+ .filter(Boolean);
67
+ } catch (_) {
68
+ return [];
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Compute stats grouped by skillId, comparing with-graph vs without-graph.
74
+ * @returns {{
75
+ * bySkill: Record<string, { withGraph: { events: number, toolCalls: number, tokensSaved: number }, withoutGraph: { events: number } }>,
76
+ * totalEvents: number,
77
+ * totalTokensSaved: number,
78
+ * totalToolCalls: number
79
+ * }}
80
+ */
81
+ function stats() {
82
+ const entries = readLog();
83
+ const bySkill = {};
84
+ let totalEvents = 0;
85
+ let totalTokensSaved = 0;
86
+ let totalToolCalls = 0;
87
+
88
+ for (const e of entries) {
89
+ const skillId = e.skillId || 'unknown';
90
+ if (!bySkill[skillId]) {
91
+ bySkill[skillId] = {
92
+ withGraph: { events: 0, toolCalls: 0, tokensSaved: 0 },
93
+ withoutGraph: { events: 0 },
94
+ };
95
+ }
96
+
97
+ if (e.hasGraph) {
98
+ bySkill[skillId].withGraph.events++;
99
+ bySkill[skillId].withGraph.toolCalls += e.toolCallsCount || 0;
100
+ bySkill[skillId].withGraph.tokensSaved += e.estimatedTokensSaved || 0;
101
+ totalToolCalls += e.toolCallsCount || 0;
102
+ totalTokensSaved += e.estimatedTokensSaved || 0;
103
+ } else {
104
+ bySkill[skillId].withoutGraph.events++;
105
+ }
106
+
107
+ totalEvents++;
108
+ }
109
+
110
+ return {
111
+ bySkill,
112
+ totalEvents,
113
+ totalTokensSaved,
114
+ totalToolCalls,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Delete the log file.
120
+ * Silent on errors (file may not exist).
121
+ */
122
+ function clearLog() {
123
+ try {
124
+ fs.unlinkSync(LOG_PATH);
125
+ } catch (_) {}
126
+ }
127
+
128
+ module.exports = {
129
+ HOME_DIR,
130
+ LOG_PATH,
131
+ logEvent,
132
+ readLog,
133
+ stats,
134
+ clearLog,
135
+ };
@@ -0,0 +1,273 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CodeGraph abstraction layer.
5
+ *
6
+ * Wraps @colbymchenry/codegraph as an optional dependency.
7
+ * Never throws to the caller — all errors are swallowed silently.
8
+ *
9
+ * IDE support matrix (MCP registration):
10
+ * - Claude Code : ~/.claude/settings.json → mcpServers.codegraph
11
+ * - Cursor : ~/.cursor/mcp.json → mcpServers.codegraph
12
+ * - OpenCode : <globalOpenCodeDir>/opencode.jsonc → mcp.codegraph
13
+ * - Codex : ~/.codex/config.toml → mcp_servers.codegraph
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+
20
+ /**
21
+ * Check whether @colbymchenry/codegraph is installed in the current Node.js resolution chain.
22
+ * @returns {boolean}
23
+ */
24
+ function isInstalled() {
25
+ try {
26
+ const { spawnSync } = require('child_process');
27
+ // shell: true is required on Windows where npm binaries are installed as .cmd files
28
+ const result = spawnSync('codegraph', ['--version'], {
29
+ encoding: 'utf8',
30
+ stdio: ['ignore', 'pipe', 'ignore'],
31
+ timeout: 5000,
32
+ shell: true,
33
+ });
34
+ return result.status === 0;
35
+ } catch (_) {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Check whether the given repo path has already been indexed by CodeGraph.
42
+ * A repo is considered indexed when a `.codegraph/` directory exists at its root.
43
+ * @param {string} repoPath - absolute path to the project root
44
+ * @returns {boolean}
45
+ */
46
+ function isInitialized(repoPath) {
47
+ try {
48
+ return fs.existsSync(path.join(repoPath, '.codegraph'));
49
+ } catch (_) {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /** Timestamp file written inside .codegraph/ each time we trigger init. */
55
+ const LAST_INIT_FILE = '.codegraph/.refacil-last-init';
56
+
57
+ /**
58
+ * Spawn a background CodeGraph indexing process for the given repo.
59
+ * Uses the global `codegraph` CLI binary (shell: true for Windows .cmd support).
60
+ * Also writes a timestamp so isStale() can compare against git history.
61
+ * Fire-and-forget — returns immediately. Never throws.
62
+ * @param {string} repoPath - absolute path to the project root
63
+ */
64
+ function init(repoPath) {
65
+ try {
66
+ const { spawn } = require('child_process');
67
+ // `codegraph init` creates .codegraph/ (structure only, 0 nodes).
68
+ // `codegraph index` builds the actual graph (must run after init).
69
+ // For first-time: chain both commands so the graph is built in one shot.
70
+ // For re-index: only `codegraph index` (structure already exists).
71
+ const cgDir = path.join(repoPath, '.codegraph');
72
+ const isFirstTime = !fs.existsSync(cgDir);
73
+ // Use shell chaining (&&) so the second command only runs if the first succeeds.
74
+ const cmd = isFirstTime ? 'codegraph init && codegraph index' : 'codegraph index';
75
+ const child = spawn(cmd, [], {
76
+ detached: true,
77
+ stdio: 'ignore',
78
+ shell: true, // required for && chaining and Windows .cmd binaries
79
+ windowsHide: true, // suppress console window pop-ups on Windows
80
+ cwd: repoPath,
81
+ });
82
+ child.unref();
83
+ // Update the timestamp on re-index (.codegraph/ exists). For first-time,
84
+ // the directory does not exist yet so we skip — isStale() falls back to mtime.
85
+ if (!isFirstTime) {
86
+ fs.writeFileSync(path.join(cgDir, '.refacil-last-init'), new Date().toISOString());
87
+ }
88
+ } catch (_) {
89
+ // Swallow: CodeGraph not installed, spawn failed, or fs write failed
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Return true when there are git commits newer than the last time we triggered
95
+ * `codegraph init` for this repo. Always returns false when git is unavailable,
96
+ * the repo has no commits, or any error occurs — never blocks, never throws.
97
+ * @param {string} repoPath - absolute path to the project root
98
+ * @returns {boolean}
99
+ */
100
+ function isStale(repoPath) {
101
+ try {
102
+ // Determine the reference date: prefer our timestamp file, fall back to .codegraph/ mtime
103
+ let refDate;
104
+ const lastInitPath = path.join(repoPath, LAST_INIT_FILE);
105
+ if (fs.existsSync(lastInitPath)) {
106
+ const parsed = new Date(fs.readFileSync(lastInitPath, 'utf8').trim());
107
+ if (!isNaN(parsed.getTime())) refDate = parsed;
108
+ }
109
+ if (!refDate) {
110
+ const cgDir = path.join(repoPath, '.codegraph');
111
+ if (!fs.existsSync(cgDir)) return false;
112
+ refDate = fs.statSync(cgDir).mtime;
113
+ }
114
+
115
+ // Check for git commits after refDate — gracefully skip if no git
116
+ const { spawnSync } = require('child_process');
117
+ const result = spawnSync(
118
+ 'git', ['-C', repoPath, 'log', '--oneline', `--after=${refDate.toISOString()}`, '-1'],
119
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 },
120
+ );
121
+ if (result.status !== 0 || result.error) return false; // no git or git error
122
+ return result.stdout.trim().length > 0;
123
+ } catch (_) {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Return the MCP tool hint string for the agent system prompt.
130
+ * All four IDEs support MCP, so this returns a non-null value for all of them
131
+ * when CodeGraph is installed.
132
+ * Returns null when CodeGraph is not installed or any error occurs.
133
+ *
134
+ * @param {string} ide - 'claude' | 'cursor' | 'opencode' | 'codex'
135
+ * @returns {string|null}
136
+ */
137
+ function mcpEntry(ide) {
138
+ try {
139
+ if (!ide) return null;
140
+ if (!isInstalled()) return null;
141
+ return (
142
+ 'CodeGraph MCP tools available: ' +
143
+ 'codegraph_search, codegraph_callers, codegraph_callees, codegraph_context, codegraph_impact. ' +
144
+ 'Use these BEFORE Grep or Read for symbol and call-graph queries when .codegraph/ is present.'
145
+ );
146
+ } catch (_) {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ // ── MCP server registration ───────────────────────────────────────────────────
152
+
153
+ const CODEGRAPH_MCP_KEY = 'codegraph';
154
+
155
+ function _upsertMcpEntry(filePath, entry) {
156
+ let config = {};
157
+ try {
158
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
159
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) config = parsed;
160
+ } catch (_) {}
161
+ if (!config.mcpServers) config.mcpServers = {};
162
+ if (config.mcpServers[CODEGRAPH_MCP_KEY]) return; // already registered — idempotent
163
+ config.mcpServers[CODEGRAPH_MCP_KEY] = entry;
164
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
165
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
166
+ }
167
+
168
+ function _registerClaudeMcp(homeDir) {
169
+ const dir = path.join(homeDir, '.claude');
170
+ if (!fs.existsSync(dir)) return;
171
+ _upsertMcpEntry(path.join(dir, 'settings.json'), {
172
+ command: 'codegraph',
173
+ args: ['serve', '--mcp', '--path', '.'],
174
+ });
175
+ }
176
+
177
+ function _registerCursorMcp(homeDir) {
178
+ const dir = path.join(homeDir, '.cursor');
179
+ if (!fs.existsSync(dir)) return;
180
+ _upsertMcpEntry(path.join(dir, 'mcp.json'), {
181
+ type: 'stdio',
182
+ command: 'codegraph',
183
+ args: ['serve', '--mcp', '--path', '${workspaceFolder}'],
184
+ });
185
+ }
186
+
187
+ function _registerOpenCodeMcp(homeDir) {
188
+ const { globalOpenCodeDir } = require('./global-paths');
189
+ const openCodeDir = globalOpenCodeDir(homeDir);
190
+ fs.mkdirSync(openCodeDir, { recursive: true });
191
+ const configPath = path.join(openCodeDir, 'opencode.jsonc');
192
+ let config = { $schema: 'https://opencode.ai/config.json' };
193
+ try {
194
+ const raw = fs.readFileSync(configPath, 'utf8').replace(/\/\/[^\n]*/g, '');
195
+ const parsed = JSON.parse(raw);
196
+ if (parsed && typeof parsed === 'object') config = parsed;
197
+ } catch (_) {}
198
+ if (!config.mcp) config.mcp = {};
199
+ if (config.mcp[CODEGRAPH_MCP_KEY]) return;
200
+ config.mcp[CODEGRAPH_MCP_KEY] = { type: 'local', command: ['codegraph', 'serve', '--mcp'], enabled: true };
201
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
202
+ }
203
+
204
+ function _registerCodexMcp(homeDir) {
205
+ const codexDir = path.join(homeDir, '.codex');
206
+ if (!fs.existsSync(codexDir)) return;
207
+ const configPath = path.join(codexDir, 'config.toml');
208
+ const smolToml = require('smol-toml');
209
+ let config = {};
210
+ try {
211
+ config = smolToml.parse(fs.readFileSync(configPath, 'utf8'));
212
+ } catch (_) {}
213
+ if (!config.mcp_servers) config.mcp_servers = {};
214
+ if (config.mcp_servers[CODEGRAPH_MCP_KEY]) return;
215
+ config.mcp_servers[CODEGRAPH_MCP_KEY] = { command: 'codegraph', args: ['serve', '--mcp'] };
216
+ fs.writeFileSync(configPath, smolToml.stringify(config), 'utf8');
217
+ }
218
+
219
+ /**
220
+ * Register the CodeGraph MCP server entry in each selected IDE's config file.
221
+ * Idempotent — no-ops if the entry already exists or the IDE dir is absent. Never throws.
222
+ *
223
+ * Accepts both dot-prefixed (.claude) and plain (claude) IDE names — both formats are normalized.
224
+ *
225
+ * IDE → file:
226
+ * claude → ~/.claude/settings.json (mcpServers.codegraph)
227
+ * cursor → ~/.cursor/mcp.json (mcpServers.codegraph)
228
+ * opencode → <globalOpenCodeDir>/opencode.jsonc (mcp.codegraph)
229
+ * codex → ~/.codex/config.toml (mcp_servers.codegraph)
230
+ *
231
+ * @param {string[]} selectedIDEs - e.g. ['.claude','.cursor'] or ['claude','cursor']
232
+ * @param {string} [homeDir] - injectable for testing
233
+ */
234
+ function registerMcp(selectedIDEs, homeDir) {
235
+ const resolvedHome = homeDir || os.homedir();
236
+ const ides = Array.isArray(selectedIDEs) ? selectedIDEs : [];
237
+ for (const ide of ides) {
238
+ try {
239
+ const name = ide.startsWith('.') ? ide.slice(1) : ide;
240
+ if (name === 'claude') _registerClaudeMcp(resolvedHome);
241
+ else if (name === 'cursor') _registerCursorMcp(resolvedHome);
242
+ else if (name === 'opencode') _registerOpenCodeMcp(resolvedHome);
243
+ else if (name === 'codex') _registerCodexMcp(resolvedHome);
244
+ } catch (_) {}
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Write the .refacil-last-init timestamp into an existing .codegraph/ directory.
250
+ * No-op when .codegraph/ does not exist. Used by checkUpdate to stamp indexes
251
+ * that were built externally (not through our setup/init commands).
252
+ * @param {string} repoPath
253
+ */
254
+ function touchTimestamp(repoPath) {
255
+ try {
256
+ const cgDir = path.join(repoPath, '.codegraph');
257
+ if (!fs.existsSync(cgDir)) return;
258
+ const tsFile = path.join(cgDir, '.refacil-last-init');
259
+ if (!fs.existsSync(tsFile)) {
260
+ fs.writeFileSync(tsFile, new Date().toISOString());
261
+ }
262
+ } catch (_) {}
263
+ }
264
+
265
+ module.exports = {
266
+ isInstalled,
267
+ isInitialized,
268
+ isStale,
269
+ init,
270
+ touchTimestamp,
271
+ mcpEntry,
272
+ registerMcp,
273
+ };
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ // Recognized IDE identifiers. Only 'cursor' gets a dedicated message path;
4
+ // all other values (including 'unknown') fall to the generic A/B options block.
5
+ const SUPPORTED_IDES = new Set(['cursor', 'claude-code', 'codex', 'opencode', 'unknown']);
6
+
7
+ function getFlag(argv, flag, fallback) {
8
+ const idx = argv.indexOf(flag);
9
+ return idx !== -1 && argv[idx + 1] !== undefined ? argv[idx + 1] : fallback;
10
+ }
11
+
12
+ function normalizeIde(raw) {
13
+ if (!raw) return 'unknown';
14
+ const lower = raw.toLowerCase();
15
+ return SUPPORTED_IDES.has(lower) ? lower : 'unknown';
16
+ }
17
+
18
+ /**
19
+ * Shared message builder. Receives a localized strings object so both language
20
+ * variants (ES / EN) share the same structural logic — only the copy differs.
21
+ *
22
+ * @param {string} changeName
23
+ * @param {string} ide
24
+ * @param {{ header: string, ideLabel: string, phases: string, cursorLines: string[], genericLines: string[] }} strings
25
+ * @returns {string}
26
+ */
27
+ function buildReadyMessage(changeName, ide, strings) {
28
+ const lines = [
29
+ strings.header,
30
+ `Change: ${changeName}`,
31
+ `${strings.ideLabel}: ${ide}`,
32
+ ``,
33
+ strings.phases,
34
+ ``,
35
+ ];
36
+
37
+ if (ide === 'cursor') {
38
+ lines.push(...strings.cursorLines);
39
+ } else {
40
+ lines.push(...strings.genericLines);
41
+ }
42
+
43
+ return lines.join('\n');
44
+ }
45
+
46
+ function buildReadyMessageEs(changeName, ide) {
47
+ return buildReadyMessage(changeName, ide, {
48
+ header: `Autopilot listo para arrancar`,
49
+ ideLabel: `IDE detectado`,
50
+ phases: `Fases: apply → test → verify → review → archive → up-code`,
51
+ cursorLines: [
52
+ `Para continuar en esta sesión, responde con una afirmación explícita:`,
53
+ ` go · ok · start · sí · dale · arrancar`,
54
+ ``,
55
+ `Hasta que no respondas así, no inicio el pipeline.`,
56
+ ],
57
+ genericLines: [
58
+ `Opciones:`,
59
+ ` A) Ejecutar aquí (pueden aparecer prompts de permisos durante el pipeline)`,
60
+ ` → responde "go" / "ok" / "start" para iniciar en esta sesión.`,
61
+ ``,
62
+ ` B) Ejecutar headless (sin interrupciones, salida visible en nueva terminal)`,
63
+ ` → cierra esta sesión y abre el IDE con el flag de auto-permisos adecuado,`,
64
+ ` luego escribe /refacil:autopilot.`,
65
+ ],
66
+ });
67
+ }
68
+
69
+ function buildReadyMessageEn(changeName, ide) {
70
+ return buildReadyMessage(changeName, ide, {
71
+ header: `Autopilot ready to start`,
72
+ ideLabel: `Detected IDE`,
73
+ phases: `Phases: apply → test → verify → review → archive → up-code`,
74
+ cursorLines: [
75
+ `To continue in this session, reply with an explicit affirmative:`,
76
+ ` go · ok · start · yes · dale`,
77
+ ``,
78
+ `The pipeline will not start until you reply.`,
79
+ ],
80
+ genericLines: [
81
+ `Options:`,
82
+ ` A) Run here (Bash permission prompts may appear during the pipeline)`,
83
+ ` → reply "go" / "ok" / "start" to begin in this session.`,
84
+ ``,
85
+ ` B) Run headless (no interruptions, output visible in a new terminal)`,
86
+ ` → close this session and open the IDE with the appropriate auto-permissions flag,`,
87
+ ` then type /refacil:autopilot.`,
88
+ ],
89
+ });
90
+ }
91
+
92
+ function handleAutopilot(sub, argv) {
93
+ if (sub === 'ready-message') {
94
+ const changeName = getFlag(argv, '--change', '');
95
+ const idRaw = getFlag(argv, '--ide', '');
96
+ const lang = getFlag(argv, '--lang', 'es');
97
+ const ide = normalizeIde(idRaw);
98
+
99
+ if (!changeName) {
100
+ process.stderr.write(' Error: --change <changeName> is required\n');
101
+ process.exit(1);
102
+ }
103
+
104
+ let message;
105
+ if (lang === 'en') {
106
+ message = buildReadyMessageEn(changeName, ide);
107
+ } else {
108
+ message = buildReadyMessageEs(changeName, ide);
109
+ }
110
+
111
+ process.stdout.write(message + '\n');
112
+ } else {
113
+ process.stderr.write(
114
+ ` Unknown autopilot subcommand: ${sub || '(none)'}. Usage: autopilot <ready-message>\n`,
115
+ );
116
+ process.exit(1);
117
+ }
118
+ }
119
+
120
+ module.exports = { handleAutopilot };
@@ -8,6 +8,7 @@ const busClient = require('../bus/client');
8
8
  const busWatch = require('../bus/watch');
9
9
  const busPresenter = require('../bus/presenter');
10
10
  const { askHasMatchingReply } = require('../bus/askFulfillment');
11
+ const { openInBrowser } = require('../open-browser');
11
12
 
12
13
  function parseBusArgs(argv) {
13
14
  const args = {};
@@ -155,13 +156,32 @@ async function busLeave(args, packageRoot) {
155
156
  }
156
157
  }
157
158
 
158
- async function busSay(args, packageRoot) {
159
- const session = args.session || defaultSessionName();
160
- const text = args.text;
161
- if (!text) {
162
- console.error(' Uso: refacil-sdd-ai bus say --text "..." [--session <s>]');
159
+ function resolveText(args, usageHint) {
160
+ const hasText = args.text !== undefined;
161
+ const hasFromEnv = args['from-env'] !== undefined;
162
+ if (hasText && hasFromEnv) {
163
+ console.error(' Error: --text and --from-env are mutually exclusive. Use one or the other.');
164
+ process.exit(1);
165
+ }
166
+ if (hasFromEnv) {
167
+ const varName = args['from-env'];
168
+ const value = process.env[varName];
169
+ if (!value) {
170
+ console.error(` Error: environment variable "${varName}" is not set or is empty.`);
171
+ process.exit(1);
172
+ }
173
+ return value;
174
+ }
175
+ if (!hasText) {
176
+ console.error(` ${usageHint}`);
163
177
  process.exit(1);
164
178
  }
179
+ return args.text;
180
+ }
181
+
182
+ async function busSay(args, packageRoot) {
183
+ const session = args.session || defaultSessionName();
184
+ const text = resolveText(args, 'Uso: refacil-sdd-ai bus say --text "..." | --from-env VAR [--session <s>]');
165
185
  const { ws } = await connectOrDie(packageRoot);
166
186
  const reply = await busClient.sendAndWait(
167
187
  ws,
@@ -183,12 +203,12 @@ async function busSay(args, packageRoot) {
183
203
  async function busAsk(args, packageRoot) {
184
204
  const session = args.session || defaultSessionName();
185
205
  const to = args.to;
186
- const text = args.text;
187
206
  const waitSec = args.wait ? parseInt(args.wait, 10) : 0;
188
- if (!to || !text) {
189
- console.error(' Uso: refacil-sdd-ai bus ask --to <name|all> --text "..." [--wait N] [--session <s>]');
207
+ if (!to) {
208
+ console.error(' Uso: refacil-sdd-ai bus ask --to <name|all> --text "..." | --from-env VAR [--wait N] [--session <s>]');
190
209
  process.exit(1);
191
210
  }
211
+ const text = resolveText(args, 'Uso: refacil-sdd-ai bus ask --to <name|all> --text "..." | --from-env VAR [--wait N] [--session <s>]');
192
212
  const { ws } = await connectOrDie(packageRoot);
193
213
  const ack = await busClient.sendAndWait(
194
214
  ws,
@@ -232,13 +252,9 @@ async function busAsk(args, packageRoot) {
232
252
 
233
253
  async function busReply(args, packageRoot) {
234
254
  const session = args.session || defaultSessionName();
235
- const text = args.text;
236
255
  const correlationId = args.correlation || null;
237
256
  const to = args.to ? args.to.replace(/^@/, '') : null;
238
- if (!text) {
239
- console.error(' Uso: refacil-sdd-ai bus reply --text "..." [--to <name>] [--correlation <id>]');
240
- process.exit(1);
241
- }
257
+ const text = resolveText(args, 'Uso: refacil-sdd-ai bus reply --text "..." | --from-env VAR [--to <name>] [--correlation <id>]');
242
258
  const { ws } = await connectOrDie(packageRoot);
243
259
  const reply = await busClient.sendAndWait(
244
260
  ws,
@@ -432,29 +448,6 @@ async function busRooms(packageRoot) {
432
448
  }
433
449
  }
434
450
 
435
- function openInBrowser(url) {
436
- const { spawn } = require('child_process');
437
- const platform = process.platform;
438
- let cmd;
439
- let cmdArgs;
440
- if (platform === 'win32') {
441
- cmd = 'cmd';
442
- cmdArgs = ['/c', 'start', '""', url];
443
- } else if (platform === 'darwin') {
444
- cmd = 'open';
445
- cmdArgs = [url];
446
- } else {
447
- cmd = 'xdg-open';
448
- cmdArgs = [url];
449
- }
450
- try {
451
- spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
452
- return true;
453
- } catch (_) {
454
- return false;
455
- }
456
- }
457
-
458
451
  async function busView(packageRoot) {
459
452
  try {
460
453
  const { info } = await busSpawn.ensureBroker(packageRoot);