@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.
@@ -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
- const FINALIZE_PATTERN = /\b(finalize\s+(the\s+)?plan\b|done\s+planning\b(?!\s+\w)|approve\s+(the\s+)?plan\b|plan\s+looks\s+good\b|ready\s+to\s+finalize\b|let'?s\s+finalize\b)/i;
128
- const ABANDON_PATTERN = /\b(abandon\s+plan(ning)?|cancel\s+plan(ning)?|stop\s+planning|exit\s+planning|quit\s+planning)\b/i;
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
- // Build final summary
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 Approved\n\n` +
241
- `The plan for **${session.projectName}** passed all checks.\n\n` +
242
- `**${issues.length} issues** created with valid dependency graph.${warningsList}\n\n` +
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
- await endPlanningSession(session.projectId, "approved", configPath);
248
- api.logger.info(`Planning: session approved for ${session.projectName}`);
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
- // Trigger DAG-based dispatch if callback provided
251
- opts?.onApproved?.(session.projectId);
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
+ }