compound-workflow 1.6.17 → 1.7.0

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.
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compound-workflow",
3
- "version": "1.6.17",
3
+ "version": "1.7.0",
4
4
  "description": "Clarify -> plan -> execute -> verify -> capture workflow for Cursor",
5
5
  "author": {
6
6
  "name": "Compound Workflow"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compound-workflow",
3
- "version": "1.6.17",
3
+ "version": "1.7.0",
4
4
  "description": "Clarify → plan → execute → verify → capture. One Install action for Cursor, Claude, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,12 +19,18 @@ const claudePlugin = JSON.parse(
19
19
  );
20
20
 
21
21
  const expected = pkg.version;
22
- if (cursorPlugin.version !== expected || claudePlugin.version !== expected) {
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
- claudePlugin.version
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));
@@ -335,20 +335,26 @@ function ensureDirs(targetRoot, dryRun) {
335
335
  */
336
336
  function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
337
337
  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
338
  const cursorSrc = path.join(PACKAGE_ROOT, ".cursor-plugin", "plugin.json");
344
339
  const claudeSrc = path.join(PACKAGE_ROOT, ".claude-plugin", "plugin.json");
345
340
  const cursorManifest = readJsonMaybe(cursorSrc);
346
341
  const claudeManifest = readJsonMaybe(claudeSrc);
347
342
  if (!cursorManifest || !claudeManifest) return;
348
343
 
349
- const cursorOut = { ...cursorManifest, ...pathOverrides };
350
- // Claude Code validator requires agents as array; Cursor uses string (unchanged).
351
- const claudeOut = { ...claudeManifest, ...pathOverrides, agents: [`${pathsBase}/agents`] };
344
+ // Cursor supports full manifest with commands/agents/skills path overrides.
345
+ const cursorOut = {
346
+ ...cursorManifest,
347
+ commands: `${pathsBase}/commands`,
348
+ agents: `${pathsBase}/agents`,
349
+ skills: `${pathsBase}/skills`,
350
+ };
351
+ // Claude Code only accepts name, description, author in plugin.json.
352
+ // Agents are discovered from the adjacent agents/ directory (flat .md files).
353
+ const claudeOut = {
354
+ name: claudeManifest.name,
355
+ description: claudeManifest.description,
356
+ author: claudeManifest.author,
357
+ };
352
358
  const cursorDir = path.join(targetRoot, ".cursor-plugin");
353
359
  const claudeDir = path.join(targetRoot, ".claude-plugin");
354
360
 
@@ -369,6 +375,37 @@ function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
369
375
  fs.writeFileSync(path.join(claudeDir, "plugin.json"), JSON.stringify(claudeOut, null, 2) + "\n", "utf8");
370
376
  fs.writeFileSync(path.join(cursorDir, "registration.json"), JSON.stringify(registrationDescriptor, null, 2) + "\n", "utf8");
371
377
 
378
+ // Sync flat agent symlinks into .claude-plugin/agents/ so Claude Code discovers them.
379
+ // Claude Code only scans the root of the agents/ directory (not subdirectories).
380
+ const claudeAgentsDir = path.join(claudeDir, "agents");
381
+ const packageAgentsDirAbs = isSelfInstall
382
+ ? path.join(PACKAGE_ROOT, "src", ".agents", "agents")
383
+ : path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "agents");
384
+ if (fs.existsSync(packageAgentsDirAbs)) {
385
+ fs.mkdirSync(claudeAgentsDir, { recursive: true });
386
+ const agentBasenames = new Set(GENERATED_MANIFEST.agents.map((a) => path.basename(a.rel)));
387
+ // Prune stale symlinks
388
+ try {
389
+ for (const entry of fs.readdirSync(claudeAgentsDir, { withFileTypes: true })) {
390
+ if (!agentBasenames.has(entry.name)) {
391
+ fs.rmSync(path.join(claudeAgentsDir, entry.name), { force: true });
392
+ }
393
+ }
394
+ } catch { /* ignore */ }
395
+ for (const agent of GENERATED_MANIFEST.agents) {
396
+ const linkPath = path.join(claudeAgentsDir, path.basename(agent.rel));
397
+ const targetPath = path.join(packageAgentsDirAbs, agent.rel);
398
+ try {
399
+ if (fs.lstatSync(linkPath)) fs.rmSync(linkPath, { force: true });
400
+ } catch { /* doesn't exist */ }
401
+ try {
402
+ fs.symlinkSync(targetPath, linkPath);
403
+ } catch (err) {
404
+ console.warn("[claude] Could not symlink agent", agent.id, err.message);
405
+ }
406
+ }
407
+ }
408
+
372
409
  // Claude Code 2.1.x+ no longer loads from installed_plugins.json; it requires marketplace flow.
373
410
  // Write a project-level marketplace so user can: /plugin marketplace add . then /plugin install compound-workflow@compound-workflow-local
374
411
  if (!isSelfInstall) {
@@ -485,87 +522,56 @@ function applyCursorRegistration(targetRoot, dryRun, noRegisterCursor, forceRegi
485
522
  }
486
523
  })();
487
524
 
488
- const targetRootReal = realpathSafe(isSelfInstall ? PACKAGE_ROOT : targetRoot);
525
+ const projectRoot = isSelfInstall ? PACKAGE_ROOT : targetRoot;
489
526
  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
527
 
538
528
  if (dryRun) {
539
- console.log("[dry-run] Would register Claude plugin:", JSON.stringify(newEntry, null, 2));
529
+ console.log("[dry-run] Would register Claude plugin (project-scoped) at:", projectRoot);
540
530
  return;
541
531
  }
542
532
 
543
- fs.mkdirSync(claudePluginsDir, { recursive: true });
544
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
545
- fs.writeFileSync(installedPath, JSON.stringify(installed, null, 2) + "\n", "utf8");
546
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
533
+ // Registration is always project-scoped: write only to <project>/.claude/settings.json.
534
+ // Claude Code manages ~/.claude/plugins/installed_plugins.json itself via marketplace flow;
535
+ // writing to user-level files causes "unregistered local marketplace" errors on startup.
536
+ const projectSettingsPath = path.join(projectRoot, ".claude", "settings.json");
537
+ let projectSettings = {};
538
+ if (fs.existsSync(projectSettingsPath)) {
539
+ try { projectSettings = readJsonMaybe(projectSettingsPath) ?? {}; } catch { projectSettings = {}; }
540
+ }
541
+ projectSettings.enabledPlugins = ensureObject(projectSettings.enabledPlugins);
542
+ projectSettings.enabledPlugins[pluginId] = true;
543
+ // Remove stale/invalid marketplace keys left by earlier install methods
544
+ if (projectSettings.extraKnownMarketplaces?.["compound-workflow"]) {
545
+ delete projectSettings.extraKnownMarketplaces["compound-workflow"];
546
+ }
547
+ projectSettings.extraKnownMarketplaces = ensureObject(projectSettings.extraKnownMarketplaces);
548
+ projectSettings.extraKnownMarketplaces["compound-workflow-local"] = {
549
+ source: { source: "file", path: ".claude-plugin/marketplace.json" },
550
+ };
551
+ fs.mkdirSync(path.join(projectRoot, ".claude"), { recursive: true });
552
+ fs.writeFileSync(projectSettingsPath, JSON.stringify(projectSettings, null, 2) + "\n", "utf8");
547
553
 
548
- if (!isSelfInstall) {
549
- const projectSettingsPath = path.join(targetRoot, ".claude", "settings.json");
550
- let projectSettings = {};
551
- if (fs.existsSync(projectSettingsPath)) {
552
- try { projectSettings = readJsonMaybe(projectSettingsPath) ?? {}; } catch { projectSettings = {}; }
553
- }
554
- projectSettings.enabledPlugins = ensureObject(projectSettings.enabledPlugins);
555
- projectSettings.enabledPlugins[pluginId] = true;
556
- // Remove invalid "local" entry if present; then register local marketplace via "file" so it appears without slash commands
557
- if (projectSettings.extraKnownMarketplaces?.["compound-workflow"]) {
558
- delete projectSettings.extraKnownMarketplaces["compound-workflow"];
559
- }
560
- projectSettings.extraKnownMarketplaces = ensureObject(projectSettings.extraKnownMarketplaces);
561
- projectSettings.extraKnownMarketplaces["compound-workflow-local"] = {
562
- source: { source: "file", path: ".claude-plugin/marketplace.json" },
563
- };
564
- fs.mkdirSync(path.join(targetRoot, ".claude"), { recursive: true });
565
- fs.writeFileSync(projectSettingsPath, JSON.stringify(projectSettings, null, 2) + "\n", "utf8");
554
+ // Clean up any stale user-level enabledPlugins entries left by previous install versions.
555
+ // These cause "unregistered local marketplace" errors on every Claude Code startup.
556
+ if (fs.existsSync(settingsPath)) {
557
+ try {
558
+ let userSettings = readJsonMaybe(settingsPath) ?? {};
559
+ const staleIds = ["compound-workflow@local", "compound-workflow@compound-workflow-local"];
560
+ let changed = false;
561
+ for (const id of staleIds) {
562
+ if (userSettings?.enabledPlugins?.[id] !== undefined) {
563
+ delete userSettings.enabledPlugins[id];
564
+ changed = true;
565
+ }
566
+ }
567
+ if (changed) {
568
+ fs.writeFileSync(settingsPath, JSON.stringify(userSettings, null, 2) + "\n", "utf8");
569
+ console.log("Cleaned up stale compound-workflow entries from ~/.claude/settings.json");
570
+ }
571
+ } catch { /* ignore */ }
566
572
  }
567
573
 
568
- console.log("Registered compound-workflow with Claude Code.");
574
+ console.log("Registered compound-workflow with Claude Code (project-scoped).");
569
575
  if (!isSelfInstall) {
570
576
  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
577
  }
@@ -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).