@forloop-cc/forloop-opencode-plugin-planner 2.2.6 → 2.2.8
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
CHANGED
|
@@ -25,6 +25,8 @@ The **ForLoop Plugin** connects [opencode](https://opencode.ai) AI agents to you
|
|
|
25
25
|
|
|
26
26
|
[ForLoop](https://forloop.cc) is an **AI agent platform** for autonomous development and deployment. Think of it as your team's command center — a shared space where AI agents and humans collaborate on sprints, stories, and shipping code.
|
|
27
27
|
|
|
28
|
+
**All built on Loop Engineering** — a continuous feedback loop of Plan → Code → Review → Deploy → Learn. Every cycle generates data that feeds into the next, making your team faster with every iteration. This plugin powers the **Plan** phase, giving your agents direct access to sprints, stories, and task tracking.
|
|
29
|
+
|
|
28
30
|
**Key features:**
|
|
29
31
|
|
|
30
32
|
- **AI Agents as team members** — specialized agents plan, code, review, and deploy. They create real branches, commits, and pull requests.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@forloop-cc/forloop-opencode-plugin-planner",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.8",
|
|
5
5
|
"description": "ForLoop integration plugin for opencode — Sprint, story, and agent tools for the ForLoop platform",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -519,6 +519,19 @@ export class ForLoopAPIClient {
|
|
|
519
519
|
body: JSON.stringify({ sprintId, sessionId }),
|
|
520
520
|
});
|
|
521
521
|
}
|
|
522
|
+
|
|
523
|
+
async sendStreamChunk(data: {
|
|
524
|
+
taskId: string;
|
|
525
|
+
trackingId: string;
|
|
526
|
+
sprintId: number;
|
|
527
|
+
chunk: string;
|
|
528
|
+
index: number;
|
|
529
|
+
}): Promise<any> {
|
|
530
|
+
return this.request('/api/opencode/agents/stream', {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
body: JSON.stringify(data),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
522
535
|
}
|
|
523
536
|
|
|
524
537
|
export interface AgentSuggestionRequest {
|
|
@@ -67,6 +67,18 @@ import { createDeveloperStatusTool } from './tools/developer-status-tool';
|
|
|
67
67
|
import { injectEnvironment } from './hooks/shell-env';
|
|
68
68
|
import { createChatMessageHook, createEventHook } from './hooks/message-recorder';
|
|
69
69
|
|
|
70
|
+
// Named re-exports — see plugins/exports.ts for details
|
|
71
|
+
export { ForLoopAPIClient } from './capabilities/api-client';
|
|
72
|
+
export type { APIError, CreateStoryRequest, CreateSprintRequest, ListSprintsParams, SprintOptions } from './capabilities/api-client';
|
|
73
|
+
export { getConfig, isLambdaExecution, PROD_API_URL, DEV_API_URL } from './capabilities/config';
|
|
74
|
+
export type { ForLoopConfig } from './capabilities/config';
|
|
75
|
+
export { getToken, setToken, clearToken } from './capabilities/token-storage';
|
|
76
|
+
export type { TokenProfile } from './capabilities/token-storage';
|
|
77
|
+
export { validateToken, detectTokenType } from './capabilities/auth';
|
|
78
|
+
export type { TokenValidationResult } from './capabilities/auth';
|
|
79
|
+
export { resolveSprintId, getCurrentBranch, resolveSprintFromBranch, getForloopRoot, getManifestPath } from './capabilities/context-resolver';
|
|
80
|
+
export type { ContextResolution } from './capabilities/context-resolver';
|
|
81
|
+
|
|
70
82
|
export default async (ctx: any) => {
|
|
71
83
|
const config = getConfig();
|
|
72
84
|
const token = (await getToken()) || '';
|
|
@@ -49,8 +49,13 @@ async function recordAssistantMessage(
|
|
|
49
49
|
await new Promise((resolve) => setTimeout(resolve, 600 * i));
|
|
50
50
|
}
|
|
51
51
|
const result = await client.recordMessage(data);
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// "no matching user message" is expected in Lambda — the server's
|
|
53
|
+
// handleAICallback creates the full Conversation record separately.
|
|
54
|
+
if (result?.message === 'no matching user message to update') {
|
|
55
|
+
console.log(`[ForLoop] No matching user message (expected in Lambda) — callback handles persistence`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
54
59
|
} catch (err: any) {
|
|
55
60
|
console.warn(`[ForLoop] Attempt ${i + 1}/${attempts} failed:`, err.message);
|
|
56
61
|
if (i === attempts - 1) {
|
|
@@ -72,19 +77,26 @@ export function createChatMessageHook(client: ForLoopAPIClient) {
|
|
|
72
77
|
const sprintId = readActiveSprintId();
|
|
73
78
|
if (!sprintId) return;
|
|
74
79
|
|
|
80
|
+
const { sessionID, agent, model, messageID } = input;
|
|
81
|
+
const { message, parts } = output;
|
|
82
|
+
const content = extractTextFromParts(parts);
|
|
83
|
+
|
|
84
|
+
// Log incoming user message for debug visibility (Lambda)
|
|
85
|
+
if (content && messageID) {
|
|
86
|
+
console.log(`[ForLoop] REQUEST — user message (${content.length} chars)`, {
|
|
87
|
+
sprintId,
|
|
88
|
+
agent: agent || 'unknown',
|
|
89
|
+
sessionId: sessionID,
|
|
90
|
+
preview: content.substring(0, 300),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
75
93
|
// Only record on user's local machine; Lambda records via its own system
|
|
76
94
|
if (isLambdaExecution()) return;
|
|
77
95
|
|
|
78
|
-
const { sessionID, agent, model, messageID } = input;
|
|
79
|
-
|
|
80
96
|
// Only record messages with a model (indicates actual LLM processing,
|
|
81
97
|
// not system events like context compaction or summary generation)
|
|
82
98
|
if (!model || !model.providerID) return;
|
|
83
99
|
|
|
84
|
-
const { message, parts } = output;
|
|
85
|
-
const content = extractTextFromParts(parts);
|
|
86
|
-
if (!content) return;
|
|
87
|
-
|
|
88
100
|
const conversationId = buildConversationId(sprintId, agent || 'unknown', sessionID);
|
|
89
101
|
|
|
90
102
|
// Fire-and-forget: don't await - must not block the LLM from receiving the message
|
|
@@ -107,28 +119,23 @@ export function createChatMessageHook(client: ForLoopAPIClient) {
|
|
|
107
119
|
export function createEventHook(client: ForLoopAPIClient) {
|
|
108
120
|
const textBuffer = new Map<string, string>();
|
|
109
121
|
const readyToStream = new Set<string>(); // messageIDs that have passed the context phase
|
|
122
|
+
const directSentText = new Map<string, string>(); // trackingId → last full text sent via direct HTTP
|
|
110
123
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
lastUpdated: Date.now(),
|
|
127
|
-
};
|
|
128
|
-
fs.writeFileSync(filePath, JSON.stringify(data));
|
|
129
|
-
} catch (err: any) {
|
|
130
|
-
console.warn('[ForLoop] Failed to write stream chunk:', err.message);
|
|
131
|
-
}
|
|
124
|
+
const sendStreamDelta = (sprintId: number, trackingId: string, taskId: string, fullText: string) => {
|
|
125
|
+
const lastSent = directSentText.get(trackingId) || '';
|
|
126
|
+
if (!fullText || fullText.length <= lastSent.length) return;
|
|
127
|
+
const delta = fullText.startsWith(lastSent) ? fullText.substring(lastSent.length) : fullText;
|
|
128
|
+
if (!delta) return;
|
|
129
|
+
directSentText.set(trackingId, fullText);
|
|
130
|
+
client.sendStreamChunk({
|
|
131
|
+
taskId: taskId || '',
|
|
132
|
+
trackingId,
|
|
133
|
+
sprintId,
|
|
134
|
+
chunk: delta,
|
|
135
|
+
index: 0,
|
|
136
|
+
}).catch((err: Error) => {
|
|
137
|
+
console.warn('[ForLoop] Direct stream chunk failed:', err.message);
|
|
138
|
+
});
|
|
132
139
|
};
|
|
133
140
|
|
|
134
141
|
return async ({ event }: { event: { type: string; properties: any } }) => {
|
|
@@ -138,14 +145,15 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
138
145
|
const trackingId = process.env.FORLOOP_TRACKING_ID;
|
|
139
146
|
const taskId = process.env.FORLOOP_TASK_ID;
|
|
140
147
|
const isLambda = isLambdaExecution();
|
|
148
|
+
const debugStream = process.env.FORLOOP_DEBUG_STREAM === 'true';
|
|
141
149
|
|
|
142
|
-
// Lambda with streaming:
|
|
150
|
+
// Lambda with streaming: send chunks directly via HTTP
|
|
143
151
|
if (isLambda && trackingId && process.env.FORLOOP_STREAM_ENABLED === 'true') {
|
|
144
152
|
switch (event.type) {
|
|
145
153
|
case 'message.part.updated': {
|
|
146
154
|
const part = event.properties?.part;
|
|
147
155
|
const messageID = part?.messageID || '';
|
|
148
|
-
console.error('[stream-event] message.part.updated', { type: part?.type, trackingId, sprintId, messageID });
|
|
156
|
+
if (debugStream) console.error('[stream-event] message.part.updated', { type: part?.type, trackingId, sprintId, messageID });
|
|
149
157
|
|
|
150
158
|
// Skip non-text parts — but track that this messageID is now in "generation" phase
|
|
151
159
|
if (!part || part.type !== 'text') {
|
|
@@ -160,7 +168,7 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
160
168
|
// Also track that we've seen the context and subsequent text is streamable.
|
|
161
169
|
const textLen = (part.text || '').length;
|
|
162
170
|
if (!readyToStream.has(messageID) && textLen > 5000) {
|
|
163
|
-
console.error('[stream-event] skipping context text', { trackingId, messageID, textLen });
|
|
171
|
+
if (debugStream) console.error('[stream-event] skipping context text', { trackingId, messageID, textLen });
|
|
164
172
|
readyToStream.add(messageID); // mark as past context
|
|
165
173
|
break;
|
|
166
174
|
}
|
|
@@ -168,8 +176,10 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
168
176
|
readyToStream.add(messageID);
|
|
169
177
|
const current = textBuffer.get(part.messageID) || '';
|
|
170
178
|
textBuffer.set(part.messageID, part.text || current);
|
|
171
|
-
console.error('[stream-event] writing chunk', { trackingId, textLen });
|
|
172
|
-
|
|
179
|
+
if (debugStream) console.error('[stream-event] writing chunk', { trackingId, textLen });
|
|
180
|
+
|
|
181
|
+
// Send delta directly for real-time streaming (primary path)
|
|
182
|
+
sendStreamDelta(sprintId, trackingId, taskId || '', part.text || '');
|
|
173
183
|
break;
|
|
174
184
|
}
|
|
175
185
|
case 'message.updated': {
|
|
@@ -177,7 +187,14 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
177
187
|
if (!info || info.role !== 'assistant') break;
|
|
178
188
|
const bufferedText = textBuffer.get(info.id);
|
|
179
189
|
if (!bufferedText) break;
|
|
180
|
-
|
|
190
|
+
|
|
191
|
+
console.log(`[ForLoop] RESPONSE — planner message (${bufferedText.length} chars)`, {
|
|
192
|
+
sprintId,
|
|
193
|
+
trackingId,
|
|
194
|
+
sessionId: info.sessionID,
|
|
195
|
+
agent: info.agent || 'unknown',
|
|
196
|
+
preview: bufferedText.substring(0, 500),
|
|
197
|
+
});
|
|
181
198
|
|
|
182
199
|
const conversationId = buildConversationId(
|
|
183
200
|
sprintId,
|