compound-workflow 1.6.17 → 1.7.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/.claude-plugin/plugin.json +1 -17
- package/.cursor-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/check-version-parity.mjs +8 -2
- package/scripts/generate-platform-artifacts.mjs +32 -7
- package/scripts/install-cli.mjs +412 -81
- package/src/.agents/commands/workflow-work.md +99 -0
- package/src/.agents/skills/presentation-composability/SKILL.md +72 -0
- package/src/AGENTS.md +2 -1
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compound-workflow",
|
|
3
|
-
"version": "1.6.17",
|
|
4
3
|
"description": "Clarify -> plan -> execute -> verify -> capture workflow: commands, skills, and agents for Claude Code",
|
|
5
4
|
"author": {
|
|
6
5
|
"name": "Compound Workflow"
|
|
7
|
-
}
|
|
8
|
-
"keywords": [
|
|
9
|
-
"workflow",
|
|
10
|
-
"planning",
|
|
11
|
-
"agents",
|
|
12
|
-
"skills",
|
|
13
|
-
"commands",
|
|
14
|
-
"claude"
|
|
15
|
-
],
|
|
16
|
-
"license": "MIT",
|
|
17
|
-
"repository": "https://github.com/cjerochim/compound-workflow",
|
|
18
|
-
"commands": "./src/.agents/commands",
|
|
19
|
-
"agents": [
|
|
20
|
-
"./src/.agents/agents"
|
|
21
|
-
],
|
|
22
|
-
"skills": "./src/.agents/skills"
|
|
6
|
+
}
|
|
23
7
|
}
|
package/package.json
CHANGED
|
@@ -19,12 +19,18 @@ const claudePlugin = JSON.parse(
|
|
|
19
19
|
);
|
|
20
20
|
|
|
21
21
|
const expected = pkg.version;
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
// Claude Code's plugin.json is intentionally minimal (see scripts/generate-platform-artifacts.mjs)
|
|
24
|
+
// and may not include a version field. When absent, treat it as not applicable.
|
|
25
|
+
const claudeVersion = claudePlugin.version;
|
|
26
|
+
const claudeOk = claudeVersion == null || claudeVersion === expected;
|
|
27
|
+
|
|
28
|
+
if (cursorPlugin.version !== expected || !claudeOk) {
|
|
23
29
|
console.error(
|
|
24
30
|
"Version mismatch: package.json=%s, .cursor-plugin/plugin.json=%s, .claude-plugin/plugin.json=%s",
|
|
25
31
|
expected,
|
|
26
32
|
cursorPlugin.version,
|
|
27
|
-
|
|
33
|
+
claudeVersion
|
|
28
34
|
);
|
|
29
35
|
process.exit(1);
|
|
30
36
|
}
|
|
@@ -152,17 +152,12 @@ function main() {
|
|
|
152
152
|
const agentRoot = roots.agents || "node_modules/compound-workflow/src/.agents/agents";
|
|
153
153
|
const skillsPath = roots.skills || "node_modules/compound-workflow/src/.agents/skills";
|
|
154
154
|
|
|
155
|
+
// Claude Code only accepts name, description, author in plugin.json.
|
|
156
|
+
// Agents are discovered from the adjacent agents/ directory (must be flat .md files).
|
|
155
157
|
const claudePlugin = {
|
|
156
158
|
name: pkg.name,
|
|
157
|
-
version: pkg.version,
|
|
158
159
|
description: "Clarify -> plan -> execute -> verify -> capture workflow: commands, skills, and agents for Claude Code",
|
|
159
160
|
author: { name: "Compound Workflow" },
|
|
160
|
-
keywords: ["workflow", "planning", "agents", "skills", "commands", "claude"],
|
|
161
|
-
license: pkg.license,
|
|
162
|
-
repository: repositoryUrl,
|
|
163
|
-
commands: "./src/.agents/commands",
|
|
164
|
-
agents: ["./src/.agents/agents"],
|
|
165
|
-
skills: "./src/.agents/skills",
|
|
166
161
|
};
|
|
167
162
|
|
|
168
163
|
const cursorPlugin = {
|
|
@@ -191,6 +186,36 @@ function main() {
|
|
|
191
186
|
writeJson(path.join(repoRoot, ".cursor-plugin", "plugin.json"), cursorPlugin, checkOnly, changed);
|
|
192
187
|
writeJson(path.join(repoRoot, "src", "generated", "opencode.managed.json"), openCodeManaged, checkOnly, changed);
|
|
193
188
|
|
|
189
|
+
// Generate flat agent symlinks in .claude-plugin/agents/ so Claude Code discovers them.
|
|
190
|
+
// Claude Code only scans the root of the agents/ directory (not subdirectories).
|
|
191
|
+
const claudeAgentsDir = path.join(repoRoot, ".claude-plugin", "agents");
|
|
192
|
+
const agentsDirAbs = path.join(agentsRoot, "agents");
|
|
193
|
+
if (!checkOnly) {
|
|
194
|
+
fs.mkdirSync(claudeAgentsDir, { recursive: true });
|
|
195
|
+
// Prune stale symlinks no longer in the agent list
|
|
196
|
+
const agentBasenames = new Set(agents.map((a) => path.basename(a.rel)));
|
|
197
|
+
if (fs.existsSync(claudeAgentsDir)) {
|
|
198
|
+
for (const entry of fs.readdirSync(claudeAgentsDir, { withFileTypes: true })) {
|
|
199
|
+
if (!agentBasenames.has(entry.name)) {
|
|
200
|
+
fs.rmSync(path.join(claudeAgentsDir, entry.name), { force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const agent of agents) {
|
|
205
|
+
const linkPath = path.join(claudeAgentsDir, path.basename(agent.rel));
|
|
206
|
+
const targetPath = path.relative(claudeAgentsDir, path.join(agentsDirAbs, agent.rel));
|
|
207
|
+
try {
|
|
208
|
+
if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
|
|
209
|
+
fs.rmSync(linkPath, { force: true });
|
|
210
|
+
}
|
|
211
|
+
} catch { /* doesn't exist */ }
|
|
212
|
+
fs.symlinkSync(targetPath, linkPath);
|
|
213
|
+
}
|
|
214
|
+
if (agents.length) {
|
|
215
|
+
console.log(`Synced ${agents.length} agent symlinks to .claude-plugin/agents/`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
194
219
|
if (checkOnly && changed.length) {
|
|
195
220
|
console.error("Generated artifacts are stale:");
|
|
196
221
|
for (const abs of changed) console.error("-", path.relative(repoRoot, abs));
|
package/scripts/install-cli.mjs
CHANGED
|
@@ -42,13 +42,14 @@ When Cursor is detected (~/.cursor), registers the plugin so skills/commands app
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function parseArgs(argv) {
|
|
45
|
-
const out = { root: process.cwd(), dryRun: false, noConfig: false, noRegisterCursor: false, registerCursor: false };
|
|
45
|
+
const out = { root: process.cwd(), dryRun: false, noConfig: false, noRegisterCursor: false, registerCursor: false, verify: false };
|
|
46
46
|
for (let i = 2; i < argv.length; i++) {
|
|
47
47
|
const arg = argv[i];
|
|
48
48
|
if (arg === "--dry-run") out.dryRun = true;
|
|
49
49
|
else if (arg === "--no-config") out.noConfig = true;
|
|
50
50
|
else if (arg === "--no-register-cursor") out.noRegisterCursor = true;
|
|
51
51
|
else if (arg === "--register-cursor") out.registerCursor = true;
|
|
52
|
+
else if (arg === "--verify") out.verify = true;
|
|
52
53
|
else if (arg === "--root") {
|
|
53
54
|
const value = argv[i + 1];
|
|
54
55
|
if (!value) usage(1);
|
|
@@ -335,20 +336,26 @@ function ensureDirs(targetRoot, dryRun) {
|
|
|
335
336
|
*/
|
|
336
337
|
function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
|
|
337
338
|
const pathsBase = isSelfInstall ? "./src/.agents" : "./node_modules/compound-workflow/src/.agents";
|
|
338
|
-
const pathOverrides = {
|
|
339
|
-
commands: `${pathsBase}/commands`,
|
|
340
|
-
agents: `${pathsBase}/agents`,
|
|
341
|
-
skills: `${pathsBase}/skills`,
|
|
342
|
-
};
|
|
343
339
|
const cursorSrc = path.join(PACKAGE_ROOT, ".cursor-plugin", "plugin.json");
|
|
344
340
|
const claudeSrc = path.join(PACKAGE_ROOT, ".claude-plugin", "plugin.json");
|
|
345
341
|
const cursorManifest = readJsonMaybe(cursorSrc);
|
|
346
342
|
const claudeManifest = readJsonMaybe(claudeSrc);
|
|
347
343
|
if (!cursorManifest || !claudeManifest) return;
|
|
348
344
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
345
|
+
// Cursor supports full manifest with commands/agents/skills path overrides.
|
|
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
|
+
};
|
|
352
359
|
const cursorDir = path.join(targetRoot, ".cursor-plugin");
|
|
353
360
|
const claudeDir = path.join(targetRoot, ".claude-plugin");
|
|
354
361
|
|
|
@@ -369,6 +376,37 @@ function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
|
|
|
369
376
|
fs.writeFileSync(path.join(claudeDir, "plugin.json"), JSON.stringify(claudeOut, null, 2) + "\n", "utf8");
|
|
370
377
|
fs.writeFileSync(path.join(cursorDir, "registration.json"), JSON.stringify(registrationDescriptor, null, 2) + "\n", "utf8");
|
|
371
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
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
372
410
|
// Claude Code 2.1.x+ no longer loads from installed_plugins.json; it requires marketplace flow.
|
|
373
411
|
// Write a project-level marketplace so user can: /plugin marketplace add . then /plugin install compound-workflow@compound-workflow-local
|
|
374
412
|
if (!isSelfInstall) {
|
|
@@ -467,10 +505,307 @@ function syncCursorSkills(targetRoot, dryRun, isSelfInstall) {
|
|
|
467
505
|
console.log("Synced", skillDirs.length, "skills to .cursor/skills/");
|
|
468
506
|
}
|
|
469
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Cursor discovers commands from .cursor/commands.
|
|
510
|
+
* Populate .cursor/commands/ with symlinks to the package commands so Cursor finds them.
|
|
511
|
+
*/
|
|
512
|
+
function syncCursorCommands(targetRoot, dryRun, isSelfInstall) {
|
|
513
|
+
const packageCommandsAbs = isSelfInstall
|
|
514
|
+
? path.join(PACKAGE_ROOT, "src", ".agents", "commands")
|
|
515
|
+
: path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "commands");
|
|
516
|
+
if (!fs.existsSync(packageCommandsAbs)) return;
|
|
517
|
+
|
|
518
|
+
const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
|
|
519
|
+
let entries;
|
|
520
|
+
try {
|
|
521
|
+
entries = fs.readdirSync(packageCommandsAbs, { withFileTypes: true });
|
|
522
|
+
} catch {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Filter .md files that are commands
|
|
527
|
+
const commandFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
528
|
+
if (commandFiles.length === 0) return;
|
|
529
|
+
|
|
530
|
+
if (dryRun) {
|
|
531
|
+
console.log("[dry-run] Would symlink", commandFiles.length, "commands into .cursor/commands/");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fs.mkdirSync(cursorCommandsDir, { recursive: true });
|
|
536
|
+
const packageCommandsReal = realpathSafe(packageCommandsAbs);
|
|
537
|
+
const commandSet = new Set(commandFiles);
|
|
538
|
+
|
|
539
|
+
// Prune: remove symlinks that point at our package but are no longer in the package
|
|
540
|
+
try {
|
|
541
|
+
for (const entry of fs.readdirSync(cursorCommandsDir, { withFileTypes: true })) {
|
|
542
|
+
if (!entry.isSymbolicLink()) continue;
|
|
543
|
+
const linkPath = path.join(cursorCommandsDir, entry.name);
|
|
544
|
+
try {
|
|
545
|
+
const resolved = realpathSafe(linkPath);
|
|
546
|
+
if (!resolved.startsWith(packageCommandsReal + path.sep) && resolved !== packageCommandsReal) continue;
|
|
547
|
+
const base = path.basename(resolved);
|
|
548
|
+
if (commandSet.has(base)) continue;
|
|
549
|
+
fs.rmSync(linkPath);
|
|
550
|
+
} catch {
|
|
551
|
+
/* ignore broken symlinks or permission errors */
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch {
|
|
555
|
+
/* .cursor/commands not readable */
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
for (const name of commandFiles) {
|
|
559
|
+
const linkPath = path.join(cursorCommandsDir, name);
|
|
560
|
+
const targetPath = path.join(packageCommandsAbs, name);
|
|
561
|
+
try {
|
|
562
|
+
if (fs.existsSync(linkPath)) {
|
|
563
|
+
const stat = fs.lstatSync(linkPath);
|
|
564
|
+
if (!stat.isSymbolicLink()) continue;
|
|
565
|
+
try {
|
|
566
|
+
if (realpathSafe(linkPath) !== realpathSafe(targetPath)) continue;
|
|
567
|
+
} catch {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
fs.rmSync(linkPath);
|
|
571
|
+
}
|
|
572
|
+
fs.symlinkSync(targetPath, linkPath, "file");
|
|
573
|
+
} catch (err) {
|
|
574
|
+
console.warn("[cursor] Could not symlink command", name, err.message);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
console.log("Synced", commandFiles.length, "commands to .cursor/commands/");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Cursor discovers agents from .cursor/agents.
|
|
582
|
+
* Populate .cursor/agents/ with symlinks to the package agents so Cursor finds them.
|
|
583
|
+
* Preserves subdirectory structure (research/, workflow/, review/).
|
|
584
|
+
*/
|
|
585
|
+
function syncCursorAgents(targetRoot, dryRun, isSelfInstall) {
|
|
586
|
+
const packageAgentsAbs = isSelfInstall
|
|
587
|
+
? path.join(PACKAGE_ROOT, "src", ".agents", "agents")
|
|
588
|
+
: path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "agents");
|
|
589
|
+
if (!fs.existsSync(packageAgentsAbs)) return;
|
|
590
|
+
|
|
591
|
+
const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
|
|
592
|
+
|
|
593
|
+
// Get all agent files from manifest (these include subdir paths like "research/repo-research-analyst.md")
|
|
594
|
+
const agentRels = GENERATED_MANIFEST.agents.map((a) => a.rel);
|
|
595
|
+
if (agentRels.length === 0) return;
|
|
596
|
+
|
|
597
|
+
if (dryRun) {
|
|
598
|
+
console.log("[dry-run] Would symlink", agentRels.length, "agents into .cursor/agents/");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
fs.mkdirSync(cursorAgentsDir, { recursive: true });
|
|
603
|
+
const packageAgentsReal = realpathSafe(packageAgentsAbs);
|
|
604
|
+
const agentSet = new Set(agentRels);
|
|
605
|
+
|
|
606
|
+
// Build set of valid subdirectories to preserve structure
|
|
607
|
+
const validSubdirs = new Set();
|
|
608
|
+
for (const rel of agentRels) {
|
|
609
|
+
const subdir = path.dirname(rel);
|
|
610
|
+
if (subdir !== ".") validSubdirs.add(subdir);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Prune: remove symlinks that point at our package but are no longer in the manifest
|
|
614
|
+
try {
|
|
615
|
+
for (const entry of fs.readdirSync(cursorAgentsDir, { withFileTypes: true })) {
|
|
616
|
+
if (entry.isDirectory()) {
|
|
617
|
+
// Check if this subdir is still valid
|
|
618
|
+
if (!validSubdirs.has(entry.name)) {
|
|
619
|
+
// Remove the entire stale subdirectory
|
|
620
|
+
fs.rmSync(path.join(cursorAgentsDir, entry.name), { recursive: true, force: true });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
// Prune stale symlinks within valid subdirectories
|
|
624
|
+
const subdirPath = path.join(cursorAgentsDir, entry.name);
|
|
625
|
+
for (const subEntry of fs.readdirSync(subdirPath, { withFileTypes: true })) {
|
|
626
|
+
if (!subEntry.isSymbolicLink()) continue;
|
|
627
|
+
const linkPath = path.join(subdirPath, subEntry.name);
|
|
628
|
+
try {
|
|
629
|
+
const resolved = realpathSafe(linkPath);
|
|
630
|
+
if (!resolved.startsWith(packageAgentsReal + path.sep) && resolved !== packageAgentsReal) continue;
|
|
631
|
+
const relFromPackage = path.relative(packageAgentsAbs, resolved);
|
|
632
|
+
if (agentSet.has(relFromPackage)) continue;
|
|
633
|
+
fs.rmSync(linkPath);
|
|
634
|
+
} catch {
|
|
635
|
+
/* ignore broken symlinks */
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else if (entry.isSymbolicLink()) {
|
|
639
|
+
// Handle flat symlinks (if any were created at root level)
|
|
640
|
+
const linkPath = path.join(cursorAgentsDir, entry.name);
|
|
641
|
+
try {
|
|
642
|
+
const resolved = realpathSafe(linkPath);
|
|
643
|
+
if (!resolved.startsWith(packageAgentsReal + path.sep) && resolved !== packageAgentsReal) continue;
|
|
644
|
+
const relFromPackage = path.relative(packageAgentsAbs, resolved);
|
|
645
|
+
if (agentSet.has(relFromPackage)) continue;
|
|
646
|
+
fs.rmSync(linkPath);
|
|
647
|
+
} catch {
|
|
648
|
+
/* ignore broken symlinks */
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch {
|
|
653
|
+
/* .cursor/agents not readable */
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Create symlinks preserving subdirectory structure
|
|
657
|
+
for (const rel of agentRels) {
|
|
658
|
+
const targetPath = path.join(packageAgentsAbs, rel);
|
|
659
|
+
const linkPath = path.join(cursorAgentsDir, rel);
|
|
660
|
+
|
|
661
|
+
// Ensure subdirectory exists
|
|
662
|
+
const subdir = path.dirname(rel);
|
|
663
|
+
if (subdir !== ".") {
|
|
664
|
+
fs.mkdirSync(path.join(cursorAgentsDir, subdir), { recursive: true });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
if (fs.existsSync(linkPath)) {
|
|
669
|
+
const stat = fs.lstatSync(linkPath);
|
|
670
|
+
if (!stat.isSymbolicLink()) continue;
|
|
671
|
+
try {
|
|
672
|
+
if (realpathSafe(linkPath) !== realpathSafe(targetPath)) continue;
|
|
673
|
+
} catch {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
fs.rmSync(linkPath);
|
|
677
|
+
}
|
|
678
|
+
fs.symlinkSync(targetPath, linkPath, "file");
|
|
679
|
+
} catch (err) {
|
|
680
|
+
console.warn("[cursor] Could not symlink agent", rel, err.message);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
console.log("Synced", agentRels.length, "agents to .cursor/agents/");
|
|
684
|
+
}
|
|
685
|
+
|
|
470
686
|
function cursorDetected() {
|
|
471
687
|
return fs.existsSync(path.join(os.homedir(), ".cursor"));
|
|
472
688
|
}
|
|
473
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Verifies plugin integrity by checking all symlinks in .cursor/ directories
|
|
692
|
+
* match the expected state from GENERATED_MANIFEST.
|
|
693
|
+
* Returns { ok: boolean, issues: string[] }
|
|
694
|
+
*/
|
|
695
|
+
function verifyPluginIntegrity(targetRoot, isSelfInstall) {
|
|
696
|
+
const issues = [];
|
|
697
|
+
const packageRoot = isSelfInstall
|
|
698
|
+
? PACKAGE_ROOT
|
|
699
|
+
: path.join(targetRoot, "node_modules", "compound-workflow");
|
|
700
|
+
|
|
701
|
+
// Verify skills
|
|
702
|
+
const cursorSkillsDir = path.join(targetRoot, ".cursor", "skills");
|
|
703
|
+
const packageSkillsDir = path.join(packageRoot, "src", ".agents", "skills");
|
|
704
|
+
if (fs.existsSync(packageSkillsDir)) {
|
|
705
|
+
const expectedSkills = new Set(
|
|
706
|
+
fs.readdirSync(packageSkillsDir, { withFileTypes: true })
|
|
707
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(packageSkillsDir, e.name, "SKILL.md")))
|
|
708
|
+
.map((e) => e.name)
|
|
709
|
+
);
|
|
710
|
+
if (fs.existsSync(cursorSkillsDir)) {
|
|
711
|
+
const actualSkills = fs.readdirSync(cursorSkillsDir, { withFileTypes: true })
|
|
712
|
+
.filter((e) => e.isSymbolicLink())
|
|
713
|
+
.map((e) => e.name);
|
|
714
|
+
for (const skill of expectedSkills) {
|
|
715
|
+
if (!actualSkills.includes(skill)) {
|
|
716
|
+
issues.push(`Missing skill symlink: .cursor/skills/${skill}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const skill of actualSkills) {
|
|
720
|
+
if (!expectedSkills.has(skill)) {
|
|
721
|
+
issues.push(`Stale skill symlink: .cursor/skills/${skill}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} else if (expectedSkills.size > 0) {
|
|
725
|
+
issues.push(`Missing .cursor/skills/ directory (${expectedSkills.size} expected)`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Verify commands
|
|
730
|
+
const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
|
|
731
|
+
const packageCommandsDir = path.join(packageRoot, "src", ".agents", "commands");
|
|
732
|
+
if (fs.existsSync(packageCommandsDir)) {
|
|
733
|
+
const expectedCommands = new Set(
|
|
734
|
+
fs.readdirSync(packageCommandsDir, { withFileTypes: true })
|
|
735
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
736
|
+
.map((e) => e.name)
|
|
737
|
+
);
|
|
738
|
+
if (fs.existsSync(cursorCommandsDir)) {
|
|
739
|
+
const actualCommands = fs.readdirSync(cursorCommandsDir, { withFileTypes: true })
|
|
740
|
+
.filter((e) => e.isSymbolicLink())
|
|
741
|
+
.map((e) => e.name);
|
|
742
|
+
for (const cmd of expectedCommands) {
|
|
743
|
+
if (!actualCommands.includes(cmd)) {
|
|
744
|
+
issues.push(`Missing command symlink: .cursor/commands/${cmd}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const cmd of actualCommands) {
|
|
748
|
+
if (!expectedCommands.has(cmd)) {
|
|
749
|
+
issues.push(`Stale command symlink: .cursor/commands/${cmd}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
} else if (expectedCommands.size > 0) {
|
|
753
|
+
issues.push(`Missing .cursor/commands/ directory (${expectedCommands.size} expected)`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Verify agents
|
|
758
|
+
const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
|
|
759
|
+
const packageAgentsDir = path.join(packageRoot, "src", ".agents", "agents");
|
|
760
|
+
if (fs.existsSync(packageAgentsDir)) {
|
|
761
|
+
// Recursively get all .md files from package agents dir
|
|
762
|
+
const expectedAgents = [];
|
|
763
|
+
function collectAgents(dir, prefix = "") {
|
|
764
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
765
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
766
|
+
if (entry.isDirectory()) {
|
|
767
|
+
collectAgents(path.join(dir, entry.name), relPath);
|
|
768
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
769
|
+
expectedAgents.push(relPath);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
collectAgents(packageAgentsDir);
|
|
774
|
+
const expectedSet = new Set(expectedAgents);
|
|
775
|
+
|
|
776
|
+
if (fs.existsSync(cursorAgentsDir)) {
|
|
777
|
+
const actualAgents = [];
|
|
778
|
+
function collectActualAgents(dir, prefix = "") {
|
|
779
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
780
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
781
|
+
if (entry.isDirectory()) {
|
|
782
|
+
collectActualAgents(path.join(dir, entry.name), relPath);
|
|
783
|
+
} else if (entry.isSymbolicLink()) {
|
|
784
|
+
actualAgents.push(relPath);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
collectActualAgents(cursorAgentsDir);
|
|
789
|
+
const actualSet = new Set(actualAgents);
|
|
790
|
+
|
|
791
|
+
for (const agent of expectedAgents) {
|
|
792
|
+
if (!actualSet.has(agent)) {
|
|
793
|
+
issues.push(`Missing agent symlink: .cursor/agents/${agent}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const agent of actualAgents) {
|
|
797
|
+
if (!expectedSet.has(agent)) {
|
|
798
|
+
issues.push(`Stale agent symlink: .cursor/agents/${agent}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} else if (expectedAgents.length > 0) {
|
|
802
|
+
issues.push(`Missing .cursor/agents/ directory (${expectedAgents.length} expected)`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return { ok: issues.length === 0, issues };
|
|
807
|
+
}
|
|
808
|
+
|
|
474
809
|
function applyCursorRegistration(targetRoot, dryRun, noRegisterCursor, forceRegister, isSelfInstall) {
|
|
475
810
|
const claudePluginsDir = path.join(os.homedir(), ".claude", "plugins");
|
|
476
811
|
const installedPath = path.join(claudePluginsDir, "installed_plugins.json");
|
|
@@ -485,87 +820,56 @@ function applyCursorRegistration(targetRoot, dryRun, noRegisterCursor, forceRegi
|
|
|
485
820
|
}
|
|
486
821
|
})();
|
|
487
822
|
|
|
488
|
-
const
|
|
823
|
+
const projectRoot = isSelfInstall ? PACKAGE_ROOT : targetRoot;
|
|
489
824
|
const pluginId = "compound-workflow@local";
|
|
490
|
-
const scope = isSelfInstall ? "user" : "project";
|
|
491
|
-
|
|
492
|
-
let installed = {};
|
|
493
|
-
if (fs.existsSync(installedPath)) {
|
|
494
|
-
try {
|
|
495
|
-
installed = readJsonMaybe(installedPath) ?? {};
|
|
496
|
-
} catch {
|
|
497
|
-
installed = {};
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
const plugins = ensureObject(installed.plugins);
|
|
501
|
-
const existingEntries = Array.isArray(plugins[pluginId]) ? plugins[pluginId] : [];
|
|
502
|
-
const cleanedEntries = scope === "project"
|
|
503
|
-
? existingEntries.filter((e) => e.scope !== "user")
|
|
504
|
-
: existingEntries;
|
|
505
|
-
const pruned = cleanedEntries.filter((e) => {
|
|
506
|
-
const manifest = path.join(e.installPath || "", ".claude-plugin", "plugin.json");
|
|
507
|
-
return fs.existsSync(manifest);
|
|
508
|
-
});
|
|
509
|
-
const matchIndex = pruned.findIndex((e) =>
|
|
510
|
-
scope === "user" ? e.scope === "user" : e.scope === "project" && e.projectPath === targetRootReal
|
|
511
|
-
);
|
|
512
|
-
const existingEntry = matchIndex >= 0 ? pruned[matchIndex] : {};
|
|
513
|
-
const now = new Date().toISOString();
|
|
514
|
-
const newEntry = {
|
|
515
|
-
scope,
|
|
516
|
-
...(scope === "project" ? { projectPath: targetRootReal } : {}),
|
|
517
|
-
installPath: targetRootReal,
|
|
518
|
-
version: pluginVersion,
|
|
519
|
-
installedAt: existingEntry.installedAt || now,
|
|
520
|
-
lastUpdated: now,
|
|
521
|
-
};
|
|
522
|
-
plugins[pluginId] = matchIndex >= 0
|
|
523
|
-
? pruned.map((e, i) => (i === matchIndex ? newEntry : e))
|
|
524
|
-
: [...pruned, newEntry];
|
|
525
|
-
installed.plugins = plugins;
|
|
526
|
-
|
|
527
|
-
let settings = {};
|
|
528
|
-
if (fs.existsSync(settingsPath)) {
|
|
529
|
-
try {
|
|
530
|
-
settings = readJsonMaybe(settingsPath) ?? {};
|
|
531
|
-
} catch {
|
|
532
|
-
settings = {};
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
settings.enabledPlugins = ensureObject(settings.enabledPlugins);
|
|
536
|
-
settings.enabledPlugins[pluginId] = true;
|
|
537
825
|
|
|
538
826
|
if (dryRun) {
|
|
539
|
-
console.log("[dry-run] Would register Claude plugin
|
|
827
|
+
console.log("[dry-run] Would register Claude plugin (project-scoped) at:", projectRoot);
|
|
540
828
|
return;
|
|
541
829
|
}
|
|
542
830
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
831
|
+
// Registration is always project-scoped: write only to <project>/.claude/settings.json.
|
|
832
|
+
// Claude Code manages ~/.claude/plugins/installed_plugins.json itself via marketplace flow;
|
|
833
|
+
// writing to user-level files causes "unregistered local marketplace" errors on startup.
|
|
834
|
+
const projectSettingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
835
|
+
let projectSettings = {};
|
|
836
|
+
if (fs.existsSync(projectSettingsPath)) {
|
|
837
|
+
try { projectSettings = readJsonMaybe(projectSettingsPath) ?? {}; } catch { projectSettings = {}; }
|
|
838
|
+
}
|
|
839
|
+
projectSettings.enabledPlugins = ensureObject(projectSettings.enabledPlugins);
|
|
840
|
+
projectSettings.enabledPlugins[pluginId] = true;
|
|
841
|
+
// Remove stale/invalid marketplace keys left by earlier install methods
|
|
842
|
+
if (projectSettings.extraKnownMarketplaces?.["compound-workflow"]) {
|
|
843
|
+
delete projectSettings.extraKnownMarketplaces["compound-workflow"];
|
|
844
|
+
}
|
|
845
|
+
projectSettings.extraKnownMarketplaces = ensureObject(projectSettings.extraKnownMarketplaces);
|
|
846
|
+
projectSettings.extraKnownMarketplaces["compound-workflow-local"] = {
|
|
847
|
+
source: { source: "file", path: ".claude-plugin/marketplace.json" },
|
|
848
|
+
};
|
|
849
|
+
fs.mkdirSync(path.join(projectRoot, ".claude"), { recursive: true });
|
|
850
|
+
fs.writeFileSync(projectSettingsPath, JSON.stringify(projectSettings, null, 2) + "\n", "utf8");
|
|
547
851
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
852
|
+
// Clean up any stale user-level enabledPlugins entries left by previous install versions.
|
|
853
|
+
// These cause "unregistered local marketplace" errors on every Claude Code startup.
|
|
854
|
+
if (fs.existsSync(settingsPath)) {
|
|
855
|
+
try {
|
|
856
|
+
let userSettings = readJsonMaybe(settingsPath) ?? {};
|
|
857
|
+
const staleIds = ["compound-workflow@local", "compound-workflow@compound-workflow-local"];
|
|
858
|
+
let changed = false;
|
|
859
|
+
for (const id of staleIds) {
|
|
860
|
+
if (userSettings?.enabledPlugins?.[id] !== undefined) {
|
|
861
|
+
delete userSettings.enabledPlugins[id];
|
|
862
|
+
changed = true;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (changed) {
|
|
866
|
+
fs.writeFileSync(settingsPath, JSON.stringify(userSettings, null, 2) + "\n", "utf8");
|
|
867
|
+
console.log("Cleaned up stale compound-workflow entries from ~/.claude/settings.json");
|
|
868
|
+
}
|
|
869
|
+
} catch { /* ignore */ }
|
|
566
870
|
}
|
|
567
871
|
|
|
568
|
-
console.log("Registered compound-workflow with Claude Code.");
|
|
872
|
+
console.log("Registered compound-workflow with Claude Code (project-scoped).");
|
|
569
873
|
if (!isSelfInstall) {
|
|
570
874
|
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");
|
|
571
875
|
}
|
|
@@ -604,6 +908,31 @@ function main() {
|
|
|
604
908
|
const args = parseArgs(process.argv);
|
|
605
909
|
const targetRoot = realpathSafe(args.root);
|
|
606
910
|
|
|
911
|
+
// Handle verification mode early (no manifest needed)
|
|
912
|
+
if (args.verify) {
|
|
913
|
+
console.log("Verifying plugin integrity...");
|
|
914
|
+
const isSelfInstall = realpathSafe(targetRoot) === realpathSafe(PACKAGE_ROOT);
|
|
915
|
+
// Try to read manifest for accurate verification, but continue without it
|
|
916
|
+
try {
|
|
917
|
+
GENERATED_MANIFEST = readGeneratedManifest();
|
|
918
|
+
} catch {
|
|
919
|
+
console.warn("Warning: Could not read generated manifest, using filesystem scan only");
|
|
920
|
+
GENERATED_MANIFEST = { commands: [], agents: [] };
|
|
921
|
+
}
|
|
922
|
+
const result = verifyPluginIntegrity(targetRoot, isSelfInstall);
|
|
923
|
+
if (result.ok) {
|
|
924
|
+
console.log("Plugin integrity: OK (all symlinks present and valid)");
|
|
925
|
+
process.exit(0);
|
|
926
|
+
} else {
|
|
927
|
+
console.error("Plugin integrity issues found:");
|
|
928
|
+
for (const issue of result.issues) {
|
|
929
|
+
console.error(` - ${issue}`);
|
|
930
|
+
}
|
|
931
|
+
console.error("\nRun 'npx compound-workflow install' to fix.");
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
607
936
|
const genScript = path.join(PACKAGE_ROOT, "scripts", "generate-platform-artifacts.mjs");
|
|
608
937
|
if (fs.existsSync(genScript)) {
|
|
609
938
|
console.log("[compound-workflow] Regenerating manifest from package source...");
|
|
@@ -647,6 +976,8 @@ function main() {
|
|
|
647
976
|
writeOpenCodeJson(targetRoot, args.dryRun, isSelfInstall);
|
|
648
977
|
writePluginManifests(targetRoot, args.dryRun, isSelfInstall);
|
|
649
978
|
syncCursorSkills(targetRoot, args.dryRun, isSelfInstall);
|
|
979
|
+
syncCursorCommands(targetRoot, args.dryRun, isSelfInstall);
|
|
980
|
+
syncCursorAgents(targetRoot, args.dryRun, isSelfInstall);
|
|
650
981
|
applyCursorRegistration(targetRoot, args.dryRun, args.noRegisterCursor, args.registerCursor, isSelfInstall);
|
|
651
982
|
reportOpenCodeIntegration(targetRoot, args.dryRun);
|
|
652
983
|
writeAgentsMd(targetRoot, args.dryRun);
|
|
@@ -361,6 +361,105 @@ The input must be a plan file path.
|
|
|
361
361
|
|
|
362
362
|
Trigger: you cannot proceed safely due to ambiguity, missing info, failing approach, or environment/tooling issue.
|
|
363
363
|
|
|
364
|
+
**Stuck Guard (auto-research pre-step)**
|
|
365
|
+
|
|
366
|
+
When the Blocker Protocol triggers, evaluate whether the guard should fire:
|
|
367
|
+
|
|
368
|
+
**Trigger 1 — Unknown territory:** The agent explicitly cannot identify a clear next step after consulting available context and codebase patterns. Requires agent self-declaration (e.g. "required API behavior is not in context," "no codebase pattern exists for this operation").
|
|
369
|
+
|
|
370
|
+
**Trigger 2 — Repeated failures:** ≥2 distinct failed approaches on the same todo step, OR ≥3 total failures on the same todo regardless of step or approach variety. Failure = a test, lint, type, or runtime check produces an error the agent cannot resolve within one further attempt.
|
|
371
|
+
|
|
372
|
+
**Guard suppression:** The Stuck Guard MUST NOT fire for todos tagged `tags: [spike]`. If a Spike is inconclusive, surface through standard Spike completion flow.
|
|
373
|
+
|
|
374
|
+
**When guard fires (steps 1–10, mandatory order):**
|
|
375
|
+
|
|
376
|
+
1. Guard trigger detected (unknown_territory OR repeated_failure)
|
|
377
|
+
2. Announce to user: "Pausing to investigate..."
|
|
378
|
+
3. **Immediately** transition todo: `ready` → `pending + tags: [blocker]`
|
|
379
|
+
4. Add placeholder Work Log entry: `"Stuck Guard triggered. Investigation in progress. [stuck_type]. [timestamp]. Partial changes may exist — review working directory before resuming."`
|
|
380
|
+
5. Dispatch sub-agents in **parallel**:
|
|
381
|
+
- **Mandatory (always):** `Task repo-research-analyst(<context>)`, `Task learnings-researcher(<context>)`
|
|
382
|
+
- **Conditional (by signal, not discretion):**
|
|
383
|
+
- If failure mentions external library/package/API → add `Task framework-docs-researcher(<context>)`
|
|
384
|
+
- If stuck on approach/pattern/architecture choice → add `Task best-practices-researcher(<context>)`
|
|
385
|
+
- If modifying existing code (not creating new) → add `Task git-history-analyzer(<context>)`
|
|
386
|
+
6. Collect findings (single-pass; no recursive guard firing)
|
|
387
|
+
7. Synthesize enriched output (format below)
|
|
388
|
+
8. Update Work Log `Blocker Decision` section with full enriched output
|
|
389
|
+
9. Present decision prompt to user
|
|
390
|
+
10. After user decision: apply existing Blocker Protocol after-decision steps (convert to todos; triage re-approval before returning to `ready`)
|
|
391
|
+
|
|
392
|
+
**Context payload for each sub-agent:**
|
|
393
|
+
```
|
|
394
|
+
{
|
|
395
|
+
todo_title: <title>,
|
|
396
|
+
todo_description: <problem statement>,
|
|
397
|
+
stuck_type: "unknown_territory" | "repeated_failure",
|
|
398
|
+
failure_description: <specific error/blocker>,
|
|
399
|
+
working_directory: <worktree path>
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Fallback when Task dispatch unavailable:** Announce: "Research sub-agents unavailable — proceeding with agent-reasoned options only." Produce standard Blocker output (no enrichment). Do NOT silently present unresearched options as researched.
|
|
404
|
+
|
|
405
|
+
**Enriched output format (replaces standard Blocker output):**
|
|
406
|
+
|
|
407
|
+
```markdown
|
|
408
|
+
## Stuck Guard Triggered
|
|
409
|
+
|
|
410
|
+
**Detected:** [unknown_territory | repeated_failure]
|
|
411
|
+
**Investigating...** Launching: repo-research-analyst, learnings-researcher[, framework-docs-researcher][, best-practices-researcher][, git-history-analyzer]
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Research Findings
|
|
416
|
+
|
|
417
|
+
- **repo-research-analyst:** [2–5 sentence summary, or "no findings returned"]
|
|
418
|
+
- **learnings-researcher:** [2–5 sentence summary, or "no findings returned"]
|
|
419
|
+
- **framework-docs-researcher:** [2–5 sentence summary, or "not invoked" | "no findings returned"]
|
|
420
|
+
- **best-practices-researcher:** [2–5 sentence summary, or "not invoked" | "no findings returned"]
|
|
421
|
+
- **git-history-analyzer:** [2–5 sentence summary, or "not invoked" | "no findings returned"]
|
|
422
|
+
|
|
423
|
+
**Synthesis confidence:** `high` | `medium` | `low` (low when all agents return empty)
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Blocker Summary
|
|
428
|
+
|
|
429
|
+
[1–2 sentences describing what blocked execution]
|
|
430
|
+
|
|
431
|
+
## Constraints Discovered
|
|
432
|
+
|
|
433
|
+
- [constraint 1]
|
|
434
|
+
- [constraint 2]
|
|
435
|
+
|
|
436
|
+
## Options
|
|
437
|
+
|
|
438
|
+
**Option 1: [Name]** *(source: [agent name(s)] | agent-reasoned)*
|
|
439
|
+
- Pros: ...
|
|
440
|
+
- Cons: ...
|
|
441
|
+
- Risk: ...
|
|
442
|
+
- Effort: ...
|
|
443
|
+
|
|
444
|
+
**Option 2: [Name]** *(source: [agent name(s)] | agent-reasoned)*
|
|
445
|
+
- ...
|
|
446
|
+
|
|
447
|
+
**Option 3: [Name]** *(source: [agent name(s)] | agent-reasoned)*
|
|
448
|
+
- ...
|
|
449
|
+
|
|
450
|
+
## Recommendation
|
|
451
|
+
|
|
452
|
+
[One option + 2–4 bullets citing research findings]
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
*Which option should we take?*
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**When findings are empty:** Produce ≥3 options marked `*(agent-reasoned — research returned no findings)*`. Set synthesis confidence to `low`. Do not fabricate citations.
|
|
460
|
+
|
|
461
|
+
**When a Spike is recommended:** Use standard Spike Candidate format from Spike Protocol (Initial priority, Depends on, Unblocks, Timebox, Deliverable, Parallelizable metadata).
|
|
462
|
+
|
|
364
463
|
Rules:
|
|
365
464
|
- Pause implementation. Do not “push through” with guesses.
|
|
366
465
|
- Timebox investigation to reach options (not a full rewrite).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: presentation-composability
|
|
3
|
+
description: Enforce folder-per-component structure for tapesv3 presentation modules. Use when creating or refactoring components under src/features/tapesv3/presentation/.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Presentation Composability
|
|
7
|
+
|
|
8
|
+
Enforce folder-per-component structure for tapesv3 presentation modules.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
- Adding new presentation components to tapesv3
|
|
13
|
+
- Refactoring existing tapesv3 presentation into composable pieces
|
|
14
|
+
- Aligning new UI with established tapesv3 patterns
|
|
15
|
+
|
|
16
|
+
## Structure Rules
|
|
17
|
+
|
|
18
|
+
### One folder per composable
|
|
19
|
+
|
|
20
|
+
Each logical UI element gets its own subfolder (PascalCase):
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
presentation/FeatureName/
|
|
24
|
+
├── index.ts # barrel
|
|
25
|
+
├── SubComponentA/
|
|
26
|
+
│ ├── index.tsx
|
|
27
|
+
│ └── styles.ts
|
|
28
|
+
├── SubComponentB/
|
|
29
|
+
│ ├── index.tsx
|
|
30
|
+
│ └── styles.ts
|
|
31
|
+
└── SubComponentC/
|
|
32
|
+
└── index.tsx # styles.ts optional if no styled components
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Barrel (`index.ts`)
|
|
36
|
+
|
|
37
|
+
Re-export from subfolders. Two patterns:
|
|
38
|
+
|
|
39
|
+
**Flat exports** (simple feature, independent components):
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export { SubComponentA } from "./SubComponentA";
|
|
43
|
+
export { SubComponentB } from "./SubComponentB";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Compound component** (Root + subcomponents used together):
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export const FeatureName = Object.assign(RootComponent, {
|
|
50
|
+
SubA: SubComponentA,
|
|
51
|
+
SubB: SubComponentB,
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Component + styles
|
|
56
|
+
|
|
57
|
+
- `index.tsx`: component logic and JSX
|
|
58
|
+
- `styles.ts`: styled-components (or emotion) only
|
|
59
|
+
- Import: `import * as S from "./styles"` or `import { Root, Item } from "./styles"`
|
|
60
|
+
|
|
61
|
+
### Reference implementations
|
|
62
|
+
|
|
63
|
+
- `SharedTabsHeader` – compound pattern (Root.Action, Root.BackgroundPicker)
|
|
64
|
+
- `PanelClipsArtist`, `FolderSelector` – compound pattern
|
|
65
|
+
- `MultiSourceThreads` – flat exports (FilterBar, Message, Input, List)
|
|
66
|
+
- `TapesTabBarTabOptions` – flat exports
|
|
67
|
+
|
|
68
|
+
## Anti-patterns
|
|
69
|
+
|
|
70
|
+
- Single file with multiple components and inline styles
|
|
71
|
+
- `ComponentName.styles.ts` – use `styles.ts` in the component folder
|
|
72
|
+
- Barrel re-exporting from sibling files instead of subfolders
|
package/src/AGENTS.md
CHANGED
|
@@ -172,7 +172,7 @@ worktree_bootstrap_notes:
|
|
|
172
172
|
## Implemented Components (Current Scope)
|
|
173
173
|
|
|
174
174
|
- Commands: `workflow:brainstorm`, `workflow:plan`, `workflow:triage`, `workflow:work`, `workflow:review`, `workflow:tech-review`, `workflow:compound` (under `.agents/commands/` as `workflow-*.md`), plus `test-browser`, `metrics`, `assess`, `install` (root commands)
|
|
175
|
-
- Skills: `brainstorming`, `document-review`, `technical-review`, `compound-docs` (alias: `compound_doc`), `capture-skill`, `file-todos`, `agent-browser`, `git-worktree`, `process-metrics`, `react-ddd-mvc-frontend`, `xstate-actor-orchestration`, `standards`, `pii-protection-prisma`, `financial-workflow-integrity`, `audit-traceability`, `data-foundations`
|
|
175
|
+
- Skills: `brainstorming`, `document-review`, `technical-review`, `compound-docs` (alias: `compound_doc`), `capture-skill`, `file-todos`, `agent-browser`, `git-worktree`, `process-metrics`, `react-ddd-mvc-frontend`, `xstate-actor-orchestration`, `standards`, `pii-protection-prisma`, `financial-workflow-integrity`, `audit-traceability`, `data-foundations`, `presentation-composability`
|
|
176
176
|
- Agents:
|
|
177
177
|
- `repo-research-analyst`
|
|
178
178
|
- `learnings-researcher`
|
|
@@ -237,6 +237,7 @@ Maintenance:
|
|
|
237
237
|
| `react-ddd-mvc-frontend` | You need React frontend architecture guidance (DDD + MVC hybrid) during planning or review to enforce feature structure, layer boundaries, composable pure components, container/controller responsibilities, and maintainable patterns. |
|
|
238
238
|
| `xstate-actor-orchestration` | You are evaluating complexity and need explicit state orchestration: React container-as-orchestrator for UI flows, or actor/state-machine orchestration for backend/internal workflows (especially multi-step async branching, retries/timeouts/cancellation, receptionist/child-actor coordination, or boolean-flag sprawl). |
|
|
239
239
|
| `standards` | You need Altai coding standards for implementation and refactoring, including domain entity patterns, XState conventions, type usage, and feature code organization. |
|
|
240
|
+
| `presentation-composability` | You are creating or refactoring presentation components in tapesv3 and need folder-per-component structure with barrel exports and styles separation. |
|
|
240
241
|
|
|
241
242
|
### Reference standards (guardrails)
|
|
242
243
|
|