@forloop-cc/forloop-opencode-plugin-planner 2.2.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/CHANGELOG.md +225 -0
- package/CODE_OF_CONDUCT.md +77 -0
- package/CONTRIBUTING.md +176 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/SECURITY.md +69 -0
- package/docs/CONFIGURATION.md +283 -0
- package/docs/DEVELOPER-VS-USER.md +129 -0
- package/docs/IMPROVED-AUTHENTICATION.md +317 -0
- package/docs/INSTALLATION-SCRIPTS.md +304 -0
- package/docs/PLANNER-AGENT-IMPROVEMENTS.md +88 -0
- package/docs/PLUGIN-ARCHITECTURE.md +308 -0
- package/docs/QUICKSTART-NON-DEV.md +94 -0
- package/docs/README-FOR-HUMANS-AND-AI.md +109 -0
- package/docs/README.md +70 -0
- package/docs/SKILLS-GAP-ANALYSIS.md +157 -0
- package/docs/USER-INTERACTION-MODEL.md +106 -0
- package/docs/file-management/2026-04-12-forloop-aivy-documents-sync.md +395 -0
- package/docs/file-management/FILE_UPLOAD_ENHANCEMENT.md +431 -0
- package/docs/file-management/FILE_UPLOAD_FLOW.md +300 -0
- package/docs/images/forloop-opencode-flow.png +0 -0
- package/docs/images/forloop-opencode-header.png +0 -0
- package/docs/superpowers/plans/2026-05-03-sprint-isolated-directory-structure.md +1450 -0
- package/docs/superpowers/plans/2026-05-29-install-system.md +850 -0
- package/docs/superpowers/specs/2026-05-03-sprint-isolated-directory-structure-design.md +149 -0
- package/docs/superpowers/specs/2026-05-19-opencode-message-recording-design.md +468 -0
- package/docs/superpowers/specs/2026-05-29-install-system-design.md +135 -0
- package/install.sh +317 -0
- package/package.json +87 -0
- package/plugins/capabilities/api-client.ts +556 -0
- package/plugins/capabilities/auth.ts +120 -0
- package/plugins/capabilities/config.ts +195 -0
- package/plugins/capabilities/context-resolver.ts +106 -0
- package/plugins/capabilities/token-storage.ts +98 -0
- package/plugins/forloop-plugin.ts +186 -0
- package/plugins/hooks/message-recorder.ts +258 -0
- package/plugins/hooks/shell-env.ts +62 -0
- package/plugins/tools/agent-sprint-tools.ts +100 -0
- package/plugins/tools/agent-suggestion-tools.ts +275 -0
- package/plugins/tools/agent-tools.ts +114 -0
- package/plugins/tools/ai-trigger-tools.ts +79 -0
- package/plugins/tools/developer-status-tool.ts +97 -0
- package/plugins/tools/file-tools.ts +240 -0
- package/plugins/tools/folder-tools.ts +75 -0
- package/plugins/tools/organization-tools.ts +213 -0
- package/plugins/tools/schedule-tools.ts +173 -0
- package/plugins/tools/sprint-tools.ts +287 -0
- package/plugins/tools/story-tools.ts +320 -0
- package/plugins/tools/sync-tools.ts +509 -0
- package/plugins/tools/template-tools.ts +170 -0
- package/plugins/tools/user-tools.ts +146 -0
- package/scripts/migrate-tokens.js +90 -0
- package/scripts/use-prod.sh +11 -0
- package/tsconfig.json +17 -0
- package/update.sh +172 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin';
|
|
2
|
+
import { ForLoopAPIClient } from '../capabilities/api-client';
|
|
3
|
+
import { resolveSprintId } from '../capabilities/context-resolver';
|
|
4
|
+
import { validateToken } from '../capabilities/auth';
|
|
5
|
+
|
|
6
|
+
export function createStoryCreateTool(client: ForLoopAPIClient) {
|
|
7
|
+
return tool({
|
|
8
|
+
description: 'Create a story of type "doc_folder" only. For all other story types, use forloopStoryTemplate with templateSlug instead to ensure the story is linked to a template.',
|
|
9
|
+
args: {
|
|
10
|
+
title: tool.schema.string()
|
|
11
|
+
.describe('Story title'),
|
|
12
|
+
description: tool.schema.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Story description'),
|
|
15
|
+
sprintId: tool.schema.number()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Target sprint ID'),
|
|
18
|
+
type: tool.schema.enum(['doc_folder'])
|
|
19
|
+
.default('doc_folder'),
|
|
20
|
+
priority: tool.schema.enum(['low', 'medium', 'high', 'critical'])
|
|
21
|
+
.default('medium'),
|
|
22
|
+
points: tool.schema.number()
|
|
23
|
+
.min(0)
|
|
24
|
+
.max(10)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Story points (0-10)'),
|
|
27
|
+
status: tool.schema.enum(['todo', 'in_progress', 'done', 'blocked'])
|
|
28
|
+
.optional()
|
|
29
|
+
.default('todo')
|
|
30
|
+
.describe('Initial story status'),
|
|
31
|
+
assigneeId: tool.schema.number()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('User ID to assign story to'),
|
|
34
|
+
assigneeType: tool.schema.enum(['user', 'agent'])
|
|
35
|
+
.optional()
|
|
36
|
+
.default('user')
|
|
37
|
+
.describe('Assignee type'),
|
|
38
|
+
assigneeAgentKey: tool.schema.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('AI agent key (required when assigneeType is agent)'),
|
|
41
|
+
x: tool.schema.number()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Canvas X position (auto-generated if not provided)'),
|
|
44
|
+
y: tool.schema.number()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Canvas Y position (auto-generated if not provided)'),
|
|
47
|
+
zIndex: tool.schema.number()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Canvas layering order (auto-generated if not provided)'),
|
|
50
|
+
templateId: tool.schema.number()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Template ID to use for story creation'),
|
|
53
|
+
templateSlug: tool.schema.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Template slug to link story to a template. Required for task/note stories. Use forloopStoryTemplate instead for standard story creation.'),
|
|
56
|
+
metadata: tool.schema.any()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Additional metadata (JSON object)'),
|
|
59
|
+
},
|
|
60
|
+
async execute(args, context) {
|
|
61
|
+
const tokenResult = await validateToken();
|
|
62
|
+
if (!tokenResult.valid) {
|
|
63
|
+
return tokenResult.error;
|
|
64
|
+
}
|
|
65
|
+
client.setToken?.(tokenResult.token);
|
|
66
|
+
|
|
67
|
+
const resolution = await resolveSprintId(args.sprintId, context.directory);
|
|
68
|
+
|
|
69
|
+
if (!resolution.sprintId) {
|
|
70
|
+
return [
|
|
71
|
+
'❌ No sprint ID provided.',
|
|
72
|
+
'',
|
|
73
|
+
'Please specify a sprint ID using one of these methods:',
|
|
74
|
+
' 1. Pass sprintId argument',
|
|
75
|
+
' 2. Set FORLOOP_SPRINT_ID environment variable',
|
|
76
|
+
' 3. Use a git branch named sprint-XXX',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const storyData: any = {
|
|
82
|
+
title: args.title,
|
|
83
|
+
description: args.description,
|
|
84
|
+
sprintId: resolution.sprintId,
|
|
85
|
+
type: args.type,
|
|
86
|
+
priority: args.priority,
|
|
87
|
+
points: args.points,
|
|
88
|
+
status: args.status,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Add assignee fields
|
|
92
|
+
if (args.assigneeId !== undefined) {
|
|
93
|
+
storyData.assigneeId = args.assigneeId;
|
|
94
|
+
}
|
|
95
|
+
if (args.assigneeType) {
|
|
96
|
+
storyData.assigneeType = args.assigneeType;
|
|
97
|
+
if (args.assigneeType === 'agent' && args.assigneeAgentKey) {
|
|
98
|
+
storyData.assigneeAgentKey = args.assigneeAgentKey;
|
|
99
|
+
}
|
|
100
|
+
} else if (args.assigneeAgentKey) {
|
|
101
|
+
storyData.assigneeType = 'agent';
|
|
102
|
+
storyData.assigneeAgentKey = args.assigneeAgentKey;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add optional canvas position fields
|
|
106
|
+
if (args.x !== undefined) storyData.x = args.x;
|
|
107
|
+
if (args.y !== undefined) storyData.y = args.y;
|
|
108
|
+
if (args.zIndex !== undefined) storyData.zIndex = args.zIndex;
|
|
109
|
+
// Handle templateSlug lookup
|
|
110
|
+
let templateId = args.templateId;
|
|
111
|
+
if (args.templateSlug) {
|
|
112
|
+
const templates = await client.listTemplates();
|
|
113
|
+
const template = templates.find(t => t.slug === args.templateSlug && !t.deletedAt);
|
|
114
|
+
if (!template) {
|
|
115
|
+
return `❌ Template "${args.templateSlug}" not found. Use forloopTemplateList to see available templates.`;
|
|
116
|
+
}
|
|
117
|
+
templateId = template.id;
|
|
118
|
+
|
|
119
|
+
// Auto-populate metadata from template fields if not provided
|
|
120
|
+
if (!args.metadata) {
|
|
121
|
+
storyData.metadata = JSON.stringify({
|
|
122
|
+
taskTitle: args.title,
|
|
123
|
+
description: args.description,
|
|
124
|
+
status: "todo",
|
|
125
|
+
priority: args.priority,
|
|
126
|
+
points: args.points,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (templateId !== undefined) storyData.templateId = templateId;
|
|
131
|
+
if (args.metadata !== undefined) storyData.metadata = args.metadata;
|
|
132
|
+
|
|
133
|
+
const story = await client.createStory(storyData);
|
|
134
|
+
|
|
135
|
+
const lines = [
|
|
136
|
+
'✅ Story created successfully!',
|
|
137
|
+
'',
|
|
138
|
+
`**#${story.id}**: ${story.title}`,
|
|
139
|
+
`**Sprint**: #${resolution.sprintId}`,
|
|
140
|
+
`**Type**: ${story.type}`,
|
|
141
|
+
`**Priority**: ${story.priority}`,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
if (story.points) {
|
|
145
|
+
lines.push(`**Points**: ${story.points}`);
|
|
146
|
+
}
|
|
147
|
+
if (story.status) {
|
|
148
|
+
lines.push(`**Status**: ${story.status}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
} catch (error: any) {
|
|
153
|
+
return `❌ Error: ${error.message}`;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function createStoryUpdateTool(client: ForLoopAPIClient) {
|
|
160
|
+
return tool({
|
|
161
|
+
description: 'Update an existing story',
|
|
162
|
+
args: {
|
|
163
|
+
storyId: tool.schema.number()
|
|
164
|
+
.describe('Story ID to update'),
|
|
165
|
+
title: tool.schema.string()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe('New title'),
|
|
168
|
+
description: tool.schema.string()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe('New description'),
|
|
171
|
+
status: tool.schema.enum(['todo', 'in_progress', 'done', 'blocked'])
|
|
172
|
+
.optional()
|
|
173
|
+
.describe('New status'),
|
|
174
|
+
priority: tool.schema.enum(['low', 'medium', 'high', 'critical'])
|
|
175
|
+
.optional()
|
|
176
|
+
.describe('New priority'),
|
|
177
|
+
points: tool.schema.number()
|
|
178
|
+
.min(0)
|
|
179
|
+
.max(10)
|
|
180
|
+
.optional()
|
|
181
|
+
.describe('New story points'),
|
|
182
|
+
},
|
|
183
|
+
async execute(args, _context) {
|
|
184
|
+
const tokenResult = await validateToken();
|
|
185
|
+
if (!tokenResult.valid) {
|
|
186
|
+
return tokenResult.error;
|
|
187
|
+
}
|
|
188
|
+
client.setToken?.(tokenResult.token);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const updateData: any = {};
|
|
192
|
+
if (args.title !== undefined) updateData.title = args.title;
|
|
193
|
+
if (args.description !== undefined) updateData.description = args.description;
|
|
194
|
+
if (args.status !== undefined) updateData.status = args.status;
|
|
195
|
+
if (args.priority !== undefined) updateData.priority = args.priority;
|
|
196
|
+
if (args.points !== undefined) updateData.points = args.points;
|
|
197
|
+
|
|
198
|
+
const story = await client.updateStory(args.storyId, updateData);
|
|
199
|
+
|
|
200
|
+
return `✅ Story #${story.id} updated successfully!`;
|
|
201
|
+
} catch (error: any) {
|
|
202
|
+
return `❌ Error: ${error.message}`;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createStoryGetTool(client: ForLoopAPIClient) {
|
|
209
|
+
return tool({
|
|
210
|
+
description: 'Get details of a specific story including sprint info, assignee, and comments. Use to understand what developer agents have implemented in a story (commit SHAs, files changed, test results).',
|
|
211
|
+
args: {
|
|
212
|
+
storyId: tool.schema.number()
|
|
213
|
+
.describe('Story ID to retrieve'),
|
|
214
|
+
includeComments: tool.schema.boolean()
|
|
215
|
+
.default(true)
|
|
216
|
+
.describe('Include story comments (shows what developer agents have done)'),
|
|
217
|
+
},
|
|
218
|
+
async execute(args, _context) {
|
|
219
|
+
const tokenResult = await validateToken();
|
|
220
|
+
if (!tokenResult.valid) {
|
|
221
|
+
return tokenResult.error;
|
|
222
|
+
}
|
|
223
|
+
client.setToken?.(tokenResult.token);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const story = await client.getStory(args.storyId, {
|
|
227
|
+
includeComments: args.includeComments !== false,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const lines = [
|
|
231
|
+
`📝 Story #${story.id}: ${story.title}`,
|
|
232
|
+
'',
|
|
233
|
+
`**Sprint**: #${story.sprint?.id || 'N/A'} ${story.sprint?.title || ''}`,
|
|
234
|
+
`**Status**: ${story.status}`,
|
|
235
|
+
`**Priority**: ${story.priority}`,
|
|
236
|
+
`**Type**: ${story.type}`,
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
if (story.points) {
|
|
240
|
+
lines.push(`**Points**: ${story.points}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (story.assignee) {
|
|
244
|
+
lines.push(`**Assignee**: ${story.assignee.name || story.assignee.email || 'Unknown'}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (story.assigneeAgentKey) {
|
|
248
|
+
lines.push(`**Agent**: ${story.assigneeAgentKey}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (story.description) {
|
|
252
|
+
lines.push('');
|
|
253
|
+
lines.push('**Description**:');
|
|
254
|
+
lines.push(story.description);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (args.includeComments !== false && story.comments?.length) {
|
|
258
|
+
lines.push('');
|
|
259
|
+
lines.push('---');
|
|
260
|
+
lines.push(`**Comments** (${story.comments.length}):`);
|
|
261
|
+
for (const comment of story.comments) {
|
|
262
|
+
const author = comment.authorAgentKey || comment.user?.name || 'Unknown';
|
|
263
|
+
const time = comment.createdAt ? new Date(comment.createdAt).toLocaleString() : '';
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push(`💬 **${author}** ${time ? `(${time})` : ''}`);
|
|
266
|
+
if (comment.status) {
|
|
267
|
+
lines.push(` Status: ${comment.status}`);
|
|
268
|
+
}
|
|
269
|
+
const commentText = comment.content || comment.body || '';
|
|
270
|
+
if (commentText) {
|
|
271
|
+
lines.push(` ${commentText}`);
|
|
272
|
+
}
|
|
273
|
+
if (comment.artifacts?.length) {
|
|
274
|
+
lines.push(` 📎 Artifacts: ${comment.artifacts.join(', ')}`);
|
|
275
|
+
}
|
|
276
|
+
if (comment.assumptions?.length) {
|
|
277
|
+
lines.push(` 💡 Assumptions: ${comment.assumptions.join(', ')}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
} catch (error: any) {
|
|
284
|
+
return `❌ Error: ${error.message}`;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createStoryDeleteTool(client: ForLoopAPIClient) {
|
|
291
|
+
return tool({
|
|
292
|
+
description: 'Delete a story',
|
|
293
|
+
args: {
|
|
294
|
+
storyId: tool.schema.number()
|
|
295
|
+
.describe('Story ID to delete'),
|
|
296
|
+
confirm: tool.schema.boolean()
|
|
297
|
+
.optional()
|
|
298
|
+
.default(false)
|
|
299
|
+
.describe('Set to true to confirm deletion'),
|
|
300
|
+
},
|
|
301
|
+
async execute(args, _context) {
|
|
302
|
+
if (!args.confirm) {
|
|
303
|
+
return '⚠️ Please confirm deletion by setting confirm: true';
|
|
304
|
+
}
|
|
305
|
+
const tokenResult = await validateToken();
|
|
306
|
+
if (!tokenResult.valid) {
|
|
307
|
+
return tokenResult.error;
|
|
308
|
+
}
|
|
309
|
+
client.setToken?.(tokenResult.token);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
await client.deleteStory(args.storyId);
|
|
313
|
+
|
|
314
|
+
return `✅ Story #${args.storyId} deleted successfully!`;
|
|
315
|
+
} catch (error: any) {
|
|
316
|
+
return `❌ Error: ${error.message}`;
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|