@every-env/compound-plugin 0.1.1 → 0.2.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.
Files changed (54) hide show
  1. package/.claude/commands/triage-prs.md +193 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.github/workflows/ci.yml +25 -0
  4. package/README.md +22 -1
  5. package/docs/plans/2026-02-08-feat-pr-triage-and-merge-plan.md +128 -0
  6. package/package.json +1 -1
  7. package/plans/grow-your-own-garden-plugin-architecture.md +1 -1
  8. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  9. package/plugins/compound-engineering/CHANGELOG.md +32 -0
  10. package/plugins/compound-engineering/CLAUDE.md +3 -4
  11. package/plugins/compound-engineering/README.md +19 -7
  12. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +2 -0
  13. package/plugins/compound-engineering/agents/research/learnings-researcher.md +2 -2
  14. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -0
  15. package/plugins/compound-engineering/agents/review/schema-drift-detector.md +139 -0
  16. package/plugins/compound-engineering/commands/deepen-plan.md +2 -2
  17. package/plugins/compound-engineering/commands/resolve_todo_parallel.md +2 -0
  18. package/plugins/compound-engineering/commands/slfg.md +31 -0
  19. package/plugins/compound-engineering/commands/technical_review.md +7 -0
  20. package/plugins/compound-engineering/commands/workflows/brainstorm.md +11 -2
  21. package/plugins/compound-engineering/commands/workflows/compound.md +64 -27
  22. package/plugins/compound-engineering/commands/workflows/plan.md +9 -9
  23. package/plugins/compound-engineering/commands/workflows/review.md +12 -0
  24. package/plugins/compound-engineering/commands/workflows/work.md +71 -1
  25. package/plugins/compound-engineering/skills/compound-docs/SKILL.md +8 -8
  26. package/plugins/compound-engineering/skills/compound-docs/assets/critical-pattern-template.md +1 -1
  27. package/plugins/compound-engineering/skills/compound-docs/assets/resolution-template.md +3 -3
  28. package/plugins/compound-engineering/skills/compound-docs/references/yaml-schema.md +1 -1
  29. package/plugins/compound-engineering/skills/create-agent-skills/SKILL.md +168 -192
  30. package/plugins/compound-engineering/skills/create-agent-skills/references/official-spec.md +74 -125
  31. package/plugins/compound-engineering/skills/create-agent-skills/references/skill-structure.md +109 -329
  32. package/plugins/compound-engineering/skills/document-review/SKILL.md +87 -0
  33. package/plugins/compound-engineering/skills/git-worktree/scripts/worktree-manager.sh +2 -10
  34. package/plugins/compound-engineering/skills/orchestrating-swarms/SKILL.md +1717 -0
  35. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +89 -0
  36. package/plugins/compound-engineering/skills/resolve-pr-parallel/scripts/get-pr-comments +68 -0
  37. package/plugins/compound-engineering/skills/resolve-pr-parallel/scripts/resolve-pr-thread +23 -0
  38. package/src/commands/sync.ts +84 -0
  39. package/src/converters/claude-to-codex.ts +59 -2
  40. package/src/converters/claude-to-opencode.ts +7 -5
  41. package/src/index.ts +2 -0
  42. package/src/parsers/claude-home.ts +65 -0
  43. package/src/sync/codex.ts +92 -0
  44. package/src/sync/opencode.ts +75 -0
  45. package/src/targets/codex.ts +7 -2
  46. package/src/targets/opencode.ts +6 -1
  47. package/src/types/claude.ts +1 -1
  48. package/src/utils/files.ts +13 -0
  49. package/src/utils/symlink.ts +43 -0
  50. package/tests/codex-converter.test.ts +83 -0
  51. package/tests/codex-writer.test.ts +32 -0
  52. package/tests/opencode-writer.test.ts +32 -0
  53. package/plugins/compound-engineering/commands/plan_review.md +0 -7
  54. package/plugins/compound-engineering/commands/resolve_pr_parallel.md +0 -49
@@ -1,6 +1,19 @@
1
1
  import { promises as fs } from "fs"
2
2
  import path from "path"
3
3
 
4
+ export async function backupFile(filePath: string): Promise<string | null> {
5
+ if (!(await pathExists(filePath))) return null
6
+
7
+ try {
8
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
9
+ const backupPath = `${filePath}.bak.${timestamp}`
10
+ await fs.copyFile(filePath, backupPath)
11
+ return backupPath
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
4
17
  export async function pathExists(filePath: string): Promise<boolean> {
5
18
  try {
6
19
  await fs.access(filePath)
@@ -0,0 +1,43 @@
1
+ import fs from "fs/promises"
2
+
3
+ /**
4
+ * Create a symlink, safely replacing any existing symlink at target.
5
+ * Only removes existing symlinks - refuses to delete real directories.
6
+ */
7
+ export async function forceSymlink(source: string, target: string): Promise<void> {
8
+ try {
9
+ const stat = await fs.lstat(target)
10
+ if (stat.isSymbolicLink()) {
11
+ // Safe to remove existing symlink
12
+ await fs.unlink(target)
13
+ } else if (stat.isDirectory()) {
14
+ // Refuse to delete real directories
15
+ throw new Error(
16
+ `Cannot create symlink at ${target}: a real directory exists there. ` +
17
+ `Remove it manually if you want to replace it with a symlink.`
18
+ )
19
+ } else {
20
+ // Regular file - remove it
21
+ await fs.unlink(target)
22
+ }
23
+ } catch (err) {
24
+ // ENOENT means target doesn't exist, which is fine
25
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
26
+ throw err
27
+ }
28
+ }
29
+ await fs.symlink(source, target)
30
+ }
31
+
32
+ /**
33
+ * Validate a skill name to prevent path traversal attacks.
34
+ * Returns true if safe, false if potentially malicious.
35
+ */
36
+ export function isValidSkillName(name: string): boolean {
37
+ if (!name || name.length === 0) return false
38
+ if (name.includes("/") || name.includes("\\")) return false
39
+ if (name.includes("..")) return false
40
+ if (name.includes("\0")) return false
41
+ if (name === "." || name === "..") return false
42
+ return true
43
+ }
@@ -89,6 +89,89 @@ describe("convertClaudeToCodex", () => {
89
89
  expect(bundle.mcpServers?.local?.args).toEqual(["hello"])
90
90
  })
91
91
 
92
+ test("transforms Task agent calls to skill references", () => {
93
+ const plugin: ClaudePlugin = {
94
+ ...fixturePlugin,
95
+ commands: [
96
+ {
97
+ name: "plan",
98
+ description: "Planning with agents",
99
+ body: `Run these agents in parallel:
100
+
101
+ - Task repo-research-analyst(feature_description)
102
+ - Task learnings-researcher(feature_description)
103
+
104
+ Then consolidate findings.
105
+
106
+ Task best-practices-researcher(topic)`,
107
+ sourcePath: "/tmp/plugin/commands/plan.md",
108
+ },
109
+ ],
110
+ agents: [],
111
+ skills: [],
112
+ }
113
+
114
+ const bundle = convertClaudeToCodex(plugin, {
115
+ agentMode: "subagent",
116
+ inferTemperature: false,
117
+ permissions: "none",
118
+ })
119
+
120
+ const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan")
121
+ expect(commandSkill).toBeDefined()
122
+ const parsed = parseFrontmatter(commandSkill!.content)
123
+
124
+ // Task calls should be transformed to skill references
125
+ expect(parsed.body).toContain("Use the $repo-research-analyst skill to: feature_description")
126
+ expect(parsed.body).toContain("Use the $learnings-researcher skill to: feature_description")
127
+ expect(parsed.body).toContain("Use the $best-practices-researcher skill to: topic")
128
+
129
+ // Original Task syntax should not remain
130
+ expect(parsed.body).not.toContain("Task repo-research-analyst")
131
+ expect(parsed.body).not.toContain("Task learnings-researcher")
132
+ })
133
+
134
+ test("transforms slash commands to prompts syntax", () => {
135
+ const plugin: ClaudePlugin = {
136
+ ...fixturePlugin,
137
+ commands: [
138
+ {
139
+ name: "plan",
140
+ description: "Planning with commands",
141
+ body: `After planning, you can:
142
+
143
+ 1. Run /deepen-plan to enhance
144
+ 2. Run /plan_review for feedback
145
+ 3. Start /workflows:work to implement
146
+
147
+ Don't confuse with file paths like /tmp/output.md or /dev/null.`,
148
+ sourcePath: "/tmp/plugin/commands/plan.md",
149
+ },
150
+ ],
151
+ agents: [],
152
+ skills: [],
153
+ }
154
+
155
+ const bundle = convertClaudeToCodex(plugin, {
156
+ agentMode: "subagent",
157
+ inferTemperature: false,
158
+ permissions: "none",
159
+ })
160
+
161
+ const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan")
162
+ expect(commandSkill).toBeDefined()
163
+ const parsed = parseFrontmatter(commandSkill!.content)
164
+
165
+ // Slash commands should be transformed to /prompts: syntax
166
+ expect(parsed.body).toContain("/prompts:deepen-plan")
167
+ expect(parsed.body).toContain("/prompts:plan_review")
168
+ expect(parsed.body).toContain("/prompts:workflows-work")
169
+
170
+ // File paths should NOT be transformed
171
+ expect(parsed.body).toContain("/tmp/output.md")
172
+ expect(parsed.body).toContain("/dev/null")
173
+ })
174
+
92
175
  test("truncates generated skill descriptions to Codex limits and single line", () => {
93
176
  const longDescription = `Line one\nLine two ${"a".repeat(2000)}`
94
177
  const plugin: ClaudePlugin = {
@@ -73,4 +73,36 @@ describe("writeCodexBundle", () => {
73
73
  expect(await exists(path.join(codexRoot, "prompts", "command-one.md"))).toBe(true)
74
74
  expect(await exists(path.join(codexRoot, "skills", "skill-one", "SKILL.md"))).toBe(true)
75
75
  })
76
+
77
+ test("backs up existing config.toml before overwriting", async () => {
78
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-backup-"))
79
+ const codexRoot = path.join(tempRoot, ".codex")
80
+ const configPath = path.join(codexRoot, "config.toml")
81
+
82
+ // Create existing config
83
+ await fs.mkdir(codexRoot, { recursive: true })
84
+ const originalContent = "# My original config\n[custom]\nkey = \"value\"\n"
85
+ await fs.writeFile(configPath, originalContent)
86
+
87
+ const bundle: CodexBundle = {
88
+ prompts: [],
89
+ skillDirs: [],
90
+ generatedSkills: [],
91
+ mcpServers: { test: { command: "echo" } },
92
+ }
93
+
94
+ await writeCodexBundle(codexRoot, bundle)
95
+
96
+ // New config should be written
97
+ const newConfig = await fs.readFile(configPath, "utf8")
98
+ expect(newConfig).toContain("[mcp_servers.test]")
99
+
100
+ // Backup should exist with original content
101
+ const files = await fs.readdir(codexRoot)
102
+ const backupFileName = files.find((f) => f.startsWith("config.toml.bak."))
103
+ expect(backupFileName).toBeDefined()
104
+
105
+ const backupContent = await fs.readFile(path.join(codexRoot, backupFileName!), "utf8")
106
+ expect(backupContent).toBe(originalContent)
107
+ })
76
108
  })
@@ -84,4 +84,36 @@ describe("writeOpenCodeBundle", () => {
84
84
  expect(await exists(path.join(outputRoot, "skills", "skill-one", "SKILL.md"))).toBe(true)
85
85
  expect(await exists(path.join(outputRoot, ".opencode"))).toBe(false)
86
86
  })
87
+
88
+ test("backs up existing opencode.json before overwriting", async () => {
89
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-backup-"))
90
+ const outputRoot = path.join(tempRoot, ".opencode")
91
+ const configPath = path.join(outputRoot, "opencode.json")
92
+
93
+ // Create existing config
94
+ await fs.mkdir(outputRoot, { recursive: true })
95
+ const originalConfig = { $schema: "https://opencode.ai/config.json", custom: "value" }
96
+ await fs.writeFile(configPath, JSON.stringify(originalConfig, null, 2))
97
+
98
+ const bundle: OpenCodeBundle = {
99
+ config: { $schema: "https://opencode.ai/config.json", new: "config" },
100
+ agents: [],
101
+ plugins: [],
102
+ skillDirs: [],
103
+ }
104
+
105
+ await writeOpenCodeBundle(outputRoot, bundle)
106
+
107
+ // New config should be written
108
+ const newConfig = JSON.parse(await fs.readFile(configPath, "utf8"))
109
+ expect(newConfig.new).toBe("config")
110
+
111
+ // Backup should exist with original content
112
+ const files = await fs.readdir(outputRoot)
113
+ const backupFileName = files.find((f) => f.startsWith("opencode.json.bak."))
114
+ expect(backupFileName).toBeDefined()
115
+
116
+ const backupContent = JSON.parse(await fs.readFile(path.join(outputRoot, backupFileName!), "utf8"))
117
+ expect(backupContent.custom).toBe("value")
118
+ })
87
119
  })
@@ -1,7 +0,0 @@
1
- ---
2
- name: plan_review
3
- description: Have multiple specialized agents review a plan in parallel
4
- argument-hint: "[plan file path or plan content]"
5
- ---
6
-
7
- Have @agent-dhh-rails-reviewer @agent-kieran-rails-reviewer @agent-code-simplicity-reviewer review this plan in parallel.
@@ -1,49 +0,0 @@
1
- ---
2
- name: resolve_pr_parallel
3
- description: Resolve all PR comments using parallel processing
4
- argument-hint: "[optional: PR number or current PR]"
5
- ---
6
-
7
- Resolve all PR comments using parallel processing.
8
-
9
- Claude Code automatically detects and understands your git context:
10
-
11
- - Current branch detection
12
- - Associated PR context
13
- - All PR comments and review threads
14
- - Can work with any PR by specifying the PR number, or ask it.
15
-
16
- ## Workflow
17
-
18
- ### 1. Analyze
19
-
20
- Get all unresolved comments for PR
21
-
22
- ```bash
23
- gh pr status
24
- bin/get-pr-comments PR_NUMBER
25
- ```
26
-
27
- ### 2. Plan
28
-
29
- Create a TodoWrite list of all unresolved items grouped by type.
30
-
31
- ### 3. Implement (PARALLEL)
32
-
33
- Spawn a pr-comment-resolver agent for each unresolved item in parallel.
34
-
35
- So if there are 3 comments, it will spawn 3 pr-comment-resolver agents in parallel. liek this
36
-
37
- 1. Task pr-comment-resolver(comment1)
38
- 2. Task pr-comment-resolver(comment2)
39
- 3. Task pr-comment-resolver(comment3)
40
-
41
- Always run all in parallel subagents/Tasks for each Todo item.
42
-
43
- ### 4. Commit & Resolve
44
-
45
- - Commit changes
46
- - Run bin/resolve-pr-thread THREAD_ID_1
47
- - Push to remote
48
-
49
- Last, check bin/get-pr-comments PR_NUMBER again to see if all comments are resolved. They should be, if not, repeat the process from 1.