compound-workflow 1.8.0 → 1.9.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.
Files changed (92) hide show
  1. package/README.md +62 -68
  2. package/package.json +2 -8
  3. package/scripts/check-pack-readme.mjs +1 -16
  4. package/scripts/check-workflow-contracts.mjs +34 -44
  5. package/scripts/install-cli.mjs +273 -555
  6. package/src/AGENTS.md +59 -192
  7. package/src/{.agents/agents → agents}/research/best-practices-researcher.md +2 -2
  8. package/src/{.agents/commands → commands}/assess.md +4 -4
  9. package/src/commands/install.md +43 -0
  10. package/src/{.agents/commands → commands}/metrics.md +1 -1
  11. package/src/{.agents/commands → commands}/test-browser.md +8 -8
  12. package/src/commands/workflow-agents.md +101 -0
  13. package/src/{.agents/commands → commands}/workflow-brainstorm.md +5 -5
  14. package/src/{.agents/commands → commands}/workflow-compound.md +1 -1
  15. package/src/{.agents/commands → commands}/workflow-plan.md +62 -85
  16. package/src/commands/workflow-work.md +839 -0
  17. package/src/{.agents/references → references}/README.md +1 -1
  18. package/src/{.agents/skills → skills}/capture-skill/SKILL.md +1 -1
  19. package/src/{.agents/skills → skills}/compound-docs/SKILL.md +6 -6
  20. package/src/{.agents/skills → skills}/compound-docs/references/yaml-schema.md +2 -2
  21. package/src/skills/setup-agents/SKILL.md +247 -0
  22. package/src/skills/standards/SKILL.md +79 -0
  23. package/src/skills/standards/references/architecture.md +228 -0
  24. package/src/skills/standards/references/code-quality.md +192 -0
  25. package/src/skills/standards/references/presentation.md +515 -0
  26. package/src/skills/standards/references/services.md +172 -0
  27. package/src/skills/standards/references/state-management.md +936 -0
  28. package/.claude-plugin/plugin.json +0 -7
  29. package/.cursor-plugin/plugin.json +0 -20
  30. package/.cursor-plugin/registration.json +0 -5
  31. package/scripts/check-version-parity.mjs +0 -36
  32. package/scripts/generate-platform-artifacts.mjs +0 -230
  33. package/src/.agents/commands/install.md +0 -51
  34. package/src/.agents/commands/workflow-work.md +0 -690
  35. package/src/.agents/registry.json +0 -48
  36. package/src/.agents/scripts/self-check.mjs +0 -227
  37. package/src/.agents/scripts/sync-opencode.mjs +0 -362
  38. package/src/.agents/skills/presentation-composability/SKILL.md +0 -72
  39. package/src/.agents/skills/react-ddd-mvc-frontend/SKILL.md +0 -51
  40. package/src/.agents/skills/react-ddd-mvc-frontend/references/feature-structure.md +0 -25
  41. package/src/.agents/skills/react-ddd-mvc-frontend/references/implementation-principles.md +0 -21
  42. package/src/.agents/skills/react-ddd-mvc-frontend/references/responsibility-gates.md +0 -41
  43. package/src/.agents/skills/react-ddd-mvc-frontend/references/source-map.md +0 -11
  44. package/src/.agents/skills/standards/SKILL.md +0 -747
  45. package/src/.agents/skills/xstate-actor-orchestration/SKILL.md +0 -197
  46. package/src/.agents/skills/xstate-actor-orchestration/agents/openai.yaml +0 -4
  47. package/src/.agents/skills/xstate-actor-orchestration/assets/statecharts/.gitkeep +0 -0
  48. package/src/.agents/skills/xstate-actor-orchestration/references/actor-system-patterns.md +0 -71
  49. package/src/.agents/skills/xstate-actor-orchestration/references/event-contracts.md +0 -73
  50. package/src/.agents/skills/xstate-actor-orchestration/references/functional-domain-patterns.md +0 -53
  51. package/src/.agents/skills/xstate-actor-orchestration/references/machine-structure-and-tags.md +0 -36
  52. package/src/.agents/skills/xstate-actor-orchestration/references/react-container-pattern.md +0 -45
  53. package/src/.agents/skills/xstate-actor-orchestration/references/reliability-observability.md +0 -39
  54. package/src/.agents/skills/xstate-actor-orchestration/references/skill-validation.md +0 -33
  55. package/src/.agents/skills/xstate-actor-orchestration/references/source-map.md +0 -44
  56. package/src/.agents/skills/xstate-actor-orchestration/references/statechart-review-and-signoff.md +0 -59
  57. package/src/.agents/skills/xstate-actor-orchestration/references/testing-strategy.md +0 -35
  58. package/src/.agents/skills/xstate-actor-orchestration/scripts/create-statechart-artifact.sh +0 -71
  59. package/src/.agents/skills/xstate-actor-orchestration/scripts/validate-skill.sh +0 -138
  60. package/src/generated/opencode.managed.json +0 -115
  61. /package/src/{.agents/agents → agents}/research/framework-docs-researcher.md +0 -0
  62. /package/src/{.agents/agents → agents}/research/git-history-analyzer.md +0 -0
  63. /package/src/{.agents/agents → agents}/research/learnings-researcher.md +0 -0
  64. /package/src/{.agents/agents → agents}/research/repo-research-analyst.md +0 -0
  65. /package/src/{.agents/agents → agents}/review/agent-native-reviewer.md +0 -0
  66. /package/src/{.agents/agents → agents}/review/planning-technical-reviewer.md +0 -0
  67. /package/src/{.agents/agents → agents}/workflow/bug-reproduction-validator.md +0 -0
  68. /package/src/{.agents/agents → agents}/workflow/lint.md +0 -0
  69. /package/src/{.agents/agents → agents}/workflow/spec-flow-analyzer.md +0 -0
  70. /package/src/{.agents/commands → commands}/workflow-review.md +0 -0
  71. /package/src/{.agents/commands → commands}/workflow-tech-review.md +0 -0
  72. /package/src/{.agents/commands → commands}/workflow-triage.md +0 -0
  73. /package/src/{.agents/references → references}/standards/README.md +0 -0
  74. /package/src/{.agents/skills → skills}/agent-browser/SKILL.md +0 -0
  75. /package/src/{.agents/skills → skills}/audit-traceability/SKILL.md +0 -0
  76. /package/src/{.agents/skills → skills}/brainstorming/SKILL.md +0 -0
  77. /package/src/{.agents/skills → skills}/compound-docs/assets/critical-pattern-template.md +0 -0
  78. /package/src/{.agents/skills → skills}/compound-docs/assets/resolution-template.md +0 -0
  79. /package/src/{.agents/skills → skills}/compound-docs/schema.project.yaml +0 -0
  80. /package/src/{.agents/skills → skills}/compound-docs/schema.yaml +0 -0
  81. /package/src/{.agents/skills → skills}/data-foundations/SKILL.md +0 -0
  82. /package/src/{.agents/skills → skills}/document-review/SKILL.md +0 -0
  83. /package/src/{.agents/skills → skills}/file-todos/SKILL.md +0 -0
  84. /package/src/{.agents/skills → skills}/file-todos/assets/todo-template.md +0 -0
  85. /package/src/{.agents/skills → skills}/financial-workflow-integrity/SKILL.md +0 -0
  86. /package/src/{.agents/skills → skills}/git-worktree/SKILL.md +0 -0
  87. /package/src/{.agents/skills → skills}/pii-protection-prisma/SKILL.md +0 -0
  88. /package/src/{.agents/skills → skills}/process-metrics/SKILL.md +0 -0
  89. /package/src/{.agents/skills → skills}/process-metrics/assets/daily-template.md +0 -0
  90. /package/src/{.agents/skills → skills}/process-metrics/assets/monthly-template.md +0 -0
  91. /package/src/{.agents/skills → skills}/process-metrics/assets/weekly-template.md +0 -0
  92. /package/src/{.agents/skills → skills}/technical-review/SKILL.md +0 -0
@@ -2,195 +2,237 @@
2
2
  /**
3
3
  * compound-workflow install
4
4
  *
5
- * Native-only install: writes opencode.json from package metadata,
6
- * merges AGENTS.md, and ensures standard docs/todo directories.
5
+ * Copies agents, skills, and commands from the package into target platform dirs:
6
+ * .claude/agents/ — flat agent .md files (Claude Code)
7
+ * .cursor/agents/ — agents preserving subdirs (Cursor)
8
+ * .cursor/skills/ — skill dirs (Cursor)
9
+ * .cursor/commands/ — command .md files (Cursor)
10
+ * .agents/agents/ — agents (OpenCode / general)
11
+ * .agents/skills/ — skills (OpenCode / general)
12
+ * .agents/commands/ — commands (OpenCode / general)
7
13
  *
8
- * DECLARATIVE SOURCE OF TRUTH (no manual wiring):
9
- * - Commands: add/remove .md under src/.agents/commands/ (frontmatter: invocation, name, description).
10
- * Registry (src/.agents/registry.json) + generate-platform-artifacts → opencode.managed.json → install.
11
- * - Agents: add/remove .md under src/.agents/agents/ (frontmatter: name, description). Same pipeline.
12
- * - Skills: add/remove dir src/.agents/skills/<name>/SKILL.md. OpenCode uses skills path.
14
+ * Also writes opencode.json, AGENTS.md, and standard docs directories.
13
15
  *
14
- * PLATFORM STRATEGY:
15
- * - Cursor: copies files into .cursor/skills/, .cursor/commands/, .cursor/agents/ for native discovery.
16
- * - Claude Code: marketplace flow via .claude-plugin/ + project .claude/settings.json.
17
- * - OpenCode: opencode.json with paths into package source.
18
- * Run install (or npm install compound-workflow) after any change; no other registration needed.
16
+ * Usage:
17
+ * (automatic) npm install compound-workflow # runs via postinstall
18
+ * (manual) npx compound-workflow install [--root <projectDir>] [--dry-run]
19
19
  */
20
20
  import fs from "node:fs";
21
21
  import path from "node:path";
22
- import os from "node:os";
23
22
  import { fileURLToPath } from "node:url";
24
23
  import { spawnSync } from "node:child_process";
25
24
 
26
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
26
 
28
- function usage(exitCode = 0) {
29
- const msg = `
30
- Usage:
31
- (automatic) npm install compound-workflow # runs install via postinstall; no npx needed
32
- (manual) npx compound-workflow install [--root <projectDir>] [--dry-run] [--no-config]
33
-
34
- Install writes opencode.json (from package), merges AGENTS.md, creates standard
35
- docs/todos directories, copies skills/commands/agents into .cursor/ for Cursor,
36
- and registers the Claude Code plugin (project-scoped).
37
-
38
- --root <dir> Project directory (default: cwd)
39
- --dry-run Print planned changes only
40
- --no-config Skip Repo Config Block reminder
41
- `;
42
- (exitCode === 0 ? console.log : console.error)(msg.trimStart());
43
- process.exit(exitCode);
44
- }
45
-
46
- function parseArgs(argv) {
47
- const out = { root: process.cwd(), dryRun: false, noConfig: false };
48
- for (let i = 2; i < argv.length; i++) {
49
- const arg = argv[i];
50
- if (arg === "--dry-run") out.dryRun = true;
51
- else if (arg === "--no-config") out.noConfig = true;
52
- else if (arg === "--root") {
53
- const value = argv[i + 1];
54
- if (!value) usage(1);
55
- out.root = value;
56
- i++;
57
- } else if (arg === "install") {
58
- continue;
59
- } else if (arg === "-h" || arg === "--help") usage(0);
60
- else usage(1);
61
- }
62
- return out;
63
- }
27
+ // ---------------------------------------------------------------------------
28
+ // Utilities
29
+ // ---------------------------------------------------------------------------
64
30
 
65
31
  function realpathSafe(p) {
66
- try {
67
- return fs.realpathSync(p);
68
- } catch {
69
- return path.resolve(p);
70
- }
32
+ try { return fs.realpathSync(p); } catch { return path.resolve(p); }
71
33
  }
72
34
 
35
+ const PACKAGE_ROOT = realpathSafe(path.join(__dirname, ".."));
36
+
73
37
  function hasCommand(cmd) {
74
38
  const checker = process.platform === "win32" ? "where" : "which";
75
- const result = spawnSync(checker, [cmd], { stdio: "ignore" });
76
- return result.status === 0;
39
+ return spawnSync(checker, [cmd], { stdio: "ignore" }).status === 0;
77
40
  }
78
41
 
79
42
  function stripJsonc(input) {
80
- let out = "";
81
- let i = 0;
82
- let inStr = false;
83
- let strQuote = "";
84
- let escape = false;
85
-
43
+ let out = "", i = 0, inStr = false, strQuote = "", escape = false;
86
44
  while (i < input.length) {
87
- const c = input[i];
88
- const n = input[i + 1];
45
+ const c = input[i], n = input[i + 1];
89
46
  if (inStr) {
90
47
  out += c;
91
48
  if (escape) escape = false;
92
49
  else if (c === "\\") escape = true;
93
50
  else if (c === strQuote) inStr = false;
94
- i++;
95
- continue;
96
- }
97
-
98
- if (c === '"' || c === "'") {
99
- inStr = true;
100
- strQuote = c;
101
- out += c;
102
- i++;
103
- continue;
104
- }
105
-
106
- if (c === "/" && n === "/") {
107
- while (i < input.length && input[i] !== "\n") i++;
108
- continue;
51
+ i++; continue;
109
52
  }
110
-
111
- if (c === "/" && n === "*") {
112
- i += 2;
113
- while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
114
- i += 2;
115
- continue;
116
- }
117
-
118
- out += c;
119
- i++;
53
+ if (c === '"' || c === "'") { inStr = true; strQuote = c; out += c; i++; continue; }
54
+ if (c === "/" && n === "/") { while (i < input.length && input[i] !== "\n") i++; continue; }
55
+ if (c === "/" && n === "*") { i += 2; while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++; i += 2; continue; }
56
+ out += c; i++;
120
57
  }
121
-
122
58
  return out;
123
59
  }
124
60
 
125
61
  function readJsonMaybe(fileAbs) {
126
62
  if (!fs.existsSync(fileAbs)) return null;
127
- const raw = fs.readFileSync(fileAbs, "utf8");
128
- return JSON.parse(stripJsonc(raw));
63
+ return JSON.parse(stripJsonc(fs.readFileSync(fileAbs, "utf8")));
129
64
  }
130
65
 
131
66
  function ensureObject(v) {
132
67
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
133
68
  }
134
69
 
135
- const PACKAGE_ROOT = realpathSafe(path.join(__dirname, ".."));
136
- const PACKAGE_AGENTS_ROOT = path.join(PACKAGE_ROOT, "src", ".agents");
137
- const GENERATED_MANIFEST_PATH = path.join(PACKAGE_ROOT, "src", "generated", "opencode.managed.json");
138
-
139
- function readGeneratedManifest() {
140
- if (!fs.existsSync(GENERATED_MANIFEST_PATH)) {
141
- throw new Error(
142
- `Missing generated OpenCode manifest at ${GENERATED_MANIFEST_PATH}. Run: npm run generate:artifacts`
143
- );
70
+ function parseFrontmatter(md) {
71
+ if (!md.startsWith("---\n") && !md.startsWith("---\r\n")) return {};
72
+ const end = md.indexOf("\n---", 4);
73
+ if (end === -1) return {};
74
+ const block = md.slice(4, end + 1);
75
+ const out = {};
76
+ for (const line of block.split(/\r?\n/)) {
77
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)\s*$/);
78
+ if (!match) continue;
79
+ out[match[1]] = (match[2] ?? "").replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
144
80
  }
145
- const manifest = JSON.parse(fs.readFileSync(GENERATED_MANIFEST_PATH, "utf8"));
146
- if (!manifest?.commandRoot || !manifest?.agentRoot || !manifest?.skillsPath) {
147
- throw new Error("Invalid generated OpenCode manifest: missing root path fields.");
81
+ return out;
82
+ }
83
+
84
+ function walkFiles(dirAbs, ext) {
85
+ const out = [];
86
+ const stack = [dirAbs];
87
+ while (stack.length) {
88
+ const cur = stack.pop();
89
+ let entries;
90
+ try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
91
+ for (const e of entries) {
92
+ const p = path.join(cur, e.name);
93
+ if (e.isDirectory()) stack.push(p);
94
+ else if (e.isFile() && (!ext || p.endsWith(ext))) out.push(p);
95
+ }
148
96
  }
149
- if (!Array.isArray(manifest?.commands) || !Array.isArray(manifest?.agents)) {
150
- throw new Error("Invalid generated OpenCode manifest: commands/agents must be arrays.");
97
+ return out.sort();
98
+ }
99
+
100
+ function copyDirRecursive(srcDir, destDir) {
101
+ fs.mkdirSync(destDir, { recursive: true });
102
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
103
+ const srcPath = path.join(srcDir, entry.name);
104
+ const destPath = path.join(destDir, entry.name);
105
+ if (entry.isDirectory()) {
106
+ copyDirRecursive(srcPath, destPath);
107
+ } else if (entry.isFile()) {
108
+ // Remove symlinks before copying so we write a real file, not through a broken link
109
+ try { if (fs.lstatSync(destPath).isSymbolicLink()) fs.rmSync(destPath, { force: true }); } catch { /* doesn't exist */ }
110
+ fs.copyFileSync(srcPath, destPath);
111
+ }
151
112
  }
152
- return manifest;
153
113
  }
154
114
 
155
- let GENERATED_MANIFEST;
156
- let PACKAGE_COMMAND_ROOT;
157
- let PACKAGE_AGENT_ROOT;
158
- let PACKAGE_SKILL_ROOT;
115
+ // ---------------------------------------------------------------------------
116
+ // Copy operations
117
+ // ---------------------------------------------------------------------------
159
118
 
160
- function getLegacyCommandRoots() {
161
- return [".agents/compound-workflow/commands", PACKAGE_COMMAND_ROOT, "src/.agents/commands"];
162
- }
163
- function getLegacyAgentRoots() {
164
- return [".agents/compound-workflow/agents", PACKAGE_AGENT_ROOT, "src/.agents/agents"];
119
+ /**
120
+ * Copy all .md files from srcDir (recursively) into destDir (flat).
121
+ * Prunes .md files in destDir not in current source.
122
+ */
123
+ function copyAgentsFlat(srcDir, destDir, dryRun, label) {
124
+ const files = walkFiles(srcDir, ".md");
125
+ const srcNames = new Set(files.map((f) => path.basename(f)));
126
+ if (dryRun) { console.log(`[dry-run] Would copy ${files.length} agents (flat) to ${label}`); return; }
127
+ fs.mkdirSync(destDir, { recursive: true });
128
+ try {
129
+ for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
130
+ if (e.name.endsWith(".md") && !srcNames.has(e.name)) fs.rmSync(path.join(destDir, e.name), { force: true });
131
+ }
132
+ } catch { /* ignore */ }
133
+ for (const f of files) {
134
+ const dest = path.join(destDir, path.basename(f));
135
+ try { if (fs.lstatSync(dest).isSymbolicLink()) fs.rmSync(dest, { force: true }); } catch { /* doesn't exist */ }
136
+ fs.copyFileSync(f, dest);
137
+ }
138
+ console.log(`Copied ${files.length} agents to ${label}`);
165
139
  }
166
140
 
167
- function managedCommandPath(entry) {
168
- const template = entry?.template;
169
- if (typeof template !== "string") return null;
170
- const match = template.match(/@([^\n\r]+)\nArguments: \$ARGUMENTS\n?$/m);
171
- if (!match) return null;
172
- return match[1].trim();
141
+ /**
142
+ * Copy srcDir recursively to destDir, pruning top-level subdirs no longer in source.
143
+ */
144
+ function copyAgentsRecursive(srcDir, destDir, dryRun, label) {
145
+ const files = walkFiles(srcDir, ".md");
146
+ if (dryRun) { console.log(`[dry-run] Would copy ${files.length} agents (recursive) to ${label}`); return; }
147
+ const validSubdirs = new Set();
148
+ for (const f of files) {
149
+ const sub = path.relative(srcDir, path.dirname(f));
150
+ if (sub && sub !== ".") validSubdirs.add(sub.split(path.sep)[0]);
151
+ }
152
+ fs.mkdirSync(destDir, { recursive: true });
153
+ try {
154
+ for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
155
+ if (e.isDirectory() && !validSubdirs.has(e.name)) fs.rmSync(path.join(destDir, e.name), { recursive: true, force: true });
156
+ }
157
+ } catch { /* ignore */ }
158
+ copyDirRecursive(srcDir, destDir);
159
+ console.log(`Copied ${files.length} agents to ${label}`);
173
160
  }
174
161
 
175
- function managedAgentPath(entry) {
176
- const prompt = entry?.prompt;
177
- if (typeof prompt !== "string") return null;
178
- const match = prompt.match(/^\{file:([^}]+)\}$/);
179
- return match ? match[1].trim() : null;
162
+ /**
163
+ * Copy skill directories (each containing SKILL.md) to destDir.
164
+ * Prunes skill dirs in destDir no longer in source.
165
+ */
166
+ function copySkills(srcDir, destDir, dryRun, label) {
167
+ if (!fs.existsSync(srcDir)) return;
168
+ const skillDirs = fs.readdirSync(srcDir, { withFileTypes: true })
169
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(srcDir, e.name, "SKILL.md")))
170
+ .map((e) => e.name);
171
+ if (dryRun) { console.log(`[dry-run] Would copy ${skillDirs.length} skills to ${label}`); return; }
172
+ const skillSet = new Set(skillDirs);
173
+ fs.mkdirSync(destDir, { recursive: true });
174
+ try {
175
+ for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
176
+ if (e.isDirectory() && fs.existsSync(path.join(destDir, e.name, "SKILL.md")) && !skillSet.has(e.name))
177
+ fs.rmSync(path.join(destDir, e.name), { recursive: true, force: true });
178
+ }
179
+ } catch { /* ignore */ }
180
+ for (const name of skillDirs) {
181
+ const dest = path.join(destDir, name);
182
+ fs.rmSync(dest, { recursive: true, force: true });
183
+ copyDirRecursive(path.join(srcDir, name), dest);
184
+ }
185
+ console.log(`Copied ${skillDirs.length} skills to ${label}`);
180
186
  }
181
187
 
182
- function isManagedCommandPath(commandPath) {
183
- return getLegacyCommandRoots().some((root) => commandPath.startsWith(`${root}/`));
188
+ /**
189
+ * Copy .md command files from srcDir to destDir (flat).
190
+ * Prunes .md files in destDir not in current source.
191
+ */
192
+ function copyCommands(srcDir, destDir, dryRun, label) {
193
+ if (!fs.existsSync(srcDir)) return;
194
+ const files = fs.readdirSync(srcDir, { withFileTypes: true })
195
+ .filter((e) => e.isFile() && e.name.endsWith(".md"))
196
+ .map((e) => e.name);
197
+ if (dryRun) { console.log(`[dry-run] Would copy ${files.length} commands to ${label}`); return; }
198
+ const fileSet = new Set(files);
199
+ fs.mkdirSync(destDir, { recursive: true });
200
+ try {
201
+ for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
202
+ if (e.name.endsWith(".md") && !fileSet.has(e.name)) fs.rmSync(path.join(destDir, e.name), { force: true });
203
+ }
204
+ } catch { /* ignore */ }
205
+ for (const name of files) fs.copyFileSync(path.join(srcDir, name), path.join(destDir, name));
206
+ console.log(`Copied ${files.length} commands to ${label}`);
184
207
  }
185
208
 
186
- function isManagedAgentPath(agentPath) {
187
- return getLegacyAgentRoots().some((root) => agentPath.startsWith(`${root}/`));
188
- }
209
+ // ---------------------------------------------------------------------------
210
+ // opencode.json
211
+ // ---------------------------------------------------------------------------
212
+
213
+ function writeOpenCodeJson(targetRoot, srcRoot, dryRun) {
214
+ const commandsDir = path.join(srcRoot, "commands");
215
+ const agentsDir = path.join(srcRoot, "agents");
216
+
217
+ const commands = [];
218
+ if (fs.existsSync(commandsDir)) {
219
+ for (const f of walkFiles(commandsDir, ".md")) {
220
+ const rel = path.relative(commandsDir, f).replaceAll(path.sep, "/");
221
+ const fm = parseFrontmatter(fs.readFileSync(f, "utf8"));
222
+ const id = fm.invocation || fm.name || path.basename(f, ".md");
223
+ commands.push({ id: id.trim(), description: (fm.description || id).trim(), rel });
224
+ }
225
+ }
189
226
 
190
- function writeOpenCodeJson(targetRoot, dryRun, isSelfInstall) {
191
- const commandRoot = isSelfInstall ? "src/.agents/commands" : PACKAGE_COMMAND_ROOT;
192
- const agentRoot = isSelfInstall ? "src/.agents/agents" : PACKAGE_AGENT_ROOT;
193
- const skillRoot = isSelfInstall ? "src/.agents/skills" : PACKAGE_SKILL_ROOT;
227
+ const agents = [];
228
+ if (fs.existsSync(agentsDir)) {
229
+ for (const f of walkFiles(agentsDir, ".md")) {
230
+ const rel = path.relative(agentsDir, f).replaceAll(path.sep, "/");
231
+ const fm = parseFrontmatter(fs.readFileSync(f, "utf8"));
232
+ const id = fm.name || path.basename(f, ".md");
233
+ agents.push({ id: id.trim(), description: (fm.description || id).trim(), rel });
234
+ }
235
+ }
194
236
 
195
237
  const opencodeAbs = path.join(targetRoot, "opencode.json");
196
238
  const existing = readJsonMaybe(opencodeAbs) ?? {};
@@ -199,112 +241,76 @@ function writeOpenCodeJson(targetRoot, dryRun, isSelfInstall) {
199
241
  next.$schema = next.$schema || "https://opencode.ai/config.json";
200
242
  next.skills = ensureObject(next.skills);
201
243
  next.skills.paths = Array.isArray(next.skills.paths) ? next.skills.paths : [];
202
-
203
- next.skills.paths = next.skills.paths.filter((p) => p !== ".agents/compound-workflow-skills");
204
- if (!next.skills.paths.includes(skillRoot)) {
205
- next.skills.paths.unshift(skillRoot);
206
- }
244
+ // Remove old package-relative paths, ensure .agents/skills is present
245
+ next.skills.paths = next.skills.paths.filter((p) => !p.includes("compound-workflow") && !p.includes("src/.agents"));
246
+ if (!next.skills.paths.includes(".agents/skills")) next.skills.paths.unshift(".agents/skills");
207
247
 
208
248
  next.command = ensureObject(next.command);
209
249
  next.agent = ensureObject(next.agent);
210
250
 
211
- const commands = GENERATED_MANIFEST.commands;
212
- const agents = GENERATED_MANIFEST.agents;
213
-
214
- for (const command of commands) {
215
- next.command[command.id] = {
216
- ...ensureObject(next.command[command.id]),
217
- description: command.description,
251
+ for (const cmd of commands) {
252
+ next.command[cmd.id] = {
253
+ ...ensureObject(next.command[cmd.id]),
254
+ description: cmd.description,
218
255
  agent: "build",
219
- template: `@AGENTS.md\n@${commandRoot}/${command.rel}\nArguments: $ARGUMENTS\n`,
256
+ template: `@AGENTS.md\n@.agents/commands/${cmd.rel}\nArguments: $ARGUMENTS\n`,
220
257
  };
221
258
  }
222
259
 
223
- const managedCommandTargets = new Set(
224
- commands.map((command) => `${commandRoot}/${command.rel}`)
225
- );
226
- for (const [id, entry] of Object.entries(next.command)) {
227
- const commandPath = managedCommandPath(entry);
228
- if (!commandPath || !isManagedCommandPath(commandPath)) continue;
229
- if (!managedCommandTargets.has(commandPath)) delete next.command[id];
230
- }
231
-
232
- for (const agent of agents) {
233
- next.agent[agent.id] = {
234
- ...ensureObject(next.agent[agent.id]),
235
- description: agent.description,
260
+ for (const ag of agents) {
261
+ next.agent[ag.id] = {
262
+ ...ensureObject(next.agent[ag.id]),
263
+ description: ag.description,
236
264
  mode: "subagent",
237
- prompt: `{file:${agentRoot}/${agent.rel}}`,
238
- permission: { ...ensureObject(next.agent[agent.id]?.permission), edit: "deny" },
265
+ prompt: `{file:.agents/agents/${ag.rel}}`,
266
+ permission: { ...ensureObject(next.agent[ag.id]?.permission), edit: "deny" },
239
267
  };
240
268
  }
241
269
 
242
- const managedAgentTargets = new Set(
243
- agents.map((agent) => `${agentRoot}/${agent.rel}`)
244
- );
245
- for (const [id, entry] of Object.entries(next.agent)) {
246
- const agentPath = managedAgentPath(entry);
247
- if (!agentPath || !isManagedAgentPath(agentPath)) continue;
248
- if (!managedAgentTargets.has(agentPath)) delete next.agent[id];
249
- }
250
-
251
- const out = JSON.stringify(next, null, 2) + "\n";
252
- if (dryRun) {
253
- console.log("[dry-run] Would write opencode.json:", opencodeAbs);
254
- return;
255
- }
256
-
257
- fs.writeFileSync(opencodeAbs, out, "utf8");
258
- console.log("Wrote:", opencodeAbs);
270
+ if (dryRun) { console.log("[dry-run] Would write opencode.json"); return; }
271
+ fs.writeFileSync(opencodeAbs, JSON.stringify(next, null, 2) + "\n", "utf8");
272
+ console.log("Wrote: opencode.json");
259
273
  }
260
274
 
275
+ // ---------------------------------------------------------------------------
276
+ // AGENTS.md merge
277
+ // ---------------------------------------------------------------------------
278
+
261
279
  function extractRepoConfigBlock(md) {
262
280
  const match = md.match(/(### Repo Config Block[^\n]*\n)?\s*```yaml\n([\s\S]*?)```/);
263
281
  if (!match) return { block: null, rest: md };
264
- const full = match[0];
265
282
  const block = match[2].trim();
266
- const rest = md.replace(full, "").replace(/\n{3,}/g, "\n\n").trim();
283
+ const rest = md.replace(match[0], "").replace(/\n{3,}/g, "\n\n").trim();
267
284
  return { block, rest };
268
285
  }
269
286
 
270
- function mergeAgentsMd(templateMd, existingMd) {
271
- const { block: existingBlock } = existingMd
272
- ? extractRepoConfigBlock(existingMd)
273
- : { block: null };
287
+ function writeAgentsMd(targetRoot, packageRoot, dryRun) {
288
+ const templatePath = path.join(packageRoot, "src", "AGENTS.md");
289
+ const targetPath = path.join(targetRoot, "AGENTS.md");
290
+ const templateMd = fs.readFileSync(templatePath, "utf8");
291
+ const existingMd = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
292
+
293
+ const { block: existingBlock } = existingMd ? extractRepoConfigBlock(existingMd) : { block: null };
274
294
  const { rest: templateRest } = extractRepoConfigBlock(templateMd);
275
295
 
276
296
  let out = templateRest;
277
297
  if (existingBlock) {
278
298
  const repoSection = `### Repo Config Block (Optional)\n\n\`\`\`yaml\n${existingBlock}\n\`\`\`\n`;
279
-
280
299
  if (!out.includes("### Repo Config Block")) {
281
- out = out.replace(
282
- "## Repo Configuration (Optional)",
283
- `## Repo Configuration (Optional)\n\n${repoSection}`
284
- );
300
+ out = out.replace("## Repo Configuration (Optional)", `## Repo Configuration (Optional)\n\n${repoSection}`);
285
301
  } else {
286
302
  out = out.replace(/### Repo Config Block[^\n]*\n\s*```yaml\n[\s\S]*?```/, repoSection);
287
303
  }
288
304
  }
289
305
 
290
- return out;
306
+ if (dryRun) { console.log("[dry-run] Would write AGENTS.md"); return; }
307
+ fs.writeFileSync(targetPath, out, "utf8");
308
+ console.log("Wrote: AGENTS.md");
291
309
  }
292
310
 
293
- function writeAgentsMd(targetRoot, dryRun) {
294
- const templatePath = path.join(PACKAGE_ROOT, "src", "AGENTS.md");
295
- const targetPath = path.join(targetRoot, "AGENTS.md");
296
- const templateMd = fs.readFileSync(templatePath, "utf8");
297
- const existingMd = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
298
- const merged = mergeAgentsMd(templateMd, existingMd);
299
-
300
- if (dryRun) {
301
- console.log("[dry-run] Would write AGENTS.md:", targetPath);
302
- return;
303
- }
304
-
305
- fs.writeFileSync(targetPath, merged, "utf8");
306
- console.log("Wrote:", targetPath);
307
- }
311
+ // ---------------------------------------------------------------------------
312
+ // Standard dirs
313
+ // ---------------------------------------------------------------------------
308
314
 
309
315
  const DIRS = [
310
316
  "docs/brainstorms",
@@ -319,378 +325,90 @@ const DIRS = [
319
325
  function ensureDirs(targetRoot, dryRun) {
320
326
  for (const d of DIRS) {
321
327
  const abs = path.join(targetRoot, d);
322
- if (dryRun && !fs.existsSync(abs)) {
323
- console.log("[dry-run] Would create:", d);
324
- } else if (!fs.existsSync(abs)) {
325
- fs.mkdirSync(abs, { recursive: true });
326
- console.log("Created:", d);
327
- }
328
- }
329
- }
330
-
331
- /**
332
- * Writes .cursor-plugin/plugin.json and .claude-plugin/plugin.json at targetRoot.
333
- * Paths (commands, agents, skills) in the written manifests are relative to project root
334
- * (parent of .cursor-plugin / .claude-plugin) so Cursor/Claude resolve assets correctly.
335
- */
336
- function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
337
- const pathsBase = isSelfInstall ? "./src/.agents" : "./node_modules/compound-workflow/src/.agents";
338
- const cursorSrc = path.join(PACKAGE_ROOT, ".cursor-plugin", "plugin.json");
339
- const claudeSrc = path.join(PACKAGE_ROOT, ".claude-plugin", "plugin.json");
340
- const cursorManifest = readJsonMaybe(cursorSrc);
341
- const claudeManifest = readJsonMaybe(claudeSrc);
342
- if (!cursorManifest || !claudeManifest) return;
343
-
344
- // All Cursor paths point directly at the package source — no symlink indirection.
345
- // This ensures frontmatter (descriptions) is parsed correctly by Cursor for all components.
346
- const cursorOut = {
347
- ...cursorManifest,
348
- commands: `${pathsBase}/commands`,
349
- agents: `${pathsBase}/agents`,
350
- skills: `${pathsBase}/skills`,
351
- };
352
- // Claude Code only accepts name, description, author in plugin.json.
353
- // Agents are discovered from the adjacent agents/ directory (flat .md files).
354
- const claudeOut = {
355
- name: claudeManifest.name,
356
- description: claudeManifest.description,
357
- author: claudeManifest.author,
358
- };
359
- const cursorDir = path.join(targetRoot, ".cursor-plugin");
360
- const claudeDir = path.join(targetRoot, ".claude-plugin");
361
-
362
- const installPathAbs = realpathSafe(targetRoot);
363
- const registrationDescriptor = {
364
- pluginId: "compound-workflow@local",
365
- scope: "user",
366
- installPath: installPathAbs,
367
- };
368
-
369
- if (dryRun) {
370
- console.log("[dry-run] Would write .cursor-plugin/plugin.json, .claude-plugin/plugin.json, .cursor-plugin/registration.json" + (isSelfInstall ? "" : ", .claude-plugin/marketplace.json"));
371
- return;
372
- }
373
- fs.mkdirSync(cursorDir, { recursive: true });
374
- fs.mkdirSync(claudeDir, { recursive: true });
375
- fs.writeFileSync(path.join(cursorDir, "plugin.json"), JSON.stringify(cursorOut, null, 2) + "\n", "utf8");
376
- fs.writeFileSync(path.join(claudeDir, "plugin.json"), JSON.stringify(claudeOut, null, 2) + "\n", "utf8");
377
- fs.writeFileSync(path.join(cursorDir, "registration.json"), JSON.stringify(registrationDescriptor, null, 2) + "\n", "utf8");
378
-
379
- // Sync flat agent symlinks into .claude-plugin/agents/ so Claude Code discovers them.
380
- // Claude Code only scans the root of the agents/ directory (not subdirectories).
381
- const claudeAgentsDir = path.join(claudeDir, "agents");
382
- const packageAgentsDirAbs = isSelfInstall
383
- ? path.join(PACKAGE_ROOT, "src", ".agents", "agents")
384
- : path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "agents");
385
- if (fs.existsSync(packageAgentsDirAbs)) {
386
- fs.mkdirSync(claudeAgentsDir, { recursive: true });
387
- const agentBasenames = new Set(GENERATED_MANIFEST.agents.map((a) => path.basename(a.rel)));
388
- // Prune stale symlinks
389
- try {
390
- for (const entry of fs.readdirSync(claudeAgentsDir, { withFileTypes: true })) {
391
- if (!agentBasenames.has(entry.name)) {
392
- fs.rmSync(path.join(claudeAgentsDir, entry.name), { force: true });
393
- }
394
- }
395
- } catch { /* ignore */ }
396
- for (const agent of GENERATED_MANIFEST.agents) {
397
- const linkPath = path.join(claudeAgentsDir, path.basename(agent.rel));
398
- const targetPath = path.join(packageAgentsDirAbs, agent.rel);
399
- try {
400
- if (fs.lstatSync(linkPath)) fs.rmSync(linkPath, { force: true });
401
- } catch { /* doesn't exist */ }
402
- try {
403
- fs.symlinkSync(targetPath, linkPath);
404
- } catch (err) {
405
- console.warn("[claude] Could not symlink agent", agent.id, err.message);
406
- }
328
+ if (!fs.existsSync(abs)) {
329
+ if (dryRun) console.log("[dry-run] Would create:", d);
330
+ else { fs.mkdirSync(abs, { recursive: true }); console.log("Created:", d); }
407
331
  }
408
332
  }
409
-
410
- // Claude Code 2.1.x+ no longer loads from installed_plugins.json; it requires marketplace flow.
411
- // Write a project-level marketplace so user can: /plugin marketplace add . then /plugin install compound-workflow@compound-workflow-local
412
- if (!isSelfInstall) {
413
- const marketplaceManifest = {
414
- name: "compound-workflow-local",
415
- owner: { name: "Compound Workflow" },
416
- plugins: [
417
- {
418
- name: "compound-workflow",
419
- source: "./node_modules/compound-workflow",
420
- description: claudeOut.description || "Clarify → plan → execute → verify → capture workflow.",
421
- },
422
- ],
423
- };
424
- fs.writeFileSync(path.join(claudeDir, "marketplace.json"), JSON.stringify(marketplaceManifest, null, 2) + "\n", "utf8");
425
- }
426
- console.log("Wrote: .cursor-plugin/plugin.json, .claude-plugin/plugin.json, .cursor-plugin/registration.json" + (isSelfInstall ? "" : ", .claude-plugin/marketplace.json"));
427
333
  }
428
334
 
335
+ // ---------------------------------------------------------------------------
336
+ // CLI args
337
+ // ---------------------------------------------------------------------------
429
338
 
339
+ function usage(exitCode = 0) {
340
+ const msg = `
341
+ Usage:
342
+ (automatic) npm install compound-workflow # runs install via postinstall
343
+ (manual) npx compound-workflow install [--root <projectDir>] [--dry-run]
430
344
 
431
- function copyDirRecursive(srcDir, destDir) {
432
- fs.mkdirSync(destDir, { recursive: true });
433
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
434
- const srcPath = path.join(srcDir, entry.name);
435
- const destPath = path.join(destDir, entry.name);
436
- if (entry.isDirectory()) {
437
- copyDirRecursive(srcPath, destPath);
438
- } else if (entry.isFile()) {
439
- fs.copyFileSync(srcPath, destPath);
440
- }
441
- }
442
- }
443
-
444
- /**
445
- * Cursor discovers skills from .cursor/skills/ (each subdir with SKILL.md).
446
- * Copy package skills so Cursor finds them with full frontmatter metadata.
447
- */
448
- function copyCursorSkills(targetRoot, dryRun, isSelfInstall) {
449
- const packageSkillsAbs = path.join(
450
- isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
451
- "src", ".agents", "skills"
452
- );
453
- if (!fs.existsSync(packageSkillsAbs)) return;
454
-
455
- const cursorSkillsDir = path.join(targetRoot, ".cursor", "skills");
456
- const skillDirs = fs.readdirSync(packageSkillsAbs, { withFileTypes: true })
457
- .filter((e) => e.isDirectory() && fs.existsSync(path.join(packageSkillsAbs, e.name, "SKILL.md")))
458
- .map((e) => e.name);
459
- if (skillDirs.length === 0) return;
460
-
461
- if (dryRun) {
462
- console.log("[dry-run] Would copy", skillDirs.length, "skills into .cursor/skills/");
463
- return;
464
- }
465
-
466
- fs.mkdirSync(cursorSkillsDir, { recursive: true });
467
- const skillSet = new Set(skillDirs);
468
-
469
- // Prune stale entries that we manage (contain SKILL.md) but are no longer in the package
470
- try {
471
- for (const entry of fs.readdirSync(cursorSkillsDir, { withFileTypes: true })) {
472
- if (!entry.isDirectory()) continue;
473
- const skillMd = path.join(cursorSkillsDir, entry.name, "SKILL.md");
474
- if (!fs.existsSync(skillMd)) continue;
475
- if (!skillSet.has(entry.name)) {
476
- fs.rmSync(path.join(cursorSkillsDir, entry.name), { recursive: true, force: true });
477
- }
478
- }
479
- } catch { /* ignore */ }
480
-
481
- for (const name of skillDirs) {
482
- const dest = path.join(cursorSkillsDir, name);
483
- fs.rmSync(dest, { recursive: true, force: true });
484
- copyDirRecursive(path.join(packageSkillsAbs, name), dest);
485
- }
486
- console.log("Copied", skillDirs.length, "skills to .cursor/skills/");
487
- }
488
-
489
- /**
490
- * Cursor discovers commands from .cursor/commands/ (.md files).
491
- * Copy package commands so Cursor finds them with full frontmatter metadata.
492
- */
493
- function copyCursorCommands(targetRoot, dryRun, isSelfInstall) {
494
- const packageCommandsAbs = path.join(
495
- isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
496
- "src", ".agents", "commands"
497
- );
498
- if (!fs.existsSync(packageCommandsAbs)) return;
499
-
500
- const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
501
- const commandFiles = fs.readdirSync(packageCommandsAbs, { withFileTypes: true })
502
- .filter((e) => e.isFile() && e.name.endsWith(".md"))
503
- .map((e) => e.name);
504
- if (commandFiles.length === 0) return;
505
-
506
- if (dryRun) {
507
- console.log("[dry-run] Would copy", commandFiles.length, "commands into .cursor/commands/");
508
- return;
509
- }
510
-
511
- fs.mkdirSync(cursorCommandsDir, { recursive: true });
512
- const commandSet = new Set(commandFiles);
513
-
514
- // Prune stale .md files and old symlinks that we previously managed
515
- try {
516
- for (const entry of fs.readdirSync(cursorCommandsDir, { withFileTypes: true })) {
517
- if (!entry.name.endsWith(".md")) continue;
518
- if (entry.isSymbolicLink() || !commandSet.has(entry.name)) {
519
- fs.rmSync(path.join(cursorCommandsDir, entry.name), { force: true });
520
- }
521
- }
522
- } catch { /* ignore */ }
523
-
524
- for (const name of commandFiles) {
525
- fs.copyFileSync(path.join(packageCommandsAbs, name), path.join(cursorCommandsDir, name));
526
- }
527
- console.log("Copied", commandFiles.length, "commands to .cursor/commands/");
528
- }
529
-
530
- /**
531
- * Cursor discovers agents from .cursor/agents/ (.md files, supports subdirs).
532
- * Copy package agents preserving subdirectory structure (research/, review/, workflow/).
533
- */
534
- function copyCursorAgents(targetRoot, dryRun, isSelfInstall) {
535
- const packageAgentsAbs = path.join(
536
- isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
537
- "src", ".agents", "agents"
538
- );
539
- if (!fs.existsSync(packageAgentsAbs)) return;
540
-
541
- const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
542
- const agentRels = GENERATED_MANIFEST.agents.map((a) => a.rel);
543
- if (agentRels.length === 0) return;
544
-
545
- if (dryRun) {
546
- console.log("[dry-run] Would copy", agentRels.length, "agents into .cursor/agents/");
547
- return;
548
- }
549
-
550
- fs.mkdirSync(cursorAgentsDir, { recursive: true });
551
-
552
- // Collect valid subdirectories from manifest
553
- const validSubdirs = new Set();
554
- for (const rel of agentRels) {
555
- const subdir = path.dirname(rel);
556
- if (subdir !== ".") validSubdirs.add(subdir);
557
- }
558
-
559
- // Prune stale subdirs and files
560
- try {
561
- for (const entry of fs.readdirSync(cursorAgentsDir, { withFileTypes: true })) {
562
- if (entry.isDirectory() && !validSubdirs.has(entry.name)) {
563
- fs.rmSync(path.join(cursorAgentsDir, entry.name), { recursive: true, force: true });
564
- }
565
- }
566
- } catch { /* ignore */ }
567
-
568
- for (const rel of agentRels) {
569
- const destPath = path.join(cursorAgentsDir, rel);
570
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
571
- fs.copyFileSync(path.join(packageAgentsAbs, rel), destPath);
572
- }
573
- console.log("Copied", agentRels.length, "agents to .cursor/agents/");
574
- }
345
+ Copies agents, skills, and commands into .claude/, .cursor/, and .agents/.
346
+ Also writes opencode.json, AGENTS.md, and standard docs directories.
575
347
 
576
- function cursorDetected() {
577
- return fs.existsSync(path.join(os.homedir(), ".cursor"));
348
+ --root <dir> Project directory (default: cwd)
349
+ --dry-run Print planned changes only
350
+ `;
351
+ (exitCode === 0 ? console.log : console.error)(msg.trimStart());
352
+ process.exit(exitCode);
578
353
  }
579
354
 
580
- /**
581
- * Register compound-workflow with Claude Code (project-scoped only).
582
- * Cursor discovery is handled separately via copyCursor* functions.
583
- */
584
- function applyClaudeRegistration(targetRoot, dryRun, isSelfInstall) {
585
- const projectRoot = isSelfInstall ? PACKAGE_ROOT : targetRoot;
586
- const pluginId = "compound-workflow@compound-workflow-local";
587
-
588
- if (dryRun) {
589
- console.log("[dry-run] Would register Claude plugin (project-scoped) at:", projectRoot);
590
- return;
591
- }
592
-
593
- const projectSettingsPath = path.join(projectRoot, ".claude", "settings.json");
594
- let projectSettings = {};
595
- if (fs.existsSync(projectSettingsPath)) {
596
- try { projectSettings = readJsonMaybe(projectSettingsPath) ?? {}; } catch { projectSettings = {}; }
597
- }
598
- projectSettings.enabledPlugins = ensureObject(projectSettings.enabledPlugins);
599
- projectSettings.enabledPlugins[pluginId] = true;
600
- if (projectSettings.extraKnownMarketplaces?.["compound-workflow"]) {
601
- delete projectSettings.extraKnownMarketplaces["compound-workflow"];
602
- }
603
- projectSettings.extraKnownMarketplaces = ensureObject(projectSettings.extraKnownMarketplaces);
604
- projectSettings.extraKnownMarketplaces["compound-workflow-local"] = {
605
- source: { source: "file", path: "." },
606
- };
607
- fs.mkdirSync(path.join(projectRoot, ".claude"), { recursive: true });
608
- fs.writeFileSync(projectSettingsPath, JSON.stringify(projectSettings, null, 2) + "\n", "utf8");
609
-
610
- console.log("Registered compound-workflow with Claude Code (project-scoped).");
611
- if (!isSelfInstall) {
612
- console.log(" Claude Code 2.1+: open /plugin, go to Discover; install 'compound-workflow' from marketplace 'compound-workflow-local', or run: claude --plugin-dir ./node_modules/compound-workflow");
355
+ function parseArgs(argv) {
356
+ const out = { root: process.cwd(), dryRun: false };
357
+ for (let i = 2; i < argv.length; i++) {
358
+ const arg = argv[i];
359
+ if (arg === "--dry-run") out.dryRun = true;
360
+ else if (arg === "--root") { const v = argv[++i]; if (!v) usage(1); out.root = v; }
361
+ else if (arg === "install") { /* subcommand, ignore */ }
362
+ else if (arg === "-h" || arg === "--help") usage(0);
363
+ else usage(1);
613
364
  }
614
- console.log(" Restart Claude Code; enable 'Include third-party Plugins, Skills, and other configs' in Settings if needed.");
365
+ return out;
615
366
  }
616
367
 
617
- function reportOpenCodeIntegration(targetRoot, dryRun) {
618
- if (dryRun) {
619
- console.log("[dry-run] OpenCode integration check skipped (state would be updated by install).");
620
- return;
621
- }
622
-
623
- const opencodeAbs = path.join(targetRoot, "opencode.json");
624
- const opencode = readJsonMaybe(opencodeAbs) ?? {};
625
- const skillPaths = Array.isArray(opencode?.skills?.paths) ? opencode.skills.paths : [];
626
- const hasSkillPath = skillPaths.includes(PACKAGE_SKILL_ROOT) || skillPaths.includes("src/.agents/skills");
627
-
628
- console.log(
629
- "OpenCode integration:",
630
- hasSkillPath ? "ok" : "incomplete",
631
- `(skills.path=${hasSkillPath ? "yes" : "no"})`
632
- );
633
- }
368
+ // ---------------------------------------------------------------------------
369
+ // Main
370
+ // ---------------------------------------------------------------------------
634
371
 
635
372
  function main() {
636
373
  const args = parseArgs(process.argv);
637
374
  const targetRoot = realpathSafe(args.root);
375
+ const isSelfInstall = targetRoot === PACKAGE_ROOT;
638
376
 
639
- const genScript = path.join(PACKAGE_ROOT, "scripts", "generate-platform-artifacts.mjs");
640
- if (fs.existsSync(genScript)) {
641
- console.log("[compound-workflow] Regenerating manifest from package source...");
642
- const result = spawnSync(process.execPath, [genScript], {
643
- cwd: PACKAGE_ROOT,
644
- stdio: "pipe",
645
- encoding: "utf8",
646
- });
647
- if (result.status !== 0) {
648
- console.error("Failed to regenerate manifest:", result.stderr || result.error || "unknown");
649
- process.exit(1);
650
- }
651
- }
377
+ const packageSrc = isSelfInstall
378
+ ? path.join(PACKAGE_ROOT, "src")
379
+ : path.join(targetRoot, "node_modules", "compound-workflow", "src");
652
380
 
653
- try {
654
- GENERATED_MANIFEST = readGeneratedManifest();
655
- PACKAGE_COMMAND_ROOT = GENERATED_MANIFEST.commandRoot;
656
- PACKAGE_AGENT_ROOT = GENERATED_MANIFEST.agentRoot;
657
- PACKAGE_SKILL_ROOT = GENERATED_MANIFEST.skillsPath;
658
- } catch (err) {
659
- console.error("Error: OpenCode manifest not found. Run 'npm run generate:artifacts' in the package or reinstall compound-workflow.");
381
+ if (!isSelfInstall && !fs.existsSync(packageSrc) && !args.dryRun) {
382
+ console.error("Error: compound-workflow not found in project. Run: npm install compound-workflow");
660
383
  process.exit(2);
661
384
  }
662
385
 
663
- if (!fs.existsSync(PACKAGE_AGENTS_ROOT)) {
664
- console.error("Error: package agents dir not found:", PACKAGE_AGENTS_ROOT);
665
- process.exit(2);
666
- }
386
+ const srcAgents = path.join(packageSrc, "agents");
387
+ const srcSkills = path.join(packageSrc, "skills");
388
+ const srcCommands = path.join(packageSrc, "commands");
667
389
 
668
- const isSelfInstall = realpathSafe(targetRoot) === realpathSafe(PACKAGE_ROOT);
669
- const pkgInTarget = path.join(targetRoot, "node_modules", "compound-workflow");
670
- if (!isSelfInstall && !fs.existsSync(pkgInTarget) && !args.dryRun) {
671
- console.error("Error: compound-workflow not found in project. Run: npm install compound-workflow");
672
- process.exit(2);
673
- }
390
+ console.log("Target:", targetRoot);
391
+ console.log("Package:", PACKAGE_ROOT);
392
+ console.log("OpenCode CLI:", hasCommand("opencode") ? "yes" : "no");
674
393
 
675
- console.log("Target root:", targetRoot);
676
- console.log("Package root:", PACKAGE_ROOT);
677
- console.log("OpenCode CLI detected:", hasCommand("opencode") ? "yes" : "no");
678
-
679
- writeOpenCodeJson(targetRoot, args.dryRun, isSelfInstall);
680
- writePluginManifests(targetRoot, args.dryRun, isSelfInstall);
681
- applyClaudeRegistration(targetRoot, args.dryRun, isSelfInstall);
682
- copyCursorSkills(targetRoot, args.dryRun, isSelfInstall);
683
- copyCursorCommands(targetRoot, args.dryRun, isSelfInstall);
684
- copyCursorAgents(targetRoot, args.dryRun, isSelfInstall);
685
- reportOpenCodeIntegration(targetRoot, args.dryRun);
686
- writeAgentsMd(targetRoot, args.dryRun);
687
- ensureDirs(targetRoot, args.dryRun);
394
+ // .claude/agents/ — flat (Claude Code requires flat .md files)
395
+ copyAgentsFlat(srcAgents, path.join(targetRoot, ".claude", "agents"), args.dryRun, ".claude/agents/");
688
396
 
689
- if (!args.noConfig && !args.dryRun && process.stdin.isTTY) {
690
- console.log("\nRepo Config: edit AGENTS.md to set default_branch, test_command, lint_command, dev_server_url.");
691
- }
397
+ // .cursor/agents/, .cursor/skills/, .cursor/commands/
398
+ copyAgentsRecursive(srcAgents, path.join(targetRoot, ".cursor", "agents"), args.dryRun, ".cursor/agents/");
399
+ copySkills(srcSkills, path.join(targetRoot, ".cursor", "skills"), args.dryRun, ".cursor/skills/");
400
+ copyCommands(srcCommands, path.join(targetRoot, ".cursor", "commands"), args.dryRun, ".cursor/commands/");
401
+
402
+ // .agents/agents/, .agents/skills/, .agents/commands/ (OpenCode / general)
403
+ copyAgentsRecursive(srcAgents, path.join(targetRoot, ".agents", "agents"), args.dryRun, ".agents/agents/");
404
+ copySkills(srcSkills, path.join(targetRoot, ".agents", "skills"), args.dryRun, ".agents/skills/");
405
+ copyCommands(srcCommands, path.join(targetRoot, ".agents", "commands"), args.dryRun, ".agents/commands/");
406
+
407
+ writeOpenCodeJson(targetRoot, packageSrc, args.dryRun);
408
+ writeAgentsMd(targetRoot, PACKAGE_ROOT, args.dryRun);
409
+ ensureDirs(targetRoot, args.dryRun);
692
410
 
693
- console.log("\nDone. Run opencode debug config to verify.");
411
+ console.log("\nDone.");
694
412
  }
695
413
 
696
414
  main();