opencode-kolchoz-loop 1.0.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.
- package/README.md +149 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +409 -0
- package/package.json +41 -0
- package/src/agents/anetka.md +89 -0
- package/src/agents/areczek.md +78 -0
- package/src/agents/grazynka.md +50 -0
- package/src/agents/januszek.md +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# opencode-kolchoz-loop
|
|
2
|
+
|
|
3
|
+
Multi-agent Ralph Loop plugin for [OpenCode](https://opencode.ai).
|
|
4
|
+
|
|
5
|
+
Four agents work in a Ralph Loop workflow and autonomously execute tasks from requirements analysis to code review.
|
|
6
|
+
|
|
7
|
+
## Agents
|
|
8
|
+
|
|
9
|
+
| Agent | Role | Mode | Color |
|
|
10
|
+
|-------|------|------|-------|
|
|
11
|
+
| **Januszek** 👔 | Orchestrator - talks to the user, delegates work | primary | orange |
|
|
12
|
+
| **Grazynka** 📋 | Requirements analyst - creates PRDs with user stories | subagent | purple |
|
|
13
|
+
| **Areczek** 🔧 | Builder - implements code, tests, commits | subagent | cyan |
|
|
14
|
+
| **Anetka** 🔍 | Reviewer - quality gate: tests, lint, typecheck, diff | subagent | pink |
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add the package to `opencode.json` in your project:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"$schema": "https://opencode.ai/config.json",
|
|
23
|
+
"plugin": ["opencode-kolchoz-loop"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On the next OpenCode start it will automatically:
|
|
28
|
+
1. Install the package via Bun
|
|
29
|
+
2. Copy agent definitions into `.opencode/agents/`
|
|
30
|
+
3. Create `.opencode/state/` for runtime state files
|
|
31
|
+
4. Add `.opencode/state/` to `.gitignore`
|
|
32
|
+
|
|
33
|
+
### From a private npm registry (internal)
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"plugin": ["@your-company/opencode-kolchoz-loop"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### From a local folder (testing)
|
|
42
|
+
|
|
43
|
+
Copy the full `src/` directory into `.opencode/plugins/` in your project:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cp -r src/ /path/to/project/.opencode/plugins/kolchoz-loop/
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Interactive mode
|
|
52
|
+
|
|
53
|
+
After OpenCode starts, Januszek is the default agent. Describe the task:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Implement an authentication system with OAuth2 and 2FA
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Januszek then:
|
|
60
|
+
1. Delegates requirement clarification to `@grazynka`
|
|
61
|
+
2. Grazynka creates a PRD with user stories
|
|
62
|
+
3. Januszek delegates implementation to `@areczek`
|
|
63
|
+
4. Areczek codes, tests, and commits
|
|
64
|
+
5. `@anetka` performs code review
|
|
65
|
+
6. If FAIL -> Areczek fixes. If PASS -> next story.
|
|
66
|
+
7. After PRD completion -> Januszek reports back.
|
|
67
|
+
|
|
68
|
+
### Manual agent calls
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
@grazynka Analyze requirements for the payments module
|
|
72
|
+
@areczek Fetch the next task and implement it
|
|
73
|
+
@anetka Review the latest changes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Check status
|
|
77
|
+
|
|
78
|
+
Januszek uses `kolchoz_status`, but you can also inspect state manually:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cat .opencode/state/prd.json | jq '.userStories[] | {id, title, status}'
|
|
82
|
+
tail -20 .opencode/state/progress.txt
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Reset (new task)
|
|
86
|
+
|
|
87
|
+
`kolchoz_reset` clears loop state while preserving accumulated knowledge in `AGENTS.md`.
|
|
88
|
+
|
|
89
|
+
## File structure
|
|
90
|
+
|
|
91
|
+
After installation this appears in your project:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
your-project/
|
|
95
|
+
├── .opencode/
|
|
96
|
+
│ ├── agents/ ← copied automatically from the package
|
|
97
|
+
│ │ ├── januszek.md
|
|
98
|
+
│ │ ├── grazynka.md
|
|
99
|
+
│ │ ├── areczek.md
|
|
100
|
+
│ │ └── anetka.md
|
|
101
|
+
│ └── state/ ← gitignored, ephemeral
|
|
102
|
+
│ ├── prd.json
|
|
103
|
+
│ ├── progress.txt
|
|
104
|
+
│ └── loop-state.json
|
|
105
|
+
├── AGENTS.md ← accumulated knowledge, COMMITTED
|
|
106
|
+
└── .gitignore ← auto-updated: .opencode/state/
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Tools (custom tools)
|
|
110
|
+
|
|
111
|
+
The plugin registers 7 tools:
|
|
112
|
+
|
|
113
|
+
| Tool | Used by | Description |
|
|
114
|
+
|------|---------|-------------|
|
|
115
|
+
| `kolchoz_create_prd` | Grazynka | Creates PRD with user stories |
|
|
116
|
+
| `kolchoz_next_task` | Areczek | Fetches next task |
|
|
117
|
+
| `kolchoz_submit_for_review` | Areczek | Submits work for review |
|
|
118
|
+
| `kolchoz_review_verdict` | Anetka | Pass/fail verdict |
|
|
119
|
+
| `kolchoz_status` | Everyone | Loop status |
|
|
120
|
+
| `kolchoz_learn` | Everyone | Writes knowledge to AGENTS.md |
|
|
121
|
+
| `kolchoz_reset` | Januszek | Clears state, keeps knowledge |
|
|
122
|
+
|
|
123
|
+
## Model configuration
|
|
124
|
+
|
|
125
|
+
Agents use `claude-sonnet-4` by default. Override in `opencode.json`:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"agent": {
|
|
130
|
+
"januszek": { "model": "anthropic/claude-opus-4-20250514" },
|
|
131
|
+
"areczek": { "model": "openai/gpt-4.1" },
|
|
132
|
+
"anetka": { "model": "google/gemini-2.5-pro" }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Recommendation: Anetka and Areczek should use different models. Cross-model review reduces shared blind spots.
|
|
138
|
+
|
|
139
|
+
## Compound engineering
|
|
140
|
+
|
|
141
|
+
The system builds knowledge in `AGENTS.md` automatically:
|
|
142
|
+
|
|
143
|
+
1. Areczek discovers a pattern -> `kolchoz_learn("pattern", "...")`
|
|
144
|
+
2. Anetka discovers a gotcha -> `kolchoz_learn("gotcha", "...")`
|
|
145
|
+
3. Future sessions read AGENTS.md -> better context -> better outcomes
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { readFile, writeFile, mkdir, readdir, copyFile, stat } from "fs/promises";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
// ── Filesystem helpers ──
|
|
6
|
+
async function ensureDir(dir) {
|
|
7
|
+
await mkdir(dir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
async function exists(path) {
|
|
10
|
+
try {
|
|
11
|
+
await stat(path);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readJson(dir, filename, fallback) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(join(dir, filename), "utf-8");
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function writeJson(dir, filename, data) {
|
|
28
|
+
await ensureDir(dir);
|
|
29
|
+
await writeFile(join(dir, filename), JSON.stringify(data, null, 2), "utf-8");
|
|
30
|
+
}
|
|
31
|
+
async function appendProgress(dir, message) {
|
|
32
|
+
await ensureDir(dir);
|
|
33
|
+
const file = join(dir, "progress.txt");
|
|
34
|
+
const timestamp = new Date().toISOString();
|
|
35
|
+
const line = `[${timestamp}] ${message}\n`;
|
|
36
|
+
try {
|
|
37
|
+
const existing = await readFile(file, "utf-8");
|
|
38
|
+
await writeFile(file, existing + line, "utf-8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
await writeFile(file, line, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── Agent provisioning ──
|
|
45
|
+
// Copies bundled .md agent files to .opencode/agents/
|
|
46
|
+
// so OpenCode discovers them as real agents.
|
|
47
|
+
async function provisionAgents(projectRoot, log) {
|
|
48
|
+
const targetDir = join(projectRoot, ".opencode", "agents");
|
|
49
|
+
await ensureDir(targetDir);
|
|
50
|
+
// Resolve the agents directory bundled inside this package
|
|
51
|
+
const packageAgentsDir = join(dirname(fileURLToPath(import.meta.url)), "agents");
|
|
52
|
+
let agentFiles;
|
|
53
|
+
try {
|
|
54
|
+
agentFiles = (await readdir(packageAgentsDir)).filter((f) => f.endsWith(".md"));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
log("WARN: Could not read bundled agents directory. Agents must be provisioned manually.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const file of agentFiles) {
|
|
61
|
+
const target = join(targetDir, file);
|
|
62
|
+
if (await exists(target)) {
|
|
63
|
+
// Don't overwrite — user may have customized
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
await copyFile(join(packageAgentsDir, file), target);
|
|
67
|
+
log(`Provisioned agent: ${file} → .opencode/agents/${file}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ── Gitignore helper ──
|
|
71
|
+
// Ensures .opencode/state/ is in .gitignore
|
|
72
|
+
async function ensureGitignore(projectRoot) {
|
|
73
|
+
const gitignorePath = join(projectRoot, ".gitignore");
|
|
74
|
+
const entry = ".opencode/state/";
|
|
75
|
+
let content = "";
|
|
76
|
+
try {
|
|
77
|
+
content = await readFile(gitignorePath, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// No .gitignore — create one
|
|
81
|
+
}
|
|
82
|
+
if (!content.includes(entry)) {
|
|
83
|
+
const addition = content.length > 0 && !content.endsWith("\n")
|
|
84
|
+
? `\n\n# Kolkhoz Loop state (ephemeral)\n${entry}\n`
|
|
85
|
+
: `\n# Kolkhoz Loop state (ephemeral)\n${entry}\n`;
|
|
86
|
+
await writeFile(gitignorePath, content + addition, "utf-8");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ── Main Plugin Export ──
|
|
90
|
+
export const KolchozLoop = async ({ project, client, $, directory, worktree }) => {
|
|
91
|
+
const projectRoot = worktree || directory;
|
|
92
|
+
const stateDir = join(projectRoot, ".opencode", "state");
|
|
93
|
+
// ── Auto-provision on first run ──
|
|
94
|
+
await ensureDir(stateDir);
|
|
95
|
+
await provisionAgents(projectRoot, (msg) => {
|
|
96
|
+
client.app.log({
|
|
97
|
+
body: { service: "kolchoz-loop", level: "info", message: msg },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
await ensureGitignore(projectRoot);
|
|
101
|
+
await client.app.log({
|
|
102
|
+
body: {
|
|
103
|
+
service: "kolchoz-loop",
|
|
104
|
+
level: "info",
|
|
105
|
+
message: `Kolkhoz Loop initialized. Project: ${projectRoot}, State: ${stateDir}`,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
// ══════════════════════════════════
|
|
110
|
+
// Custom Tools
|
|
111
|
+
// ══════════════════════════════════
|
|
112
|
+
tool: {
|
|
113
|
+
// ── Grazynka: create/update PRD ──
|
|
114
|
+
kolchoz_create_prd: tool({
|
|
115
|
+
description: "[Grazynka] Create or update a structured PRD with user stories " +
|
|
116
|
+
"and acceptance criteria. Writes to .opencode/state/prd.json.",
|
|
117
|
+
args: {
|
|
118
|
+
title: tool.schema.string(),
|
|
119
|
+
context: tool.schema.string(),
|
|
120
|
+
constraints: tool.schema.string(),
|
|
121
|
+
stories: tool.schema.string(), // JSON array
|
|
122
|
+
},
|
|
123
|
+
async execute(args) {
|
|
124
|
+
const stories = JSON.parse(args.stories).map((s, i) => ({
|
|
125
|
+
id: s.id || `story-${i + 1}`,
|
|
126
|
+
title: s.title || `Story ${i + 1}`,
|
|
127
|
+
description: s.description || "",
|
|
128
|
+
acceptanceCriteria: s.acceptanceCriteria || [],
|
|
129
|
+
priority: s.priority || "medium",
|
|
130
|
+
status: "pending",
|
|
131
|
+
assignedTo: "areczek",
|
|
132
|
+
iteration: 0,
|
|
133
|
+
}));
|
|
134
|
+
const prd = {
|
|
135
|
+
title: args.title,
|
|
136
|
+
createdBy: "grazynka",
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
userStories: stories,
|
|
139
|
+
context: args.context,
|
|
140
|
+
constraints: args.constraints.split(",").map((c) => c.trim()),
|
|
141
|
+
};
|
|
142
|
+
await writeJson(stateDir, "prd.json", prd);
|
|
143
|
+
await appendProgress(stateDir, `[Grazynka] PRD created: "${args.title}" with ${stories.length} stories`);
|
|
144
|
+
return `PRD created with ${stories.length} stories. Saved to .opencode/state/prd.json`;
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
// ── Areczek: get next task ──
|
|
148
|
+
kolchoz_next_task: tool({
|
|
149
|
+
description: "[Areczek] Get the next pending or failed story from the PRD. " +
|
|
150
|
+
"Returns story details or signals completion.",
|
|
151
|
+
args: {},
|
|
152
|
+
async execute() {
|
|
153
|
+
const prd = await readJson(stateDir, "prd.json", null);
|
|
154
|
+
if (!prd)
|
|
155
|
+
return "ERROR: No PRD found. Ask Januszek to start the process.";
|
|
156
|
+
const next = prd.userStories.find((s) => s.status === "pending" || s.status === "failed");
|
|
157
|
+
if (!next) {
|
|
158
|
+
const allDone = prd.userStories.every((s) => s.status === "done");
|
|
159
|
+
if (allDone)
|
|
160
|
+
return "<promise>COMPLETE</promise> — All stories are done!";
|
|
161
|
+
return "No actionable stories. Some may be in review.";
|
|
162
|
+
}
|
|
163
|
+
next.status = "in_progress";
|
|
164
|
+
next.iteration += 1;
|
|
165
|
+
await writeJson(stateDir, "prd.json", prd);
|
|
166
|
+
const state = await readJson(stateDir, "loop-state.json", {
|
|
167
|
+
currentIteration: 0,
|
|
168
|
+
maxIterations: 10,
|
|
169
|
+
currentStoryId: null,
|
|
170
|
+
phase: "idle",
|
|
171
|
+
startedAt: new Date().toISOString(),
|
|
172
|
+
history: [],
|
|
173
|
+
});
|
|
174
|
+
state.phase = "building";
|
|
175
|
+
state.currentStoryId = next.id;
|
|
176
|
+
state.currentIteration += 1;
|
|
177
|
+
await writeJson(stateDir, "loop-state.json", state);
|
|
178
|
+
await appendProgress(stateDir, `[Areczek] Starting: ${next.id} "${next.title}" (iteration ${next.iteration})`);
|
|
179
|
+
return JSON.stringify({
|
|
180
|
+
storyId: next.id,
|
|
181
|
+
title: next.title,
|
|
182
|
+
description: next.description,
|
|
183
|
+
acceptanceCriteria: next.acceptanceCriteria,
|
|
184
|
+
priority: next.priority,
|
|
185
|
+
iteration: next.iteration,
|
|
186
|
+
feedback: next.feedback || null,
|
|
187
|
+
}, null, 2);
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
// ── Areczek: submit for review ──
|
|
191
|
+
kolchoz_submit_for_review: tool({
|
|
192
|
+
description: "[Areczek] Submit current story for review by Anetka.",
|
|
193
|
+
args: {
|
|
194
|
+
storyId: tool.schema.string(),
|
|
195
|
+
summary: tool.schema.string(),
|
|
196
|
+
filesChanged: tool.schema.string(),
|
|
197
|
+
},
|
|
198
|
+
async execute(args) {
|
|
199
|
+
const prd = await readJson(stateDir, "prd.json", null);
|
|
200
|
+
if (!prd)
|
|
201
|
+
return "ERROR: No PRD found.";
|
|
202
|
+
const story = prd.userStories.find((s) => s.id === args.storyId);
|
|
203
|
+
if (!story)
|
|
204
|
+
return `ERROR: Story ${args.storyId} not found.`;
|
|
205
|
+
story.status = "review";
|
|
206
|
+
await writeJson(stateDir, "prd.json", prd);
|
|
207
|
+
const state = await readJson(stateDir, "loop-state.json", {
|
|
208
|
+
currentIteration: 0, maxIterations: 10, currentStoryId: null,
|
|
209
|
+
phase: "idle", startedAt: new Date().toISOString(), history: [],
|
|
210
|
+
});
|
|
211
|
+
state.phase = "reviewing";
|
|
212
|
+
await writeJson(stateDir, "loop-state.json", state);
|
|
213
|
+
await appendProgress(stateDir, `[Areczek → Anetka] Story ${args.storyId} submitted. Files: ${args.filesChanged}`);
|
|
214
|
+
return `Story ${args.storyId} submitted for review. @anetka will verify.`;
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
// ── Anetka: review verdict ──
|
|
218
|
+
kolchoz_review_verdict: tool({
|
|
219
|
+
description: "[Anetka] Submit review verdict. Pass = done. Fail = back to Areczek.",
|
|
220
|
+
args: {
|
|
221
|
+
storyId: tool.schema.string(),
|
|
222
|
+
verdict: tool.schema.enum(["pass", "fail"]),
|
|
223
|
+
feedback: tool.schema.string(),
|
|
224
|
+
testsRun: tool.schema.string(),
|
|
225
|
+
issuesFound: tool.schema.string(),
|
|
226
|
+
},
|
|
227
|
+
async execute(args) {
|
|
228
|
+
const prd = await readJson(stateDir, "prd.json", null);
|
|
229
|
+
if (!prd)
|
|
230
|
+
return "ERROR: No PRD found.";
|
|
231
|
+
const story = prd.userStories.find((s) => s.id === args.storyId);
|
|
232
|
+
if (!story)
|
|
233
|
+
return `ERROR: Story ${args.storyId} not found.`;
|
|
234
|
+
const state = await readJson(stateDir, "loop-state.json", {
|
|
235
|
+
currentIteration: 0, maxIterations: 10, currentStoryId: null,
|
|
236
|
+
phase: "idle", startedAt: new Date().toISOString(), history: [],
|
|
237
|
+
});
|
|
238
|
+
if (args.verdict === "pass") {
|
|
239
|
+
story.status = "done";
|
|
240
|
+
story.feedback = undefined;
|
|
241
|
+
state.phase = "idle";
|
|
242
|
+
await appendProgress(stateDir, `[Anetka ✅] Story ${args.storyId} PASSED. ${args.feedback}`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
story.status = "failed";
|
|
246
|
+
story.feedback = args.feedback;
|
|
247
|
+
state.phase = "building";
|
|
248
|
+
await appendProgress(stateDir, `[Anetka ❌] Story ${args.storyId} FAILED. ${args.feedback}`);
|
|
249
|
+
}
|
|
250
|
+
state.history.push({
|
|
251
|
+
iteration: state.currentIteration,
|
|
252
|
+
storyId: args.storyId,
|
|
253
|
+
phase: "review",
|
|
254
|
+
result: args.verdict,
|
|
255
|
+
timestamp: new Date().toISOString(),
|
|
256
|
+
summary: `${args.verdict.toUpperCase()}: ${args.feedback}`,
|
|
257
|
+
});
|
|
258
|
+
await writeJson(stateDir, "prd.json", prd);
|
|
259
|
+
await writeJson(stateDir, "loop-state.json", state);
|
|
260
|
+
const allDone = prd.userStories.every((s) => s.status === "done");
|
|
261
|
+
if (allDone)
|
|
262
|
+
return "<promise>COMPLETE</promise> — All stories verified!";
|
|
263
|
+
if (args.verdict === "fail") {
|
|
264
|
+
if (story.iteration >= state.maxIterations) {
|
|
265
|
+
return `BLOCKED: Story ${args.storyId} failed ${story.iteration} times. Escalating to Januszek.`;
|
|
266
|
+
}
|
|
267
|
+
return `Story ${args.storyId} needs rework. Feedback sent to Areczek.`;
|
|
268
|
+
}
|
|
269
|
+
return `Story ${args.storyId} done. Next task available.`;
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
// ── Status ──
|
|
273
|
+
kolchoz_status: tool({
|
|
274
|
+
description: "Get Kolkhoz Loop status — PRD progress, phase, iteration count.",
|
|
275
|
+
args: {},
|
|
276
|
+
async execute() {
|
|
277
|
+
const prd = await readJson(stateDir, "prd.json", null);
|
|
278
|
+
const state = await readJson(stateDir, "loop-state.json", null);
|
|
279
|
+
if (!prd)
|
|
280
|
+
return "No active PRD. Describe your requirements to Januszek to start.";
|
|
281
|
+
const byStatus = (s) => prd.userStories.filter((st) => st.status === s).length;
|
|
282
|
+
return JSON.stringify({
|
|
283
|
+
prdTitle: prd.title,
|
|
284
|
+
totalStories: prd.userStories.length,
|
|
285
|
+
done: byStatus("done"),
|
|
286
|
+
pending: byStatus("pending"),
|
|
287
|
+
inProgress: byStatus("in_progress"),
|
|
288
|
+
inReview: byStatus("review"),
|
|
289
|
+
failed: byStatus("failed"),
|
|
290
|
+
currentPhase: state?.phase || "idle",
|
|
291
|
+
currentIteration: state?.currentIteration || 0,
|
|
292
|
+
currentStory: state?.currentStoryId || null,
|
|
293
|
+
}, null, 2);
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
// ── Learning (AGENTS.md in project root) ──
|
|
297
|
+
kolchoz_learn: tool({
|
|
298
|
+
description: "Append a learning/pattern/gotcha to AGENTS.md (project root). " +
|
|
299
|
+
"Persists knowledge across Ralph Loop iterations. Committed to git.",
|
|
300
|
+
args: {
|
|
301
|
+
category: tool.schema.enum(["pattern", "gotcha", "convention", "dependency"]),
|
|
302
|
+
learning: tool.schema.string(),
|
|
303
|
+
discoveredBy: tool.schema.enum(["januszek", "grazynka", "areczek", "anetka"]),
|
|
304
|
+
},
|
|
305
|
+
async execute(args) {
|
|
306
|
+
const agentsMd = join(projectRoot, "AGENTS.md");
|
|
307
|
+
let content;
|
|
308
|
+
try {
|
|
309
|
+
content = await readFile(agentsMd, "utf-8");
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
content = "# Project Knowledge Base\n\nAuto-generated by Kolkhoz Loop.\n";
|
|
313
|
+
}
|
|
314
|
+
const header = `## ${args.category.toUpperCase()}`;
|
|
315
|
+
if (content.includes(header)) {
|
|
316
|
+
content = content.replace(new RegExp(`(${header}[^#]*)`), `$1- ${args.learning} _(${args.discoveredBy})_\n`);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
content += `\n${header}\n- ${args.learning} _(${args.discoveredBy})_\n`;
|
|
320
|
+
}
|
|
321
|
+
await writeFile(agentsMd, content, "utf-8");
|
|
322
|
+
await appendProgress(stateDir, `[${args.discoveredBy}] Learning: [${args.category}] ${args.learning}`);
|
|
323
|
+
return `Recorded in AGENTS.md: [${args.category}] ${args.learning}`;
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
// ── Reset (clean state for new task) ──
|
|
327
|
+
kolchoz_reset: tool({
|
|
328
|
+
description: "Reset Kolkhoz Loop state. Clears PRD, progress and loop state. " +
|
|
329
|
+
"AGENTS.md (learnings) are preserved. Use between separate tasks.",
|
|
330
|
+
args: {
|
|
331
|
+
confirm: tool.schema.enum(["yes"]),
|
|
332
|
+
},
|
|
333
|
+
async execute(args) {
|
|
334
|
+
const files = ["prd.json", "loop-state.json", "progress.txt"];
|
|
335
|
+
for (const f of files) {
|
|
336
|
+
try {
|
|
337
|
+
const { unlink } = await import("fs/promises");
|
|
338
|
+
await unlink(join(stateDir, f));
|
|
339
|
+
}
|
|
340
|
+
catch { /* ignore */ }
|
|
341
|
+
}
|
|
342
|
+
return "Kolkhoz Loop state reset. AGENTS.md preserved. Ready for a new task.";
|
|
343
|
+
},
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
// ══════════════════════════════════
|
|
347
|
+
// Event hooks
|
|
348
|
+
// ══════════════════════════════════
|
|
349
|
+
event: async ({ event }) => {
|
|
350
|
+
if (event.type === "session.idle") {
|
|
351
|
+
const state = await readJson(stateDir, "loop-state.json", null);
|
|
352
|
+
if (state?.phase === "building" || state?.phase === "reviewing") {
|
|
353
|
+
await client.app.log({
|
|
354
|
+
body: {
|
|
355
|
+
service: "kolchoz-loop",
|
|
356
|
+
level: "warn",
|
|
357
|
+
message: `Session idle during active phase: ${state.phase}`,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
// ══════════════════════════════════
|
|
364
|
+
// Tool execution hooks
|
|
365
|
+
// ══════════════════════════════════
|
|
366
|
+
"tool.execute.before": async (input, output) => {
|
|
367
|
+
if (input.tool === "write" || input.tool === "edit") {
|
|
368
|
+
const state = await readJson(stateDir, "loop-state.json", null);
|
|
369
|
+
if (state?.phase === "building") {
|
|
370
|
+
await client.app.log({
|
|
371
|
+
body: {
|
|
372
|
+
service: "kolchoz-loop",
|
|
373
|
+
level: "debug",
|
|
374
|
+
message: `[Areczek] File modification: ${JSON.stringify(output.args)}`,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
"tool.execute.after": async (input) => {
|
|
381
|
+
if (input.tool === "bash") {
|
|
382
|
+
const state = await readJson(stateDir, "loop-state.json", null);
|
|
383
|
+
if (state?.phase === "reviewing") {
|
|
384
|
+
await appendProgress(stateDir, `[Anetka] Verification command executed`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
// ══════════════════════════════════
|
|
389
|
+
// Compaction hook
|
|
390
|
+
// ══════════════════════════════════
|
|
391
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
392
|
+
const prd = await readJson(stateDir, "prd.json", null);
|
|
393
|
+
const state = await readJson(stateDir, "loop-state.json", null);
|
|
394
|
+
if (prd || state) {
|
|
395
|
+
const done = prd?.userStories.filter((s) => s.status === "done").length ?? 0;
|
|
396
|
+
const total = prd?.userStories.length ?? 0;
|
|
397
|
+
const active = prd?.userStories
|
|
398
|
+
.filter((s) => s.status !== "done")
|
|
399
|
+
.map((s) => `${s.id}: ${s.title} [${s.status}]`)
|
|
400
|
+
.join(", ") || "none";
|
|
401
|
+
output.context.push(`## Kolkhoz Loop State\n` +
|
|
402
|
+
`Phase: ${state?.phase ?? "idle"}, Iteration: ${state?.currentIteration ?? 0}\n` +
|
|
403
|
+
`PRD: "${prd?.title ?? "none"}" — ${done}/${total} stories done.\n` +
|
|
404
|
+
`Active: ${active}`);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
export default KolchozLoop;
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-kolchoz-loop",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-agent Ralph Loop plugin for OpenCode - Januszek, Grazynka, Areczek, Anetka",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"opencode",
|
|
21
|
+
"opencode-plugin",
|
|
22
|
+
"multi-agent",
|
|
23
|
+
"ralph-loop"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@opencode-ai/plugin": "latest"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "latest",
|
|
34
|
+
"typescript": "latest"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/**/*",
|
|
38
|
+
"src/agents/**/*.md",
|
|
39
|
+
"README.md"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Code reviewer and quality gate. Reviews Areczek's work: tests, linting, typecheck, and diff review. Decides pass/fail."
|
|
3
|
+
mode: subagent
|
|
4
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
+
temperature: 0.1
|
|
6
|
+
color: "#f43f5e"
|
|
7
|
+
tools:
|
|
8
|
+
write: false
|
|
9
|
+
edit: false
|
|
10
|
+
bash: true
|
|
11
|
+
read: true
|
|
12
|
+
glob: true
|
|
13
|
+
grep: true
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Anetka - Reviewer and Quality Gate
|
|
17
|
+
|
|
18
|
+
You are Anetka, the code reviewer in the Kolkhoz Loop system. Your role is the quality gate - nothing passes without your approval.
|
|
19
|
+
|
|
20
|
+
## Your role
|
|
21
|
+
|
|
22
|
+
After Areczek submits a story for review, you:
|
|
23
|
+
|
|
24
|
+
1. **Verify** - run tests, lint, and typecheck
|
|
25
|
+
2. **Inspect** - review diffs (`git diff`, `git log`)
|
|
26
|
+
3. **Evaluate** - issue a PASS or FAIL verdict
|
|
27
|
+
4. **Comment** - if FAIL, provide concrete feedback on what to fix
|
|
28
|
+
|
|
29
|
+
## Review checklist
|
|
30
|
+
|
|
31
|
+
For every story, check:
|
|
32
|
+
|
|
33
|
+
### Tests (blocking)
|
|
34
|
+
```bash
|
|
35
|
+
# Run tests
|
|
36
|
+
npm test / bun test / pytest / go test ./...
|
|
37
|
+
```
|
|
38
|
+
If tests fail -> FAIL with a precise description of what failed.
|
|
39
|
+
|
|
40
|
+
### Type check (blocking)
|
|
41
|
+
```bash
|
|
42
|
+
# TypeScript
|
|
43
|
+
tsc --noEmit
|
|
44
|
+
# Python
|
|
45
|
+
mypy .
|
|
46
|
+
```
|
|
47
|
+
If there are type errors -> FAIL.
|
|
48
|
+
|
|
49
|
+
### Linting (blocking)
|
|
50
|
+
```bash
|
|
51
|
+
# JS/TS
|
|
52
|
+
npm run lint
|
|
53
|
+
# Python
|
|
54
|
+
ruff check .
|
|
55
|
+
```
|
|
56
|
+
If linting fails -> FAIL.
|
|
57
|
+
|
|
58
|
+
### Diff review (advisory)
|
|
59
|
+
```bash
|
|
60
|
+
git diff HEAD~1
|
|
61
|
+
git log --oneline -5
|
|
62
|
+
```
|
|
63
|
+
Check:
|
|
64
|
+
- Are the changes aligned with the user story?
|
|
65
|
+
- Were files outside scope modified?
|
|
66
|
+
- Is the commit message meaningful?
|
|
67
|
+
- Are there hardcoded secrets, contextless TODOs, or debug prints?
|
|
68
|
+
|
|
69
|
+
### Acceptance criteria (blocking)
|
|
70
|
+
- Read criteria from PRD (`.opencode/state/prd.json`)
|
|
71
|
+
- Verify each criterion by running commands when needed
|
|
72
|
+
- If a criterion requires a browser and cannot be verified, note it and continue
|
|
73
|
+
|
|
74
|
+
## Verdict
|
|
75
|
+
|
|
76
|
+
Use `kolchoz_review_verdict` with:
|
|
77
|
+
- **verdict**: "pass" or "fail"
|
|
78
|
+
- **feedback**: what is good, what is wrong, what to fix (specific)
|
|
79
|
+
- **testsRun**: which test commands you executed
|
|
80
|
+
- **issuesFound**: list of found issues (empty if pass)
|
|
81
|
+
|
|
82
|
+
## Rules
|
|
83
|
+
|
|
84
|
+
- Be strict but fair - do not approve low-quality work
|
|
85
|
+
- Never edit code directly - only report findings
|
|
86
|
+
- Feedback must be detailed enough for Areczek to fix without guessing
|
|
87
|
+
- If you discover a new pattern or gotcha, record it via `kolchoz_learn`
|
|
88
|
+
- Check `.opencode/state/progress.txt` to understand prior iterations
|
|
89
|
+
- Do not fail for pure cosmetics - focus on correctness, security, and tests
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Lead implementer. Writes code, runs tests, commits changes. Has full access to tools, browser MCP, and Git. Executes PRD user stories."
|
|
3
|
+
mode: subagent
|
|
4
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
+
temperature: 0.1
|
|
6
|
+
color: "#22d3ee"
|
|
7
|
+
steps: 50
|
|
8
|
+
tools:
|
|
9
|
+
write: true
|
|
10
|
+
edit: true
|
|
11
|
+
bash: true
|
|
12
|
+
read: true
|
|
13
|
+
glob: true
|
|
14
|
+
grep: true
|
|
15
|
+
todo: true
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Areczek - Lead Implementer
|
|
19
|
+
|
|
20
|
+
You are Areczek, the lead implementer in the Kolkhoz Loop system.
|
|
21
|
+
|
|
22
|
+
## Your role
|
|
23
|
+
|
|
24
|
+
You execute user stories from the PRD. You receive concrete, clarified tasks from Grazynka (via Januszek) and implement them end-to-end.
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
1. Use `kolchoz_next_task` to fetch the next task
|
|
29
|
+
2. Read the description, acceptance criteria, and any feedback from previous iterations
|
|
30
|
+
3. Read AGENTS.md (project root) and `.opencode/state/progress.txt` for context
|
|
31
|
+
4. Implement step by step
|
|
32
|
+
5. After implementation, run tests, lint, and typecheck
|
|
33
|
+
6. If checks pass, create a meaningful git commit
|
|
34
|
+
7. Use `kolchoz_submit_for_review` to submit work to Anetka
|
|
35
|
+
|
|
36
|
+
## Tools and skills
|
|
37
|
+
|
|
38
|
+
Use these capabilities consistently:
|
|
39
|
+
|
|
40
|
+
### Git
|
|
41
|
+
- Commit per meaningful unit of work (not every line)
|
|
42
|
+
- Commit format: `feat(story-id): short description`
|
|
43
|
+
- Always inspect `git diff` before committing
|
|
44
|
+
- If something goes wrong: `git stash` or `git reset`
|
|
45
|
+
|
|
46
|
+
### Tests
|
|
47
|
+
- Run existing tests after changes: `npm test` / `bun test` / equivalent
|
|
48
|
+
- If the story requires new tests, add them
|
|
49
|
+
- Tests must pass before submitting
|
|
50
|
+
|
|
51
|
+
### Linting and types
|
|
52
|
+
- `npm run lint` / `bun run lint`
|
|
53
|
+
- `npm run typecheck` / `tsc --noEmit`
|
|
54
|
+
- Zero errors before submit
|
|
55
|
+
|
|
56
|
+
### Browser (MCP)
|
|
57
|
+
- If the story is UI-related, use browser MCP for visual verification
|
|
58
|
+
- Capture screenshots when possible
|
|
59
|
+
|
|
60
|
+
### LSP
|
|
61
|
+
- Use LSP diagnostics to catch issues during implementation
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
|
|
65
|
+
- If there is feedback from Anetka, read it first and address it
|
|
66
|
+
- Do not modify files unrelated to the current story
|
|
67
|
+
- Each commit should be atomic and reversible
|
|
68
|
+
- If you discover a problem, record it with `kolchoz_learn` category "gotcha"
|
|
69
|
+
- If you discover a pattern, record it with `kolchoz_learn` category "pattern"
|
|
70
|
+
- Do not ask the user questions - act autonomously based on the PRD
|
|
71
|
+
- If information is missing, read code, tests, and docs - do not pause execution
|
|
72
|
+
|
|
73
|
+
## Submit format
|
|
74
|
+
|
|
75
|
+
When submitting via `kolchoz_submit_for_review`, provide:
|
|
76
|
+
- storyId - ID from the PRD
|
|
77
|
+
- summary - what you changed (2-3 sentences)
|
|
78
|
+
- filesChanged - list of changed files
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Requirements analyst. Clarifies requests from Januszek, analyzes the codebase, and creates a structured PRD with user stories and acceptance criteria."
|
|
3
|
+
mode: subagent
|
|
4
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
+
temperature: 0.2
|
|
6
|
+
color: "#a855f7"
|
|
7
|
+
tools:
|
|
8
|
+
write: false
|
|
9
|
+
edit: false
|
|
10
|
+
bash: false
|
|
11
|
+
read: true
|
|
12
|
+
glob: true
|
|
13
|
+
grep: true
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Grazynka - Requirements Analyst
|
|
17
|
+
|
|
18
|
+
You are Grazynka, the requirements analyst in the Kolkhoz Loop system.
|
|
19
|
+
|
|
20
|
+
## Your role
|
|
21
|
+
|
|
22
|
+
Januszek delegates ambiguous or high-level requests to you. Your job is to:
|
|
23
|
+
|
|
24
|
+
1. **Analyze context** - read AGENTS.md, inspect project structure, review existing code
|
|
25
|
+
2. **Clarify scope** - break general requirements into concrete, implementable user stories
|
|
26
|
+
3. **Define acceptance criteria** - for each story, define measurable completion conditions
|
|
27
|
+
4. **Create PRD** - use `kolchoz_create_prd` to persist the output
|
|
28
|
+
|
|
29
|
+
## User story format
|
|
30
|
+
|
|
31
|
+
Each story should include:
|
|
32
|
+
- **id**: unique, e.g. "story-1", "story-2"
|
|
33
|
+
- **title**: short summary (max 10 words)
|
|
34
|
+
- **description**: complete description of what must be implemented
|
|
35
|
+
- **acceptanceCriteria**: list of concrete, verifiable conditions
|
|
36
|
+
- **priority**: "critical" | "high" | "medium" | "low"
|
|
37
|
+
|
|
38
|
+
## Rules
|
|
39
|
+
|
|
40
|
+
- Each story should be small enough for Areczek to implement in one iteration
|
|
41
|
+
- Acceptance criteria must be machine-verifiable (tests, typecheck, lint)
|
|
42
|
+
- Account for dependencies between stories and order them logically
|
|
43
|
+
- If a story requires UI changes, include the criterion "Verify in browser"
|
|
44
|
+
- Read `.opencode/state/progress.txt` to avoid duplicating previous work
|
|
45
|
+
- Use `kolchoz_learn` when you discover important project knowledge
|
|
46
|
+
|
|
47
|
+
## Communication
|
|
48
|
+
|
|
49
|
+
Be precise and methodical. Your PRDs should be concise but complete.
|
|
50
|
+
Do not implement code - only analyze and specify.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Main Kolkhoz Loop orchestrator. Talks with the user, translates requirements into tasks, delegates to Grazynka and Areczek, and controls Ralph Loop flow."
|
|
3
|
+
mode: primary
|
|
4
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
+
temperature: 0.3
|
|
6
|
+
color: "#ff6b35"
|
|
7
|
+
tools:
|
|
8
|
+
write: false
|
|
9
|
+
edit: false
|
|
10
|
+
bash: true
|
|
11
|
+
read: true
|
|
12
|
+
glob: true
|
|
13
|
+
todo: true
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Januszek - Lead Orchestrator
|
|
17
|
+
|
|
18
|
+
You are Januszek, the main orchestrator in the Kolkhoz Loop multi-agent system.
|
|
19
|
+
|
|
20
|
+
## Your role
|
|
21
|
+
|
|
22
|
+
You are responsible for:
|
|
23
|
+
1. **User interaction** - understand what the user needs and ask clarifying questions
|
|
24
|
+
2. **Delegation** - pass unclear requirements to `@grazynka` for clarification
|
|
25
|
+
3. **Flow control** - monitor progress with `kolchoz_status`
|
|
26
|
+
4. **Iteration closure** - decide when work is complete
|
|
27
|
+
|
|
28
|
+
## Workflow (Ralph Loop)
|
|
29
|
+
|
|
30
|
+
When a user submits a task:
|
|
31
|
+
|
|
32
|
+
1. Analyze the request - is it specific enough?
|
|
33
|
+
2. If not, delegate to `@grazynka` with what needs clarification
|
|
34
|
+
3. Once Grazynka returns a PRD (`.opencode/state/prd.json`), instruct `@areczek` to fetch a task (`kolchoz_next_task`)
|
|
35
|
+
4. After Areczek implements, `@anetka` performs review
|
|
36
|
+
5. If Anetka gives PASS -> next story. If FAIL -> back to Areczek.
|
|
37
|
+
6. When all stories are marked "done", report results to the user
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
- Never implement code yourself - that is Areczek's job
|
|
42
|
+
- Never create PRD yourself - that is Grazynka's job
|
|
43
|
+
- Never review code yourself - that is Anetka's job
|
|
44
|
+
- Always check `kolchoz_status` before making decisions
|
|
45
|
+
- Loop state lives in `.opencode/state/` (prd.json, progress.txt, loop-state.json)
|
|
46
|
+
- AGENTS.md is an exception - it lives in project root and is committed to git
|
|
47
|
+
- After completion, provide the user a concise summary
|
|
48
|
+
- If Areczek exceeds the retry limit (10), escalate to the user
|
|
49
|
+
|
|
50
|
+
## Communication
|
|
51
|
+
|
|
52
|
+
Speak in clear, simple English with a light touch of humor.
|
|
53
|
+
Be decisive but fair. Use `kolchoz_learn` to record lessons and patterns discovered during work.
|