opengate 0.2.2 → 0.2.4

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/dist/index.js CHANGED
@@ -4,18 +4,29 @@ function resolveConfig(raw) {
4
4
  const apiKey = typeof raw.apiKey === "string" ? raw.apiKey : "";
5
5
  if (!url) throw new Error("[opengate] plugin config missing: url");
6
6
  if (!apiKey) throw new Error("[opengate] plugin config missing: apiKey");
7
+ const home = process.env.HOME ?? "/home/unknown";
7
8
  return {
8
9
  url,
9
10
  apiKey,
10
11
  agentId: typeof raw.agentId === "string" ? raw.agentId : "main",
11
12
  model: typeof raw.model === "string" ? raw.model : void 0,
12
13
  pollIntervalMs: typeof raw.pollIntervalMs === "number" ? raw.pollIntervalMs : 3e4,
13
- maxConcurrent: typeof raw.maxConcurrent === "number" ? raw.maxConcurrent : 3
14
+ maxConcurrent: typeof raw.maxConcurrent === "number" ? raw.maxConcurrent : 3,
15
+ projectsDir: typeof raw.projectsDir === "string" ? raw.projectsDir : `${home}/Projects`
14
16
  };
15
17
  }
16
18
 
17
19
  // src/bootstrap.ts
18
- function buildBootstrapPrompt(task, openGateUrl, apiKey) {
20
+ function repoNameFromUrl(repoUrl) {
21
+ try {
22
+ const url = new URL(repoUrl);
23
+ const segments = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean);
24
+ return segments.length > 0 ? segments[segments.length - 1] : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function buildBootstrapPrompt(task, openGateUrl, apiKey, project, projectsDir) {
19
30
  const tags = Array.isArray(task.tags) && task.tags.length > 0 ? task.tags.join(", ") : "none";
20
31
  const contextBlock = task.context && Object.keys(task.context).length > 0 ? `
21
32
  ## Task Context
@@ -23,42 +34,107 @@ function buildBootstrapPrompt(task, openGateUrl, apiKey) {
23
34
  ${JSON.stringify(task.context, null, 2)}
24
35
  \`\`\`
25
36
  ` : "";
37
+ const projectId = task.project_id ?? "unknown";
38
+ const defaultBranch = project?.default_branch ?? "main";
39
+ let workspacePath = null;
40
+ if (project?.repo_url) {
41
+ const repoName = repoNameFromUrl(project.repo_url);
42
+ if (repoName && projectsDir) {
43
+ workspacePath = `${projectsDir}/${repoName}`;
44
+ }
45
+ }
46
+ const workspaceBlock = workspacePath ? `### Phase 4: Workspace Setup
47
+ 6. **Set up your workspace:**
48
+ - \`cd ${workspacePath}\`
49
+ - Pull latest: \`git fetch origin && git checkout ${defaultBranch} && git pull origin ${defaultBranch}\`
50
+ - Create a feature branch: \`git checkout -b <branch-name>\`
51
+ - Branch naming: use the task title slugified, e.g. \`feat/add-user-auth\` or \`fix/null-pointer-in-parser\`
52
+ - If \`${workspacePath}\` does not exist, clone it: \`git clone ${project.repo_url} ${workspacePath}\`` : `### Phase 4: Workspace Setup
53
+ 6. **Set up your workspace:**
54
+ - Fetch project info: \`GET /api/projects/${projectId}\` \u2014 read \`repo_url\` and \`default_branch\`
55
+ - Derive the local path from the repo name (under ~/Projects/)
56
+ - If the directory doesn't exist, clone it
57
+ - Create a feature branch from the default branch`;
26
58
  return `You are an autonomous coding agent assigned a task via OpenGate.
27
59
 
28
- ## Your Task
60
+ ## Your Task (Summary)
29
61
  **ID:** ${task.id}
30
62
  **Title:** ${task.title}
31
63
  **Priority:** ${task.priority ?? "medium"}
32
64
  **Tags:** ${tags}
33
- **Project ID:** ${task.project_id ?? "unknown"}
65
+ **Project ID:** ${projectId}
34
66
 
35
67
  **Description:**
36
68
  ${task.description ?? "(no description provided)"}
37
69
  ${contextBlock}
38
70
  ## OpenGate API
39
71
  - **Base URL:** ${openGateUrl}
40
- - **Auth:** Bearer ${apiKey}
72
+ - **Auth header:** Authorization: Bearer ${apiKey}
73
+
74
+ All API calls below use this base URL and auth header.
41
75
 
42
76
  ## Protocol \u2014 Follow This Exactly
43
- You MUST follow these steps in order. Skipping any step is not acceptable.
44
77
 
45
- 1. **Claim** \u2014 \`POST /api/tasks/${task.id}/claim\`
46
- 2. **Post starting comment** \u2014 \`POST /api/tasks/${task.id}/activity\` with body: \`{"content": "Starting: <your plan in 1-2 sentences>"}\`
47
- 3. **Do the work** \u2014 read relevant files, write code, run tests, commit to a branch
48
- 4. **Post results comment** \u2014 \`POST /api/tasks/${task.id}/activity\` with a summary of what changed (files modified, commit hash, test results)
49
- 5. **Complete** \u2014 \`POST /api/tasks/${task.id}/complete\` with body: \`{"summary": "<what was done>", "output": {"branch": "...", "commits": [...]}}\`
78
+ ### Phase 1: Claim
79
+ 1. **Claim the task** \u2014 \`POST /api/tasks/${task.id}/claim\`
80
+
81
+ ### Phase 2: Gather Context
82
+ Before writing any code, gather all available context:
83
+
84
+ 2. **Fetch full task details** \u2014 \`GET /api/tasks/${task.id}\`
85
+ - Read the \`activities\` array for comments, instructions, and prior discussion
86
+ - Read the \`artifacts\` array for any attached files or links
87
+ - Read the \`dependencies\` array \u2014 if any dependency has status != "done", note it as a potential blocker
88
+ - Pay close attention to reviewer comments or change requests in activities
89
+
90
+ 3. **Search project knowledge base** \u2014 \`GET /api/projects/${projectId}/knowledge/search?q=${task.title}\`
91
+ - Also search by tags if present: \`GET /api/projects/${projectId}/knowledge/search?tags=${tags}\`
92
+ - Read any returned entries \u2014 they contain architecture decisions, patterns, gotchas, and conventions for this project
93
+ - Follow these conventions in your implementation
94
+
95
+ ### Phase 3: Plan & Announce
96
+ 4. **Post starting comment** \u2014 \`POST /api/tasks/${task.id}/activity\`
97
+ Body: \`{"content": "Starting work. Plan: <your plan informed by the context you gathered, 2-4 sentences>"}\`
98
+ - Your plan should reflect what you learned from the knowledge base, existing comments, and dependencies
99
+
100
+ ${workspaceBlock}
101
+
102
+ ### Phase 5: Do the Work
103
+ 7. **Implement the solution:**
104
+ - Follow patterns and conventions from the knowledge base
105
+ - Write clean, tested code
106
+ - Run the project's test suite and fix any failures
107
+ - Commit your changes with a descriptive message referencing the task
50
108
 
109
+ ### Phase 6: Report & Complete
110
+ 8. **Post results comment** \u2014 \`POST /api/tasks/${task.id}/activity\`
111
+ Body: \`{"content": "<summary of what changed: files modified, approach taken, test results, commit hash>"}\`
112
+
113
+ 9. **Write knowledge** (if you discovered something worth sharing) \u2014 \`PUT /api/projects/${projectId}/knowledge/<key>\`
114
+ Body: \`{"title": "...", "content": "...", "tags": [...], "category": "<architecture|pattern|gotcha|decision|reference>"}\`
115
+ - Write entries for: architectural decisions you made, gotchas you encountered, patterns you established
116
+
117
+ 10. **Complete the task** \u2014 \`POST /api/tasks/${task.id}/complete\`
118
+ Body: \`{"summary": "<what was done>", "output": {"branch": "<branch-name>", "commits": ["<hash>"]}}\`
119
+
120
+ ## Handling Blockers
51
121
  If you encounter a blocker that requires human input:
52
- - Post a question: \`POST /api/tasks/${task.id}/activity\` with \`{"content": "BLOCKED: <question>"}\`
122
+ - Post a question: \`POST /api/tasks/${task.id}/activity\` with \`{"content": "BLOCKED: <describe the issue and what you need>"}\`
53
123
  - Block the task: \`POST /api/tasks/${task.id}/block\` with \`{"reason": "<reason>"}\`
54
124
  - Then stop \u2014 do NOT mark it complete.
55
125
 
56
- ## Notes
126
+ If a dependency is not yet done:
127
+ - Post a comment noting which dependency is blocking you
128
+ - Block the task with the dependency info
129
+ - Stop and let the orchestrator handle sequencing
130
+
131
+ ## Rules
57
132
  - Always work on a feature branch, never commit directly to main
58
- - Run tests before completing
59
- - If you discover something worth remembering (pattern, gotcha, decision), write it to a file in your workspace
133
+ - Run tests before completing \u2014 do not complete with failing tests
134
+ - Respect existing patterns found in the knowledge base
135
+ - Keep commits atomic and descriptive
60
136
 
61
- Now begin. Start by claiming the task.`;
137
+ Now begin. Start with Phase 1: claim the task.`;
62
138
  }
63
139
 
64
140
  // src/spawner.ts
@@ -165,6 +241,19 @@ var TaskState = class {
165
241
  };
166
242
 
167
243
  // src/poller.ts
244
+ async function fetchProject(url, apiKey, projectId) {
245
+ try {
246
+ const resp = await fetch(`${url}/api/projects/${projectId}`, {
247
+ headers: { Authorization: `Bearer ${apiKey}` },
248
+ signal: AbortSignal.timeout(1e4)
249
+ });
250
+ if (!resp.ok) return null;
251
+ const body = await resp.json();
252
+ return body.project ?? body;
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
168
257
  async function fetchInbox(url, apiKey) {
169
258
  const resp = await fetch(`${url}/api/agents/me/inbox`, {
170
259
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -186,6 +275,7 @@ var OpenGatePoller = class {
186
275
  timer = null;
187
276
  state;
188
277
  running = false;
278
+ projectCache = /* @__PURE__ */ new Map();
189
279
  start() {
190
280
  if (this.running) return;
191
281
  this.running = true;
@@ -239,9 +329,26 @@ var OpenGatePoller = class {
239
329
  await this.spawnTask(task);
240
330
  }
241
331
  }
332
+ async resolveProject(projectId) {
333
+ const cached = this.projectCache.get(projectId);
334
+ if (cached) return cached;
335
+ const project = await fetchProject(this.pluginCfg.url, this.pluginCfg.apiKey, projectId);
336
+ if (project) this.projectCache.set(projectId, project);
337
+ return project;
338
+ }
242
339
  async spawnTask(task) {
243
340
  this.logger.info(`[opengate] Spawning session for task: "${task.title}" (${task.id})`);
244
- const prompt = buildBootstrapPrompt(task, this.pluginCfg.url, this.pluginCfg.apiKey);
341
+ let project = null;
342
+ if (task.project_id) {
343
+ project = await this.resolveProject(task.project_id);
344
+ }
345
+ const prompt = buildBootstrapPrompt(
346
+ task,
347
+ this.pluginCfg.url,
348
+ this.pluginCfg.apiKey,
349
+ project,
350
+ this.pluginCfg.projectsDir
351
+ );
245
352
  const result = await spawnTaskSession(
246
353
  task.id,
247
354
  prompt,
@@ -2,7 +2,7 @@
2
2
  "id": "opengate",
3
3
  "name": "OpenGate",
4
4
  "description": "Polls OpenGate for assigned tasks and spawns isolated OpenClaw sessions to execute them. Turns OpenGate into the orchestrator.",
5
- "version": "0.2.2",
5
+ "version": "0.2.4",
6
6
  "skills": ["./skills/opengate"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -32,6 +32,10 @@
32
32
  "maxConcurrent": {
33
33
  "type": "number",
34
34
  "description": "Max concurrent task sessions (default: 3)"
35
+ },
36
+ "projectsDir": {
37
+ "type": "string",
38
+ "description": "Base directory where project repos live on disk (default: ~/Projects)"
35
39
  }
36
40
  }
37
41
  },
@@ -41,6 +45,7 @@
41
45
  "agentId": { "label": "OpenClaw Agent ID", "placeholder": "main" },
42
46
  "model": { "label": "Model Override", "placeholder": "anthropic/claude-sonnet-4-6" },
43
47
  "pollIntervalMs": { "label": "Poll Interval (ms)", "advanced": true },
44
- "maxConcurrent": { "label": "Max Concurrent Tasks", "advanced": true }
48
+ "maxConcurrent": { "label": "Max Concurrent Tasks", "advanced": true },
49
+ "projectsDir": { "label": "Projects Directory", "placeholder": "~/Projects", "advanced": true }
45
50
  }
46
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opengate",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "OpenGate task executor plugin for OpenClaw — polls assigned tasks and spawns isolated agent sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",