@calltelemetry/openclaw-linear 0.8.0 → 0.8.2
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 +152 -34
- package/openclaw.plugin.json +3 -2
- package/package.json +1 -1
- package/prompts.yaml +27 -1
- package/src/agent/agent.test.ts +49 -0
- package/src/agent/agent.ts +26 -1
- package/src/infra/doctor.ts +2 -2
- package/src/pipeline/e2e-planning.test.ts +77 -54
- package/src/pipeline/intent-classify.test.ts +285 -0
- package/src/pipeline/intent-classify.ts +259 -0
- package/src/pipeline/planner.test.ts +159 -40
- package/src/pipeline/planner.ts +98 -32
- package/src/pipeline/webhook.ts +340 -376
- package/src/tools/claude-tool.ts +6 -0
- package/src/tools/code-tool.test.ts +3 -3
- package/src/tools/code-tool.ts +2 -2
- package/src/tools/planner-tools.test.ts +1 -1
- package/src/tools/planner-tools.ts +10 -2
package/src/pipeline/planner.ts
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
buildPlanSnapshot,
|
|
24
24
|
auditPlan,
|
|
25
25
|
} from "../tools/planner-tools.js";
|
|
26
|
+
import { runClaude } from "../tools/claude-tool.js";
|
|
27
|
+
import { runCodex } from "../tools/codex-tool.js";
|
|
28
|
+
import { runGemini } from "../tools/gemini-tool.js";
|
|
26
29
|
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
31
|
// Types
|
|
@@ -39,6 +42,7 @@ interface PlannerPrompts {
|
|
|
39
42
|
interview: string;
|
|
40
43
|
audit_prompt: string;
|
|
41
44
|
welcome: string;
|
|
45
|
+
review: string;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
// ---------------------------------------------------------------------------
|
|
@@ -51,6 +55,7 @@ function loadPlannerPrompts(pluginConfig?: Record<string, unknown>): PlannerProm
|
|
|
51
55
|
interview: "Project: {{projectName}}\n\nPlan:\n{{planSnapshot}}\n\nUser said: {{userMessage}}\n\nContinue planning.",
|
|
52
56
|
audit_prompt: "Run audit_plan for {{projectName}}.",
|
|
53
57
|
welcome: "Entering planning mode for **{{projectName}}**. What are the main feature areas?",
|
|
58
|
+
review: "Plan for {{projectName}} passed checks. {{reviewModel}} recommends:\n{{crossModelFeedback}}\n\nReview and suggest changes, then invite the user to approve.",
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
const parsed = loadRawPromptYaml(pluginConfig);
|
|
@@ -60,6 +65,7 @@ function loadPlannerPrompts(pluginConfig?: Record<string, unknown>): PlannerProm
|
|
|
60
65
|
interview: parsed.planner.interview ?? defaults.interview,
|
|
61
66
|
audit_prompt: parsed.planner.audit_prompt ?? defaults.audit_prompt,
|
|
62
67
|
welcome: parsed.planner.welcome ?? defaults.welcome,
|
|
68
|
+
review: parsed.planner.review ?? defaults.review,
|
|
63
69
|
};
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -124,35 +130,19 @@ export async function initiatePlanningSession(
|
|
|
124
130
|
// Interview turn
|
|
125
131
|
// ---------------------------------------------------------------------------
|
|
126
132
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Handle a planning conversation turn. Intent detection (finalize/abandon)
|
|
135
|
+
* is handled by the webhook via intent-classify.ts before calling this function.
|
|
136
|
+
* This is a pure "continue planning" function.
|
|
137
|
+
*/
|
|
130
138
|
export async function handlePlannerTurn(
|
|
131
139
|
ctx: PlannerContext,
|
|
132
140
|
session: PlanningSession,
|
|
133
141
|
input: { issueId: string; commentBody: string; commentorName: string },
|
|
134
|
-
opts?: { onApproved?: (projectId: string) => void },
|
|
135
142
|
): Promise<void> {
|
|
136
143
|
const { api, linearApi, pluginConfig } = ctx;
|
|
137
144
|
const configPath = pluginConfig?.planningStatePath as string | undefined;
|
|
138
145
|
|
|
139
|
-
// Detect finalization intent
|
|
140
|
-
if (FINALIZE_PATTERN.test(input.commentBody)) {
|
|
141
|
-
await runPlanAudit(ctx, session, { onApproved: opts?.onApproved });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Detect abandon intent
|
|
146
|
-
if (ABANDON_PATTERN.test(input.commentBody)) {
|
|
147
|
-
await endPlanningSession(session.projectId, "abandoned", configPath);
|
|
148
|
-
await linearApi.createComment(
|
|
149
|
-
session.rootIssueId,
|
|
150
|
-
`Planning mode ended for **${session.projectName}**. Session abandoned.`,
|
|
151
|
-
);
|
|
152
|
-
api.logger.info(`Planning: session abandoned for ${session.projectName}`);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
146
|
// Increment turn count
|
|
157
147
|
const newTurnCount = session.turnCount + 1;
|
|
158
148
|
await updatePlanningSession(session.projectId, { turnCount: newTurnCount }, configPath);
|
|
@@ -186,6 +176,8 @@ export async function handlePlannerTurn(
|
|
|
186
176
|
linearApi,
|
|
187
177
|
projectId: session.projectId,
|
|
188
178
|
teamId: session.teamId,
|
|
179
|
+
api,
|
|
180
|
+
pluginConfig,
|
|
189
181
|
});
|
|
190
182
|
|
|
191
183
|
try {
|
|
@@ -217,7 +209,6 @@ export async function handlePlannerTurn(
|
|
|
217
209
|
export async function runPlanAudit(
|
|
218
210
|
ctx: PlannerContext,
|
|
219
211
|
session: PlanningSession,
|
|
220
|
-
opts?: { onApproved?: (projectId: string) => void },
|
|
221
212
|
): Promise<void> {
|
|
222
213
|
const { api, linearApi, pluginConfig } = ctx;
|
|
223
214
|
const configPath = pluginConfig?.planningStatePath as string | undefined;
|
|
@@ -229,26 +220,63 @@ export async function runPlanAudit(
|
|
|
229
220
|
const result = auditPlan(issues);
|
|
230
221
|
|
|
231
222
|
if (result.pass) {
|
|
232
|
-
//
|
|
223
|
+
// Transition to plan_review (not approved yet — cross-model review first)
|
|
224
|
+
await updatePlanningSession(session.projectId, { status: "plan_review" }, configPath);
|
|
225
|
+
|
|
233
226
|
const snapshot = buildPlanSnapshot(issues);
|
|
234
227
|
const warningsList = result.warnings.length > 0
|
|
235
228
|
? `\n\n**Warnings:**\n${result.warnings.map((w) => `- ${w}`).join("\n")}`
|
|
236
229
|
: "";
|
|
237
230
|
|
|
231
|
+
// Determine review model and post "running review" message
|
|
232
|
+
const reviewModel = resolveReviewModel(pluginConfig);
|
|
233
|
+
const reviewModelName = reviewModel.charAt(0).toUpperCase() + reviewModel.slice(1);
|
|
234
|
+
|
|
238
235
|
await linearApi.createComment(
|
|
239
236
|
session.rootIssueId,
|
|
240
|
-
`## Plan
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
`### Final Plan\n${snapshot}\n\n` +
|
|
244
|
-
`---\n*Planning mode complete. Project is ready for implementation dispatch.*`,
|
|
237
|
+
`## Plan Passed Checks\n\n` +
|
|
238
|
+
`**${issues.length} issues** with valid dependency graph.${warningsList}\n\n` +
|
|
239
|
+
`Let me have **${reviewModelName}** audit this and make recommendations.`,
|
|
245
240
|
);
|
|
246
241
|
|
|
247
|
-
|
|
248
|
-
api
|
|
242
|
+
// Run cross-model review
|
|
243
|
+
const crossReview = await runCrossModelReview(api, reviewModel, snapshot, pluginConfig);
|
|
244
|
+
|
|
245
|
+
// Run planner agent with review prompt + cross-model feedback
|
|
246
|
+
const prompts = loadPlannerPrompts(pluginConfig);
|
|
247
|
+
const reviewPrompt = renderTemplate(prompts.review, {
|
|
248
|
+
projectName: session.projectName,
|
|
249
|
+
planSnapshot: snapshot,
|
|
250
|
+
issueCount: String(issues.length),
|
|
251
|
+
reviewModel: reviewModelName,
|
|
252
|
+
crossModelFeedback: crossReview,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const agentId = (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
249
256
|
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
setActivePlannerContext({
|
|
258
|
+
linearApi,
|
|
259
|
+
projectId: session.projectId,
|
|
260
|
+
teamId: session.teamId,
|
|
261
|
+
api,
|
|
262
|
+
pluginConfig,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const agentResult = await runAgent({
|
|
267
|
+
api,
|
|
268
|
+
agentId,
|
|
269
|
+
sessionId: `planner-${session.rootIdentifier}-review`,
|
|
270
|
+
message: `${prompts.system}\n\n${reviewPrompt}`,
|
|
271
|
+
});
|
|
272
|
+
if (agentResult.output) {
|
|
273
|
+
await linearApi.createComment(session.rootIssueId, agentResult.output);
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
clearActivePlannerContext();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
api.logger.info(`Planning: entered plan_review for ${session.projectName} (reviewed by ${reviewModelName})`);
|
|
252
280
|
} else {
|
|
253
281
|
// Post problems and keep planning
|
|
254
282
|
const problemsList = result.problems.map((p) => `- ${p}`).join("\n");
|
|
@@ -267,3 +295,41 @@ export async function runPlanAudit(
|
|
|
267
295
|
api.logger.info(`Planning: audit failed for ${session.projectName} (${result.problems.length} problems)`);
|
|
268
296
|
}
|
|
269
297
|
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Cross-model review
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
export async function runCrossModelReview(
|
|
304
|
+
api: OpenClawPluginApi,
|
|
305
|
+
model: "claude" | "codex" | "gemini",
|
|
306
|
+
planSnapshot: string,
|
|
307
|
+
pluginConfig?: Record<string, unknown>,
|
|
308
|
+
): Promise<string> {
|
|
309
|
+
const prompt = `You are reviewing a project plan. Analyze it and suggest specific improvements.\n\n${planSnapshot}\n\nFocus on: missing acceptance criteria, dependency gaps, estimation accuracy, testability, and edge cases. Reference specific issue identifiers. Be concise and actionable.`;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const runner = model === "claude" ? runClaude
|
|
313
|
+
: model === "codex" ? runCodex
|
|
314
|
+
: runGemini;
|
|
315
|
+
const result = await runner(api, { prompt } as any, pluginConfig);
|
|
316
|
+
return result.success ? (result.output ?? "(no feedback)") : `(${model} review failed: ${result.error})`;
|
|
317
|
+
} catch (err) {
|
|
318
|
+
api.logger.warn(`Cross-model review failed: ${err}`);
|
|
319
|
+
return "(cross-model review unavailable)";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function resolveReviewModel(pluginConfig?: Record<string, unknown>): "claude" | "codex" | "gemini" {
|
|
324
|
+
// User override in config
|
|
325
|
+
const configured = (pluginConfig as any)?.plannerReviewModel as string | undefined;
|
|
326
|
+
if (configured && ["claude", "codex", "gemini"].includes(configured)) {
|
|
327
|
+
return configured as "claude" | "codex" | "gemini";
|
|
328
|
+
}
|
|
329
|
+
// Always the complement of the user's primary model
|
|
330
|
+
const currentModel = (pluginConfig as any)?.agents?.defaults?.model?.primary as string ?? "";
|
|
331
|
+
if (currentModel.includes("claude") || currentModel.includes("anthropic")) return "codex";
|
|
332
|
+
if (currentModel.includes("codex") || currentModel.includes("openai")) return "gemini";
|
|
333
|
+
if (currentModel.includes("gemini") || currentModel.includes("google")) return "codex";
|
|
334
|
+
return "gemini"; // Kimi, Mistral, other, or unconfigured → Gemini reviews
|
|
335
|
+
}
|