refacil-sdd-ai 4.4.1 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  **SDD-AI** (Specification-Driven Development with AI) packaged as a CLI.
4
4
 
5
- Installs **skills** and **sub-agents** for **Claude Code** and **Cursor** that guide the developer through a structured AI-assisted development workflow, using **`refacil-sdd/`** as the specification store, plus a **local bus** so agents across different repos can communicate with each other.
5
+ Installs **skills** and **sub-agents** for **Claude Code**, **Cursor**, and **OpenCode** that guide the developer through a structured AI-assisted development workflow, using **`refacil-sdd/`** as the specification store, plus a **local bus** so agents across different repos can communicate with each other.
6
6
 
7
7
  ---
8
8
 
9
9
  ## Requirements
10
10
 
11
11
  - **Node.js >= 20.0.0**
12
- - **Claude Code >= 2.1.89** (required by the `compact-bash` hook for silent rewrite via `updatedInput`) or **Cursor**
12
+ - One or more supported IDEs: **Claude Code >= 2.1.89**, **Cursor**, or **OpenCode**
13
13
 
14
- `refacil-sdd-ai init` checks the Claude Code version and warns if it is below 2.1.89. With an older version the rest of the methodology works, but `compact-bash` will have no effect.
14
+ `refacil-sdd-ai init` checks the Claude Code version and warns if it is below 2.1.89. With an older version the rest of the methodology works, but `compact-bash` will have no effect (Claude Code only — OpenCode and Cursor have their own hook delivery mechanisms).
15
15
 
16
16
  ---
17
17
 
@@ -25,10 +25,12 @@ npm install -g refacil-sdd-ai
25
25
 
26
26
  # 2. In the repo root
27
27
  refacil-sdd-ai init
28
- # Copies skills to .claude/ and .cursor/, creates CLAUDE.md + .cursorrules, configures hooks
29
- # Creates/updates .claudeignore and .cursorignore with base exclusions
28
+ # Interactive IDE selector (Claude Code / Cursor / OpenCode) pre-selects IDEs
29
+ # whose folder already exists. Use --all to install for all three without prompting.
30
+ # Copies skills and sub-agents to the selected IDEs, configures hooks,
31
+ # and creates/updates .claudeignore, .cursorignore and .opencodeignore.
30
32
 
31
- # 3. Restart your Claude Code or Cursor session
33
+ # 3. Restart your IDE session
32
34
  # (new skills are not detected until you restart)
33
35
 
34
36
  # 4. In the IDE
@@ -43,12 +45,12 @@ npm update -g refacil-sdd-ai
43
45
  refacil-sdd-ai update # in each repo where it is used
44
46
  ```
45
47
 
46
- In Claude Code / Cursor the `check-update` hook (every session) syncs skills and `compact-guidance`. Only if the automatic detection (`lib/methodology-migration-pending.js`) finds a pending methodology migration does it write the flag and allow `notify-update` to prompt `/refacil:update`. If there is no migration, the user is not interrupted. The `/refacil:update` skill uses `refacil-sdd-ai migration-pending` as the same criterion.
48
+ `update` detects which IDEs are installed by folder presence (`.claude/`, `.cursor/`, `.opencode/`) and only updates those — it never creates IDE directories that did not exist before. In Claude Code and Cursor the `check-update` hook (every session) syncs skills and `compact-guidance`. In OpenCode the equivalent runs via the `session.created` handler of the embedded plugin (`.opencode/plugins/refacil-hooks.js`). Only if the automatic detection (`lib/methodology-migration-pending.js`) finds a pending methodology migration does it write the flag and allow `notify-update` / `tui.prompt.append` to prompt `/refacil:update`. If there is no migration, the user is not interrupted. The `/refacil:update` skill uses `refacil-sdd-ai migration-pending` as the same criterion.
47
49
 
48
50
  ### Uninstall
49
51
 
50
52
  ```bash
51
- refacil-sdd-ai clean # in the repo (removes skills + SDD-AI hooks)
53
+ refacil-sdd-ai clean # in the repo (removes skills + SDD-AI hooks for all IDEs)
52
54
  npm uninstall -g refacil-sdd-ai
53
55
  ```
54
56
 
@@ -133,7 +135,7 @@ Run **`refacil-sdd-ai help`** for the full list including `bus` and `compact` su
133
135
 
134
136
  ## Available IDE Skills
135
137
 
136
- All invoked as `/refacil:<name>` in Claude Code or Cursor.
138
+ All invoked as `/refacil:<name>` in Claude Code, Cursor, or OpenCode.
137
139
 
138
140
  ### SDD cycle
139
141
 
@@ -170,7 +172,7 @@ Some skills delegate their heavy work to **sub-agents** that run in isolated con
170
172
 
171
173
  **Model**: `refacil-proposer` runs with `model: opusplan` (uses Opus during plan mode for highest-stakes planning, then switches to Sonnet for execution). Other sub-agents use `model: sonnet` by default.
172
174
 
173
- **Dual-platform**: `.claude/agents/refacil-*.md` uses `tools:` (granular allowlist). `.cursor/agents/refacil-*.md` is auto-generated: `readonly: true` for agents without `Edit`/`Write`, `readonly: false` for those that have them; always `model: inherit`. The installer transforms the frontmatter automatically.
175
+ **Triple-platform**: `.claude/agents/refacil-*.md` uses `tools:` (granular allowlist). `.cursor/agents/refacil-*.md` is auto-generated: `readonly: true` for agents without `Edit`/`Write`, `readonly: false` for those that have them; always `model: inherit`. `.opencode/agents/refacil-*.md` is auto-generated via `transformFrontmatterForOpenCode()`: converts `tools:` to a `permission:` block (`edit: allow/deny`, `bash: allow/deny`, `webfetch: deny`), adds `mode: subagent`, adds `hidden: true` for internal agents, and removes `model:`. The installer transforms the frontmatter automatically for all three IDEs.
174
176
 
175
177
  **Two-pass `refacil:bug` flow**: the wrapper first invokes the sub-agent in `investigation` mode (writes nothing) → the user confirms the hypothesis and approves the fix → the wrapper validates the working branch → invokes the sub-agent in `fix` mode to implement.
176
178
 
@@ -259,16 +261,23 @@ From there, the full cycle is:
259
261
 
260
262
  ## Automatic Hooks
261
263
 
262
- Installed in `.claude/settings.json` **and** `.cursor/settings.json` during `init` / `update`. Apply to both **Claude Code** and **Cursor**.
264
+ Installed during `init` / `update` for each selected IDE. The same four behaviors are active in Claude Code, Cursor, and OpenCode — each through its own delivery mechanism.
263
265
 
264
- | Hook | Event | What it does |
265
- |---|---|---|
266
- | `check-update` | `SessionStart` | On startup deletes `.refacil-pending-update` if no migration is pending (stale flags). Then: npm check, sync skills, **compact-guidance**. If skills were synced **and** a migration is pending, writes the flag for `notify-update`. |
267
- | `notify-update` | `UserPromptSubmit` (Claude Code) / `beforeSubmitPrompt` (Cursor) | If the flag exists **and** a methodology migration is pending (same table as `/refacil:update`), injects the instruction or pauses the first message; if the sync happened without a migration, the flag is not created or is discarded silently. |
268
- | `compact-bash` | `PreToolUse` (Bash) | Silently rewrites bare Bash commands via `updatedInput`. No extra turns, the IDE does not see the change. Requires Claude Code >= 2.1.89. |
269
- | `check-review` | `PreToolUse` (Bash) | Intercepts `git push` and blocks if `.review-passed` is missing in any active change. |
266
+ | Behavior | Claude Code | Cursor | OpenCode |
267
+ |---|---|---|---|
268
+ | **check-update** | `SessionStart` hook in `.claude/settings.json` | `SessionStart` hook in `.cursor/settings.json` | `session.created` handler in `.opencode/plugins/refacil-hooks.js` |
269
+ | **notify-update** | `UserPromptSubmit` hook | `beforeSubmitPrompt` hook | `tui.prompt.append` handler |
270
+ | **compact-bash** | `PreToolUse` (Bash) hook | `PreToolUse` (Bash) hook | `tool.execute.before` handler for bash tool |
271
+ | **check-review** | `PreToolUse` (Bash) hook | `PreToolUse` (Bash) hook | `tool.execute.before` handler for bash tool |
272
+
273
+ | Behavior | What it does |
274
+ |---|---|
275
+ | `check-update` | On startup: deletes `.refacil-pending-update` if no migration is pending (stale flags). Then: npm check, sync skills, **compact-guidance**. If skills were synced **and** a migration is pending, writes the flag for `notify-update`. Always refreshes the flag content when a migration is pending (keeps the `to` version current). |
276
+ | `notify-update` | If the flag exists **and** a methodology migration is pending (same table as `/refacil:update`), injects the instruction before the agent processes the next user message; if the sync happened without a migration, the flag is not created or is discarded silently. |
277
+ | `compact-bash` | Silently rewrites bare Bash commands. No extra turns, the IDE does not see the change. Requires Claude Code >= 2.1.89 for the `updatedInput` path. |
278
+ | `check-review` | Intercepts `git push` and blocks if `.review-passed` is missing in any active change. |
270
279
 
271
- All four hooks are installed in `.claude/settings.json` (Claude Code) and `.cursor/settings.json` (Cursor) with the same parametric logic.
280
+ > **OpenCode plugin**: a single file (`.opencode/plugins/refacil-hooks.js`) implements all four behaviors. It loads `lib/compact/rules.js` from the package to reuse the same rewrite rules — no duplicated logic. If the rules file is not resolvable, compact-bash is disabled gracefully with a warning to stderr; the plugin never crashes the session.
272
281
 
273
282
  > **Why two hooks for updates?** `SessionStart` does the silent sync when opening the session without user interaction. `notify-update` on `UserPromptSubmit` / `beforeSubmitPrompt` injects the instruction just before the agent processes the next user message, ensuring it is not ignored.
274
283
 
@@ -406,18 +415,35 @@ Local bus (WebSocket over `127.0.0.1`) so agents across different repos can comm
406
415
 
407
416
  ## What Gets Installed in Your Repo
408
417
 
418
+ Only the IDEs selected during `init` (or detected during `update`) receive files. The three IDE targets are independent — selecting only `.opencode` does not create `.claude/` or `.cursor/` directories.
419
+
409
420
  ```
410
- .claude/skills/refacil-*/ # Claude Code skills (includes refacil-prereqs: METHODOLOGY-CONTRACT.md, BUS-CROSS-REPO.md, …)
421
+ # Claude Code (if selected)
422
+ .claude/skills/refacil-*/ # Skills (includes refacil-prereqs: METHODOLOGY-CONTRACT.md, BUS-CROSS-REPO.md, …)
411
423
  .claude/agents/refacil-*.md # Read-only sub-agents: auditor, investigator, validator
412
424
  # Write sub-agents: tester, implementer, debugger, proposer
413
- .claude/settings.json # Hooks: check-update + check-review + compact-bash
425
+ .claude/settings.json # Hooks: check-update + notify-update + check-review + compact-bash
426
+ .claude/.sdd-version # Installed methodology version (used by check-update)
427
+
428
+ # Cursor (if selected)
414
429
  .cursor/skills/refacil-*/ # Cursor skills (equivalent)
415
- .cursor/agents/refacil-*.md # Cursor sub-agents (readonly:true/false + model:inherit auto-generated)
416
- .cursor/settings.json # Hooks: check-update + check-review + compact-bash (mirror of .claude/)
430
+ .cursor/agents/refacil-*.md # Cursor sub-agents (readonly:true/false + model:inherit, auto-generated)
431
+ .cursor/settings.json # Hooks: check-update + notify-update + check-review + compact-bash
432
+ .cursor/.sdd-version # Installed methodology version
433
+
434
+ # OpenCode (if selected)
435
+ .opencode/skills/refacil-*/ # OpenCode skills (byte-for-byte copy — same spec as Claude Code)
436
+ .opencode/agents/refacil-*.md # OpenCode sub-agents (permission block + mode:subagent, auto-generated)
437
+ .opencode/plugins/refacil-hooks.js # Embedded plugin: session.created + tui.prompt.append + tool.execute.before
438
+ .opencode/opencode.json # Created/merged with $schema (user keys preserved)
439
+ .opencode/.sdd-version # Installed methodology version
440
+
441
+ # Shared (IDE-agnostic)
417
442
  CLAUDE.md # Minimal index → points to AGENTS.md
418
- .cursorrules # Same in Cursor format
443
+ .cursorrules # Cursor format equivalent of CLAUDE.md
419
444
  .claudeignore # Base exclusions (node_modules, dist, .env, *.key, etc.)
420
445
  .cursorignore # Same content as .claudeignore
446
+ .opencodeignore # Same content as .claudeignore
421
447
  AGENTS.md # Project index → generated by /refacil:setup
422
448
  # Points to .agents/ + includes auto-managed blocks
423
449
  # (compact-guidance and bus presentation)
@@ -436,6 +462,7 @@ refacil-sdd/ # SDD artifacts store
436
462
  - [AGENTS.md](https://agents.md/) — universal AI instructions standard
437
463
  - [Claude Code](https://claude.ai/code) — Anthropic CLI
438
464
  - [Cursor](https://cursor.sh) — AI IDE
465
+ - [OpenCode](https://opencode.ai) — open-source AI development agent
439
466
 
440
467
  ## License
441
468
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: refacil-validator
3
- description: Validates implementation against SDD specs (CA/CR) and tests. Delegated by /refacil:verify — do not invoke directly. Read-only: does not apply fixes.
3
+ description: Validates implementation against SDD specs (CA/CR) and tests. Delegated by /refacil:verify — do not invoke directly. Never modifies files.
4
4
  tools: Read, Grep, Glob, Bash
5
5
  model: sonnet
6
6
  ---
package/bin/cli.js CHANGED
@@ -12,7 +12,9 @@ const compactBash = require('../lib/compact/bash');
12
12
  const {
13
13
  installSkills,
14
14
  installAgents,
15
+ installOpenCodeJson,
15
16
  removeSkills,
17
+ removeOpenCodeArtifacts,
16
18
  removeOpenspecLegacyAssets,
17
19
  createClaudeMd,
18
20
  createCursorRules,
@@ -22,7 +24,7 @@ const {
22
24
  checkNodeVersion,
23
25
  checkClaudeCodeVersion,
24
26
  } = require('../lib/installer');
25
- const { installHooks, uninstallHooks, cleanLegacySettingsHooks } = require('../lib/hooks');
27
+ const { installHooks, uninstallHooks, cleanLegacySettingsHooks, installOpenCodePlugin, uninstallOpenCodePlugin } = require('../lib/hooks');
26
28
  const { handleCompact } = require('../lib/commands/compact');
27
29
  const { handleBus } = require('../lib/commands/bus');
28
30
  const { handleSdd, autoMigrateOpenspec, findProjectRoot } = require('../lib/commands/sdd');
@@ -119,10 +121,108 @@ function notifyUpdate() {
119
121
  function repoIsInitialized() {
120
122
  return (
121
123
  fs.existsSync(path.join(projectRoot, '.claude', 'skills')) ||
122
- fs.existsSync(path.join(projectRoot, '.cursor', 'skills'))
124
+ fs.existsSync(path.join(projectRoot, '.cursor', 'skills')) ||
125
+ fs.existsSync(path.join(projectRoot, '.opencode', 'skills'))
123
126
  );
124
127
  }
125
128
 
129
+ /**
130
+ * Inline readline-based multi-select for IDE targets.
131
+ * Shows a checklist of available IDEs and returns the selected ones.
132
+ * Falls back to all IDEs if stdin is not a TTY.
133
+ * @param {Array<{label: string, value: string, selected: boolean}>} options
134
+ * @returns {Promise<string[]>} selected values
135
+ */
136
+ function readlineMultiSelect(options) {
137
+ return new Promise((resolve) => {
138
+ const readline = require('readline');
139
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
140
+
141
+ const selected = options.map((o) => o.selected);
142
+ let renderCount = 0;
143
+
144
+ function render() {
145
+ if (renderCount > 0) {
146
+ process.stdout.write(`\x1B[${options.length + 7}A\x1B[J`);
147
+ }
148
+ renderCount++;
149
+ console.log('\n Select IDEs to install refacil-sdd-ai into:');
150
+ console.log(' (space = toggle, enter = confirm, a = toggle all)\n');
151
+ for (let i = 0; i < options.length; i++) {
152
+ const check = selected[i] ? '[x]' : '[ ]';
153
+ console.log(` ${check} ${options[i].label}`);
154
+ }
155
+ console.log('\n Enter numbers to toggle (e.g. 1,2,3) or "a" for all, then Enter to confirm:');
156
+ }
157
+
158
+ render();
159
+
160
+ rl.on('line', (line) => {
161
+ const input = line.trim().toLowerCase();
162
+ if (input === '') {
163
+ rl.close();
164
+ resolve(options.filter((_, i) => selected[i]).map((o) => o.value));
165
+ return;
166
+ }
167
+ if (input === 'a') {
168
+ const anySelected = selected.some(Boolean);
169
+ for (let i = 0; i < selected.length; i++) selected[i] = !anySelected;
170
+ } else {
171
+ const nums = input.split(/[\s,]+/).map(Number).filter((n) => !isNaN(n) && n >= 1 && n <= options.length);
172
+ for (const n of nums) selected[n - 1] = !selected[n - 1];
173
+ }
174
+ render();
175
+ });
176
+
177
+ rl.on('close', () => {
178
+ resolve(options.filter((_, i) => selected[i]).map((o) => o.value));
179
+ });
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Show interactive IDE selector if TTY and --all not in args.
185
+ * Pre-selects IDEs by folder presence.
186
+ * Returns selected IDE dirs (e.g. ['.claude', '.cursor', '.opencode']).
187
+ */
188
+ async function selectIDEs() {
189
+ const allFlag = process.argv.includes('--all');
190
+ const allIDEs = ['.claude', '.cursor', '.opencode'];
191
+
192
+ // --all or non-TTY: install all three
193
+ if (allFlag || !process.stdout.isTTY) {
194
+ return allIDEs;
195
+ }
196
+
197
+ const options = [
198
+ { label: 'Claude Code (.claude/)', value: '.claude', selected: fs.existsSync(path.join(projectRoot, '.claude')) },
199
+ { label: 'Cursor (.cursor/)', value: '.cursor', selected: fs.existsSync(path.join(projectRoot, '.cursor')) },
200
+ { label: 'OpenCode (.opencode/)', value: '.opencode', selected: fs.existsSync(path.join(projectRoot, '.opencode')) },
201
+ ];
202
+
203
+ // Try @clack/prompts first, fall back to inline readline
204
+ let selected;
205
+ try {
206
+ // @clack/prompts is an optional peer dep — try to load it without crashing if absent
207
+ const clack = require('@clack/prompts');
208
+ const result = await clack.multiselect({
209
+ message: 'Select IDEs to install refacil-sdd-ai into:',
210
+ options: options.map((o) => ({ label: o.label, value: o.value, selected: o.selected })),
211
+ required: false,
212
+ });
213
+ if (clack.isCancel(result)) {
214
+ console.log('\n Installation cancelled.\n');
215
+ process.exit(0);
216
+ }
217
+ selected = result;
218
+ } catch (_) {
219
+ // @clack/prompts not available — use inline readline fallback
220
+ selected = await readlineMultiSelect(options);
221
+ }
222
+
223
+ return selected;
224
+ }
225
+
126
226
  function semverGt(a, b) {
127
227
  if (!a || !b) return false;
128
228
  const pa = a.split('.').map(Number);
@@ -297,7 +397,7 @@ function checkReview() {
297
397
 
298
398
  // --- High-level commands ---
299
399
 
300
- function init() {
400
+ async function init() {
301
401
  console.log('\n refacil-sdd-ai: Initializing SDD-AI methodology...\n');
302
402
 
303
403
  const nodeOk = checkNodeVersion();
@@ -313,35 +413,69 @@ function init() {
313
413
  console.log(' Update with: npm install -g @anthropic-ai/claude-code\n');
314
414
  }
315
415
 
316
- const count = installSkills(packageRoot, projectRoot);
317
- console.log(` ${count} skills installed in .claude/skills/ and .cursor/skills/`);
416
+ // Select target IDEs (interactive selector or --all / non-TTY)
417
+ const selectedIDEs = await selectIDEs();
418
+
419
+ if (selectedIDEs.length === 0) {
420
+ console.log('\n No IDEs selected. Nothing installed.\n');
421
+ console.log(' Re-run with: refacil-sdd-ai init --all to install for all IDEs');
422
+ process.exit(0);
423
+ return;
424
+ }
425
+
426
+ const installClaude = selectedIDEs.includes('.claude');
427
+ const installCursor = selectedIDEs.includes('.cursor');
428
+ const installOpenCode = selectedIDEs.includes('.opencode');
318
429
 
319
- const agentsCount = installAgents(packageRoot, projectRoot);
430
+ const count = installSkills(packageRoot, projectRoot, selectedIDEs);
431
+ const ideList = selectedIDEs.map((d) => `${d}/skills/`).join(', ');
432
+ console.log(` ${count} skills installed in ${ideList}`);
433
+
434
+ const agentsCount = installAgents(packageRoot, projectRoot, selectedIDEs);
320
435
  if (agentsCount > 0) {
321
- console.log(` ${agentsCount} sub-agents installed in .claude/agents/ and .cursor/agents/`);
436
+ const agentList = selectedIDEs.map((d) => `${d}/agents/`).join(', ');
437
+ console.log(` ${agentsCount} sub-agents installed in ${agentList}`);
322
438
  }
323
439
 
324
440
  writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
325
441
 
326
- if (createClaudeMd(packageRoot, projectRoot)) console.log(' CLAUDE.md OK');
327
- if (createCursorRules(packageRoot, projectRoot)) console.log(' .cursorrules OK');
442
+ if (installClaude) {
443
+ if (createClaudeMd(packageRoot, projectRoot)) console.log(' CLAUDE.md OK');
444
+ if (installHooks('.claude', projectRoot)) {
445
+ console.log(' Hook check-update added to .claude/settings.json');
446
+ }
447
+ }
328
448
 
329
- if (installHooks('.claude', projectRoot)) {
330
- console.log(' Hook check-update added to .claude/settings.json');
449
+ if (installCursor) {
450
+ if (createCursorRules(packageRoot, projectRoot)) console.log(' .cursorrules OK');
451
+ if (installHooks('.cursor', projectRoot)) {
452
+ console.log(' Hook check-update added to .cursor/hooks.json');
453
+ }
331
454
  }
332
- if (installHooks('.cursor', projectRoot)) {
333
- console.log(' Hook check-update added to .cursor/settings.json');
455
+
456
+ if (installOpenCode) {
457
+ try {
458
+ installOpenCodeJson(projectRoot);
459
+ console.log(' .opencode/opencode.json created/updated');
460
+ } catch (err) {
461
+ console.error(` Warning: could not create opencode.json: ${err.message}`);
462
+ }
463
+ if (installHooks('.opencode', projectRoot)) {
464
+ console.log(' OpenCode plugin installed to .opencode/plugins/refacil-hooks.js');
465
+ }
334
466
  }
335
467
 
336
468
  try {
337
- const ignoreResult = syncIgnoreFiles(projectRoot);
338
- const s = ignoreResult.claude;
339
- if (s.status === 'created') {
340
- console.log(' .claudeignore and .cursorignore created');
341
- } else if (s.status === 'updated') {
342
- console.log(` .claudeignore and .cursorignore updated (${s.added} entries added)`);
343
- } else {
344
- console.log(' .claudeignore and .cursorignore are up to date');
469
+ const IDE_TO_IGNORE = { '.claude': '.claudeignore', '.cursor': '.cursorignore', '.opencode': '.opencodeignore' };
470
+ const ignoreResult = syncIgnoreFiles(projectRoot, selectedIDEs);
471
+ const ignoreNames = selectedIDEs.map((d) => IDE_TO_IGNORE[d]).filter(Boolean).join(', ');
472
+ const s = ignoreResult.claude || ignoreResult.cursor || ignoreResult.opencode;
473
+ if (s && s.status === 'created') {
474
+ console.log(` ${ignoreNames} created`);
475
+ } else if (s && s.status === 'updated') {
476
+ console.log(` ${ignoreNames} updated (${s.added} entries added)`);
477
+ } else if (ignoreNames) {
478
+ console.log(` ${ignoreNames} are up to date`);
345
479
  }
346
480
  } catch (err) {
347
481
  console.error(` Warning: could not sync ignore files: ${err.message}`);
@@ -359,7 +493,7 @@ function init() {
359
493
  }
360
494
 
361
495
  console.log('\n Next steps:\n');
362
- console.log(' 1. RESTART your Claude Code or Cursor session');
496
+ console.log(' 1. RESTART your IDE session');
363
497
  console.log(' (new skills are not detected until the session is restarted)\n');
364
498
  console.log(' 2. Run: /refacil:setup');
365
499
  console.log(' (generates AGENTS.md for your project)\n');
@@ -368,12 +502,29 @@ function init() {
368
502
  function update() {
369
503
  console.log('\n refacil-sdd-ai: Updating skills...\n');
370
504
 
371
- const count = installSkills(packageRoot, projectRoot);
372
- console.log(` ${count} skills updated in .claude/skills/ and .cursor/skills/`);
505
+ // Detect installed IDEs by folder presence
506
+ const hasClaudeDir = fs.existsSync(path.join(projectRoot, '.claude'));
507
+ const hasCursorDir = fs.existsSync(path.join(projectRoot, '.cursor'));
508
+ const hasOpenCodeDir = fs.existsSync(path.join(projectRoot, '.opencode'));
373
509
 
374
- const agentsCount = installAgents(packageRoot, projectRoot);
510
+ const detectedIDEs = [
511
+ hasClaudeDir && '.claude',
512
+ hasCursorDir && '.cursor',
513
+ hasOpenCodeDir && '.opencode',
514
+ ].filter(Boolean);
515
+
516
+ const count = installSkills(packageRoot, projectRoot, detectedIDEs);
517
+ const installedDirs = detectedIDEs.map((d) => `${d}/skills/`);
518
+ console.log(` ${count} skills updated in ${installedDirs.join(', ') || '(none detected)'}`);
519
+
520
+ const agentsCount = installAgents(packageRoot, projectRoot, detectedIDEs);
375
521
  if (agentsCount > 0) {
376
- console.log(` ${agentsCount} sub-agents updated in .claude/agents/ and .cursor/agents/`);
522
+ const agentDirs = [
523
+ hasClaudeDir && '.claude/agents/',
524
+ hasCursorDir && '.cursor/agents/',
525
+ hasOpenCodeDir && '.opencode/agents/',
526
+ ].filter(Boolean);
527
+ console.log(` ${agentsCount} sub-agents updated in ${agentDirs.join(', ')}`);
377
528
  }
378
529
 
379
530
  try {
@@ -387,25 +538,42 @@ function update() {
387
538
 
388
539
  writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
389
540
 
390
- createClaudeMd(packageRoot, projectRoot);
391
- createCursorRules(packageRoot, projectRoot);
541
+ if (hasClaudeDir) {
542
+ createClaudeMd(packageRoot, projectRoot);
543
+ if (installHooks('.claude', projectRoot)) {
544
+ console.log(' Hook check-update added to .claude/settings.json');
545
+ }
546
+ }
392
547
 
393
- if (installHooks('.claude', projectRoot)) {
394
- console.log(' Hook check-update added to .claude/settings.json');
548
+ if (hasCursorDir) {
549
+ createCursorRules(packageRoot, projectRoot);
550
+ if (installHooks('.cursor', projectRoot)) {
551
+ console.log(' Hook check-update added to .cursor/hooks.json');
552
+ }
395
553
  }
396
- if (installHooks('.cursor', projectRoot)) {
397
- console.log(' Hook check-update added to .cursor/settings.json');
554
+
555
+ if (hasOpenCodeDir) {
556
+ try {
557
+ installOpenCodeJson(projectRoot);
558
+ } catch (err) {
559
+ console.error(` Warning: could not update opencode.json: ${err.message}`);
560
+ }
561
+ if (installHooks('.opencode', projectRoot)) {
562
+ console.log(' OpenCode plugin updated at .opencode/plugins/refacil-hooks.js');
563
+ }
398
564
  }
399
565
 
400
566
  try {
401
- const ignoreResult = syncIgnoreFiles(projectRoot);
402
- const s = ignoreResult.claude;
403
- if (s.status === 'created') {
404
- console.log(' .claudeignore and .cursorignore created');
405
- } else if (s.status === 'updated') {
406
- console.log(` .claudeignore and .cursorignore updated (${s.added} entries added)`);
407
- } else {
408
- console.log(' .claudeignore and .cursorignore are up to date');
567
+ const IDE_TO_IGNORE = { '.claude': '.claudeignore', '.cursor': '.cursorignore', '.opencode': '.opencodeignore' };
568
+ const ignoreResult = syncIgnoreFiles(projectRoot, detectedIDEs);
569
+ const ignoreNames = detectedIDEs.map((d) => IDE_TO_IGNORE[d]).filter(Boolean).join(', ');
570
+ const s = ignoreResult.claude || ignoreResult.cursor || ignoreResult.opencode;
571
+ if (s && s.status === 'created') {
572
+ console.log(` ${ignoreNames} created`);
573
+ } else if (s && s.status === 'updated') {
574
+ console.log(` ${ignoreNames} updated (${s.added} entries added)`);
575
+ } else if (ignoreNames) {
576
+ console.log(` ${ignoreNames} are up to date`);
409
577
  }
410
578
  } catch (err) {
411
579
  console.error(` Warning: could not sync ignore files: ${err.message}`);
@@ -422,7 +590,7 @@ function update() {
422
590
  console.error(` Warning: could not sync compact-guidance: ${err.message}`);
423
591
  }
424
592
 
425
- console.log('\n RESTART your Claude Code or Cursor session to apply the changes.\n');
593
+ console.log('\n RESTART your IDE session to apply the changes.\n');
426
594
  }
427
595
 
428
596
  function clean() {
@@ -440,6 +608,19 @@ function clean() {
440
608
  console.log(' SDD-AI hooks removed from .cursor/settings.json');
441
609
  }
442
610
 
611
+ // Clean OpenCode artifacts if .opencode/ directory is present
612
+ if (fs.existsSync(path.join(projectRoot, '.opencode'))) {
613
+ try {
614
+ removeOpenCodeArtifacts(projectRoot);
615
+ console.log(' OpenCode skills and agents removed from .opencode/');
616
+ } catch (err) {
617
+ console.error(` Warning: could not remove OpenCode artifacts: ${err.message}`);
618
+ }
619
+ if (uninstallOpenCodePlugin(projectRoot)) {
620
+ console.log(' OpenCode plugin removed from .opencode/plugins/');
621
+ }
622
+ }
623
+
443
624
  try {
444
625
  const result = removeCompactGuidance(projectRoot);
445
626
  if (result.status === 'removed') {
@@ -460,8 +641,10 @@ function help() {
460
641
  refacil-sdd-ai — SDD-AI Methodology
461
642
 
462
643
  Commands:
463
- init Install skills in .claude/ and .cursor/, create CLAUDE.md and .cursorrules
464
- update Re-copy skills (to update to a new package version)
644
+ init Install skills in .claude/, .cursor/ and/or .opencode/ (interactive IDE selector).
645
+ Use --all to install for all three IDEs without prompting.
646
+ Creates CLAUDE.md, .cursorrules and .opencode/opencode.json as appropriate.
647
+ update Re-copy skills for all detected IDEs (.claude/, .cursor/, .opencode/)
465
648
  migration-pending Same validation as hooks/notify-update: list migrations (exit 1 if any; --json)
466
649
  check-update Sync skills and compact-guidance at session start (SessionStart hook)
467
650
  notify-update Notify methodology migration only if applicable (UserPromptSubmit hook)
@@ -496,18 +679,24 @@ function help() {
496
679
  sdd mark-reviewed <name> Write .review-passed (requires --verdict and --summary)
497
680
  sdd tasks-update <name> Mark task N as completed (--task N --done)
498
681
  sdd validate-name <name> Validate change name format
499
- clean Remove skills and SDD-AI hooks from .claude/settings.json and .cursor/settings.json
682
+ clean Remove skills and SDD-AI hooks from all detected IDEs
683
+ (.claude/settings.json, .cursor/hooks.json, .opencode/plugins/)
500
684
  help Show this help
501
685
 
502
686
  Full flow:
503
687
  1. npm install -g refacil-sdd-ai
504
688
  2. refacil-sdd-ai init
505
- 3. RESTART the Claude Code or Cursor session
689
+ 3. RESTART your IDE session (Claude Code, Cursor, or OpenCode)
506
690
  4. Run: /refacil:setup (generates AGENTS.md for your project)
507
691
 
692
+ IDE support:
693
+ - Claude Code: .claude/skills/, .claude/agents/, .claude/settings.json hooks
694
+ - Cursor: .cursor/skills/, .cursor/agents/, .cursor/hooks.json hooks
695
+ - OpenCode: .opencode/skills/, .opencode/agents/, .opencode/plugins/refacil-hooks.js
696
+
508
697
  Requirements:
509
698
  - Node.js >= 20.0.0
510
- - Claude Code >= 2.1.89 (required by compact-bash for silent rewrite) or Cursor
699
+ - Claude Code >= 2.1.89 (required by compact-bash for silent rewrite), Cursor, or OpenCode
511
700
  `);
512
701
  }
513
702
 
@@ -522,7 +711,10 @@ if (command === '--version' || command === '-v') {
522
711
 
523
712
  switch (command) {
524
713
  case 'init':
525
- init();
714
+ init().catch((err) => {
715
+ console.error(` Error during init: ${err.message}`);
716
+ process.exit(1);
717
+ });
526
718
  break;
527
719
  case 'update':
528
720
  update();
package/lib/hooks.js CHANGED
@@ -219,18 +219,54 @@ function cleanLegacySettingsHooks(projectRoot) {
219
219
  }
220
220
  }
221
221
 
222
+ // ── OpenCode: plugin file ────────────────────────────────────────────────────
223
+
224
+ function installOpenCodePlugin(projectRoot) {
225
+ const pluginsDir = path.join(projectRoot, '.opencode', 'plugins');
226
+ fs.mkdirSync(pluginsDir, { recursive: true });
227
+
228
+ const srcPlugin = path.join(__dirname, 'opencode-plugin', 'index.js');
229
+ const destPlugin = path.join(pluginsDir, 'refacil-hooks.js');
230
+
231
+ try {
232
+ fs.copyFileSync(srcPlugin, destPlugin);
233
+ return true;
234
+ } catch (err) {
235
+ process.stderr.write(`[refacil-sdd-ai] Could not install OpenCode plugin: ${err.message}\n`);
236
+ return false;
237
+ }
238
+ }
239
+
240
+ function uninstallOpenCodePlugin(projectRoot) {
241
+ const pluginPath = path.join(projectRoot, '.opencode', 'plugins', 'refacil-hooks.js');
242
+ if (!fs.existsSync(pluginPath)) return false;
243
+ try {
244
+ fs.unlinkSync(pluginPath);
245
+ return true;
246
+ } catch (err) {
247
+ process.stderr.write(`[refacil-sdd-ai] Could not remove OpenCode plugin: ${err.message}\n`);
248
+ return false;
249
+ }
250
+ }
251
+
222
252
  // ── Fachada pública ──────────────────────────────────────────────────────────
223
253
 
224
254
  function installHooks(ideDir, projectRoot) {
225
- return ideDir === '.cursor'
226
- ? installCursorHooks(projectRoot)
227
- : installClaudeHooks(projectRoot);
255
+ if (ideDir === '.cursor') return installCursorHooks(projectRoot);
256
+ if (ideDir === '.opencode') return installOpenCodePlugin(projectRoot);
257
+ return installClaudeHooks(projectRoot);
228
258
  }
229
259
 
230
260
  function uninstallHooks(ideDir, projectRoot) {
231
- return ideDir === '.cursor'
232
- ? uninstallCursorHooks(projectRoot)
233
- : uninstallClaudeHooks(projectRoot);
261
+ if (ideDir === '.cursor') return uninstallCursorHooks(projectRoot);
262
+ if (ideDir === '.opencode') return uninstallOpenCodePlugin(projectRoot);
263
+ return uninstallClaudeHooks(projectRoot);
234
264
  }
235
265
 
236
- module.exports = { installHooks, uninstallHooks, cleanLegacySettingsHooks };
266
+ module.exports = {
267
+ installHooks,
268
+ uninstallHooks,
269
+ cleanLegacySettingsHooks,
270
+ installOpenCodePlugin,
271
+ uninstallOpenCodePlugin,
272
+ };
@@ -79,10 +79,19 @@ function syncIgnoreFile(filePath) {
79
79
  return { status: 'updated', added: missing.length };
80
80
  }
81
81
 
82
- function syncIgnoreFiles(projectRoot) {
83
- const claude = syncIgnoreFile(path.join(projectRoot, '.claudeignore'));
84
- const cursor = syncIgnoreFile(path.join(projectRoot, '.cursorignore'));
85
- return { claude, cursor };
82
+ function syncIgnoreFiles(projectRoot, ideDirs) {
83
+ const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
84
+ const result = {};
85
+ if (dirs.includes('.claude')) {
86
+ result.claude = syncIgnoreFile(path.join(projectRoot, '.claudeignore'));
87
+ }
88
+ if (dirs.includes('.cursor')) {
89
+ result.cursor = syncIgnoreFile(path.join(projectRoot, '.cursorignore'));
90
+ }
91
+ if (dirs.includes('.opencode')) {
92
+ result.opencode = syncIgnoreFile(path.join(projectRoot, '.opencodeignore'));
93
+ }
94
+ return result;
86
95
  }
87
96
 
88
97
  module.exports = { syncIgnoreFiles, BASE_ENTRIES };
package/lib/installer.js CHANGED
@@ -35,7 +35,18 @@ const AGENTS = [
35
35
  'proposer',
36
36
  ];
37
37
 
38
- const REPO_VERSION_FILES = ['.claude/.sdd-version', '.cursor/.sdd-version'];
38
+ // All 7 refacil agents are internal sub-agents — hidden from the agent picker in OpenCode
39
+ const INTERNAL_AGENTS = [
40
+ 'investigator',
41
+ 'validator',
42
+ 'auditor',
43
+ 'tester',
44
+ 'implementer',
45
+ 'debugger',
46
+ 'proposer',
47
+ ];
48
+
49
+ const REPO_VERSION_FILES = ['.claude/.sdd-version', '.cursor/.sdd-version', '.opencode/.sdd-version'];
39
50
 
40
51
  function copyDir(src, dest) {
41
52
  fs.mkdirSync(dest, { recursive: true });
@@ -51,23 +62,59 @@ function copyDir(src, dest) {
51
62
  }
52
63
  }
53
64
 
54
- function installSkills(packageRoot, projectRoot) {
65
+ function installSkills(packageRoot, projectRoot, ideDirs) {
66
+ const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
55
67
  let installed = 0;
56
68
  for (const skill of SKILLS) {
57
69
  const srcDir = path.join(packageRoot, 'skills', skill);
58
70
  if (!fs.existsSync(srcDir)) continue;
59
71
 
60
- const claudeDest = path.join(projectRoot, '.claude', 'skills', `refacil-${skill}`);
61
- copyDir(srcDir, claudeDest);
62
-
63
- const cursorDest = path.join(projectRoot, '.cursor', 'skills', `refacil-${skill}`);
64
- copyDir(srcDir, cursorDest);
72
+ if (dirs.includes('.claude')) {
73
+ copyDir(srcDir, path.join(projectRoot, '.claude', 'skills', `refacil-${skill}`));
74
+ }
75
+ if (dirs.includes('.cursor')) {
76
+ copyDir(srcDir, path.join(projectRoot, '.cursor', 'skills', `refacil-${skill}`));
77
+ }
78
+ if (dirs.includes('.opencode')) {
79
+ // OpenCode: byte-for-byte copy (same as Claude Code — no transformation needed)
80
+ copyDir(srcDir, path.join(projectRoot, '.opencode', 'skills', `refacil-${skill}`));
81
+ }
65
82
 
66
83
  installed++;
67
84
  }
68
85
  return installed;
69
86
  }
70
87
 
88
+ // Create or safely merge .opencode/opencode.json with SDD-AI managed keys
89
+ // Preserves any pre-existing keys — never destructive
90
+ function installOpenCodeJson(projectRoot) {
91
+ const ocDir = path.join(projectRoot, '.opencode');
92
+ fs.mkdirSync(ocDir, { recursive: true });
93
+ const ocJsonPath = path.join(ocDir, 'opencode.json');
94
+
95
+ let existing = {};
96
+ if (fs.existsSync(ocJsonPath)) {
97
+ try {
98
+ existing = JSON.parse(fs.readFileSync(ocJsonPath, 'utf8'));
99
+ } catch (_) {
100
+ existing = {};
101
+ }
102
+ }
103
+
104
+ // SDD-AI managed keys (minimal — only $schema)
105
+ const sddKeys = {
106
+ '$schema': 'https://opencode.ai/config.json',
107
+ };
108
+
109
+ const merged = Object.assign({}, sddKeys, existing);
110
+ // Ensure $schema is always the SDD-AI value if not already set by user
111
+ if (!existing['$schema']) {
112
+ merged['$schema'] = sddKeys['$schema'];
113
+ }
114
+
115
+ fs.writeFileSync(ocJsonPath, JSON.stringify(merged, null, 2) + '\n');
116
+ }
117
+
71
118
  // Claude Code: tools allowlist granular, model: sonnet|opus|haiku
72
119
  // Cursor: readonly: true|false (booleano), model: inherit (default)
73
120
  function transformFrontmatterForCursor(content) {
@@ -113,13 +160,72 @@ function transformFrontmatterForCursor(content) {
113
160
  return `---\n${out.join('\n')}\n---\n${body}`;
114
161
  }
115
162
 
116
- function installAgents(packageRoot, projectRoot) {
163
+ // OpenCode: tools → permission mapping, adds mode: subagent, hidden: true for internal agents, removes model:
164
+ // tools:[Edit,Write,NotebookEdit] → edit:allow, tools:[Bash] → bash:allow, WebFetch → always deny
165
+ function transformFrontmatterForOpenCode(content) {
166
+ const normalized = content.replace(/\r\n/g, '\n');
167
+ const match = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
168
+ if (!match) return content;
169
+
170
+ const [, frontmatterRaw, body] = match;
171
+ const lines = frontmatterRaw.split('\n');
172
+ const out = [];
173
+ let toolsLine = null;
174
+ let agentName = null;
175
+
176
+ for (const line of lines) {
177
+ if (line.startsWith('tools:')) {
178
+ toolsLine = line;
179
+ // Do not emit tools: line — OpenCode uses permission block instead
180
+ continue;
181
+ }
182
+ if (line.startsWith('model:')) {
183
+ // Remove model: line — OpenCode manages model selection separately
184
+ continue;
185
+ }
186
+ if (line.startsWith('name:')) {
187
+ const nameVal = line.slice('name:'.length).trim();
188
+ // Extract the base agent name from "refacil-<name>" or plain "<name>"
189
+ const nameMatch = nameVal.match(/refacil-(\S+)/);
190
+ agentName = nameMatch ? nameMatch[1] : nameVal;
191
+ out.push(line);
192
+ continue;
193
+ }
194
+ out.push(line);
195
+ }
196
+
197
+ // Determine permission values from tools list
198
+ const toolsList = toolsLine ? toolsLine.slice('tools:'.length).trim() : '';
199
+ const canEdit = /\b(Edit|Write|NotebookEdit)\b/.test(toolsList);
200
+ const canBash = /\bBash\b/.test(toolsList);
201
+
202
+ // Build permission block
203
+ out.push(`permission:`);
204
+ out.push(` edit: ${canEdit ? 'allow' : 'deny'}`);
205
+ out.push(` bash: ${canBash ? 'allow' : 'deny'}`);
206
+ out.push(` webfetch: deny`);
207
+
208
+ // Add mode: subagent
209
+ out.push(`mode: subagent`);
210
+
211
+ // Add hidden: true for internal agents
212
+ if (agentName && INTERNAL_AGENTS.includes(agentName)) {
213
+ out.push(`hidden: true`);
214
+ }
215
+
216
+ return `---\n${out.join('\n')}\n---\n${body}`;
217
+ }
218
+
219
+ function installAgents(packageRoot, projectRoot, ideDirs) {
220
+ const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
117
221
  let installed = 0;
118
222
 
119
223
  const claudeDir = path.join(projectRoot, '.claude', 'agents');
120
224
  const cursorDir = path.join(projectRoot, '.cursor', 'agents');
121
- fs.mkdirSync(claudeDir, { recursive: true });
122
- fs.mkdirSync(cursorDir, { recursive: true });
225
+ const openCodeDir = path.join(projectRoot, '.opencode', 'agents');
226
+ if (dirs.includes('.claude')) fs.mkdirSync(claudeDir, { recursive: true });
227
+ if (dirs.includes('.cursor')) fs.mkdirSync(cursorDir, { recursive: true });
228
+ if (dirs.includes('.opencode')) fs.mkdirSync(openCodeDir, { recursive: true });
123
229
 
124
230
  for (const agent of AGENTS) {
125
231
  const srcFile = path.join(packageRoot, 'agents', `${agent}.md`);
@@ -127,11 +233,21 @@ function installAgents(packageRoot, projectRoot) {
127
233
 
128
234
  const content = fs.readFileSync(srcFile, 'utf8');
129
235
 
130
- fs.writeFileSync(path.join(claudeDir, `refacil-${agent}.md`), content);
131
- fs.writeFileSync(
132
- path.join(cursorDir, `refacil-${agent}.md`),
133
- transformFrontmatterForCursor(content),
134
- );
236
+ if (dirs.includes('.claude')) {
237
+ fs.writeFileSync(path.join(claudeDir, `refacil-${agent}.md`), content);
238
+ }
239
+ if (dirs.includes('.cursor')) {
240
+ fs.writeFileSync(
241
+ path.join(cursorDir, `refacil-${agent}.md`),
242
+ transformFrontmatterForCursor(content),
243
+ );
244
+ }
245
+ if (dirs.includes('.opencode')) {
246
+ fs.writeFileSync(
247
+ path.join(openCodeDir, `refacil-${agent}.md`),
248
+ transformFrontmatterForOpenCode(content),
249
+ );
250
+ }
135
251
 
136
252
  installed++;
137
253
  }
@@ -139,6 +255,50 @@ function installAgents(packageRoot, projectRoot) {
139
255
  return installed;
140
256
  }
141
257
 
258
+ function removeOpenCodeArtifacts(projectRoot) {
259
+ // Remove .opencode/skills/refacil-*/
260
+ for (const skill of SKILLS) {
261
+ const skillDir = path.join(projectRoot, '.opencode', 'skills', `refacil-${skill}`);
262
+ if (fs.existsSync(skillDir)) {
263
+ try { fs.rmSync(skillDir, { recursive: true }); } catch (_) {}
264
+ }
265
+ }
266
+
267
+ // Remove .opencode/agents/refacil-*.md
268
+ const agentsDir = path.join(projectRoot, '.opencode', 'agents');
269
+ if (fs.existsSync(agentsDir)) {
270
+ try {
271
+ const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
272
+ for (const entry of entries) {
273
+ if (entry.isFile() && entry.name.startsWith('refacil-') && entry.name.endsWith('.md')) {
274
+ fs.unlinkSync(path.join(agentsDir, entry.name));
275
+ }
276
+ }
277
+ } catch (_) {}
278
+ }
279
+
280
+ // Remove .opencode/plugins/refacil-hooks.js
281
+ const pluginFile = path.join(projectRoot, '.opencode', 'plugins', 'refacil-hooks.js');
282
+ if (fs.existsSync(pluginFile)) {
283
+ try { fs.unlinkSync(pluginFile); } catch (_) {}
284
+ }
285
+
286
+ // Revert SDD-AI keys from .opencode/opencode.json (currently only $schema key, leave file if other keys remain)
287
+ const ocJsonPath = path.join(projectRoot, '.opencode', 'opencode.json');
288
+ if (fs.existsSync(ocJsonPath)) {
289
+ try {
290
+ const json = JSON.parse(fs.readFileSync(ocJsonPath, 'utf8'));
291
+ delete json['$schema'];
292
+ const remaining = Object.keys(json);
293
+ if (remaining.length === 0) {
294
+ fs.unlinkSync(ocJsonPath);
295
+ } else {
296
+ fs.writeFileSync(ocJsonPath, JSON.stringify(json, null, 2) + '\n');
297
+ }
298
+ } catch (_) {}
299
+ }
300
+ }
301
+
142
302
  function writeGuideFile(destPath, header, label) {
143
303
  const content =
144
304
  `# ${header}\n\n` +
@@ -309,10 +469,14 @@ function checkNodeVersion() {
309
469
  module.exports = {
310
470
  SKILLS,
311
471
  AGENTS,
472
+ INTERNAL_AGENTS,
312
473
  copyDir,
313
474
  installSkills,
475
+ installOpenCodeJson,
314
476
  transformFrontmatterForCursor,
477
+ transformFrontmatterForOpenCode,
315
478
  installAgents,
479
+ removeOpenCodeArtifacts,
316
480
  createClaudeMd,
317
481
  createCursorRules,
318
482
  readRepoVersion,
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * refacil-sdd-ai OpenCode plugin
5
+ *
6
+ * Provides 4 hook equivalents for OpenCode:
7
+ * - session.created → check-update logic (sync compact-guidance, flag pending migrations)
8
+ * - tui.prompt.append → notify-update logic (prompt user to run /refacil:update if pending)
9
+ * - tool.execute.before → check-review + compact-bash logic
10
+ *
11
+ * This file is installed as .opencode/plugins/refacil-hooks.js.
12
+ * It resolves lib/compact/rules.js relative to its own __dirname at install time.
13
+ */
14
+
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+
18
+ // ── Resolve compact rules ────────────────────────────────────────────────────
19
+ // When installed, this file lives at .opencode/plugins/refacil-hooks.js.
20
+ // The compact rules live at <package>/lib/compact/rules.js.
21
+ // We walk up from __dirname looking for the package (node_modules/refacil-sdd-ai or
22
+ // the package root directly), falling back gracefully if not found.
23
+
24
+ let findRule = null;
25
+
26
+ (function loadCompactRules() {
27
+ const candidates = [
28
+ // Installed as plugin in .opencode/plugins/ — package is in node_modules
29
+ path.resolve(__dirname, '..', '..', 'node_modules', 'refacil-sdd-ai', 'lib', 'compact', 'rules.js'),
30
+ // Running from source (lib/opencode-plugin/index.js)
31
+ path.resolve(__dirname, '..', 'compact', 'rules.js'),
32
+ ];
33
+
34
+ for (const candidate of candidates) {
35
+ try {
36
+ if (fs.existsSync(candidate)) {
37
+ const rules = require(candidate);
38
+ if (typeof rules.findRule === 'function') {
39
+ findRule = rules.findRule;
40
+ break;
41
+ }
42
+ }
43
+ } catch (_) {
44
+ // Try next candidate
45
+ }
46
+ }
47
+
48
+ if (!findRule) {
49
+ process.stderr.write('[refacil-sdd-ai] WARNING: Could not load compact/rules.js — compact-bash hook disabled.\n');
50
+ }
51
+ })();
52
+
53
+ // ── Helpers ──────────────────────────────────────────────────────────────────
54
+
55
+ function getPendingUpdateFlagPath(projectRoot) {
56
+ return path.join(projectRoot, '.refacil-pending-update');
57
+ }
58
+
59
+ function readPendingUpdateFlag(projectRoot) {
60
+ const flagPath = getPendingUpdateFlagPath(projectRoot);
61
+ if (!fs.existsSync(flagPath)) return null;
62
+ try {
63
+ return JSON.parse(fs.readFileSync(flagPath, 'utf8'));
64
+ } catch (_) {
65
+ return {};
66
+ }
67
+ }
68
+
69
+ function writePendingUpdateFlag(projectRoot, from, to) {
70
+ try {
71
+ fs.writeFileSync(getPendingUpdateFlagPath(projectRoot), JSON.stringify({ from, to }));
72
+ } catch (_) {}
73
+ }
74
+
75
+ function clearPendingUpdateFlag(projectRoot) {
76
+ try {
77
+ const flagPath = getPendingUpdateFlagPath(projectRoot);
78
+ if (fs.existsSync(flagPath)) fs.unlinkSync(flagPath);
79
+ } catch (_) {}
80
+ }
81
+
82
+ function readRepoVersion(projectRoot) {
83
+ const versionFiles = ['.opencode/.sdd-version', '.claude/.sdd-version', '.cursor/.sdd-version'];
84
+ for (const rel of versionFiles) {
85
+ try {
86
+ const raw = fs.readFileSync(path.join(projectRoot, rel), 'utf8').trim();
87
+ if (raw) return raw;
88
+ } catch (_) {}
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function methodologyMigrationPending(projectRoot) {
94
+ // Look for refacil-sdd/changes with active (non-archived) tasks that still have pending migrations
95
+ // This is a lightweight check: look for changes that have tasks.md but no .review-passed
96
+ const changesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
97
+ if (!fs.existsSync(changesDir)) return { pending: false, reasons: [] };
98
+
99
+ let entries;
100
+ try {
101
+ entries = fs.readdirSync(changesDir, { withFileTypes: true });
102
+ } catch (_) {
103
+ return { pending: false, reasons: [] };
104
+ }
105
+
106
+ const reasons = [];
107
+ for (const entry of entries) {
108
+ if (!entry.isDirectory() || entry.name === 'archive') continue;
109
+ const tasksPath = path.join(changesDir, entry.name, 'tasks.md');
110
+ const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf8') : '';
111
+ // Look for unchecked tasks: "- [ ]" pattern
112
+ if (/- \[ \]/.test(tasksContent)) {
113
+ reasons.push(`Change '${entry.name}' has pending tasks`);
114
+ }
115
+ }
116
+
117
+ return { pending: reasons.length > 0, reasons };
118
+ }
119
+
120
+ // ── Hook handlers ────────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * session.created — equivalent of check-update (SessionStart hook)
124
+ * Checks if the installed skills are out of date and flags a pending update.
125
+ */
126
+ async function checkUpdateHandler(event) {
127
+ const projectRoot = event.projectRoot || process.cwd();
128
+
129
+ // Check if there is a pending methodology migration
130
+ try {
131
+ const mig = methodologyMigrationPending(projectRoot);
132
+ const repoVersion = readRepoVersion(projectRoot);
133
+
134
+ // Try to get the current package version via refacil-sdd-ai CLI
135
+ let packageVersion = null;
136
+ try {
137
+ const { execSync } = require('child_process');
138
+ packageVersion = execSync('refacil-sdd-ai --version', {
139
+ encoding: 'utf8',
140
+ timeout: 5000,
141
+ stdio: ['pipe', 'pipe', 'pipe'],
142
+ }).trim();
143
+ } catch (_) {}
144
+
145
+ const existingFlag = readPendingUpdateFlag(projectRoot);
146
+
147
+ if (mig.pending) {
148
+ writePendingUpdateFlag(projectRoot, repoVersion, packageVersion);
149
+ } else if (existingFlag) {
150
+ clearPendingUpdateFlag(projectRoot);
151
+ }
152
+ } catch (err) {
153
+ process.stderr.write(`[refacil-sdd-ai] check-update handler error: ${err.message}\n`);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * tui.prompt.append — equivalent of notify-update (UserPromptSubmit hook)
159
+ * Returns an instruction string if there is a pending update, otherwise returns nothing.
160
+ * Also clears the flag if the user is running /refacil:update.
161
+ */
162
+ async function notifyUpdateHandler(event) {
163
+ const projectRoot = event.projectRoot || process.cwd();
164
+ const prompt = (event.prompt || '').trim().toLowerCase();
165
+
166
+ // If user is running /refacil:update, clear the flag and let it through
167
+ if (prompt.includes('refacil:update') || prompt.includes('refacil/update')) {
168
+ clearPendingUpdateFlag(projectRoot);
169
+ return;
170
+ }
171
+
172
+ const flagInfo = readPendingUpdateFlag(projectRoot);
173
+ if (!flagInfo) return;
174
+
175
+ const mig = methodologyMigrationPending(projectRoot);
176
+ if (!mig.pending) {
177
+ clearPendingUpdateFlag(projectRoot);
178
+ return;
179
+ }
180
+
181
+ const fromLabel = flagInfo.from ? `v${flagInfo.from}` : 'previous version';
182
+ const toLabel = flagInfo.to ? `v${flagInfo.to}` : 'latest';
183
+
184
+ return (
185
+ `[refacil-sdd-ai] Methodology update detected (${fromLabel} → ${toLabel}). ` +
186
+ `Run /refacil:update to apply pending migrations before continuing.`
187
+ );
188
+ }
189
+
190
+ /**
191
+ * tool.execute.before — handles Bash tool calls:
192
+ * (a) check-review: blocks git push if any active change is missing .review-passed
193
+ * (b) compact-bash: rewrites matched commands to reduce token usage
194
+ */
195
+ async function toolExecuteBeforeHandler(event) {
196
+ // Only handle Bash tool calls
197
+ if (!event || !event.tool || event.tool !== 'bash') return;
198
+
199
+ const command = (event.input && event.input.command) || (event.params && event.params.command) || '';
200
+ if (!command) return;
201
+
202
+ const projectRoot = event.projectRoot || process.cwd();
203
+
204
+ // (a) check-review: block git push if missing .review-passed
205
+ if (/git\s+push/.test(command)) {
206
+ const sddChangesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
207
+ if (fs.existsSync(sddChangesDir)) {
208
+ let entries;
209
+ try {
210
+ entries = fs.readdirSync(sddChangesDir, { withFileTypes: true });
211
+ } catch (_) {
212
+ entries = [];
213
+ }
214
+
215
+ const activeChanges = entries.filter(
216
+ (e) => e.isDirectory() && e.name !== 'archive',
217
+ );
218
+
219
+ if (activeChanges.length > 0) {
220
+ const missing = activeChanges.filter(
221
+ (e) => !fs.existsSync(path.join(sddChangesDir, e.name, '.review-passed')),
222
+ );
223
+
224
+ if (missing.length > 0) {
225
+ const names = missing.map((e) => e.name).join(', ');
226
+ const reason =
227
+ missing.length === 1
228
+ ? `[refacil-sdd-ai] Review pending for: ${names}. ` +
229
+ 'Stop the push and run /refacil:review on that change before pushing code. ' +
230
+ 'If the review passes, retry the git push.'
231
+ : `[refacil-sdd-ai] Multiple changes without approved review: ${names}. ` +
232
+ 'Stop the push and ask the user to explicitly select which change they want to push. ' +
233
+ 'Then run /refacil:review <change-name> for that specific change and retry the push.';
234
+
235
+ throw new Error(reason);
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ // (b) compact-bash: rewrite matched commands to reduce token usage
242
+ // Skip if COMPACT=0 is set or findRule is not available
243
+ if (!findRule) return;
244
+ if (/\bCOMPACT=0\b/.test(command)) return;
245
+
246
+ const rule = findRule(command);
247
+ if (!rule) return;
248
+
249
+ const rewritten = rule.rewrite(command);
250
+ // Return the rewritten command for OpenCode to use instead
251
+ return { command: rewritten };
252
+ }
253
+
254
+ // ── Plugin export ────────────────────────────────────────────────────────────
255
+
256
+ module.exports = {
257
+ hooks: {
258
+ 'session.created': checkUpdateHandler,
259
+ 'tui.prompt.append': notifyUpdateHandler,
260
+ 'tool.execute.before': toolExecuteBeforeHandler,
261
+ },
262
+ };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "refacil-sdd-ai",
3
- "version": "4.4.1",
4
- "description": "SDD-AI: Specification-Driven Development with AI — development methodology using AI with Claude Code and Cursor",
3
+ "version": "4.5.1",
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"
7
7
  },
8
8
  "files": [
9
9
  "bin/",
10
10
  "lib/",
11
+ "lib/opencode-plugin/",
11
12
  "skills/",
12
13
  "agents/",
13
14
  "templates/",
@@ -35,9 +36,12 @@
35
36
  "node": ">=20.0.0"
36
37
  },
37
38
  "scripts": {
38
- "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"
39
+ "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"
39
40
  },
40
41
  "dependencies": {
41
42
  "ws": "^8.18.0"
43
+ },
44
+ "optionalDependencies": {
45
+ "@clack/prompts": "^0.9.0"
42
46
  }
43
47
  }