compound-workflow 1.4.4 → 1.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compound-workflow",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "Clarify → plan → execute → verify → capture. One Install action for Cursor, Claude, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,7 +19,8 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "postinstall": "node scripts/postinstall.mjs",
22
- "check:pack-readme": "node scripts/check-pack-readme.mjs"
22
+ "check:pack-readme": "node scripts/check-pack-readme.mjs",
23
+ "test:install": "node --test tests/install-cli.test.mjs"
23
24
  },
24
25
  "engines": {
25
26
  "node": ">=18"
@@ -87,6 +87,16 @@ const requiredChecks = [
87
87
  pattern: "HARD GATE - WORKTREE FIRST",
88
88
  description: "worktree hard-gate wording in work command",
89
89
  },
90
+ {
91
+ file: "src/.agents/commands/workflow/work.md",
92
+ pattern: "required prompt/create gate",
93
+ description: "mandatory worktree decision prompt/create gate in work command",
94
+ },
95
+ {
96
+ file: "src/.agents/commands/workflow/work.md",
97
+ pattern: "Do not infer or assume an answer when the user has not answered.",
98
+ description: "worktree decision cannot be silently assumed",
99
+ },
90
100
  {
91
101
  file: "src/.agents/commands/workflow/work.md",
92
102
  pattern:
@@ -14,29 +14,32 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  function usage(exitCode = 0) {
15
15
  const msg = `
16
16
  Usage:
17
- npx compound-workflow install [all|--all] [--root <projectDir>] [--dry-run] [--no-config] [--cursor]
17
+ npx compound-workflow install [all|--all] [--root <projectDir>] [--dry-run] [--no-config]
18
18
 
19
19
  One action: writes opencode.json (loads from package), merges AGENTS.md, creates dirs,
20
20
  and prompts for Repo Config Block (unless --no-config).
21
21
 
22
- all, --all Full install shortcut (enables Cursor integration; creates .cursor if needed)
22
+ all, --all Kept for compatibility
23
23
  --root <dir> Project directory (default: cwd)
24
24
  --dry-run Print planned changes only
25
25
  --no-config Skip Repo Config Block prompt (only write opencode.json + AGENTS.md + dirs)
26
- --cursor Force Cursor integration (create .cursor if missing, then wire links)
27
26
  `;
28
27
  (exitCode === 0 ? console.log : console.error)(msg.trimStart());
29
28
  process.exit(exitCode);
30
29
  }
31
30
 
32
31
  function parseArgs(argv) {
33
- const out = { root: process.cwd(), dryRun: false, noConfig: false, cursor: false };
32
+ const out = { root: process.cwd(), dryRun: false, noConfig: false };
34
33
  for (let i = 2; i < argv.length; i++) {
35
34
  const a = argv[i];
36
35
  if (a === "--dry-run") out.dryRun = true;
37
36
  else if (a === "--no-config") out.noConfig = true;
38
- else if (a === "--cursor") out.cursor = true;
39
- else if (a === "--all" || a === "all") out.cursor = true;
37
+ else if (a === "--cursor") {
38
+ // Deprecated compatibility alias; install now auto-detects .cursor.
39
+ }
40
+ else if (a === "--all" || a === "all") {
41
+ // Deprecated compatibility alias; install now auto-detects .cursor.
42
+ }
40
43
  else if (a === "--root") {
41
44
  const v = argv[i + 1];
42
45
  if (!v) usage(1);
@@ -58,7 +61,10 @@ function realpathSafe(p) {
58
61
 
59
62
  const packageRoot = realpathSafe(path.join(__dirname, ".."));
60
63
  const packageAgents = path.join(packageRoot, "src", ".agents");
61
- const PKG_PREFIX = "node_modules/compound-workflow";
64
+ const LOCAL_RUNTIME_ROOT = ".agents/compound-workflow";
65
+ const LOCAL_COMMANDS_ROOT = `${LOCAL_RUNTIME_ROOT}/commands`;
66
+ const LOCAL_AGENTS_ROOT = `${LOCAL_RUNTIME_ROOT}/agents`;
67
+ const LOCAL_REFERENCES_ROOT = `${LOCAL_RUNTIME_ROOT}/references`;
62
68
 
63
69
  function walkFiles(dirAbs, predicate) {
64
70
  const out = [];
@@ -101,14 +107,14 @@ function discoverCommands(agentsRoot) {
101
107
  const files = walkFiles(commandsDir, (p) => p.endsWith(".md"));
102
108
  const map = new Map();
103
109
  for (const fileAbs of files) {
104
- const rel = path.relative(packageRoot, fileAbs).replaceAll(path.sep, "/");
110
+ const relWithinCommands = path.relative(commandsDir, fileAbs).replaceAll(path.sep, "/");
105
111
  const md = fs.readFileSync(fileAbs, "utf8");
106
112
  const fm = parseFrontmatter(md);
107
113
  const id = (fm.invocation || fm.name || path.basename(fileAbs, ".md")).trim();
108
114
  const description = (fm.description || id).trim();
109
115
  if (!id) continue;
110
- const pkgRel = `${PKG_PREFIX}/${rel}`;
111
- map.set(id, { id, rel: pkgRel, description });
116
+ const localRel = `${LOCAL_COMMANDS_ROOT}/${relWithinCommands}`;
117
+ map.set(id, { id, rel: localRel, description });
112
118
  }
113
119
  return map;
114
120
  }
@@ -118,18 +124,65 @@ function discoverAgents(agentsRoot) {
118
124
  const files = walkFiles(agentsDir, (p) => p.endsWith(".md"));
119
125
  const map = new Map();
120
126
  for (const fileAbs of files) {
121
- const rel = path.relative(packageRoot, fileAbs).replaceAll(path.sep, "/");
127
+ const relWithinAgents = path.relative(agentsDir, fileAbs).replaceAll(path.sep, "/");
122
128
  const md = fs.readFileSync(fileAbs, "utf8");
123
129
  const fm = parseFrontmatter(md);
124
130
  const id = (fm.name || path.basename(fileAbs, ".md")).trim();
125
131
  const description = (fm.description || id).trim();
126
132
  if (!id) continue;
127
- const pkgRel = `${PKG_PREFIX}/${rel}`;
128
- map.set(id, { id, rel: pkgRel, description });
133
+ const localRel = `${LOCAL_AGENTS_ROOT}/${relWithinAgents}`;
134
+ map.set(id, { id, rel: localRel, description });
129
135
  }
130
136
  return map;
131
137
  }
132
138
 
139
+ function copyDirContents(sourceDir, targetDir) {
140
+ if (!fs.existsSync(sourceDir)) return;
141
+ fs.mkdirSync(targetDir, { recursive: true });
142
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
143
+ for (const entry of entries) {
144
+ const src = path.join(sourceDir, entry.name);
145
+ const dst = path.join(targetDir, entry.name);
146
+ if (entry.isDirectory()) {
147
+ copyDirContents(src, dst);
148
+ continue;
149
+ }
150
+ if (entry.isFile()) {
151
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
152
+ fs.copyFileSync(src, dst);
153
+ }
154
+ if (entry.isSymbolicLink()) {
155
+ const real = realpathSafe(src);
156
+ const realStat = fs.statSync(real);
157
+ if (realStat.isDirectory()) {
158
+ copyDirContents(real, dst);
159
+ } else if (realStat.isFile()) {
160
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
161
+ fs.copyFileSync(real, dst);
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ function syncRuntimeAssets(targetRoot, dryRun) {
168
+ const mappings = [
169
+ { label: "commands", src: path.join(packageAgents, "commands"), dst: path.join(targetRoot, LOCAL_COMMANDS_ROOT) },
170
+ { label: "agents", src: path.join(packageAgents, "agents"), dst: path.join(targetRoot, LOCAL_AGENTS_ROOT) },
171
+ { label: "references", src: path.join(packageAgents, "references"), dst: path.join(targetRoot, LOCAL_REFERENCES_ROOT) },
172
+ ];
173
+
174
+ for (const mapping of mappings) {
175
+ if (!fs.existsSync(mapping.src)) continue;
176
+ if (dryRun) {
177
+ console.log("[dry-run] Would sync", mapping.label, "to", path.relative(targetRoot, mapping.dst));
178
+ continue;
179
+ }
180
+ fs.rmSync(mapping.dst, { recursive: true, force: true });
181
+ copyDirContents(mapping.src, mapping.dst);
182
+ console.log("Synced", mapping.label + ":", path.relative(targetRoot, mapping.dst));
183
+ }
184
+ }
185
+
133
186
  function ensureObject(v) {
134
187
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
135
188
  }
@@ -237,39 +290,29 @@ function ensureSkillsSymlink(targetRoot, dryRun) {
237
290
  }
238
291
  }
239
292
 
240
- function ensureCursorDirSymlink(targetRoot, cursorSubdir, pkgSubdir, dryRun, label, cursorReady) {
293
+ function ensureCursorDirSync(targetRoot, cursorSubdir, pkgSubdir, dryRun, label, cursorReady) {
241
294
  const cursorDir = path.join(targetRoot, ".cursor");
242
295
  if (!cursorReady) return { status: "skipped-missing-cursor" };
243
296
  const pkgPath = path.join(packageRoot, "src", ".agents", pkgSubdir);
244
297
  if (!fs.existsSync(pkgPath)) return { status: "skipped-missing-package-path" };
245
298
 
246
- const linkPath = path.join(cursorDir, cursorSubdir);
247
- const targetRel = path.join("..", "node_modules", "compound-workflow", "src", ".agents", pkgSubdir);
248
- const targetAbs = path.resolve(path.dirname(linkPath), targetRel);
249
-
299
+ const targetPath = path.join(cursorDir, cursorSubdir);
250
300
  if (dryRun) {
251
- console.log("[dry-run] Would create .cursor/" + cursorSubdir, "symlink (Cursor)");
301
+ console.log("[dry-run] Would sync .cursor/" + cursorSubdir, "from", label || pkgSubdir, "(Cursor)");
252
302
  return { status: "dry-run" };
253
303
  }
254
304
 
255
- let needCreate = true;
256
- try {
257
- const stat = fs.lstatSync(linkPath);
258
- if (stat.isSymbolicLink() && symlinkPointsTo(linkPath, targetAbs)) needCreate = false;
259
- else if (!stat.isSymbolicLink()) {
260
- console.warn("Skipped", ".cursor/" + cursorSubdir, "because it exists and is not a symlink");
261
- return { status: "blocked-nonsymlink", path: linkPath };
305
+ if (fs.existsSync(targetPath)) {
306
+ const stat = fs.lstatSync(targetPath);
307
+ if (!stat.isDirectory()) {
308
+ return { status: "blocked-nondirectory", path: targetPath };
262
309
  }
263
- } catch (_) {}
264
-
265
- if (needCreate) {
266
- removePathIfExists(linkPath);
267
- const type = process.platform === "win32" ? "dir" : "dir";
268
- fs.symlinkSync(targetRel, linkPath, type);
269
- console.log("Created", ".cursor/" + cursorSubdir, "->", label || pkgSubdir, "(Cursor)");
270
- return { status: "created" };
271
310
  }
272
- return { status: "ok" };
311
+
312
+ fs.rmSync(targetPath, { recursive: true, force: true });
313
+ copyDirContents(pkgPath, targetPath);
314
+ console.log("Synced", ".cursor/" + cursorSubdir, "from", label || pkgSubdir, "(Cursor)");
315
+ return { status: "synced" };
273
316
  }
274
317
 
275
318
  function ensureCursorSkills(targetRoot, dryRun, cursorReady) {
@@ -278,29 +321,13 @@ function ensureCursorSkills(targetRoot, dryRun, cursorReady) {
278
321
 
279
322
  const packageSkillsDir = path.join(packageRoot, "src", ".agents", "skills");
280
323
  if (!fs.existsSync(packageSkillsDir)) return { blocked: [] };
281
-
282
- const skillNames = [];
283
- try {
284
- for (const name of fs.readdirSync(packageSkillsDir)) {
285
- const skillPath = path.join(packageSkillsDir, name);
286
- if (fs.statSync(skillPath).isDirectory() && fs.existsSync(path.join(skillPath, "SKILL.md"))) {
287
- skillNames.push(name);
288
- }
289
- }
290
- } catch (_) {
291
- return;
292
- }
293
-
294
324
  const skillsDir = path.join(cursorDir, "skills");
295
- const type = process.platform === "win32" ? "dir" : "dir";
296
-
297
325
  if (dryRun) {
298
- console.log("[dry-run] Would create .cursor/skills/<skill> symlinks for:", skillNames.join(", "));
326
+ console.log("[dry-run] Would sync .cursor/skills from package skills (Cursor)");
299
327
  return { blocked: [] };
300
328
  }
301
329
 
302
- if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
303
- else {
330
+ if (fs.existsSync(skillsDir)) {
304
331
  const stat = fs.lstatSync(skillsDir);
305
332
  if (!stat.isDirectory()) {
306
333
  console.warn("Skipped .cursor/skills because it exists and is not a directory");
@@ -308,29 +335,10 @@ function ensureCursorSkills(targetRoot, dryRun, cursorReady) {
308
335
  }
309
336
  }
310
337
 
311
- const blocked = [];
312
- for (const name of skillNames) {
313
- const linkPath = path.join(skillsDir, name);
314
- const targetRel = path.join("..", "..", "node_modules", "compound-workflow", "src", ".agents", "skills", name);
315
- const targetAbs = path.resolve(path.dirname(linkPath), targetRel);
316
- let needCreate = true;
317
- try {
318
- const stat = fs.lstatSync(linkPath);
319
- if (stat.isSymbolicLink() && symlinkPointsTo(linkPath, targetAbs)) needCreate = false;
320
- else if (!stat.isSymbolicLink()) {
321
- console.warn("Skipped", ".cursor/skills/" + name, "because it exists and is not a symlink");
322
- blocked.push(linkPath);
323
- continue;
324
- }
325
- } catch (_) {}
326
-
327
- if (needCreate) {
328
- removePathIfExists(linkPath);
329
- fs.symlinkSync(targetRel, linkPath, type);
330
- console.log("Created", ".cursor/skills/" + name, "-> package skill (Cursor)");
331
- }
332
- }
333
- return { blocked };
338
+ fs.rmSync(skillsDir, { recursive: true, force: true });
339
+ copyDirContents(packageSkillsDir, skillsDir);
340
+ console.log("Synced .cursor/skills from package skills (Cursor)");
341
+ return { blocked: [] };
334
342
  }
335
343
 
336
344
  function verifyCursorIntegration(targetRoot) {
@@ -345,13 +353,12 @@ function verifyCursorIntegration(targetRoot) {
345
353
  const issues = [];
346
354
 
347
355
  for (const check of checks) {
348
- const linkPath = path.join(cursorDir, check.rel);
349
- const expectedAbs = path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", check.rel);
350
- if (!fs.existsSync(linkPath)) issues.push(`${check.name} is missing`);
356
+ const dirPath = path.join(cursorDir, check.rel);
357
+ if (!fs.existsSync(dirPath)) issues.push(`${check.name} is missing`);
351
358
  else {
352
- const stat = fs.lstatSync(linkPath);
353
- if (stat.isSymbolicLink() && !symlinkPointsTo(linkPath, expectedAbs)) {
354
- issues.push(`${check.name} symlink points to unexpected target`);
359
+ const stat = fs.lstatSync(dirPath);
360
+ if (!stat.isDirectory()) {
361
+ issues.push(`${check.name} exists but is not a directory`);
355
362
  }
356
363
  }
357
364
  }
@@ -413,9 +420,9 @@ function ensureCursorIntegration(targetRoot, dryRun, forceCursor) {
413
420
 
414
421
  const skillReport = ensureCursorSkills(targetRoot, dryRun, cursorReady);
415
422
  const dirReports = [
416
- ensureCursorDirSymlink(targetRoot, "agents", "agents", dryRun, "package agents", cursorReady),
417
- ensureCursorDirSymlink(targetRoot, "commands", "commands", dryRun, "package commands", cursorReady),
418
- ensureCursorDirSymlink(targetRoot, "references", "references", dryRun, "package references", cursorReady),
423
+ ensureCursorDirSync(targetRoot, "agents", "agents", dryRun, "package agents", cursorReady),
424
+ ensureCursorDirSync(targetRoot, "commands", "commands", dryRun, "package commands", cursorReady),
425
+ ensureCursorDirSync(targetRoot, "references", "references", dryRun, "package references", cursorReady),
419
426
  ];
420
427
 
421
428
  const issues = [];
@@ -423,8 +430,8 @@ function ensureCursorIntegration(targetRoot, dryRun, forceCursor) {
423
430
  for (const p of skillReport.blocked) issues.push(`${path.relative(targetRoot, p)} blocks symlink creation (not a symlink)`);
424
431
  }
425
432
  for (const report of dirReports) {
426
- if (report?.status === "blocked-nonsymlink") {
427
- issues.push(`${path.relative(targetRoot, report.path)} blocks symlink creation (not a symlink)`);
433
+ if (report?.status === "blocked-nondirectory") {
434
+ issues.push(`${path.relative(targetRoot, report.path)} blocks sync (not a directory)`);
428
435
  }
429
436
  }
430
437
 
@@ -579,12 +586,14 @@ function main() {
579
586
  console.log("Package root:", packageRoot);
580
587
  console.log("OpenCode CLI detected:", hasCommand("opencode") ? "yes" : "no");
581
588
 
589
+ syncRuntimeAssets(targetRoot, args.dryRun);
582
590
  writeOpenCodeJson(targetRoot, args.dryRun);
583
591
  ensureSkillsSymlink(targetRoot, args.dryRun);
584
592
  reportOpenCodeIntegration(targetRoot, args.dryRun);
585
- const cursorReport = ensureCursorIntegration(targetRoot, args.dryRun, args.cursor);
593
+ const cursorExists = fs.existsSync(path.join(targetRoot, ".cursor"));
594
+ const cursorReport = ensureCursorIntegration(targetRoot, args.dryRun, cursorExists);
586
595
  if (cursorReport.status === "skipped-no-cursor") {
587
- console.log("Cursor integration: skipped (.cursor missing). Run install with `all` or `--cursor` to create it.");
596
+ console.log("Cursor integration: skipped (.cursor not found).");
588
597
  } else {
589
598
  console.log("Cursor integration: verified skills, agents, commands, and references.");
590
599
  }
@@ -138,16 +138,23 @@ The input must be a plan file path.
138
138
  - If you are already on a branch that clearly matches this plan, continue.
139
139
  - Otherwise, continue anyway — the current active branch remains the reference/base for a new worktree unless the user explicitly requests a different base.
140
140
 
141
- 2) Ask the user (required decision prompt):
141
+ 2) Resolve the user decision (required prompt/create gate):
142
142
 
143
- - "Use a worktree for this work? (default: Yes; required unless you explicitly opt out)"
143
+ - If the user already gave an explicit instruction in this run:
144
+ - "create a worktree" / "yes use a worktree" => use worktree path
145
+ - "do not use a worktree" / "no worktree" => opt-out path
146
+ - Otherwise, you MUST ask this exact decision before proceeding:
147
+ - "Use a worktree for this work? (Yes/No; default recommendation: Yes)"
144
148
  - Options:
145
149
  - Yes (worktree)
146
150
  - No (stay in current checkout; create/switch to a feature branch)
147
151
 
148
- If Yes: ask for the new branch name (e.g., `feat/<slug>`, `fix/<slug>`).
152
+ Mandatory behavior:
149
153
 
150
- If the user does not explicitly choose "No", proceed with "Yes" by default.
154
+ - Do not infer or assume an answer when the user has not answered.
155
+ - Do not run `skill: git-worktree` until the user has answered Yes (or already explicitly requested worktree creation).
156
+ - If Yes: ask for the new branch name when missing (e.g., `feat/<slug>`, `fix/<slug>`), then continue.
157
+ - If No: require explicit opt-out confirmation, then continue with the non-worktree path.
151
158
 
152
159
  3) If worktree is chosen, run:
153
160