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.
|
|
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:
|
package/scripts/install-cli.mjs
CHANGED
|
@@ -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]
|
|
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
|
|
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
|
|
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")
|
|
39
|
-
|
|
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
|
|
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
|
|
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
|
|
111
|
-
map.set(id, { id, rel:
|
|
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
|
|
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
|
|
128
|
-
map.set(id, { id, rel:
|
|
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
|
|
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
|
|
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
|
|
301
|
+
console.log("[dry-run] Would sync .cursor/" + cursorSubdir, "from", label || pkgSubdir, "(Cursor)");
|
|
252
302
|
return { status: "dry-run" };
|
|
253
303
|
}
|
|
254
304
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
|
326
|
+
console.log("[dry-run] Would sync .cursor/skills from package skills (Cursor)");
|
|
299
327
|
return { blocked: [] };
|
|
300
328
|
}
|
|
301
329
|
|
|
302
|
-
if (
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
349
|
-
|
|
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(
|
|
353
|
-
if (stat.
|
|
354
|
-
issues.push(`${check.name}
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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-
|
|
427
|
-
issues.push(`${path.relative(targetRoot, report.path)} blocks
|
|
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
|
|
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
|
|
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)
|
|
141
|
+
2) Resolve the user decision (required prompt/create gate):
|
|
142
142
|
|
|
143
|
-
-
|
|
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
|
-
|
|
152
|
+
Mandatory behavior:
|
|
149
153
|
|
|
150
|
-
|
|
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
|
|